Release #4
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Cut a release, end to end, from one manual trigger. | |
| # | |
| # Two jobs in one run, deliberately not tag-triggered: pushes made with the | |
| # workflow's own GITHUB_TOKEN never fire other workflows (GitHub's recursion | |
| # guard), so a "bump job pushes a tag, tag triggers a build workflow" design | |
| # silently does nothing. Chaining the build job onto the bump job with | |
| # `needs:` sidesteps that entirely and keeps the whole release visible as a | |
| # single run. | |
| # | |
| # workflow_dispatch (choose patch/minor/major) | |
| # └─ cut (ubuntu): compute next version from the latest v* tag, | |
| # scripts/set-version.sh, commit, tag, push to main | |
| # └─ build (macos): checkout that tag, build agent + CLI + daemon, | |
| # SHA256SUMS, create the GitHub release | |
| name: Release | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| bump: | |
| description: "Version bump (house rule: minor for routine releases)" | |
| type: choice | |
| options: [patch, minor, major] | |
| default: minor | |
| permissions: | |
| contents: write | |
| jobs: | |
| cut: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| tag: ${{ steps.ver.outputs.tag }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| ref: main | |
| fetch-depth: 0 # need the tag history to compute the next version | |
| - name: Compute next version | |
| id: ver | |
| run: | | |
| latest="$(git tag --list 'v*' --sort=-v:refname | head -1)" | |
| latest="${latest:-v0.0.0}" | |
| IFS=. read -r major minor patch <<< "${latest#v}" | |
| case "${{ inputs.bump }}" in | |
| major) version="$((major + 1)).0.0" ;; | |
| minor) version="${major}.$((minor + 1)).0" ;; | |
| patch) version="${major}.${minor}.$((patch + 1))" ;; | |
| esac | |
| echo "version=${version}" >> "$GITHUB_OUTPUT" | |
| echo "tag=v${version}" >> "$GITHUB_OUTPUT" | |
| echo "bumping ${latest} -> v${version}" | |
| - name: Bump, commit, tag, push | |
| run: | | |
| scripts/set-version.sh "${{ steps.ver.outputs.version }}" | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| # The tree can already be at the target version — it happened on the | |
| # very first release, and it happens on any re-dispatch after a cut | |
| # that pushed the commit to main but failed before the tag landed | |
| # (the two refs in the push below are not atomic). Committing nothing | |
| # and tagging HEAD makes that rerun self-heal; don't "clean this up". | |
| if ! git diff --quiet; then | |
| git commit -am "Release ${{ steps.ver.outputs.tag }}" | |
| fi | |
| git tag "${{ steps.ver.outputs.tag }}" | |
| git push origin main "${{ steps.ver.outputs.tag }}" | |
| build: | |
| needs: cut | |
| runs-on: macos-26 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ needs.cut.outputs.tag }} | |
| - uses: oven-sh/setup-bun@v2 | |
| - name: Build phantom-agent | |
| run: | | |
| cd phantom-agent | |
| swift build -c release | |
| cp .build/release/phantom-agent ../dist-agent | |
| ../dist-agent --version | |
| - name: Build phantom CLI | |
| run: | | |
| cd phantom-cli | |
| bun install --frozen-lockfile | |
| bun build src/main.ts --compile --minify --outfile ../dist-phantom | |
| bun build src/main-admin.ts --compile --minify --outfile ../dist-phantom-admin | |
| ../dist-phantom --version | |
| ../dist-phantom-admin --version | |
| - name: Import signing certificate | |
| env: | |
| CERT_P12: ${{ secrets.MACOS_CERT_P12 }} | |
| CERT_PASSWORD: ${{ secrets.MACOS_CERT_PASSWORD }} | |
| run: | | |
| # Throwaway keychain, dies with the runner. The partition-list step | |
| # is what lets codesign use the key without a UI prompt. | |
| keychain="$RUNNER_TEMP/release.keychain-db" | |
| keychain_pass="$(uuidgen)" | |
| security create-keychain -p "$keychain_pass" "$keychain" | |
| security set-keychain-settings -lut 3600 "$keychain" | |
| security unlock-keychain -p "$keychain_pass" "$keychain" | |
| echo "$CERT_P12" | base64 -d > "$RUNNER_TEMP/cert.p12" | |
| security import "$RUNNER_TEMP/cert.p12" -k "$keychain" \ | |
| -P "$CERT_PASSWORD" -T /usr/bin/codesign | |
| rm "$RUNNER_TEMP/cert.p12" | |
| security set-key-partition-list -S apple-tool:,apple: \ | |
| -k "$keychain_pass" "$keychain" > /dev/null | |
| security list-keychains -d user -s "$keychain" login.keychain-db | |
| - name: Build phantom daemon | |
| run: | | |
| # ARCHS=arm64: Release otherwise also builds x86_64, where the | |
| # VZMac* API family (macOS guests) doesn't exist at all. | |
| # CODE_SIGN_INJECT_BASE_ENTITLEMENTS=NO: plain `xcodebuild build` | |
| # (as opposed to archive/export) injects get-task-allow otherwise, | |
| # and notarization rejects it. | |
| xcodebuild -project phantom.xcodeproj -scheme phantom \ | |
| -configuration Release ARCHS=arm64 \ | |
| CODE_SIGN_STYLE=Manual \ | |
| CODE_SIGN_IDENTITY="Developer ID Application" \ | |
| DEVELOPMENT_TEAM=5FP33J8924 \ | |
| OTHER_CODE_SIGN_FLAGS=--timestamp \ | |
| CODE_SIGN_INJECT_BASE_ENTITLEMENTS=NO \ | |
| build | |
| # The project pins its build location (products land in the repo's | |
| # DerivedData/, and -derivedDataPath is ignored) — ask xcodebuild | |
| # where the app actually went instead of hardcoding it. | |
| app_dir="$(xcodebuild -project phantom.xcodeproj -scheme phantom \ | |
| -configuration Release -showBuildSettings 2>/dev/null \ | |
| | awk -F' = ' '/ TARGET_BUILD_DIR =/{print $2; exit}')" | |
| echo "app_dir=$app_dir" >> "$GITHUB_ENV" | |
| # Belt and braces: a debug entitlement in a release is a build bug. | |
| if codesign -d --entitlements :- "${app_dir}/Phantom.app" 2>/dev/null \ | |
| | grep -q get-task-allow; then | |
| echo "::error::release app carries get-task-allow" >&2 | |
| exit 1 | |
| fi | |
| - name: Notarize and staple | |
| env: | |
| NOTARY_KEY_P8: ${{ secrets.NOTARY_KEY_P8 }} | |
| NOTARY_KEY_ID: ${{ secrets.NOTARY_KEY_ID }} | |
| NOTARY_ISSUER_ID: ${{ secrets.NOTARY_ISSUER_ID }} | |
| run: | | |
| key="$RUNNER_TEMP/AuthKey.p8" | |
| printf '%s\n' "$NOTARY_KEY_P8" > "$key" | |
| ditto -c -k --keepParent "${app_dir}/Phantom.app" notarize.zip | |
| out="$(xcrun notarytool submit notarize.zip \ | |
| --key "$key" --key-id "$NOTARY_KEY_ID" --issuer "$NOTARY_ISSUER_ID" \ | |
| --wait --timeout 30m --output-format json)" | |
| echo "$out" | |
| status="$(echo "$out" | jq -r .status)" | |
| sub_id="$(echo "$out" | jq -r .id)" | |
| # notarytool's exit code is not a reliable verdict — gate on the | |
| # status field, and surface Apple's log when it isn't Accepted. | |
| if [ "$status" != "Accepted" ]; then | |
| xcrun notarytool log "$sub_id" \ | |
| --key "$key" --key-id "$NOTARY_KEY_ID" --issuer "$NOTARY_ISSUER_ID" || true | |
| echo "::error::notarization $status" >&2 | |
| exit 1 | |
| fi | |
| xcrun stapler staple "${app_dir}/Phantom.app" | |
| # Re-zip after stapling: the ticket must ship inside the asset so | |
| # Gatekeeper can verify offline. | |
| ditto -c -k --keepParent "${app_dir}/Phantom.app" dist-phantom-app.zip | |
| # No checksum file: GitHub computes an immutable SHA-256 per asset at | |
| # upload and serves it as `asset.digest` (REST/GraphQL/gh, since | |
| # 2025-06) — a SHA256SUMS.txt in the same release would duplicate that | |
| # from inside the same trust domain. | |
| # | |
| # agent-install.sh is the guest bootstrap: a fresh VM fetches it from | |
| # `releases/latest/download/` and pipes it to root sh, so it embeds the | |
| # agent's SHA-256 and downloads the binary from this release's | |
| # *versioned* URL — `latest` resolving mid-publish must not mix a new | |
| # script with an old binary. | |
| - name: Assemble | |
| run: | | |
| mkdir release | |
| cp dist-agent release/phantom-agent | |
| cp dist-phantom release/phantom-cli | |
| cp dist-phantom-admin release/phantom-cli-admin | |
| cp dist-phantom-app.zip release/phantom-daemon.zip | |
| phantom-agent/make-install-script.sh dist-agent \ | |
| "https://github.com/${{ github.repository }}/releases/download/${{ needs.cut.outputs.tag }}/phantom-agent" \ | |
| > release/agent-install.sh | |
| - name: Create release | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| gh release create "${{ needs.cut.outputs.tag }}" \ | |
| release/* \ | |
| --title "${{ needs.cut.outputs.tag }}" \ | |
| --generate-notes |