-
Notifications
You must be signed in to change notification settings - Fork 0
200 lines (185 loc) · 8.51 KB
/
Copy pathrelease.yml
File metadata and controls
200 lines (185 loc) · 8.51 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
# 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@v7
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@v7
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
../dist-phantom --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.
#
# phantom-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-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/phantom-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