Skip to content

Commit 8eea600

Browse files
committed
ci(release): the darwin-arm64 binary is built on a macOS runner and both publishes are gated on it; the per-push macOS build job
1 parent d66d658 commit 8eea600

4 files changed

Lines changed: 249 additions & 21 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,33 @@
11
name: ci
22
on: [push, pull_request]
33
jobs:
4+
# --- P9a-4: the SAME macOS build as release.yml's `build-platform`, minus the tag pin, on EVERY
5+
# push -- so a broken darwin build fails here, before a `v*` tag ever exists to discover it at
6+
# publish time. Runs the smoke's EXECUTE path (real `--version` spawn of the just-built binary)
7+
# rather than the SKIP path the two ubuntu `pack-smoke*` jobs below take on this same publishable
8+
# package (M1: `os`/`cpu` mismatch there is expected and permanent, never a reason to fail). This
9+
# job never publishes anything -- `release-gates.test.ts` pins that alongside every other job here.
10+
build-platform:
11+
runs-on: macos-15
12+
steps:
13+
- uses: actions/checkout@v4
14+
- name: "assert a REAL macOS arm64 runner (P9a-4) -- never trust the `macos-15` label alone"
15+
run: |
16+
set -euo pipefail
17+
UNAME_S="$(uname -s)"
18+
UNAME_M="$(uname -m)"
19+
if [ "$UNAME_S" != "Darwin" ] || [ "$UNAME_M" != "arm64" ]; then
20+
echo "build-platform: expected Darwin/arm64, got $UNAME_S/$UNAME_M -- refusing to build the darwin-arm64 binary here" >&2
21+
exit 1
22+
fi
23+
- uses: oven-sh/setup-bun@v2
24+
- uses: pnpm/action-setup@v4
25+
with: { version: 10 }
26+
- run: pnpm install
27+
- run: bun run scripts/build-runtime.ts --platform-package
28+
- run: ./packages/platform/darwin-arm64/bin/winter --version
29+
- run: bun run scripts/smoke-installed.ts --runtime=bun # the EXECUTE path (P9a-5): os/cpu match here
30+
431
build:
532
runs-on: ubuntu-latest
633
steps:

‎.github/workflows/release.yml‎

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,70 @@ on:
1111
tags: ["v*"]
1212
workflow_dispatch: {}
1313
jobs:
14+
# --- P9a-3/P9a-4: THE DARWIN-ARM64 BINARY, BUILT ONLY ON A MATCHING RUNNER --------------------
15+
#
16+
# `bun build --compile` targets the CURRENT host -- it does not cross-compile (M1/P9a-4) -- so the
17+
# darwin-arm64 binary can only ever be produced on a `darwin`/`arm64` runner, and this job's FIRST
18+
# step asserts that with `uname` rather than trusting the `macos-15` label (a label names an image,
19+
# not a guarantee of what `uname` reports on it). Both publish jobs `needs: build-platform` and
20+
# download this job's artifact rather than rebuilding -- GitHub Actions strips the executable bit
21+
# from artifact uploads, which is why this job tars the binary and the publish jobs untar it and
22+
# restore the bit explicitly, verifying identity (Mach-O arm64, matching sha256) before either
23+
# registry ever sees it. This job never runs a publish command -- `release-gates.test.ts` pins that.
24+
build-platform:
25+
runs-on: macos-15
26+
steps:
27+
- uses: actions/checkout@v4
28+
- name: "assert a REAL macOS arm64 runner (P9a-4) -- never trust the `macos-15` label alone"
29+
run: |
30+
set -euo pipefail
31+
UNAME_S="$(uname -s)"
32+
UNAME_M="$(uname -m)"
33+
if [ "$UNAME_S" != "Darwin" ] || [ "$UNAME_M" != "arm64" ]; then
34+
echo "build-platform: expected Darwin/arm64, got $UNAME_S/$UNAME_M -- refusing to build the darwin-arm64 binary here" >&2
35+
exit 1
36+
fi
37+
- uses: oven-sh/setup-bun@v2
38+
- uses: pnpm/action-setup@v4
39+
with: { version: 10 }
40+
- run: pnpm install
41+
- run: bun run scripts/build-runtime.ts --platform-package
42+
- name: "the binary's own --version matches the tag being published (workflow_dispatch has no tag: skipped)"
43+
run: |
44+
set -euo pipefail
45+
BIN=packages/platform/darwin-arm64/bin/winter
46+
ACTUAL="$("$BIN" --version)"
47+
echo "build-platform: binary reports --version $ACTUAL"
48+
case "${GITHUB_REF:-}" in
49+
refs/tags/v*)
50+
EXPECTED="${GITHUB_REF#refs/tags/v}"
51+
if [ "$ACTUAL" != "$EXPECTED" ]; then
52+
echo "build-platform: binary reports --version $ACTUAL but the pushed tag names $EXPECTED" >&2
53+
exit 1
54+
fi
55+
;;
56+
*) echo "build-platform: no v* tag on this ref (workflow_dispatch) -- version-vs-tag check skipped" ;;
57+
esac
58+
- run: shasum -a 256 packages/platform/darwin-arm64/bin/winter | tee winter-darwin-arm64.sha256
59+
- run: tar -cf winter-darwin-arm64.tar -C packages/platform/darwin-arm64 bin/winter
60+
- uses: actions/upload-artifact@v4
61+
with:
62+
name: winter-darwin-arm64
63+
path: |
64+
winter-darwin-arm64.tar
65+
winter-darwin-arm64.sha256
66+
retention-days: 7
67+
1468
# --- TWO REGISTRIES, TWO JOBS (P7a pre-publish item 5; user ruling 2026-09-08) ------------------
1569
#
1670
# GitHub Packages first, npm second, and SEPARATE jobs on purpose: the npm leg needs a secret this
1771
# repository may not have, and a missing `NPM_TOKEN` must never block the GitHub Packages publish.
1872
# `needs:` makes the dependency explicit -- npm runs only if GitHub Packages SUCCEEDED, so the two
1973
# registries can never disagree about which versions exist because the first leg failed halfway.
74+
# BOTH jobs now also `needs: build-platform` (P9a-4): the darwin-arm64 binary this workflow ships
75+
# must exist, on the real artifact, before either registry publish begins.
2076
publish:
77+
needs: build-platform
2178
runs-on: ubuntu-latest
2279
# R-7-1: GitHub Packages, scope @yanlinglabs. `contents: read` is the default job permission
2380
# this repeats explicitly; `packages: write` is the one elevated grant a publish needs and the
@@ -47,6 +104,20 @@ jobs:
47104
registry-url: "https://npm.pkg.github.com"
48105
scope: "@yanlinglabs"
49106
- run: pnpm install
107+
# P9a-4: restore the darwin-arm64 binary `build-platform` produced, and VERIFY it before this
108+
# job's own gates or the publish step ever run -- a downloaded GitHub Actions artifact loses its
109+
# executable bit, so this is not optional ceremony; without `chmod +x` the smoke below fails.
110+
- uses: actions/download-artifact@v4
111+
with: { name: winter-darwin-arm64 }
112+
- name: "restore + verify the platform binary before anything downstream can publish it"
113+
run: |
114+
set -euo pipefail
115+
tar -xf winter-darwin-arm64.tar -C packages/platform/darwin-arm64
116+
BIN=packages/platform/darwin-arm64/bin/winter
117+
chmod +x "$BIN"
118+
test -x "$BIN"
119+
file "$BIN" | grep -q "Mach-O 64-bit executable arm64" || { echo "restore: $BIN is not a Mach-O 64-bit arm64 executable" >&2; file "$BIN" >&2; exit 1; }
120+
shasum -a 256 -c winter-darwin-arm64.sha256
50121
51122
# --- the gates: the exact verification sequence ci.yml's `build` and `official-fixture-compile`
52123
# jobs already run on every push, repeated here so a tag pushed out of step with CI history (or
@@ -160,7 +231,10 @@ jobs:
160231
# job -- carries `id-token: write`. `contents: read` is repeated explicitly beside it; nothing here
161232
# needs `packages: write` (that is the other job's grant and stays there).
162233
publish-npm:
163-
needs: publish
234+
# P9a-4: keeps `needs: publish` (npm never runs until GitHub Packages succeeded) AND gains
235+
# `needs: build-platform` (the darwin-arm64 binary this job's own npm publish ships must exist on
236+
# the real artifact too -- this job checks out and downloads afresh, exactly like job 1).
237+
needs: [publish, build-platform]
164238
runs-on: ubuntu-latest
165239
permissions:
166240
id-token: write
@@ -179,6 +253,18 @@ jobs:
179253
registry-url: "https://registry.npmjs.org"
180254
scope: "@yanlinglabs"
181255
- run: pnpm install
256+
# P9a-4: same restore + verify as job 1 -- BEFORE this job's own gates or its publish step.
257+
- uses: actions/download-artifact@v4
258+
with: { name: winter-darwin-arm64 }
259+
- name: "restore + verify the platform binary before anything downstream can publish it"
260+
run: |
261+
set -euo pipefail
262+
tar -xf winter-darwin-arm64.tar -C packages/platform/darwin-arm64
263+
BIN=packages/platform/darwin-arm64/bin/winter
264+
chmod +x "$BIN"
265+
test -x "$BIN"
266+
file "$BIN" | grep -q "Mach-O 64-bit executable arm64" || { echo "restore: $BIN is not a Mach-O 64-bit arm64 executable" >&2; file "$BIN" >&2; exit 1; }
267+
shasum -a 256 -c winter-darwin-arm64.sha256
182268
# The same two gates the GitHub Packages job ran, repeated here because this job checks out
183269
# afresh: the version/tag agreement, and the compiled emit the tarballs must contain.
184270
- run: bun run scripts/check-release-version.ts

‎RELEASING.md‎

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,54 @@ pnpm strips `scripts` from the packed manifest, so the published package carries
2929

3030
| Registry | Job | Packages | Credential |
3131
| --- | --- | --- | --- |
32-
| GitHub Packages | `publish` | all five | `secrets.GITHUB_TOKEN`, `packages: write` |
33-
| public npm | `publish-npm` | the closure of the wrapper + the two harness roots — all five today | `secrets.NPM_TOKEN`, `id-token: write` for provenance |
32+
| GitHub Packages | `publish` | all six | `secrets.GITHUB_TOKEN`, `packages: write` |
33+
| public npm | `publish-npm` | the closure of the wrapper + the two harness roots + the wrapper's `optionalDependency` — all six today | `secrets.NPM_TOKEN`, `id-token: write` for provenance |
3434

3535
Neither registry is chosen on a command line. `--registry` sets only `registries.default`, and both
3636
pnpm and npm consult the **scope** binding first — a committed `@yanlinglabs:registry` line would
3737
therefore beat it, which is why the project `.npmrc` pins nothing. Each job binds its own scope *and*
3838
credential with `actions/setup-node` (`registry-url` + `scope`), and
3939
`scripts/publish-routing.test.ts` proves the routing with real `npm publish --dry-run` runs.
4040

41+
## The darwin-arm64 platform package (P9a-3/P9a-4)
42+
43+
`@yanlinglabs/winter-agent-sdk-darwin-arm64` ships the compiled `winter` runtime binary — the
44+
artifact the wrapper spawns — as an `optionalDependency` of `@yanlinglabs/winter-agent-sdk`. It is
45+
bin-only: no `main`, no `types`, no `exports` at all, just `bin: { winter: "bin/winter" }`, gated on
46+
`os: ["darwin"]` / `cpu: ["arm64"]` so a package manager that honours those fields (npm; `bun install`
47+
partially — see below) simply does not fetch it on any other platform.
48+
49+
**Built ONLY on a macOS `arm64` runner.** `bun build --compile` targets the CURRENT host — it does not
50+
cross-compile — so a `build-platform` job (`runs-on: macos-15`) exists in BOTH workflows: `ci.yml`
51+
runs it on every push (no tag pin, and it runs the smoke's real EXECUTE path —
52+
`smoke-installed.ts --runtime=bun` actually spawns the just-built binary, since `os`/`cpu` match on
53+
that runner) so a broken darwin build fails long before a tag exists; `release.yml` runs the same
54+
build, additionally checks the binary's own `--version` against the pushed tag, tars it (GitHub
55+
Actions artifact uploads drop the executable bit, which is exactly why this matters), and uploads it
56+
as one artifact. Both jobs assert `uname -s`/`uname -m` themselves rather than trusting the `macos-15`
57+
label.
58+
59+
**Both publish jobs `needs: build-platform`, download that ONE artifact, and restore + verify it —
60+
`chmod +x`, `test -x`, `file` reports `Mach-O 64-bit executable arm64`, `shasum -a 256 -c` against the
61+
recorded checksum — BEFORE their own version-tag gate and BEFORE the publish step.** Neither job
62+
rebuilds the binary itself: they ship exactly what `build-platform` produced.
63+
64+
**Locally**, `bun run build:runtime --platform-package` (or `bun run scripts/build-runtime.ts
65+
--platform-package`) stages `packages/platform/darwin-arm64/bin/winter` — git-ignored, built on
66+
demand, never committed. `scripts/release-pack.ts` HARD-FAILS a pack attempted on a matching host
67+
(`darwin`/`arm64`) with that file missing, naming the exact command to run first; on a non-matching
68+
host (e.g. this repo's own `ubuntu-latest` `pack-smoke` jobs) the same absence is expected and
69+
tolerated — a package manager on Linux was never going to fetch this binary either.
70+
71+
**The smoke (`scripts/smoke-installed.ts`) treats a bin-only package differently from an importable
72+
one**: it executes `<bin> --version` and compares it to the package's own `version` when `os`/`cpu`
73+
match the current host, and prints an explicit `SKIP … (bin-only; os/cpu mismatch on …)` line
74+
otherwise — never a silent no-op, and never an import attempt that would fail for the wrong reason.
75+
76+
A Norma consumer (`packages/core`) resolves this package via `createRequire(...).resolve` and never
77+
needs `dist/winter` built from an SDK checkout once it installs from a real release —
78+
see that repo's `runtime-sdk/executable.ts` and P9a-8/P9a-9.
79+
4180
The npm set is **data** (`winter.publish.npm` per manifest), asserted to equal the transitive
4281
workspace `dependencies` closure of the ROOTS: the wrapper, plus every package flagged
4382
`winter.publish.harness` (R-7b-5 — the two conformance harnesses, which the out-of-repo router
@@ -101,7 +140,7 @@ English, and a regex over English is a heuristic:
101140
Round 4 narrowed the negation skip to the matched clause and accepts unbackticked `src/`, which closes
102141
the two evasions the review demonstrated. The class remains: these gates catch the mistakes people
103142
actually make (a stale sentence surviving a rewrite) and cannot prove a README is true. **When you
104-
change what ships, re-read the five package READMEs** — the gate is a net, not a proof.
143+
change what ships, re-read the six package READMEs** — the gate is a net, not a proof.
105144

106145
## What ships
107146

0 commit comments

Comments
 (0)