-
Notifications
You must be signed in to change notification settings - Fork 0
284 lines (263 loc) · 14.8 KB
/
Copy pathrelease.yml
File metadata and controls
284 lines (263 loc) · 14.8 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
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
name: 'CI & Release'
# The SINGLE CI + release workflow. There is deliberately NO per-push / per-PR CI —
# validation runs only here, at release time. On a v* tag it validates the whole build
# (guest kernel + erofs rootfs, guest PID 1 `vinit` lint, host swift build + selftest)
# and publishes a signed GitHub Release; a manual workflow_dispatch runs the same
# validation WITHOUT publishing (use it as pure CI). Automated auto-bumps additionally
# self-validate before tagging, in version-watch.yml (CLAUDE.md §9).
#
# Cut a release:
# 1. bump VELOX_VERSION in versions.env (the single source of truth, CLAUDE.md §2)
# 2. git commit -am "release vX.Y.Z"
# 3. git tag vX.Y.Z && git push origin vX.Y.Z
# The tag MUST equal VELOX_VERSION (the verify step fails the build otherwise); the
# updater reads /releases/latest.
#
# The guest kernel + rootfs are CACHED on their own inputs: a GUI-only change rebuilds
# neither (both restored from cache); a velox.fragment change rebuilds only the kernel;
# a vinit/Dockerfile change rebuilds only the rootfs. VELOX_VERSION is deliberately NOT
# in the cache keys, so bumping it every release doesn't force a kernel recompile.
on:
push:
tags: ['v*']
workflow_dispatch: {} # manual runs build + package but don't publish a Release
# Serialize releases: two closely-spaced v* tags would otherwise run the pipeline in
# parallel and race on the shared build cache. Keyed per ref so distinct tags queue
# rather than cancel each other (never cancel an in-flight release).
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
# Least privilege at the workflow level; only the job that publishes the release gets
# write. Previously every job — including the one that fetches and executes moby's
# check-config.sh, and the one that resolves vinit's dependency tree — held a
# write-scoped GITHUB_TOKEN that `actions/checkout` leaves in .git/config for the job's
# lifetime.
permissions:
contents: read
jobs:
# ---------------------------------------------------------------------------
# 1) Guest (custom kernel + erofs rootfs) on a NATIVE arm64 Linux runner —
# Docker is preinstalled and linux/arm64 runs without qemu. Cached per-input.
# ---------------------------------------------------------------------------
guest:
runs-on: ubuntu-24.04-arm # GitHub-hosted Linux arm64 (native — no qemu). NB: label is
# `-arm`, not `-arm64`; ubuntu-26.04 has no hosted runner yet.
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with: { persist-credentials: false } # nothing here pushes
- name: Verify tag matches VELOX_VERSION
if: startsWith(github.ref, 'refs/tags/')
run: |
set -a; . ./versions.env; set +a
if [ "${GITHUB_REF_NAME}" != "v${VELOX_VERSION}" ]; then
echo "::error::tag ${GITHUB_REF_NAME} != v${VELOX_VERSION} (versions.env). \
versions.env is the single source of truth — bump VELOX_VERSION to match the tag."
exit 1
fi
- name: Compute cache keys (kernel + rootfs inputs, NOT VELOX_VERSION)
id: keys
run: |
set -a; . ./versions.env; set +a
# Enumerate the inputs with `git ls-files` rather than by hand: the old explicit list
# was correct only as long as nobody added a file. A new guest/kernel/* input would
# have shipped a STALE cached kernel with no signal at all.
KHASH=$( { echo "$KERNEL_ORG_VERSION $KERNEL_ORG_SHA256 $KERNEL_BUILDER_IMAGE $MOBY_CHECKCONFIG_REF $MOBY_CHECKCONFIG_SHA256"; \
git ls-files -z guest/kernel Scripts/build-kernel.sh | sort -z | xargs -0 sha256sum; \
} | sha256sum | cut -c1-16 )
RHASH=$( { echo "$DOCKER_VERSION $RUST_BUILD_IMAGE $ALPINE_IMAGE $QEMU_USER_VERSION $QEMU_USER_DEB_SHA256"; \
git ls-files -z guest/rootfs guest/vinit Scripts/make-guest.sh | sort -z | xargs -0 sha256sum; \
} | sha256sum | cut -c1-16 )
echo "kernel=kernel-${KERNEL_ORG_VERSION}-${KHASH}" >> "$GITHUB_OUTPUT"
echo "rootfs=rootfs-${DOCKER_VERSION}-${RHASH}" >> "$GITHUB_OUTPUT"
- name: Restore cached kernel
id: kcache
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5
with:
path: Assets/velox-vmlinux
key: ${{ steps.keys.outputs.kernel }}
- name: Build kernel (only when its inputs changed)
if: steps.kcache.outputs.cache-hit != 'true'
run: ./Scripts/build-kernel.sh
- name: Restore cached rootfs
id: rcache
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5
with:
path: guest/build/root.img
key: ${{ steps.keys.outputs.rootfs }}
- name: Build guest rootfs (only when its inputs changed)
if: steps.rcache.outputs.cache-hit != 'true'
run: ./Scripts/make-guest.sh
# Assert the artifacts regardless of where they came from. `build-kernel.sh` checks the
# ARM64 raw-Image magic and a sane size band after a build — but on a cache HIT no build
# runs, so none of that executed and the job just uploaded whatever bytes the cache
# returned. That is the normal path on the auto-release route.
- name: Sanity-check the guest artifacts (cache hit or fresh build)
run: |
set -euo pipefail
K=Assets/velox-vmlinux; R=guest/build/root.img
[ -f "$K" ] && [ -f "$R" ] || { echo "::error::guest artifacts missing"; exit 1; }
# arm64 raw kernel Image: magic "ARM\x64" at offset 56.
magic=$(dd if="$K" bs=1 skip=56 count=4 2>/dev/null | xxd -p)
[ "$magic" = "41524d64" ] || { echo "::error::$K is not an arm64 raw Image (magic=$magic)"; exit 1; }
ksz=$(stat -c %s "$K"); rsz=$(stat -c %s "$R")
[ "$ksz" -ge 4194304 ] && [ "$ksz" -le 67108864 ] || { echo "::error::$K size $ksz outside 4-64 MB"; exit 1; }
[ "$rsz" -ge 16777216 ] || { echo "::error::$R size $rsz implausibly small"; exit 1; }
echo "kernel ${ksz} bytes, rootfs ${rsz} bytes — OK"
- name: Upload guest artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: guest
path: |
Assets/velox-vmlinux
guest/build/root.img
if-no-files-found: error
retention-days: 1
# ---------------------------------------------------------------------------
# 1b) Lint the guest PID 1 (vinit) against its real musl target (it doesn't
# compile for the host). Fast; gates the release so a lint regression can't
# ship. This absorbed the old standalone `ci` vinit job.
# ---------------------------------------------------------------------------
lint-guest:
runs-on: ubuntu-24.04-arm
defaults:
run: { working-directory: guest/vinit }
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with: { persist-credentials: false } # nothing here pushes
- name: Install musl target
run: rustup target add aarch64-unknown-linux-musl
- name: Clippy (deny warnings)
run: cargo clippy --target aarch64-unknown-linux-musl -- -D warnings
- name: Check (release profile)
run: cargo check --release --target aarch64-unknown-linux-musl
# ---------------------------------------------------------------------------
# 2) Build + package the macOS app (also runs the host selftest — the old `ci`
# swift job). Bundles the guest from job 1; signs ad-hoc by default (Developer
# ID + notarize when secrets exist). Gated on the guest build AND the lint.
# ---------------------------------------------------------------------------
app:
needs: [guest, lint-guest]
runs-on: xcode-27 # macOS 27 + Xcode 27 (GitHub preview label; the SDK needs Xcode's SwiftUIMacros plugin)
permissions:
contents: write # creates the GitHub Release
# `secrets` can't be referenced in `if:` (it isn't a valid context there), so surface
# whether each one is SET — never the value. Job-level `env` is inherited by every step,
# including third-party actions (see `softprops/action-gh-release` below), and log masking
# redacts output but does not stop an action from reading `process.env`. Putting the
# release signing key here would hand it to anyone who compromises any action in this job,
# and with it the ability to forge an update the whole fleet auto-installs.
env:
HAS_SIGNING: ${{ secrets.MACOS_CERT_P12_BASE64 != '' }}
HAS_NOTARY: ${{ secrets.AC_API_KEY_ID != '' }}
HAS_RELEASE_KEY: ${{ secrets.VELOX_ED25519_PRIVATE_KEY != '' }}
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with: { persist-credentials: false } # nothing here pushes
- name: Fetch guest artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with: { name: guest, path: . }
- run: swift --version && xcode-select -p && xcrun --sdk macosx --show-sdk-version
# The dependency-free test runner (Tests/SelfTest). A red selftest blocks the release.
- name: Selftest
run: |
./Scripts/gen-versions.sh
swift run velox-selftest
# Developer ID signing (optional). Secrets:
# MACOS_CERT_P12_BASE64, MACOS_CERT_PASSWORD, MACOS_SIGN_IDENTITY
- name: Import signing certificate
if: ${{ env.HAS_SIGNING == 'true' }}
env:
P12: ${{ secrets.MACOS_CERT_P12_BASE64 }}
P12_PW: ${{ secrets.MACOS_CERT_PASSWORD }}
run: |
KEYCHAIN="$RUNNER_TEMP/velox-signing.keychain-db"
security create-keychain -p actions "$KEYCHAIN"
security set-keychain-settings -lut 21600 "$KEYCHAIN"
security unlock-keychain -p actions "$KEYCHAIN"
echo "$P12" | base64 --decode > "$RUNNER_TEMP/cert.p12"
security import "$RUNNER_TEMP/cert.p12" -k "$KEYCHAIN" -P "$P12_PW" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k actions "$KEYCHAIN" >/dev/null
# word-splitting is intentional: pass each existing keychain path as its own arg.
# shellcheck disable=SC2046
security list-keychains -d user -s "$KEYCHAIN" $(security list-keychains -d user | tr -d '"')
- name: Build + package (Velox.app -> .zip)
env:
VELOX_SIGN_IDENTITY: ${{ secrets.MACOS_SIGN_IDENTITY }} # empty → ad-hoc
run: ./Scripts/build-app.sh release
# Notarize + staple (optional). Secrets: AC_API_KEY_ID, AC_API_ISSUER_ID, AC_API_KEY_BASE64
- name: Notarize
if: ${{ env.HAS_NOTARY == 'true' }}
env:
KEY_ID: ${{ secrets.AC_API_KEY_ID }}
ISSUER: ${{ secrets.AC_API_ISSUER_ID }}
KEY_B64: ${{ secrets.AC_API_KEY_BASE64 }}
run: |
echo "$KEY_B64" | base64 --decode > "$RUNNER_TEMP/ac.p8"
# Notarize the .zip; the app inside is stamped, so first launch verifies online.
for f in dist/Velox-*.zip; do
[ -f "$f" ] && xcrun notarytool submit "$f" --key "$RUNNER_TEMP/ac.p8" \
--key-id "$KEY_ID" --issuer "$ISSUER" --wait || true
done
# Ed25519-sign each release .zip (what the in-app updater verifies against the
# public key baked into the build — VELOX_RELEASE_PUBKEY in versions.env) and
# publish SHA256SUMS (what install.sh verifies). One-time key setup:
# swift Scripts/release-sign.swift keygen
# → private key to the VELOX_ED25519_PRIVATE_KEY repo secret, public key to versions.env.
- name: Sign release artifacts (Ed25519)
if: ${{ env.HAS_RELEASE_KEY == 'true' }}
env:
RELEASE_KEY: ${{ secrets.VELOX_ED25519_PRIVATE_KEY }}
run: |
set -euo pipefail
# Guard the glob: with nullglob unset an empty match makes `$f` the literal pattern,
# so this loop would "succeed" having signed nothing.
ls -1 dist/Velox-*.zip >/dev/null
for f in dist/Velox-*.zip; do
# Key via the environment, never argv — argv is world-readable through `ps -E`.
swift Scripts/release-sign.swift sign "$f"
done
# Fail closed. Every client build bakes in VELOX_RELEASE_PUBKEY (versions.env) and
# its updater REFUSES to auto-install a release whose .sig is missing or doesn't
# verify. So if a public key is configured, each .zip MUST carry a signature that
# verifies against it — otherwise this release would silently break auto-update for
# the whole fleet (users fall back to a manual download). This one guard catches both
# failure modes the `if:`-gated signing step can't: a missing signing secret (no .sig
# produced) and a rotated/mismatched keypair (signature present but invalid).
- name: Verify release signatures match the shipped public key
run: |
set -euo pipefail
PUBKEY="$(grep -E '^VELOX_RELEASE_PUBKEY=' versions.env | head -1 | cut -d= -f2-)"
if [ -z "$PUBKEY" ]; then
echo "VELOX_RELEASE_PUBKEY is empty (dev build) — signature verification skipped."
exit 0
fi
# Assert the glob matched at all — otherwise `$f` is the literal pattern, the
# `[ -f ]` test fails, `continue` fires, and this "fail closed" gate exits 0 having
# verified nothing.
if [ "$(ls -1 dist/Velox-*.zip 2>/dev/null | wc -l)" -lt 1 ]; then
echo "::error::no dist/Velox-*.zip to verify — the app build produced nothing." >&2
exit 1
fi
for f in dist/Velox-*.zip; do
[ -f "$f" ] || continue
if [ ! -f "$f.sig" ]; then
echo "::error::$f is unsigned but VELOX_RELEASE_PUBKEY is set — the VELOX_ED25519_PRIVATE_KEY secret is missing. Every client would reject this update." >&2
exit 1
fi
swift Scripts/release-sign.swift verify "$f" "$PUBKEY"
done
- name: Generate SHA256SUMS
run: cd dist && shasum -a 256 Velox-*.zip > SHA256SUMS
- name: Upload build artifacts (every run, for inspection)
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with: { name: velox-app, path: dist/Velox-*, retention-days: 7 }
- name: Create GitHub Release
if: startsWith(github.ref, 'refs/tags/') # tags publish; manual runs don't
uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3
with:
# Velox-*.zip* also picks up the .sig files when the signing secret is set.
files: |
dist/Velox-*.zip*
dist/SHA256SUMS
generate_release_notes: true
fail_on_unmatched_files: true