-
Notifications
You must be signed in to change notification settings - Fork 0
291 lines (280 loc) · 14.2 KB
/
Copy pathrelease.yml
File metadata and controls
291 lines (280 loc) · 14.2 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
285
286
287
288
289
290
291
name: Release
# Release scheme (issue #16): one workflow, two modes. Everything is still
# 0.x/UNSTABLE, but releases are caret-honest (README §Consuming): within a
# minor line releases stay compatible; anything breaking bumps the minor.
#
# PRERELEASE (default; invoked automatically from ci.yml's `release` job on
# every GREEN main ref update via workflow_call, and manually via
# workflow_dispatch as the escape hatch): a GitHub release tagged
# `pre-<shorthash>` carrying the artifact set below. It publishes NOTHING
# to a registry (policy as of #223 — JSR and npm are published by CUT
# releases only), so there is no version stamping either: the manifests
# are left exactly as committed, and a prerelease names a commit rather
# than a version. Consumers tracking `main` between cuts take these
# release assets or a git reference (README §Consuming).
#
# RELEASE (workflow_dispatch with release=true): cuts the manifest version
# — JSR publish at exactly <next> (the manifests always carry the NEXT
# release: bumped to the next patch right after each cut, or to the next
# minor by the PR that lands a breaking change), tag `v<next>`, GitHub
# release without the prerelease mark, same artifact set. Guards: the four
# manifests agree (lockstep), the tag does not exist, and the commit
# already carries its `pre-<shorthash>` release — proof this exact commit
# went green through the full core+browser pipeline (the automatic path
# only fires on green main). Cut, then land the manifest-bump PR.
#
# Artifacts (both modes): the prebuilt translator wasm (standard release
# build — the exact artifact every test suite runs against — with the
# size tuning documented in crates/translator-shim/README.md) and the
# embedder bundle, plus SHA256SUMS, so consumers do not need a Rust
# toolchain. The tag-exists guard makes re-runs no-ops-by-failure rather
# than duplicate releases.
#
# VERSION GUARDS (tools/version-guard/check.ts; unit-tested by `just
# test-version-guard`). Two of its three modes run here — the third is
# ci.yml's per-PR early warning:
#
# `publish` (BOTH modes, before the JSR publish step) — the protocol
# identity guard. @polyengine/protocol is outside the lockstep and is
# SKIPPED as already-published whenever its manifest version already
# exists on JSR; if the in-tree protocol has moved since that version was
# published, a publish ships every dependent against the registry's older
# copy. That is the #219 tear: protocol/src changed without a manifest
# bump (its merge assumed 0.2.0 was unpublished; under the pre-#223 flow
# every green main published, and one had published 0.2.0 hours earlier),
# so runtime@0.4.0-pre.* went out importing exports protocol@0.2.0 did
# not have — an import-time failure for consumers, repaired by #221. The
# guard compares in-tree protocol to the published manifest byte for
# byte, both directions. It runs in BOTH modes for different reasons: on
# the cut path it is authoritative, standing between a tear and the only
# registry publish there is; on the prerelease path nothing publishes, so
# it is pure early detection — a red there means the NEXT CUT would tear,
# and main hears about it on the first green run instead of at cut time.
#
# `cut` (RELEASE=true only) — reads the breaking/* labels of every PR
# merged since the last cut tag, LIVE from the API (so a label corrected
# after the merge still counts, which is why the enforcement point is the
# cut), and requires a minor bump when any of them declares a break. It
# also emits the release notes' Breaking/Changes sections, spliced into
# the notes below. Prerelease notes are unchanged: a prerelease is one
# commit, not a window.
on:
workflow_dispatch:
inputs:
release:
description: >-
Cut the manifest version as a release (tag v<version>, exact JSR
publish) instead of a pre-<shorthash> prerelease.
type: boolean
default: false
workflow_call:
permissions:
contents: write
id-token: write # JSR OIDC publishing
actions: write # dispatching npm-publish.yml (see the last step)
jobs:
release:
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-unknown-unknown,wasm32-wasip2
- uses: taiki-e/install-action@v2
with:
tool: just@1.54.0
- uses: denoland/setup-deno@v2
with:
deno-version: "2.9.5"
- name: compute tag and version
id: tag
# The manifest version is the single source of truth (the NEXT
# release); the lockstep guard keeps a half-bumped workspace from
# publishing a torn version set. VERSION is the manifest version in
# both modes — no stamping since #223 — but only the cut path
# consumes it, since a prerelease publishes to no registry and names
# its commit instead. @polyengine/protocol is deliberately outside
# the lockstep: it versions independently (embedder-api A10),
# publishes at the next cut after its manifest bumps, and is skipped
# as already-published at every cut after that.
run: |
VERSION=$(jq -r '.version' runtime/deno.json)
for p in translator wasi ct-runner; do
v=$(jq -r '.version' "$p/deno.json")
if [ "$v" != "$VERSION" ]; then
echo "lockstep violation: runtime=$VERSION $p=$v" >&2
exit 1
fi
done
SHORT=$(git rev-parse --short HEAD)
if [ "$RELEASE" = "true" ]; then
TAG="v$VERSION"
if ! gh release view "pre-$SHORT" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
echo "no pre-$SHORT prerelease exists: this commit has not passed the green-main pipeline" >&2
exit 1
fi
else
TAG="pre-$SHORT"
fi
if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
echo "release $TAG already exists" >&2
exit 1
fi
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
env:
GH_TOKEN: ${{ github.token }}
RELEASE: ${{ inputs.release }}
- name: protocol identity guard
# Both modes, and BEFORE the JSR publish step — see the header. If
# protocol/deno.json's version is unpublished this passes trivially
# (the next cut creates it); if it is already published, the in-tree
# protocol must be byte-identical to what is on JSR under that
# version, because `deno publish` will skip it and every dependent
# published alongside will resolve to the registry's copy.
#
# On the CUT path that makes it the authoritative guard: it is the
# last thing between a tear and the only registry publish there is.
# On the PRERELEASE path nothing publishes at all since #223, so it
# is pure early detection — a red means the next cut would tear, and
# main hears about it on the first green run after the mistake
# lands rather than at cut time. Cheap either way (two HTTP GETs).
#
# Exact identity is deliberate: a "meaningful difference" heuristic
# is what talks a torn release into shipping (softer variants are
# parked in issue #222).
run: >-
deno run --allow-net=jsr.io --allow-read=.
tools/version-guard/check.ts publish
- name: release window guard and notes
# RELEASE=true only. Enumerates the PRs merged since the last cut
# tag with their CURRENT labels and fails if a breaking/* label is
# not matched by a minor bump; writes the Breaking/Changes sections
# to changes.md for the "create release" step below. A prerelease
# has no window to enumerate, and its notes stay as they were.
if: inputs.release
run: >-
deno run --allow-net=jsr.io --allow-run --allow-read=. --allow-write=changes.md
--allow-env=GITHUB_REPOSITORY,GITHUB_SHA,VERSION
tools/version-guard/check.ts cut --out changes.md
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ steps.tag.outputs.version }}
- name: build the release artifacts
# `just release-artifacts` (root justfile): the standard shim, the
# size-tuned shim (flags per crates/translator-shim/README.md), the
# embedder bundle (tools/release-bundle/entry.ts, gated by
# bundle_test.ts in the core matrix), and SHA256SUMS.
run: just release-artifacts
- name: publish to JSR
# CUT RELEASES ONLY (#223): the registries carry cut versions and
# nothing else, so there is no prerelease publish and no version
# stamping — the manifests publish exactly as committed. Consumers
# tracking `main` between cuts use the prerelease's artifacts or a
# git reference (README §Consuming).
# Auth: OIDC (permissions.id-token) — the @polyengine packages must
# exist on jsr.io and be linked to this repository (one-time
# manual setup; see the #16 checklist). The scope's "actor must be
# a scope member" restriction is OFF: merges armed by the machine
# account make the workflow actor `lannbot`, which broke the
# publish (actorNotScopeMember) until the setting was disabled.
# Runs BEFORE the GitHub release: it is the step most likely to
# fail, and ordering it first keeps partial failures re-runnable —
# a re-run passes the tag guard (no release yet) and `deno
# publish` skips already-published versions, so it converges
# instead of stranding a release that claims unpublished JSR
# versions.
# --allow-dirty: the working tree holds the untracked release
# artifacts built above; checkout-at-sha plus the tag guard carry
# the actual integrity.
if: inputs.release
run: |
echo "publishing @polyengine/* ${VERSION}"
deno publish --allow-dirty
env:
VERSION: ${{ steps.tag.outputs.version }}
- name: create release
# RELEASE=true splices the guard's changes.md (Breaking / Changes,
# empty sections omitted) between the intro and the artifacts
# paragraph. A prerelease has no window to describe (it is one
# commit; the next cut describes the whole span) and, since #223,
# nothing on a registry to point at — so its notes say what it
# actually is: artifacts for a green main commit.
run: |
{
if [ "$RELEASE" = "true" ]; then
echo "Release \`${VERSION}\` at ${GITHUB_SHA}."
echo
echo "Published to JSR as \`@polyengine/{runtime,translator,wasi,ct-runner}@${VERSION}\`,"
echo "and to npm under the same names (npm-publish.yml, triggered by this release)."
echo "Still 0.x/unstable, but caret-honest: releases within a minor line stay"
echo "compatible; breaking changes bump the minor. See README.md §Consuming."
if [ -s changes.md ]; then
echo
cat changes.md
fi
else
echo "Prerelease \`${TAG}\` at ${GITHUB_SHA}."
echo
echo "Artifacts only: nothing here is published to JSR or npm — the"
echo "registries carry cut releases (\`v*\`) exclusively. To track \`main\`"
echo "between cuts, use the assets below or a git reference; see"
echo "README.md §Consuming."
fi
echo
echo "Artifacts: the prebuilt translator shim wasm (size-tuned — the"
echo "exact artifact every test suite runs against) and the embedder"
echo "bundle polyengine-embedder.mjs (one platform-neutral ES module:"
echo "embedder API + Translator + ct-runner + the wasi package — browsers"
echo "and plain Node, no flags). Consumers need no Rust toolchain."
echo
echo '```'
cat SHA256SUMS
echo '```'
} > notes.md
PRERELEASE_FLAG="--prerelease"
if [ "$RELEASE" = "true" ]; then PRERELEASE_FLAG=""; fi
gh release create "$TAG" \
--repo "$GITHUB_REPOSITORY" \
--target "$GITHUB_SHA" \
$PRERELEASE_FLAG \
--title "polyengine $TAG" \
--notes-file notes.md \
polyengine-translator-shim.wasm \
polyengine-embedder.mjs SHA256SUMS
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ steps.tag.outputs.tag }}
VERSION: ${{ steps.tag.outputs.version }}
RELEASE: ${{ inputs.release }}
- name: publish to npm
# CUT RELEASES ONLY (#223), matching the JSR step: prereleases
# publish to no registry, so there is nothing to dispatch for them.
#
# An explicit dispatch, not npm-publish.yml's `release: published`
# trigger: GitHub deliberately does NOT start workflow runs from events
# raised with GITHUB_TOKEN, so the release created above fires nothing.
# `workflow_dispatch` is the documented exception to that rule, and it
# keeps npm's OIDC audience correct — the run's entry workflow is
# npm-publish.yml itself, which is what the trusted publisher is keyed
# on (npm validates the CALLING workflow's filename, and this workflow
# is itself reached two different ways; see npm-publish.yml's header).
#
# `--ref main` selects the workflow DEFINITION; the run checks out the
# release tag for the sources. Fire-and-forget by design: npm is a
# second registry, and a failure there must not retroactively fail a
# release whose JSR publish and GitHub release already succeeded. The
# dispatched run is re-runnable on its own.
if: inputs.release
run: |
gh workflow run npm-publish.yml \
--repo "$GITHUB_REPOSITORY" \
--ref main \
-f tag="$TAG"
echo "dispatched npm-publish.yml for $TAG"
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ steps.tag.outputs.tag }}