1name: Portal release23on:4 workflow_dispatch:5 inputs:6 tag:7 description: Existing immutable vVERSION tag with a Sigil draft release8 required: true9 type: string1011permissions:12 contents: read1314concurrency:15 group: public-release-${{ inputs.tag }}16 cancel-in-progress: false1718jobs:19 policy:20 name: Verify release policy21 runs-on: ubuntu-24.0422 timeout-minutes: 1023 steps:24 - name: Check out exact tag25 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.126 with:27 ref: ${{ inputs.tag }}28 fetch-depth: 02930 - name: Verify source, tests, and website31 env:32 RELEASE_TAG: ${{ inputs.tag }}33 run: |34 python3 scripts/verify-portal-release.py source \35 --repo-dir "$GITHUB_WORKSPACE" \36 --release-tag "$RELEASE_TAG"37 ./scripts/tests/portal-release.sh38 ./scripts/verify-website.sh3940 publish:41 name: Sign, notarize, and publish macOS arm6442 needs: policy43 runs-on: macos-2644 timeout-minutes: 9045 environment:46 name: main47 permissions:48 artifact-metadata: write49 attestations: write50 contents: write51 id-token: write52 env:53 RELEASE_TAG: ${{ inputs.tag }}54 steps:55 - name: Check out exact tag56 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.157 with:58 ref: ${{ inputs.tag }}59 fetch-depth: 06061 - name: Require workflow execution from the exact tag62 run: |63 test "$GITHUB_REF" = "refs/tags/$RELEASE_TAG"64 test "$GITHUB_SHA" = "$(git rev-parse HEAD)"6566 - name: Verify native arm64 runner67 run: |68 test "$(uname -s)" = Darwin69 test "$(uname -m)" = arm6470 rustup target add aarch64-apple-darwin7172 - name: Install pinned Tauri CLI73 run: cargo install tauri-cli --version 2.11.3 --locked7475 - name: Import Developer ID certificate76 env:77 APPLE_CERTIFICATE_BASE64: ${{ secrets.PORTAL_APPLE_CERTIFICATE_BASE64 }}78 APPLE_CERTIFICATE_PASSWORD: ${{ secrets.PORTAL_APPLE_CERTIFICATE_PASSWORD }}79 run: |80 certificate_path="$RUNNER_TEMP/portal-developer-id.p12"81 keychain_path="$RUNNER_TEMP/portal-signing.keychain-db"82 keychain_password="$(openssl rand -hex 32)"83 echo "::add-mask::$keychain_password"84 python3 - "$certificate_path" <<'PY'85 import base6486 import os87 import pathlib88 import sys8990 encoded = os.environ.get("APPLE_CERTIFICATE_BASE64", "")91 if not encoded:92 raise SystemExit("PORTAL_APPLE_CERTIFICATE_BASE64 is not configured")93 pathlib.Path(sys.argv[1]).write_bytes(base64.b64decode(encoded, validate=True))94 PY95 chmod 0600 "$certificate_path"96 security create-keychain -p "$keychain_password" "$keychain_path"97 security set-keychain-settings -lut 21600 "$keychain_path"98 security unlock-keychain -p "$keychain_password" "$keychain_path"99 security default-keychain -s "$keychain_path"100 security import "$certificate_path" \101 -k "$keychain_path" \102 -P "$APPLE_CERTIFICATE_PASSWORD" \103 -T /usr/bin/codesign \104 -T /usr/bin/security105 security set-key-partition-list \106 -S apple-tool:,apple: \107 -s \108 -k "$keychain_password" \109 "$keychain_path" >/dev/null110111 - name: Build and verify signed release assets112 env:113 APPLE_SIGNING_IDENTITY: ${{ secrets.PORTAL_APPLE_SIGNING_IDENTITY }}114 APPLE_ID: ${{ secrets.PORTAL_APPLE_ID }}115 APPLE_PASSWORD: ${{ secrets.PORTAL_APPLE_PASSWORD }}116 APPLE_TEAM_ID: ${{ secrets.PORTAL_APPLE_TEAM_ID }}117 run: |118 release_dir="$RUNNER_TEMP/portal-release"119 mkdir -m 0700 "$release_dir"120 ./scripts/package-macos-client.sh \121 --release-tag "$RELEASE_TAG" \122 --output-dir "$release_dir"123124 - name: Attest exact Portal build provenance125 uses: actions/attest@f7c74d28b9d84cb8768d0b8ca14a4bac6ef463e6 # v4.1.0126 with:127 subject-path: '${{ runner.temp }}/portal-release/Portal-*'128129 - name: Require the exact Sigil candidate draft130 env:131 GH_TOKEN: ${{ github.token }}132 run: |133 release_json="$(gh release view "$RELEASE_TAG" --json isDraft,tagName,assets)"134 test "$(jq -r '.isDraft' <<<"$release_json")" = true135 test "$(jq -r '.tagName' <<<"$release_json")" = "$RELEASE_TAG"136 target_contract="$(<release/sigil-target-contract.txt)"137 test "$target_contract" = linux-glibc2.17-x86_64138 sigil_asset="sigil-$RELEASE_TAG-$target_contract.tar.gz"139 expected="$(printf '%s\n' "$sigil_asset" "$sigil_asset.sha256" | sort)"140 actual="$(jq -r '.assets[].name' <<<"$release_json" | sort)"141 test "$actual" = "$expected" || {142 echo "draft must contain only the two verified Sigil candidate assets before Portal upload" >&2143 exit 1144 }145146 - name: Upload exact Portal assets147 env:148 GH_TOKEN: ${{ github.token }}149 run: |150 version="${RELEASE_TAG#v}"151 release_dir="$RUNNER_TEMP/portal-release"152 dmg="Portal-${version}-arm64.dmg"153 checksum="${dmg}.sha256"154 manifest="Portal-${version}-arm64.json"155 gh release upload "$RELEASE_TAG" \156 "$release_dir/$dmg" \157 "$release_dir/$checksum" \158 "$release_dir/$manifest"159160 - name: Verify the combined draft remains complete and unpublished161 env:162 GH_TOKEN: ${{ github.token }}163 run: |164 release_json="$(gh release view "$RELEASE_TAG" --json isDraft,tagName,assets)"165 test "$(jq -r '.isDraft' <<<"$release_json")" = true166 test "$(jq -r '.tagName' <<<"$release_json")" = "$RELEASE_TAG"167 version="${RELEASE_TAG#v}"168 target_contract="$(<release/sigil-target-contract.txt)"169 test "$target_contract" = linux-glibc2.17-x86_64170 sigil_asset="sigil-$RELEASE_TAG-$target_contract.tar.gz"171 expected="$(printf '%s\n' \172 "$sigil_asset" \173 "$sigil_asset.sha256" \174 "Portal-${version}-arm64.dmg" \175 "Portal-${version}-arm64.dmg.sha256" \176 "Portal-${version}-arm64.json" | sort)"177 actual="$(jq -r '.assets[].name' <<<"$release_json" | sort)"178 test "$actual" = "$expected" || {179 echo "combined draft does not match the five-asset pre-signature contract" >&2180 exit 1181 }182 asset_names="$RUNNER_TEMP/portal-release-assets.json"183 jq '[.assets[].name | select(startswith("Portal-"))]' \184 <<<"$release_json" >"$asset_names"185 python3 scripts/verify-portal-release.py release-assets \186 --release-tag "$RELEASE_TAG" \187 --names-json "$asset_names"188189 - name: Remove signing keychain190 if: always()191 run: |192 security delete-keychain "$RUNNER_TEMP/portal-signing.keychain-db" 2>/dev/null || true193 python3 - "$RUNNER_TEMP/portal-developer-id.p12" <<'PY'194 import pathlib195 import sys196197 path = pathlib.Path(sys.argv[1])198 if path.exists():199 path.write_bytes(b"\0" * path.stat().st_size)200 path.unlink()201 PY