Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/justfile
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,12 @@ _step-tolerated recipe:
fi

# The required per-push/PR matrix job (ci.yml `core`).
# `version-guard-pr` runs FIRST and costs seconds: a versioning mistake is
# cheap to hear about before a 40-minute matrix, and the recipe no-ops
# outside pull_request runs (no PR_NUMBER), so pushes and `just ci` are
# unaffected. ci.yml supplies PR_NUMBER / PR_BASE_SHA / GH_TOKEN.
core:
@just gha::_step version-guard-pr
@just gha::_step build
@just gha::_step test-rust
@just gha::_step shim
Expand All @@ -57,6 +62,7 @@ core:
@just gha::_step test-sockets-node
@just gha::_step test-ct-runner
@just gha::_step test-bundle
@just gha::_step test-version-guard
@just gha::_step publish-check
@just gha::_step examples
@just gha::_step test-translate
Expand Down
44 changes: 34 additions & 10 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,19 @@ jobs:
# lives on the root-justfile recipes it composes. Suite order and
# arch-conditional lanes (jsc-pinned is x64-only) are all encoded
# there.
#
# The env below is the only thing the version guard's `pr` mode needs
# (its first step): it reads the PR's LIVE labels and diffs against the
# base. All three are empty on push runs, where the guard skips itself —
# deliberately, since there is no PR to have labels. Note that label
# edits do NOT re-trigger this workflow (no `types:` addition above, on
# purpose): the PR pass is an early warning, release.yml's `cut` mode is
# the enforcement point, and a re-run picks up corrected labels.
- run: just gha::core
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
GH_TOKEN: ${{ github.token }}

browser:
runs-on: ubuntu-24.04
Expand Down Expand Up @@ -90,19 +102,31 @@ jobs:
# required lanes + the best-effort webkit lane, per the recipes.
- run: just gha::browser

# Every GREEN main ref update ships a prerelease (issue #16's interim
# scheme): tag pre-<shorthash>, shim artifacts + SHA256SUMS. Gated on this
# same run's core matrix + browser job, so "green" is the release
# criterion by construction. Notes: (a) the concurrency group above
# cancels superseded runs, so rapid consecutive pushes release only the
# surviving tip — a cancelled run was never green; (b) continue-on-error
# steps (the webkit lane) do not block a release, by the same policy
# that keeps them non-blocking in CI.
# Every GREEN main ref update ships a GitHub PRERELEASE: tag
# pre-<shorthash>, shim artifacts + SHA256SUMS, and nothing on a registry
# — JSR and npm carry cut releases only (#223). Gated on this same run's
# core matrix + browser job, so "green" is the release criterion by
# construction, and the prerelease doubles as the proof a cut later
# requires ("a pre-<shorthash> release exists for this commit"). Notes:
# (a) the concurrency group above cancels superseded runs, so rapid
# consecutive pushes release only the surviving tip — a cancelled run was
# never green; (b) continue-on-error steps (the webkit lane) do not block
# a release, by the same policy that keeps them non-blocking in CI.
release:
needs: [core, browser]
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
# The prerelease path only ever uses `contents: write` (create a GitHub
# release, upload assets) — no registry is published from a prerelease,
# so id-token (JSR OIDC) and actions (the npm dispatch) are dead weight
# here. They are granted anyway BECAUSE THEY MUST BE: a reusable-workflow
# call fails at STARTUP unless the caller grants everything the called
# workflow's `permissions` block requests, if-skipped steps included
# (observed on #223: "requesting 'actions: write, id-token: write', but
# is only allowed 'none'"), and release.yml requests all three for its
# cut mode. The unused tokens are inert: the steps that would spend them
# are `if: inputs.release`, which a workflow_call run cannot set.
permissions:
contents: write
id-token: write # JSR OIDC publishing (the called workflow requests it)
actions: write # ditto: it dispatches npm-publish.yml
id-token: write # unused here; required by release.yml's request (cut mode)
actions: write # ditto
uses: ./.github/workflows/release.yml
84 changes: 39 additions & 45 deletions .github/workflows/npm-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,18 @@ name: Publish to npm
# The npm half of the release scheme. JSR is published inline by release.yml;
# npm is published HERE, for the release that release.yml has just created.
#
# How this is reached: release.yml DISPATCHES it explicitly (its last step).
# The `release: published` trigger below is a backstop for a release created by
# hand — it cannot serve the automated path, because GitHub does not start
# workflow runs from events raised with GITHUB_TOKEN, and `workflow_dispatch`
# is the documented exception to that rule.
# CUT RELEASES ONLY (#223): the registries carry `v<version>` cuts and
# nothing else. A `pre-<shorthash>` tag is REFUSED by the version step
# below rather than ignored, which also gives the `release: published`
# backstop teeth — a hand-made GitHub prerelease can start this workflow,
# but it cannot publish anything.
#
# How this is reached: release.yml DISPATCHES it explicitly (its last step,
# on cut releases only). The `release: published` trigger below is a
# backstop for a release created by hand — it cannot serve the automated
# path, because GitHub does not start workflow runs from events raised with
# GITHUB_TOKEN, and `workflow_dispatch` is the documented exception to that
# rule.
#
# Why not a step inside release.yml, next to the JSR publish: npm's trusted
# publishing (OIDC) validates the CALLING workflow's filename, and release.yml
Expand All @@ -32,7 +39,7 @@ on:
workflow_dispatch:
inputs:
tag:
description: The release tag to publish to npm (e.g. v0.1.0 or pre-abc1234).
description: The release tag to publish to npm (a cut tag, e.g. v0.1.0).
required: true
type: string

Expand Down Expand Up @@ -69,27 +76,28 @@ jobs:
- name: compute the version
id: ver
# Mirrors release.yml's scheme exactly, derived from the tag so the two
# cannot drift: a cut release tags `v<version>`; a prerelease tags
# `pre-<shorthash>` and takes <next> from the committed manifest, which
# release.yml stamps only in its own working tree. This VERSION is the
# build stamp for the lockstep four (runtime, translator, wasi,
# ct-runner) only — build.ts passes it as `--version` and it does NOT
# affect @polyengine/protocol, which always emits at its own manifest
# version (A10). The dist-tag is no longer computed here: each
# package's actual built version decides its own tag in the publish
# step below (a hyphen means prerelease), since protocol's version can
# differ from the stamp.
# cannot drift: a cut release tags `v<version>` and publishes at
# exactly that version. `pre-<shorthash>` tags are refused, not
# skipped: prereleases are GitHub releases carrying artifacts and are
# published to NO registry (#223), so reaching this workflow with one
# means something dispatched or triggered it in error, and a clear
# failure beats a surprise publish. This VERSION is the build stamp
# for the lockstep four (runtime, translator, wasi, ct-runner) only —
# build.ts passes it as `--version` and it does NOT affect
# @polyengine/protocol, which always emits at its own manifest
# version (A10).
run: |
TAG='${{ steps.rel.outputs.tag }}'
case "$TAG" in
v*)
VERSION="${TAG#v}"
;;
pre-*)
VERSION="$(jq -r '.version' runtime/deno.json)-pre.g${TAG#pre-}"
echo "tag '$TAG' is a prerelease: prereleases are not published to npm or JSR (policy as of #223) — only cut releases (v<version>) are" >&2
exit 1
;;
*)
echo "tag '$TAG' is neither v<version> nor pre-<shorthash>" >&2
echo "tag '$TAG' is not a cut tag (v<version>)" >&2
exit 1
;;
esac
Expand Down Expand Up @@ -118,28 +126,18 @@ jobs:
- name: publish
# Dependency order — protocol first: it is not part of the lockstep
# (A10), so it publishes once per manifest bump, at its own version,
# and is skipped as already-published on every other run (JSR parity
# with release.yml's "compute tag and version" step). Publishing it
# before its dependents guarantees the version they reference (via a
# caret dependency — see build.ts) already exists on the registry the
# first time it is needed. Already-published versions are skipped
# rather than failed for every package — same convergence property as
# the JSR step, so a re-run after a partial failure completes the set
# instead of stranding it.
# and is skipped as already-published at every cut after that (npm
# parity with release.yml's JSR step). Publishing it before its
# dependents guarantees the version they reference (via a caret
# dependency — see build.ts) already exists on the registry the first
# time it is needed. Already-published versions are skipped rather
# than failed for every package — a re-run after a partial failure
# completes the set instead of stranding it.
#
# Dist-tag is derived per package from its OWN built version, not the
# lockstep stamp: a hyphen (prerelease, e.g. 0.3.1-pre.gabc1234) means
# `--tag pre`; a bare version means `--tag latest`. Note the one case
# this cannot control: npm points `latest` at a package's FIRST
# published version regardless of --tag, and refuses to remove the tag
# (403) — the bootstrap publish that created these packages was a
# prerelease, so `latest` named it until the first cut release
# overwrote it. protocol's next publish (its manifest is 0.1.0) will
# move npm `latest` for @polyengine/protocol BACKWARD from whatever
# lockstep-stamped prerelease last got tagged `latest` there (e.g. an
# orphaned 0.3.0) — this is intended, not a bug: the orphan stays
# published and reachable, because runtime@0.3.0 (etc.) pins it
# exactly rather than through `latest`.
# Every version reaching this loop is a bare cut version (the step
# above refuses anything else), so `latest` is the only dist-tag in
# play; the `pre` tag belongs to the retired prerelease-publishing
# flow and stays frozen wherever it last pointed.
#
# Provenance attestations are generated automatically for trusted
# publishing from a public repository; no --provenance flag needed.
Expand All @@ -152,10 +150,6 @@ jobs:
echo "$name@$V already published — skipping"
continue
fi
case "$V" in
*-*) TAG=pre ;;
*) TAG=latest ;;
esac
echo "publishing $name@$V (dist-tag $TAG)"
npm publish --tag "$TAG" --workspaces=false ./npm/"$p"
echo "publishing $name@$V (dist-tag latest)"
npm publish --tag latest --workspaces=false ./npm/"$p"
done
Loading
Loading