Skip to content

ci(release): preview-artifact build-once workflow, LTS cut dry-run script and GO/NO-GO checklist (rail 3.8.58–.59) - #16210

Merged
diegosouzapw merged 6 commits into
release/v3.8.52from
ci/rail-lts-cut-rehearsal
Oct 10, 2026
Merged

diegosouzapw merged 6 commits into
release/v3.8.52from
ci/rail-lts-cut-rehearsal

Conversation

@diegosouzapw

@diegosouzapw diegosouzapw commented Oct 10, 2026 •

Copy link
Copy Markdown
Owner

Summary

Rail Task 16 — rehearsal of the 3.9.0 LTS cut (3.8.58) and the GO/NO-GO checklist (3.8.59). Three pieces:

  1. preview-artifact.yml: the build-once PR preview lane from feat(backend): PR preview artifacts with packaged-runtime validation and build-once promotion #8084 slice (a). It builds one tarball from the PR head, validates that build, records its identity, uploads it and attests it.
  2. scripts/release/dry-run-lts-cut.mjs (npm run release:dry-run-lts-cut): prints and checks the whole cut without running anything. With --execute, it runs the same steps against a fork remote only.
  3. Docs: the runbook in RELEASE_CHECKLIST.md ("3.9.0 LTS cut (rehearsed in 3.8.58)") and a new docs/ops/LTS_GO_NO_GO.md (rail gates plus the 10-item battery, with a "how to prove" command per row and an empty evidence column for the owner).

The terminology follows #16073 (RELEASE_STRATEGY.md: stable/v3, develop, latest/next/nightly, the preview-artifact label). The dormant workflows and dist-tag.mjs from #16172 (now on the base) are not duplicated: the dry-run reads those workflows from the cut tip and resolves channels with the real scripts/release/dist-tag.mjs. A dormant workflow missing at the cut source makes the result NOT READY.

What changed

.github/workflows/preview-artifact.yml (new)

  • Triggers: workflow_dispatch with pr_number, or pull_request: [labeled] with the label preview-artifact.
  • Fork PRs are refused on both paths. The label path checks github.event.pull_request.head.repo.full_name == github.repository in the job if:. The dispatch path re-checks head.repo.full_name through the API before any checkout. pr_number is validated as an integer.
  • resolve → build (checkout of the head SHA, npm ci with no cache, npm run build:release, npm run check:pack-artifact, npm pack, then npm run check:pack-boot with fake JWT_SECRET/API_KEY_SECRET and an ephemeral DATA_DIR).
  • Build-once proof: after the boot-smoke the job re-packs the tree and fails unless the sha256 matches the tarball being uploaded.
  • Writes artifact-identity.json and SHA256SUMS, then uploads them with actions/upload-artifact (14-day retention).
  • attest runs in its own job on a GitHub-hosted runner. It is the only job with id-token: write / attestations: write. It never checks out or runs repository code: it downloads the artifact, runs sha256sum -c, then runs actions/attest-build-provenance@4d10147… # v4.2.2.
  • The build job has only contents: read and reads no repository secret. Every action is pinned by SHA. Nothing is published. DB-clone is left out, as feat(backend): PR preview artifacts with packaged-runtime validation and build-once promotion #8084 asks.

scripts/release/artifact-identity.mjs (new, TDD): prints {headSha, baseSha, lockfileHash, platform, arch, nodeAbi, nodeVersion, bundler, buildPolicyVersion, identityKey, artifact?}.

  • identityKey is a sha256 over the semantic inputs only, so it works as a reuse key.
  • The tarball digest is recorded for promotion integrity but does not feed the key.
  • It fails closed on a non-40-hex SHA, an empty lockfile, or a missing platform/ABI.

scripts/release/dry-run-lts-cut.mjs (new, TDD) and the npm script release:dry-run-lts-cut.

  • Preconditions:
    • the source resolves
    • tag v3.8.59 exists on origin
    • package.json at the source is 3.9.0
    • a release-freeze issue is open
    • there is no open Release branch not green: release/v3.9.0 issue, and the branch exists (a missing branch reports ? unknown, never green)
    • G11: the Mergify release queue (.mergify.yml queue_rules, checks_timeout, the queue label) is set up
    • the release/* ruleset keeps deletion and non_fast_forward
    • stable/v3 and develop are absent
  • Plan:
    1. stable/v3 from the tip.
    2. develop = the tip plus a 4.0.0 bump commit (package.json ×3, lockfile, openapi — the same set as a cycle-open commit), built with git plumbing so no working tree is touched.
    3. The dormant workflows: which triggers and if: conditions turn true, which stay gated by vars.*, and which are pinned to the canonical repo (false in a fork rehearsal).
    4. Expected dist-tags (latest → 3.9.0, next/nightly empty), cross-checked against scripts/release/dist-tag.mjs#resolveDistTag.
    5. Rollback (delete both branches, npm dist-tag add omniroute@3.8.59 latest).
  • Exit codes: 0 READY · 1 a blocking check failed · 2 usage error. --advisory <ids> downgrades a check to a warning without hiding it.
  • --execute:
    • requires --remote <name>
    • refuses origin and any remote whose URL is the canonical repo
    • asks for confirmation on the terminal before each step
    • supports --rollback
    • never changes npm dist-tags
  • Every external command runs through execFile with argv arrays. No shell is involved.

Docs

Validation

Gate Command Result
New tests node --import tsx/esm --test tests/unit/dry-run-lts-cut.test.ts tests/unit/artifact-identity.test.ts tests/unit/preview-artifact-workflows.test.ts 42/42 pass
Sibling workflow tests … tests/unit/workflows-no-foreign-fork-publishers.test.ts tests/unit/check-workflows-provenance-runner.test.ts pass (49/49 together with the above)
Contract-test mutation check Three edits to the YAML, one per run: fork guard → true, id-token: none, build contents: write each one makes the contract test fail
actionlint actionlint .github/workflows/preview-artifact.yml 0 findings
zizmor zizmor --config .zizmor.yml --persona=pedantic .github/workflows/preview-artifact.yml 0 findings
Workflow ratchet npm run check:workflows -- --ratchet exit 0 — zizmor 210 (≤ baseline), provenance-on-self-hosted 0, scheduled-guard 0
ESLint npx eslint on the new tests (+ lint-staged on commit) clean (scripts/** is eslint-ignored)
Typecheck npm run typecheck:core exit 0
Docs npm run check:docs-all exit 0 (drift warnings are pre-existing, advisory)
Dead code npm run check:dead-code 370 ≤ baseline 377
File size npm run check:file-size OK
Real dry-run npm run release:dry-run-lts-cut on this tip prints the full plan; NOT READY (no v3.8.59 tag, version 3.8.52, no freeze), exit 1 as designed
Rehearsal parameters … -- --target-version 3.8.52 --previous-tag v3.8.51 --advisory freeze detects the real base-red #15306
Real dormant workflows analyzeDormantWorkflow on #16172's forward-port / validate-stable-pr / nightly-v4-build push/PR triggers on stable/v3 fire; "develop exists" checks turn true; nightly publish stays gated by vars.NIGHTLY_PUBLISH; forward-port is pinned to the canonical repo (does not fire in a fork); nightly is pinned only for scheduled runs
--execute end-to-end Synthetic repo with a local bare "fork" stable/v3 = tip; develop = one commit with only the 7 version lines changed to 4.0.0; --rollback removes both refs; a re-run with the branches present refuses with exit 1; a remote pointing at github.com/diegosouzapw/OmniRoute is refused before any push

Not run here (per the campaign rules): the full unit suite and the full build. The workflow itself has not run on GitHub yet. After merge it can be exercised with gh workflow run preview-artifact.yml -f pr_number=<N>, or by adding the preview-artifact label to a same-repo PR.

Fix round 1: G11 now means the Mergify release queue instead of a ruleset merge_queue rule, and base-green reports unknown when the branch is missing. On the current tip, merge-queue fails only on the missing checks_timeout, which arrives with #16066.

Fix round 2: merged the updated base (#16073, #16180, #16172, #16066). There was one package.json conflict; both scripts were kept. The built-in dist-tag fallback is gone; the script now uses dist-tag.mjs. The docs now link RELEASE_STRATEGY.md and name secrets.FORWARD_PORT_TOKEN / vars.NIGHTLY_PUBLISH. On the merged tip, the real dry-run shows ✓ merge-queue (G11 GO). It also shows ✓ release-ruleset and finds all three dormant workflows.

Refs #8084

⚠️ base-red inherited: #15306

…ity (#8084)

Manual (workflow_dispatch pr_number) or `preview-artifact` label lane that
builds the PR head once (npm ci + build:release), validates that build with
check:pack-artifact and check:pack-boot (fake secrets, ephemeral DATA_DIR),
proves the uploaded tarball is byte-identical to a re-pack of the validated
tree, records its identity (head/base SHA, lockfile hash, platform/arch/node
ABI, bundler, build policy) and uploads tarball + identity + SHA256SUMS.
A separate GitHub-hosted `attest` job, the only one holding id-token/
attestations write, verifies the checksums and attests the tarball. Fork PRs
are refused on both trigger paths; nothing publishes.

Refs #8084
`npm run release:dry-run-lts-cut` prints and validates the cut without
executing anything: preconditions (source resolves, previous tag v3.8.59,
package.json = 3.9.0, release-freeze open, release branch green per the
"Release branch not green" issue, stable/v3 + develop absent), stable/v3 from
the tip, develop with the 4.0.0 bump (package.json x3, lockfile, openapi),
which dormant-workflow triggers/`if:` turn true (and which stay gated by a
repository variable or are pinned to the canonical repo), expected dist-tags
(latest -> 3.9.0, next/nightly empty; uses scripts/release/dist-tag.mjs when
present, a minimal fallback otherwise) and the rollback.

`--execute --remote <fork>` runs the two branch steps (or `--rollback`) only
against a fork: origin and any remote whose URL is the canonical repository
are refused, every step is confirmed on the terminal, the develop bump commit
is built with git plumbing (no working tree touched) and every command is an
execFile argv array. Rehearsal parameters (`--target-version`,
`--previous-tag`, `--advisory`) let 3.8.58 run it on the current tip.
….58-.59)

RELEASE_CHECKLIST gains "3.9.0 LTS cut (rehearsed in 3.8.58)": the read-only
dry-run (`npm run release:dry-run-lts-cut`), the fork-only rehearsal and its
rollback, the PR preview-artifact lane (download, checksum, attestation
verify, install the same bytes) and the ordered real cut.

New docs/ops/LTS_GO_NO_GO.md: rail gates G0/G1/G2/G11/G12/G13/G14 and the
10-item closing battery, each with the command that proves it and an empty
evidence column for the owner, ending in the GO / NO-GO line. Linked from the
ROADMAP (Phase 3), the docs index and the ops sidebar.
… unknown for missing branches

G11 is the Mergify `release` queue (GitHub's native merge queue was rejected
in v3.8.49: no branch wildcards on a personal-account repo), not a ruleset
`merge_queue` rule. The dry-run now reads `.mergify.yml` at the cut source
(queue_rules[release] targeting release/*, entered via the `queue` label, with
a checks_timeout) and checks the `queue` label exists; the release/* ruleset
is checked separately for deletion + non_fast_forward only.

base-green no longer reads "green" when the release branch does not exist on
origin: no open base-red issue is not evidence for a branch the observer
cannot see, so it reports `unknown ... (not verifiable)` and fails closed.
Brings in #16073 (RELEASE_STRATEGY.md, ROADMAP See-also), #16180
(dev:candidate, RELEASE_CHECKLIST, meta.json), #16172 (dist-tag.mjs and the
dormant LTS workflows) and #16066 (Mergify checks_timeout). package.json keeps
both release:dry-run-lts-cut and labels:sync.

Now that the base carries them, the LTS-cut dry-run uses the real
scripts/release/dist-tag.mjs resolver (the built-in fallback is gone), treats a
dormant workflow missing at the cut source as NOT READY, and the runbook /
GO-NO-GO docs link RELEASE_STRATEGY.md and name the real forward-port secret
and nightly publish variable. The G11 fixture is the real .mergify.yml.
@diegosouzapw
diegosouzapw merged commit 1d5128f into release/v3.8.52 Oct 10, 2026
9 checks passed
Comment thread .github/workflows/preview-artifact.yml Dismissed
Comment thread .github/workflows/preview-artifact.yml Dismissed
Comment thread .github/workflows/preview-artifact.yml Dismissed
Comment thread .github/workflows/preview-artifact.yml Dismissed
Comment thread .github/workflows/preview-artifact.yml Dismissed
Comment thread .github/workflows/preview-artifact.yml Dismissed
@github-actions

Copy link
Copy Markdown
Contributor

CI Coverage Report

  • Coverage job: skipped
  • PR test policy: success

Coverage artifact was not available for this run.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants