fix(release): separate code and release SHA validation (#105948)

This commit is contained in:
Vincent Koc
2026-07-13 13:38:19 +08:00
committed by GitHub
parent a778f891fa
commit 6a0eecdb44
24 changed files with 689 additions and 286 deletions
@@ -6,9 +6,10 @@ description: Regenerate OpenClaw release changelog sections from git history bef
# OpenClaw Changelog Update
Use this for release changelog rewrites and GitHub release-note source text.
This is mandatory before every beta, beta rerun, stable release, or stable
rerun. Use it with `release-openclaw-maintainer`; this skill owns changelog
content, ordering, grouping, and attribution discipline.
Run it once after the final Code SHA has green Full Release Validation. Do not
rerun it for same-candidate tooling retries, resumed publication, or promotion.
Use it with `release-openclaw-maintainer`; this skill owns changelog content,
ordering, grouping, and attribution discipline.
## Goal
@@ -25,7 +26,8 @@ every human `Thanks @...` attribution.
the target; a newer but divergent tag is not a valid history boundary. Use
an explicit shipped/main-closeout SHA only when it is also reachable from the
target.
- Target ref: exact branch/SHA being released.
- Target ref: the exact green Code SHA. The changelog commit created from this
input becomes the Release SHA.
- Canonical main ref: current `origin/main`, fetched before verification. Release
notes cite the original merged main PR when the same work is carried by a
backport. A release-branch PR is used only while no forward-port exists on
@@ -33,10 +35,12 @@ every human `Thanks @...` attribution.
## Workflow
1. Start on `main` before branching when possible:
1. Confirm the release branch is at the fully validated Code SHA:
- `git fetch --tags origin`
- `git pull --ff-only`
- confirm clean `git status -sb`
- record `git rev-parse HEAD` as the Code SHA
- record the successful Full Release Validation run id and attempt
- stop if any product/version/backport change is still pending
2. Audit history, including direct commits:
- `git log --first-parent --date=iso-strict --pretty=format:'%h%x09%ad%x09%s' <base-tag>..<target-ref>`
- `git log --first-parent --grep='(#' --date=short --pretty=format:'%h%x09%ad%x09%s' <base-tag>..<target-ref>`
@@ -241,7 +245,13 @@ every human `Thanks @...` attribution.
- `git diff --check`
- for docs/changelog-only changes, no broad tests are required
- commit with `scripts/committer "docs(changelog): refresh YYYY.M.PATCH notes" CHANGELOG.md`
- push, pull/rebase if needed, then branch/rebase release from latest `main`
- record the new commit as the Release SHA and require
`git diff --name-only <code-sha>..<release-sha>` to print only
`CHANGELOG.md`
- push the release branch without rebasing it onto moving `main`
- dispatch SHA-pinned Full Release Validation for the Release SHA with evidence
reuse enabled. It must select `changelog-only-release-v1`; any other changed
path returns the release to the Code SHA validation loop
## Quota / API Outage Rule
+24 -21
View File
@@ -288,7 +288,8 @@ rerun after a focused patch.
### Full Release Validation
`Full Release Validation` (`.github/workflows/full-release-validation.yml`) is
the manual "everything before release" umbrella. It resolves a target ref, then
the manual product-validation umbrella. Run the full child matrix on the
product-complete pre-changelog **Code SHA**. It resolves a target ref, then
dispatches:
- manual `CI` for the full normal CI graph, with Android enabled via
@@ -300,30 +301,23 @@ dispatches:
Telegram release lanes
- optional post-publish Telegram E2E when a package spec is supplied
Run it only when validating an actual release candidate, after broad shared CI
or release orchestration changes, or when explicitly asked:
Run the full matrix only when validating an actual Code SHA, after broad shared
CI or release orchestration changes, or when explicitly asked:
```bash
gh workflow run full-release-validation.yml \
--repo openclaw/openclaw \
--ref main \
-f ref=<branch-or-sha> \
-f provider=openai \
-f mode=both \
-f release_profile=stable
node scripts/full-release-validation-at-sha.mjs \
--sha <code-sha> \
--target-ref release/YYYY.M.PATCH
```
Run the workflow itself from the trusted current ref, normally `--ref main`;
child workflows are dispatched from that same ref even when `ref` points at an
older release branch or tag. Full Release Validation has no separate child
workflow ref input; choose the trusted harness by choosing the workflow run ref.
Use `release_profile=minimum|stable|full` to control live/provider breadth:
`minimum` keeps the fastest OpenAI/core release-critical set, `stable` adds the
stable provider/backend set, and `full` adds the broad advisory provider/media
matrix. Do not make `full` faster by silently dropping suites; optimize setup,
artifact reuse, and sharding instead. The parent verifier job appends a child
overview plus slowest-job tables for child runs; rerun only that verifier after
a child rerun turns green.
The helper pins the trusted workflow revision on current `main` while targeting
the historical release SHA and recording the canonical release branch as
context. It infers `beta` for alpha/beta package versions and `stable` for
stable/correction versions. Pass `-f release_profile=full` only for the broad
advisory provider/media sweep. Do not make `full` faster by silently dropping
suites; optimize setup, artifact reuse, and sharding instead. The parent
verifier job appends a child overview plus slowest-job tables for child runs;
rerun only that verifier after a child rerun turns green.
Standalone manual `CI` dispatches do not run the plugin prerelease suite, the
extension batch sweep, or the release-only `agentic-plugins` Vitest shard. Those
@@ -341,6 +335,15 @@ parent gate. If a child workflow failed but was later rerun successfully, rerun
only the failed parent verifier job; do not dispatch a new full umbrella unless
the release evidence is stale.
Once the Code SHA is green, generate and commit only `CHANGELOG.md`. The new
**Release SHA** is eligible for product-evidence reuse only when GitHub proves
that it is a descendant of the Code SHA and the complete changed path set is
exactly `CHANGELOG.md`. Dispatch the same SHA-pinned helper for the Release SHA;
the resulting parent records `changelog-only-release-v1` and reuses the Code
SHA children. Package, install/update, and release-note proof still runs on the
Release SHA because its tarball bytes changed. Any non-changelog path
invalidates reuse and requires a new Code SHA full matrix.
For bounded recovery after a focused fix, pass `-f rerun_group=<group>`.
Supported umbrella groups are `all`, `ci`, `plugin-prerelease`,
`release-checks`, `install-smoke`, `cross-os`, `live-e2e`, `package`, `qa`,
+38 -18
View File
@@ -24,6 +24,10 @@ Use this with `$release-openclaw-maintainer` and `$openclaw-testing` when a rele
fails, the parent cancels the remaining child matrix and prints the failed
job summary. Inspect that first red job instead of waiting for unrelated
matrix tails.
- Treat the product-complete pre-changelog commit as the Code SHA. Full product
validation and performance evidence bind to that SHA. The later Release SHA
may reuse those results only when it is a descendant whose complete changed
path set is exactly `CHANGELOG.md`.
- In a sparse worktree or Testbox source sync, first confirm `package.json`,
`pnpm-lock.yaml`, and every source path the selected check reads. If any are
absent, that checkout cannot validate a release dependency or Docker lane:
@@ -68,14 +72,14 @@ non-billable credentials fail before the expensive release matrix.
## Dispatch
Start product performance evidence as early as the release SHA exists, in
Start product performance evidence as early as the Code SHA exists, in
parallel with other release work:
```bash
gh workflow run openclaw-performance.yml \
--repo openclaw/openclaw \
--ref main \
-f target_ref=<release-sha> \
-f target_ref=<code-sha> \
-f profile=release \
-f repeat=3 \
-f deep_profile=false \
@@ -93,7 +97,7 @@ gh workflow run openclaw-performance.yml \
early standalone run is for overlap and faster regression discovery, but a
regression or missing child run blocks the parent validation.
Prefer the trusted workflow on `main`, target the exact release SHA:
Prefer an immutable trusted-main workflow revision, target the exact Code SHA:
- Keep trusted-workflow checks compatible with frozen release targets. If
`main` adds a target-owned guard script or package command after the release
@@ -103,23 +107,30 @@ Prefer the trusted workflow on `main`, target the exact release SHA:
newer `main`-only check.
```bash
gh workflow run full-release-validation.yml \
--repo openclaw/openclaw \
--ref main \
-f ref=<release-sha> \
-f provider=openai \
-f mode=both \
-f release_profile=full \
-f rerun_group=all
node scripts/full-release-validation-at-sha.mjs \
--sha <code-sha> \
--target-ref release/YYYY.M.PATCH
```
For immutable workflow proof on a moving `main`, use
`pnpm ci:full-release --sha <release-sha>`. Its canonical `release-ci/*` ref
keeps exact-target evidence reuse enabled after proving the workflow commit is
still on trusted `main` lineage. Pass `-f reuse_evidence=false` only when the
operator intentionally needs a fresh full run.
`pnpm ci:full-release --sha <code-sha> --target-ref
release/YYYY.M.PATCH`. Its canonical `release-ci/*` ref keeps evidence reuse
enabled after proving the workflow commit is still on trusted `main` lineage.
Pass `-f reuse_evidence=false` only when the operator intentionally needs a
fresh full run.
Use `release_profile=stable` unless the operator explicitly asks for the broad advisory provider/media matrix. Stable and full profiles force the release soak; the beta profile may opt in with `run_release_soak=true`. Use narrow `rerun_group` after focused fixes.
After the Code SHA is green, commit only `CHANGELOG.md` and run the same helper
against the Release SHA. The parent must report
`policy=changelog-only-release-v1`, `evidenceSha=<code-sha>`, and
`changedPaths=["CHANGELOG.md"]`; it should reuse the product matrix instead of
dispatching child lanes. Npm preflight and package/install acceptance still run
against the exact Release SHA and its new tarball bytes.
The SHA-pinned helper infers `beta` for alpha/beta package versions and `stable`
for stable/correction versions. Pass `release_profile=full` only when the
operator explicitly asks for the broad advisory provider/media matrix. Stable
and full profiles force the release soak; the beta profile may opt in with
`run_release_soak=true`. Use narrow `rerun_group` after focused fixes.
Publish with `openclaw-release-publish.yml` using `release_profile=from-validation`
unless a maintainer intentionally wants to cross-check a specific profile; the
publish workflow reads the effective profile from the full-validation manifest.
@@ -154,7 +165,15 @@ Stop watchers before ending the turn or switching strategy.
them in a clean-home CLI probe, never as a substitute for a required
Anthropic API-key lane.
5. For live-cache failures, inspect whether it is missing/invalid key, empty text, provider refusal, timeout, or baseline miss. Do not weaken release gates without clear provider evidence.
6. Fix narrowly, run local/changed proof, commit, push, rerun the smallest matching group.
6. Classify before editing:
- product/code failure: fix the release branch, freeze a new Code SHA, run
focused proof, then obtain green full validation for that new SHA
- workflow/harness/infrastructure/credential failure: fix that owner
separately and rerun the same Code SHA
- changelog/release-note failure: change only `CHANGELOG.md`, keep Code SHA
evidence, and repeat Release SHA proof
- publish child/registry selector failure: keep Release SHA and resume the
failed child; never rebuild an immutable version that already published
7. If a required PR CI run is capacity-stalled with queued jobs and no active
jobs, do not cancel unrelated work or accept a generic manual dispatch.
From the PR head branch, dispatch the explicit exact-SHA fallback:
@@ -173,7 +192,8 @@ target_ref=<full-pr-sha> -f include_android=true -f release_gate=true`.
Record:
- release SHA
- Code SHA and Release SHA
- evidence-reuse policy and complete changed-path set
- full parent run URL
- child run IDs and conclusions: CI, Release Checks, Plugin Prerelease, NPM Telegram, Product Performance
- performance comparison result versus earlier releases when available
@@ -39,10 +39,22 @@ GHSA-specific advisory work outside this skill.
`main` while release validation runs.
- Before release branching, commit any dirty files in coherent groups, push,
pull/rebase, and create the release branch from the intended `main` head.
Finish version preparation, backports, release-only fixes, and their required
forward-ports before generating `CHANGELOG.md`. The changelog rewrite is the
final candidate source mutation; freeze the candidate SHA immediately after
it and do not restart completed evidence for metadata-only reruns.
Finish version preparation plus any operator-selected backports,
release-only fixes, and required forward-ports. Backports are optional.
Freeze this product-complete tree as the **Code SHA** without changing the
release changelog.
- Full product validation belongs to the Code SHA. If validation finds a code
defect, fix it, freeze a new Code SHA, and validate that SHA. If the failure
belongs to trusted workflow tooling, the harness, credentials, or
infrastructure, repair that surface separately and rerun against the same
Code SHA. Never mutate the release candidate to satisfy newer tooling.
- Generate `CHANGELOG.md` only after the Code SHA is green. The resulting
**Release SHA** must be a descendant whose complete diff from the Code SHA is
exactly `CHANGELOG.md`. Release-note checks, npm preflight/package bytes,
install/update acceptance, tagging, and publication run against the Release
SHA. Full product validation is reused through the
`changelog-only-release-v1` evidence policy; any non-changelog source change
returns to the Code SHA loop.
- During release planning, inspect both `src/plugins/compat/registry.ts` and
`src/commands/doctor/shared/deprecation-compat.ts` before branching and again
before final publish. For every deprecated or removal-pending compatibility
@@ -66,23 +78,20 @@ GHSA-specific advisory work outside this skill.
the next beta number until the matching npm package has actually published.
If a published beta needs a fix, commit the fix on the release branch and
increment to the next `-beta.N`.
- For a beta release train, keep Full Release Validation as a pre-publish gate
unless the operator explicitly waives it. Run the fast local preflight, npm
preflight, full release validation, and performance in parallel where safe.
If anything fails before npm publish, fix it on the release branch,
forward-port the fix to `main`, move the unpublished beta tag/prerelease to
the fixed commit, and rerun the affected pre-publish gates. If anything fails
after npm publish, fix it, forward-port to `main`, increment beta number, and
repeat. After each beta publish, run the published-package roster focused on
install/update/Docker/Parallels/NPM Telegram. For later beta attempts, rerun
only lanes whose evidence changed unless the fix touches broad release,
install/update, plugin, Docker, Parallels, or live QA behavior. After each
beta is live, scan current `main` once for critical fixes that landed after
the release branch cut and backport only important low-risk fixes. Operators
may authorize up to 4 autonomous beta attempts; after 4 failed beta attempts,
stop and report.
- As soon as the release candidate SHA exists, dispatch `OpenClaw Performance`
with `target_ref=<release-sha>` in parallel with the other release work. Do
- For a beta release train, keep Full Release Validation as a pre-publish Code
SHA gate unless the operator explicitly waives it. Run independent validation
lanes in parallel where safe, but do not start changelog or package
finalization until the Code SHA is green. After the changelog-only Release SHA
exists, run npm preflight and the package/install/update acceptance roster
against its exact bytes. If a product defect appears, return to a new Code
SHA; if a release-tooling or publication child fails, repair/resume that child
without changing the candidate. After a published beta needs a code fix,
forward-port it, increment the beta number, and repeat. A post-beta scan of
current `main` may suggest optional low-risk backports; it does not require
them. Operators may authorize up to 4 autonomous beta attempts; after 4
failed beta attempts, stop and report.
- As soon as the Code SHA exists, dispatch `OpenClaw Performance`
with `target_ref=<code-sha>` in parallel with the other release work. Do
not wait for full release validation to start the performance signal.
- Before publish/closeout, compare available product performance metrics with
earlier releases: Kova agent-turn/resource metrics, gateway startup
@@ -90,8 +99,8 @@ GHSA-specific advisory work outside this skill.
or clawgrit reports. Report regressions explicitly. A major regression is a
release blocker unless the operator waives it or the data clearly proves
infrastructure noise.
- Heal CI before tagging or publishing. The exact candidate SHA must have green
`Full Release Validation`, including the root Dockerfile/install-smoke path.
- Heal CI before changelog, tagging, or publishing. The exact Code SHA must have
green `Full Release Validation`, including the root Dockerfile/install-smoke path.
Treat a red Docker, package, or release workflow lane as a release-branch
defect until the smallest correct fix is landed and proven; do not waive it
because npm preflight or another sibling lane passed.
@@ -128,8 +137,10 @@ target_ref=<full-pr-sha> -f include_android=true -f release_gate=true`.
use this coverage when the artifact workflow has started, failed, been
cancelled, or been skipped. Then rerun `OPENCLAW_TESTBOX=1 scripts/pr
prepare-run <PR>`.
- Generate the changelog before every beta, beta rerun, stable release, or
stable rerun, after version preparation and all source/backport work. Use
- Generate the changelog once after the final Code SHA is fully green. Do not
regenerate it for same-candidate tooling reruns, resumed publication, or
promotion. If code changes, validate the replacement Code SHA first and then
regenerate the release section once for that new history. Use
`$openclaw-changelog-update` for the rewrite. Do not continue release prep if
the target `CHANGELOG.md` section does not have `### Highlights`,
`### Changes`, and `### Fixes`, grouped by user-facing surface while
@@ -330,19 +341,19 @@ HEAD/worktree-bound manifest under git metadata for cutover review.
- `CHANGELOG.md` is release-owned. Normal PRs and direct `main` fixes should
not edit it.
- After release preparation and all intended backports/fixes are complete,
rewrite the target `CHANGELOG.md` section from history, not existing notes.
Use the last reachable stable or beta release tag as the base, then inspect
every commit through the target release SHA. This rewrite is the final source
task before freezing the candidate SHA and starting or reusing evidence.
- After the product-complete Code SHA passes Full Release Validation, rewrite
the target `CHANGELOG.md` section from history, not existing notes. Use the
last reachable stable or beta release tag as the base, then inspect every
commit through the Code SHA. This is the one release-note mutation that
creates the Release SHA.
- Generate `$openclaw-changelog-update`'s full contribution manifest before
the editorial rewrite. It is the required source for `### Highlights`,
`### Changes`, and `### Fixes`; do not preserve old grouped prose without
comparing it to the manifest's PRs, contributors, direct commits, and
unlinked commits.
- The changelog rewrite is not optional for beta reruns: any `beta.N` after a
rebase or backport must refresh the same stable-base `## YYYY.M.PATCH` section
after the new version/backport work and before the candidate SHA freezes.
- A same-Code-SHA retry reuses the existing changelog. A new beta containing
code changes must first validate its replacement Code SHA, then refresh the
same stable-base `## YYYY.M.PATCH` section once.
- Always fetch and pass current `origin/main` as the canonical main ref.
Equivalent release/backport PRs are omitted in favor of the original merged
main PR. A release-branch PR remains only until that change is forward-ported.
@@ -547,16 +558,17 @@ pnpm test:install:smoke
package-local runtime, and pass the npm and ClawHub release metadata checks
before a tag or publish workflow can start. Do not defer README, entrypoint,
or packed-artifact failures to postpublish verification.
- Before tagging, require green CI for the exact release-candidate SHA, not an
earlier branch SHA. Heal every related red CI, release-check, packaging, or
root-Dockerfile lane on the release branch, forward-port the fix to `main`,
and rerun the affected exact-SHA gates. Never waive a red Docker lane because
npm preflight passed.
- Root Dockerfile proof is mandatory before every beta and stable tag. Run the
release `install-smoke` group or equivalent root Dockerfile build for the
exact candidate SHA and require it to pass. The tag-triggered Docker Release
workflow is post-tag publishing, not the first valid proof that the root
Dockerfile can build.
- Before generating the changelog, require green CI for the exact Code SHA, not
an earlier branch SHA. Heal every related red CI, release-check, packaging,
or root-Dockerfile lane on the release branch, forward-port product fixes to
`main`, and rerun the affected exact-SHA gates. Never waive a red Docker lane
because npm preflight passed.
- Root Dockerfile proof is mandatory on the Code SHA before every beta and
stable tag. The changelog-only Release SHA reuses that product proof, while
exact Release SHA npm preflight and package/install acceptance prove the
changed package bytes. The tag-triggered Docker Release workflow is
post-tag publishing, not the first valid proof that the root Dockerfile can
build.
- Before tagging, diff publishable plugin package manifests against the last
reachable stable/beta release tag. For every newly publishable package
(`openclaw.release.publishToNpm: true` or `publishToClawHub: true`) whose
@@ -907,65 +919,70 @@ node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>
worktree is clean.
4. Pull latest `main` and confirm current `main` CI is green.
5. Create `release/YYYY.M.PATCH` from the intended `main` commit.
6. Make every repo version location match the beta tag and finish all intended
backports or release-only fixes. Forward-port fixes to `main` where required.
7. Run `/changelog` for the stable base target version on the release branch as
the final candidate source task, using current `origin/main` for canonical PR
provenance. Keep the heading as `## YYYY.M.PATCH`, not
`## YYYY.M.PATCH-beta.N`.
8. Commit the changelog rewrite, push the release branch, and freeze that exact
candidate SHA. Any later source change must finish first, then rerun only
this final changelog step before freezing the replacement candidate.
9. Immediately dispatch Actions > `OpenClaw Performance` from `main` with
`target_ref=<release-sha>`, `profile=release`, `repeat=3`, deep profiling
6. Make every repo version location match the beta tag. Apply only explicitly
selected backports or release fixes, and forward-port fixes to `main` where
required. Freeze the result as the Code SHA.
7. Immediately dispatch Actions > `OpenClaw Performance` from the pinned
trusted workflow source with `target_ref=<code-sha>`, `profile=release`,
`repeat=3`, deep profiling
off, live OpenAI off, and regression failure off. Let it run in parallel
with preflight and validation work.
10. Run the fast local beta preflight from the release branch before any npm
preflight or publish. Require exact-SHA CI and root Dockerfile install-smoke
to be green before tagging. Keep the remaining expensive Docker, Parallels,
and published-package install/update lanes for after the beta is live unless
the operator asks to run them before beta publication.
11. For beta releases, skip mac app build/sign/notarize unless beta scope or a
with Code SHA validation.
8. Run the deterministic source preflight, then Full Release Validation against
the exact Code SHA with
`node scripts/full-release-validation-at-sha.mjs --sha <code-sha> --target-ref release/YYYY.M.PATCH`.
Use one transition watcher. Product failures return to step 6 with a new
Code SHA; tooling/harness failures are fixed separately and rerun against the
same Code SHA.
9. After the Code SHA is green, run `/changelog` once for the stable-base target
version using current `origin/main` for canonical PR provenance. Keep the
heading as `## YYYY.M.PATCH`, not `## YYYY.M.PATCH-beta.N`.
10. Commit only `CHANGELOG.md` and freeze the Release SHA. Verify
`<code-sha>..<release-sha>` changes exactly `CHANGELOG.md`; any other path
returns to step 6.
11. Dispatch Full Release Validation for the Release SHA with evidence reuse
enabled. It must select `changelog-only-release-v1`, reuse the green Code SHA
product matrix, and run no product lanes again.
12. Run npm preflight and release-note/package/install/update acceptance against
the exact Release SHA and prepared tarball. A package or install failure that
exposes a product defect returns to step 6; a tooling failure keeps the
Release SHA unchanged.
13. For beta releases, skip mac app build/sign/notarize unless beta scope or a
release blocker specifically requires it. For stable releases, include the
mac app, signing, notarization, and appcast path.
12. Confirm the target npm version is not already published.
13. Create and push the git tag from the release branch.
14. Do not create or publish the matching GitHub release page yet. The real
14. Confirm the target npm version is not already published.
15. Create and push the git tag from the Release SHA.
16. Do not create or publish the matching GitHub release page yet. The real
publish workflow creates or undrafts it only after postpublish verification
and release evidence upload pass.
15. Dispatch Actions > `QA-Lab - All Lanes` against the release tag and wait
for the mock parity, live Matrix, and live Telegram credentialed-channel
lanes to pass.
16. Start `.github/workflows/openclaw-npm-release.yml` from the release branch
with `preflight_only=true`
and choose the intended `npm_dist_tag` (`beta` default; `latest` only for
an intentional direct stable publish). Wait for it to pass. Save that run id
because the real publish requires it to reuse the prepared npm tarball.
17. Before real publish, review the early performance run if it has completed.
17. Run `pnpm release:candidate -- --tag <tag> --full-release-run
<release-sha-validation-run-id> --npm-preflight-run <preflight-run-id>
--skip-dispatch` to consume the existing reused full evidence and exact
Release SHA preflight instead of dispatching either again. It completes
package/install proof and prints the publish command.
18. Start publication only after the candidate bundle is green. Reuse successful
immutable child runs/artifacts on retry; do not rebuild or republish versions
that already succeeded.
19. Before real publish, review the early performance run if it has completed.
Compare against earlier release evidence or clawgrit reports where
available. Call out minor regressions in the release proof; block on major
regressions unless waived or proven noisy.
18. For stable releases, start `.github/workflows/macos-release.yml` in
20. For stable releases, start `.github/workflows/macos-release.yml` in
`openclaw/openclaw` and wait for the public validation-only run to pass.
19. For stable releases, start
21. For stable releases, start
`openclaw/releases/.github/workflows/openclaw-macos-validate.yml` with the
same tag and wait for the release-ops mac validation lane to pass.
20. For stable releases, start
22. For stable releases, start
`openclaw/releases/.github/workflows/openclaw-macos-publish.yml` with
`preflight_only=true` and wait for it to pass. Save that run id because the
real publish requires it to reuse the notarized mac artifacts.
21. If any preflight or validation run fails, fix the issue on a new commit,
delete the tag and any accidental draft/incomplete GitHub release, recreate
the tag from the fixed commit, and rerun all relevant preflights from
scratch before continuing. Never reuse old preflight results after the
commit changes. Once the npm version exists, do not rerun the publish
workflow for that same version; finalize the existing draft/evidence state
manually or cut a correction tag. For pushed or published beta tags, do not
delete/recreate; increment to the next beta tag. For preflight-only failures
where npm did not publish the beta version, delete/recreate the same beta
tag and any accidental draft/incomplete prerelease at the fixed commit
instead of skipping a prerelease number.
22. Start `.github/workflows/openclaw-release-publish.yml` from trusted `main`
23. Classify every failure before changing git state. Product defects return to
step 6 and invalidate downstream Code/Release SHA evidence. Changelog or
release-note defects change only the Release SHA and reuse the green Code
SHA evidence after the exact delta is reverified. Tooling, credential,
approval, registry selector, or publication-child failures keep the
candidate unchanged and resume the smallest failed surface.
24. Start `.github/workflows/openclaw-release-publish.yml` from the exact pinned
trusted workflow source
with the same tag for the real beta or stable publish, choose `npm_dist_tag` (`beta` default,
`latest` only when you intentionally want direct stable publish), keep it
the same as the preflight run, and pass the successful npm
@@ -974,8 +991,8 @@ node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>
For stable publish, also pass the exact non-prerelease
`openclaw/openclaw-windows-node` tag as `windows_node_tag` and its
candidate-approved installer digest map as `windows_node_installer_digests`.
23. Wait for `npm-release` approval from `@openclaw/openclaw-release-managers`.
24. Wait for the real publish workflow to run postpublish verification,
25. Wait for `npm-release` approval from `@openclaw/openclaw-release-managers`.
26. Wait for the real publish workflow to run postpublish verification,
create or update the GitHub release as a draft, upload dependency evidence,
promote and verify the required Windows Hub assets for stable releases,
append release verification proof, and only then undraft/publish it. If a
@@ -989,10 +1006,11 @@ node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>
with the original child run IDs and an evidence output path before manually
recreating the workflow's draft, dependency evidence asset, proof section,
and publish step.
25. Run the post-published beta verification roster. First scan current `main`
27. Run the post-published beta verification roster. First scan current `main`
for critical fixes that landed after the release branch cut; backport only
important low-risk fixes before starting expensive lanes, or increment to
the next beta if the fix must change the already-published package. If any
operator-selected low-risk fixes before starting expensive lanes, or
increment to the next beta if the fix must change the already-published
package. If any
lane fails after the beta package is published, fix, commit/push/pull,
increment to the next beta tag, and rerun the affected beta evidence. Once
the beta is live, start remote/manual rosters where they
@@ -1003,10 +1021,10 @@ node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>
If a pre-npm lane fails before any tag/package leaves the machine, fix and
rerun the same intended beta attempt. Repeat up to the operator's
authorized beta-attempt limit, normally 4.
26. Announce the beta/stable release on Discord best-effort using the configured secret workflow.
27. If the operator requested beta only, stop after beta verification and the
28. Announce the beta/stable release on Discord best-effort using the configured secret workflow.
29. If the operator requested beta only, stop after beta verification and the
announcement.
28. If the stable release was published to `beta`, use the light stable
30. If the stable release was published to `beta`, use the light stable
promotion roster when the matching beta already carried the full confidence
pass: published npm postpublish verify, Docker install/update smoke,
macOS-only Parallels install/update smoke, and required QA signal.
@@ -1014,25 +1032,25 @@ node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>
`openclaw/releases/.github/workflows/openclaw-npm-dist-tags.yml` workflow
to promote that stable version from `beta` to `latest`, then verify
`latest` now points at that version.
29. If the stable release was published directly to `latest` and `beta` should
31. If the stable release was published directly to `latest` and `beta` should
follow it, start that same release-ops dist-tag workflow to point `beta` at
the stable version, then verify both `latest` and `beta` point at that
version.
30. For stable releases, start
32. For stable releases, start
`openclaw/releases/.github/workflows/openclaw-macos-publish.yml` for the
real publish with the successful release-ops mac `preflight_run_id` and wait
for success.
31. Verify the successful real release-ops mac run uploaded the `.zip`, `.dmg`,
33. Verify the successful real release-ops mac run uploaded the `.zip`, `.dmg`,
and `.dSYM.zip` artifacts to the existing GitHub release in
`openclaw/openclaw`.
32. For stable releases, download `macos-appcast-<tag>` from the successful
34. For stable releases, download `macos-appcast-<tag>` from the successful
release-ops mac run, update `appcast.xml` on `main`, verify the feed, then
complete the **Close stable releases on main** gate.
33. For beta releases, publish the mac assets only when intentionally requested;
35. For beta releases, publish the mac assets only when intentionally requested;
expect no shared production
`appcast.xml` artifact and do not update the shared production feed unless a
separate beta feed exists.
34. After stable main closeout, verify npm and the attached release artifacts.
36. After stable main closeout, verify npm and the attached release artifacts.
## GHSA advisory work
+25 -11
View File
@@ -65,7 +65,7 @@ on:
- npm-telegram
- performance
reuse_evidence:
description: Reuse the newest prior green full validation only for the exact same target SHA and inputs
description: Reuse matching green product validation for the same target or a changelog-only Release SHA
required: false
default: true
type: boolean
@@ -256,6 +256,7 @@ jobs:
evidence_root_run_id: ${{ steps.find.outputs.evidence_root_run_id }}
evidence_run_url: ${{ steps.find.outputs.evidence_run_url }}
evidence_sha: ${{ steps.find.outputs.evidence_sha }}
evidence_policy: ${{ steps.find.outputs.evidence_policy }}
evidence_manifest: ${{ steps.find.outputs.evidence_manifest }}
changed_paths: ${{ steps.find.outputs.changed_paths }}
steps:
@@ -342,7 +343,7 @@ jobs:
if [[ "$REUSE" == "true" ]]; then
echo "- Reusing evidence: ${EVIDENCE_RUN_URL}"
echo "- Evidence SHA: \`${EVIDENCE_SHA}\`"
echo "- Exact-target reuse changed paths: \`${changed_paths_summary}\`"
echo "- Reused validation changed paths: \`${changed_paths_summary}\`"
else
echo "- No reusable evidence: ${REUSE_REASON:-unknown}"
fi
@@ -379,7 +380,7 @@ jobs:
needs: [resolve_target, evidence_reuse]
if: ${{ always() && needs.resolve_target.result == 'success' && contains(fromJSON('["all","ci"]'), inputs.rerun_group) && needs.evidence_reuse.outputs.reuse != 'true' }}
runs-on: ubuntu-24.04
timeout-minutes: ${{ inputs.release_profile != 'minimum' && 240 || 60 }}
timeout-minutes: ${{ inputs.release_profile != 'beta' && 240 || 60 }}
outputs:
run_id: ${{ steps.dispatch.outputs.run_id }}
url: ${{ steps.dispatch.outputs.url }}
@@ -752,7 +753,7 @@ jobs:
needs: [resolve_target, evidence_reuse]
if: ${{ always() && needs.resolve_target.result == 'success' && contains(fromJSON('["all","release-checks","install-smoke","cross-os","live-e2e","package","qa","qa-parity","qa-live"]'), inputs.rerun_group) && needs.evidence_reuse.outputs.reuse != 'true' }}
runs-on: ubuntu-24.04
timeout-minutes: ${{ inputs.release_profile != 'minimum' && 240 || 60 }}
timeout-minutes: ${{ inputs.release_profile != 'beta' && 240 || 60 }}
outputs:
run_id: ${{ steps.dispatch.outputs.run_id }}
url: ${{ steps.dispatch.outputs.url }}
@@ -1430,6 +1431,7 @@ jobs:
EVIDENCE_ROOT_RUN_ID: ${{ needs.evidence_reuse.outputs.evidence_root_run_id }}
EVIDENCE_RUN_URL: ${{ needs.evidence_reuse.outputs.evidence_run_url }}
EVIDENCE_SHA: ${{ needs.evidence_reuse.outputs.evidence_sha }}
EVIDENCE_POLICY: ${{ needs.evidence_reuse.outputs.evidence_policy }}
EVIDENCE_MANIFEST: ${{ needs.evidence_reuse.outputs.evidence_manifest }}
RERUN_GROUP: ${{ inputs.rerun_group }}
TARGET_SHA: ${{ needs.resolve_target.outputs.sha }}
@@ -1709,12 +1711,25 @@ jobs:
fi
done < <(jq -r '[.childRuns.normalCi // "", .childRuns.pluginPrerelease // "", .childRuns.releaseChecks // "", .childRuns.npmTelegram // "", (.childRuns.productPerformance.runId // "")] | map(select(. != "")) | .[]' <<< "$EVIDENCE_MANIFEST")
if [[ "$failed" == "0" ]]; then
emit_reused_child_dispatch() {
local workflow="$1"
local run_id="$2"
if [[ -n "${run_id// }" ]]; then
echo "Dispatched ${workflow}: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
fi
}
emit_reused_child_dispatch "ci.yml" "$(jq -r '.childRuns.normalCi // ""' <<< "$EVIDENCE_MANIFEST")"
emit_reused_child_dispatch "plugin-prerelease.yml" "$(jq -r '.childRuns.pluginPrerelease // ""' <<< "$EVIDENCE_MANIFEST")"
emit_reused_child_dispatch "openclaw-release-checks.yml" "$(jq -r '.childRuns.releaseChecks // ""' <<< "$EVIDENCE_MANIFEST")"
emit_reused_child_dispatch "npm-telegram-beta-e2e.yml" "$(jq -r '.childRuns.npmTelegram // ""' <<< "$EVIDENCE_MANIFEST")"
emit_reused_child_dispatch "openclaw-performance.yml" "$(jq -r '.childRuns.productPerformance.runId // ""' <<< "$EVIDENCE_MANIFEST")"
{
echo "### Reused validation evidence"
echo
echo "- Evidence run: ${EVIDENCE_RUN_URL}"
echo "- Evidence SHA: \`${EVIDENCE_SHA}\`"
echo "- Target SHA: \`${TARGET_SHA}\` (exact-target evidence reuse)"
echo "- Target SHA: \`${TARGET_SHA}\`"
echo "- Reuse policy: \`${EVIDENCE_POLICY}\`"
} >> "$GITHUB_STEP_SUMMARY"
fi
elif [[ "$RERUN_GROUP" == "all" && "$DOCKER_RUNTIME_ASSETS_PREFLIGHT_RESULT" != "success" ]]; then
@@ -1804,19 +1819,16 @@ jobs:
RELEASE_CHECKS_RESULT: ${{ needs.release_checks.result }}
EVIDENCE_REUSE: ${{ needs.evidence_reuse.outputs.reuse }}
EVIDENCE_ROOT_RUN_ID: ${{ needs.evidence_reuse.outputs.evidence_root_run_id }}
EVIDENCE_POLICY: ${{ needs.evidence_reuse.outputs.evidence_policy }}
run: |
set -euo pipefail
if [[ "$RELEASE_CHECKS_RESULT" == "skipped" && "$EVIDENCE_REUSE" != "true" ]]; then
echo "Release checks were skipped by rerun group; skipping automatic release evidence update."
exit 0
fi
# In reuse mode the child runs live on the chain-root validation run
# (the evidence consumer scrapes dispatch logs from the given run),
# so durable evidence must reference that run id, not this wrapper.
notes="Automatically requested by Full Release Validation ${GITHUB_RUN_ID_VALUE} after child workflows completed; the parent summary re-checks current child run conclusions."
if [[ "$EVIDENCE_REUSE" == "true" && -n "${EVIDENCE_ROOT_RUN_ID// }" ]]; then
notes="Automatically requested by Full Release Validation ${GITHUB_RUN_ID_VALUE}, which reused green evidence from chain-root run ${EVIDENCE_ROOT_RUN_ID} for the exact same target SHA and inputs."
GITHUB_RUN_ID_VALUE="$EVIDENCE_ROOT_RUN_ID"
notes="Automatically requested by Full Release Validation ${GITHUB_RUN_ID_VALUE}, which reused green product evidence from chain-root run ${EVIDENCE_ROOT_RUN_ID} under policy ${EVIDENCE_POLICY}."
fi
if [[ -z "${RELEASES_DISPATCH_TOKEN// }" ]]; then
echo "OPENCLAW_RELEASES_DISPATCH_TOKEN is not configured; skipping automatic release evidence update."
@@ -1898,6 +1910,7 @@ jobs:
EVIDENCE_RUN_ID: ${{ needs.evidence_reuse.outputs.evidence_run_id }}
EVIDENCE_ROOT_RUN_ID: ${{ needs.evidence_reuse.outputs.evidence_root_run_id }}
EVIDENCE_SHA: ${{ needs.evidence_reuse.outputs.evidence_sha }}
EVIDENCE_POLICY: ${{ needs.evidence_reuse.outputs.evidence_policy }}
EVIDENCE_CHANGED_PATHS: ${{ needs.evidence_reuse.outputs.changed_paths }}
EVIDENCE_MANIFEST: ${{ needs.evidence_reuse.outputs.evidence_manifest }}
PROVIDER: ${{ inputs.provider }}
@@ -1927,6 +1940,7 @@ jobs:
--arg evidenceRunId "$EVIDENCE_RUN_ID" \
--arg evidenceRootRunId "$EVIDENCE_ROOT_RUN_ID" \
--arg evidenceSha "$EVIDENCE_SHA" \
--arg evidencePolicy "$EVIDENCE_POLICY" \
--argjson evidenceChangedPaths "$EVIDENCE_CHANGED_PATHS" \
'. + {
version: 3,
@@ -1939,7 +1953,7 @@ jobs:
targetRef: $targetRef,
targetSha: $targetSha,
evidenceReuse: {
policy: "exact-target-full-validation-v1",
policy: $evidencePolicy,
runId: $evidenceRootRunId,
selectedRunId: $evidenceRunId,
evidenceSha: $evidenceSha,
+10 -2
View File
@@ -152,7 +152,7 @@ Skills own workflows; root owns hard policy and routing.
- Full suites, changed gates, builds, typechecks, lint fan-out, Docker/package/E2E/live/cross-OS proof, or anything computationally intensive: Crabbox/Testbox.
- If an allowed local fallback fans out or becomes expensive, stop it and move the work to the pre-warmed remote box.
- Before handoff/push: prove touched surface. Before landing to `main`: issue proof plus appropriate full/broad proof unless scope is clearly narrow.
- Release-branch full validation: use `node scripts/full-release-validation-at-sha.mjs --sha <exact-sha> --target-ref release/<branch>`; no raw dispatch without `target_context_ref`.
- Release-branch full validation: freeze the product-complete **Code SHA**, then use `node scripts/full-release-validation-at-sha.mjs --sha <code-sha> --target-ref release/YYYY.M.PATCH`; no raw dispatch without `target_context_ref`.
- Pre-land/pre-commit code changes: mandatory fresh `$autoreview` until no accepted/actionable findings remain. Do not land code on CI, ClawSweeper, prior review comments, or your own manual review alone unless user explicitly opts out or scope is truly trivial/docs-only. If findings want refactor, refactor; no ugly fixes.
- If proof is blocked, say exactly what is missing and why.
- Do not land related failing format/lint/type/build/tests. If unrelated on latest `origin/main`, say so with scoped proof.
@@ -301,7 +301,15 @@ Skills own workflows; root owns hard policy and routing.
- Releases/publish/version bumps need explicit approval. Use `$release-openclaw-maintainer`.
- Release versions use `YYYY.M.PATCH`, where `PATCH` is a sequential monthly release-train number, never the calendar day. Stable and beta tags determine the current train; alpha-only tags do not consume or advance the beta/stable patch number. After `2026.6.5`, the next beta train is `2026.6.6-beta.1` even if higher alpha-only tags exist.
- Alpha/nightly versions use the next unreleased train plus an incrementing prerelease number. Repeated nightlies for the same train increment only `alpha.N`; they must not mint a new patch number from the date.
- Backport means apply to newest open `release/` branch unless user names another target.
- Backports are optional. Apply only the operator-selected set; when requested without a target, use the newest open `release/` branch.
- Regular beta/stable flow has two immutable identities:
- **Code SHA**: version prep plus any optional backports/release fixes, with no release changelog mutation. Full product validation belongs here.
- **Release SHA**: a descendant of the green Code SHA whose complete diff is exactly `CHANGELOG.md`. Tag, npm preflight, package/install acceptance, and publish belong here.
- Never generate the release changelog before the Code SHA has green Full Release Validation. A product/code failure changes the Code SHA and restarts product validation. A workflow/harness/infrastructure failure is fixed in trusted tooling and rerun against the same Code SHA; do not mutate the candidate to satisfy newer tooling.
- After green Code SHA validation, generate and review `CHANGELOG.md` once. Dispatch Full Release Validation for the Release SHA with evidence reuse enabled; `changelog-only-release-v1` may reuse the Code SHA product evidence only when GitHub independently proves the entire descendant delta is `CHANGELOG.md`. Any other path change requires a new Code SHA and fresh full validation.
- Release-SHA proof is intentionally narrow: release-note/provenance checks, npm preflight/package bytes, install/update acceptance, and publish readiness. Do not rerun the full product matrix merely because the changelog changed.
- Pass the successful Release-SHA validation run and npm preflight run into `release:candidate`; do not let the candidate helper dispatch duplicate copies of evidence that already passed.
- Keep one release operator and one watcher per release identity. Resume partial publish from successful immutable child artifacts/runs; never rebuild or republish an already-published package version.
- GHSA/advisories: `$openclaw-ghsa-maintainer` / `$security-triage`. Secret scanning: `$openclaw-secret-scanning-maintainer`.
- Beta tag/version match: `vYYYY.M.PATCH-beta.N` -> npm `YYYY.M.PATCH-beta.N --tag beta`.
+61 -53
View File
@@ -161,21 +161,32 @@ steps for this npm-only extended-stable path.
This checklist is the public shape of the release flow. Private credentials, signing, notarization, dist-tag recovery, and emergency rollback details stay in the maintainer-only release runbook.
1. Start from current `main`: pull latest, confirm the target commit is pushed, and confirm `main` CI is green enough to branch from.
2. Generate the top `CHANGELOG.md` section from merged PRs and all direct commits since the last reachable release tag. Keep entries user-facing, dedupe overlapping PR/direct-commit entries, commit, push, and rebase/pull once more before branching. When a divergent shipped tag or later forward-port re-associates already-released PRs, pass that tag explicitly as `--shipped-ref`; the verifier uses explicit PR rows from complete contribution records in numbered sections of the tag snapshot, ignores `Unreleased`, and records the exact excluded PR inventory and count.
3. Review release compatibility records in `src/plugins/compat/registry.ts` and `src/commands/doctor/shared/deprecation-compat.ts`. Remove expired compatibility only when the upgrade path stays covered, or record why it is intentionally carried.
4. Create `release/YYYY.M.PATCH` from current `main`. Do not do normal release work directly on `main`.
5. Bump every required version location for the tag, then run `pnpm release:prep`. It refreshes plugin versions, npm shrinkwraps, plugin inventory, base config schema, bundled channel config metadata, config docs baseline, plugin SDK exports, and plugin SDK API baseline in order. Commit any generated drift before tagging, then run the local deterministic preflight: `pnpm check:test-types`, `pnpm check:architecture`, `pnpm build && pnpm ui:build`, and `pnpm release:check`.
6. Run `OpenClaw NPM Release` with `preflight_only=true`. Before a tag exists, a full 40-character release-branch SHA is allowed for validation-only preflight. The preflight generates dependency release evidence for the exact checked-out dependency graph and stores it in the npm preflight artifact. Save the successful `preflight_run_id`.
7. Kick off all pre-release tests with `Full Release Validation` for the release branch, tag, or full commit SHA. This is the one manual entrypoint for the four big release test boxes: Vitest, Docker, QA Lab, and Package. Save the `full_release_validation_run_id` and exact `full_release_validation_run_attempt`; both are required inputs for `OpenClaw NPM Release` and `OpenClaw Release Publish`.
8. If validation fails, fix on the release branch and rerun the smallest failed file, lane, workflow job, package profile, provider, or model allowlist that proves the fix. Rerun the full umbrella only when the changed surface makes prior evidence stale.
9. For a tagged beta candidate, run `pnpm release:candidate -- --tag vYYYY.M.PATCH-beta.N` from the matching `release/YYYY.M.PATCH` branch. For stable, also pass the required Windows source release: `pnpm release:candidate -- --tag vYYYY.M.PATCH --windows-node-tag vX.Y.Z`. The helper uses trusted `main` as the workflow source while each workflow targets the exact tag. It checkpoints immutable candidate/tooling identity and dispatched run IDs in `.artifacts/release-candidate/<tag>/release-candidate-state.json`; rerunning the same command resumes those exact runs, while any candidate, tooling, profile, or option drift fails closed. Before it dispatches the full validation matrix, the helper deterministically renders the exact tag's GitHub release body and rejects a missing version heading, an over-limit body that cannot use the canonical compact form, or contribution-record base/target provenance that is not reachable from the tag. It also validates any explicit shipped-baseline exclusion metadata against the referenced cumulative tag records. It then runs the local generated-release checks, dispatches or verifies full release validation and npm preflight evidence, runs Parallels fresh/update proof against the exact prepared tarball plus Telegram package proof, records plugin npm and ClawHub plans, and prints the exact `OpenClaw Release Publish` command only after the evidence bundle is green.
2. Create `release/YYYY.M.PATCH` from that commit. Backports are optional; apply only the operator-selected set. Bump every required version location, run `pnpm release:prep`, finish release fixes and required forward-ports, and review `src/plugins/compat/registry.ts` plus `src/commands/doctor/shared/deprecation-compat.ts`.
3. Freeze the product-complete pre-changelog commit as the **Code SHA**. Run the deterministic source preflight, then use `node scripts/full-release-validation-at-sha.mjs --sha <code-sha> --target-ref release/YYYY.M.PATCH`. This pins trusted workflow tooling while the full Vitest, Docker, QA, package, and performance matrix targets the exact Code SHA.
4. Classify failures before editing. A product/code failure creates a new Code SHA and requires green full validation for that SHA. A workflow, harness, credential, approval, or infrastructure failure is repaired in its owning surface and rerun against the same Code SHA.
5. Only after the Code SHA is green, generate the top `CHANGELOG.md` section from merged PRs and direct commits since the last reachable shipped tag. Keep entries user-facing and deduplicated. When a divergent shipped tag or later forward-port re-associates already-released PRs, pass it explicitly as `--shipped-ref`.
6. Commit only `CHANGELOG.md`. This commit is the **Release SHA**. The complete diff from Code SHA to Release SHA must be exactly `CHANGELOG.md`; any other changed path returns the release to step 2.
7. Run SHA-pinned Full Release Validation for the Release SHA with evidence reuse enabled. The lightweight parent must record `changelog-only-release-v1`, point at the green Code SHA, and dispatch no product child lanes. This reuses product evidence; it does not reuse package bytes.
8. Run `OpenClaw NPM Release` with `preflight_only=true` against the Release SHA/tag. Save the successful `preflight_run_id`. This builds and checks the exact package bytes that include the final changelog.
9. Tag the Release SHA, then run the candidate helper with the successful Release-SHA validation parent and npm preflight instead of dispatching either again:
```bash
pnpm release:candidate -- \
--tag vYYYY.M.PATCH-beta.N \
--full-release-run <release-sha-validation-run-id> \
--npm-preflight-run <preflight-run-id> \
--skip-dispatch
```
For stable, also pass `--windows-node-tag vX.Y.Z`. The helper verifies release-note provenance, npm preflight bytes, Parallels install/update proof, Telegram package proof, and plugin publish plans, then prints the publish command.
`OpenClaw Release Publish` dispatches the selected or all-publishable plugin packages to npm and the same set to ClawHub in parallel, then promotes the prepared OpenClaw npm preflight artifact with the matching dist-tag once plugin npm publish succeeds. The release checkout remains the product/data root, while planning and final verification execute from the exact trusted workflow-source checkout so an older release commit cannot silently use obsolete release tooling. Before any publish child starts, it renders and caches the exact GitHub release body. When the complete matching `CHANGELOG.md` section fits GitHub's 125,000-character limit and the renderer's matching 125,000-byte safety ceiling, the page contains that exact `## YYYY.M.PATCH` section including its heading. When the source section does not fit, the page keeps the exact grouped editorial notes and replaces the oversized contribution record with a stable link to the full record in the tag-pinned `CHANGELOG.md`; partial records and truncated bullets are never published. The workflow chooses that full or compact body before adding `### Release verification`; if the proof tail would exceed the limit, it keeps the canonical body and relies on the immutable attached evidence instead. Stable releases published to npm `latest` become the GitHub latest release, while stable maintenance releases kept on npm `beta` are created with GitHub `latest=false`. The workflow also uploads the preflight dependency evidence, the full-validation manifest, and postpublish registry verification evidence to the GitHub release for post-release incident response. It prints child run IDs immediately, auto-approves release environment gates the workflow token is allowed to approve, summarizes failed child jobs with log tails, creates the draft GitHub release page up front and promotes Windows and Android assets concurrently with the OpenClaw npm publish, closes out the release page and dependency evidence once those stages succeed, waits for ClawHub whenever OpenClaw npm is being published, then runs the trusted-main beta verifier and uploads postpublish evidence for the GitHub release, npm package, selected plugin npm packages, selected ClawHub packages, child workflow run IDs, and optional NPM Telegram run ID. The ClawHub bootstrap verifier requires the exact trusted-main workflow path and SHA, producer and terminal run attempts, release SHA, requested package set, immutable package artifact tuple, and terminal registry readback artifact; a successful legacy release-ref run is not accepted.
Then run the post-publish package acceptance against the published `openclaw@YYYY.M.PATCH-beta.N` or `openclaw@beta` package. If a pushed or published prerelease needs a fix, cut the next matching prerelease number; never delete or rewrite the old one.
10. For stable, continue only after the vetted beta or release candidate has the required validation evidence. Stable npm publish also goes through `OpenClaw Release Publish`, reusing the successful preflight artifact via `preflight_run_id`. Stable macOS release readiness also requires the packaged `.zip`, `.dmg`, `.dSYM.zip`, and updated `appcast.xml` on `main`; the macOS publish workflow publishes the signed appcast to public `main` automatically after release assets verify, or opens/updates an appcast PR if branch protection blocks the direct push. Stable Windows Hub readiness requires the signed `OpenClawCompanion-Setup-x64.exe`, `OpenClawCompanion-Setup-arm64.exe`, and `OpenClawCompanion-SHA256SUMS.txt` assets on the OpenClaw GitHub release. Pass the exact signed `openclaw/openclaw-windows-node` release tag as `windows_node_tag` and its candidate-approved installer digest map as `windows_node_installer_digests`; `OpenClaw Release Publish` keeps the release draft, dispatches `Windows Node Release`, and verifies all three assets before publication.
11. After publish, run the npm post-publish verifier, optional standalone published-npm Telegram E2E when you need post-publish channel proof, dist-tag promotion when needed, verify the generated GitHub release page, run the release announcement steps, then complete [Stable main closeout](#stable-main-closeout) before calling a stable release finished.
10. On a failed publish attempt, keep the Release SHA unchanged unless the failure proves a product or changelog defect. Resume successful immutable children and artifacts; never rebuild or republish a package version that already succeeded.
11. For stable, continue only after the vetted beta or release candidate has the required validation evidence. Stable npm publish also goes through `OpenClaw Release Publish`, reusing the successful preflight artifact via `preflight_run_id`. Stable macOS release readiness also requires the packaged `.zip`, `.dmg`, `.dSYM.zip`, and updated `appcast.xml` on `main`; the macOS publish workflow publishes the signed appcast to public `main` automatically after release assets verify, or opens/updates an appcast PR if branch protection blocks the direct push. Stable Windows Hub readiness requires the signed `OpenClawCompanion-Setup-x64.exe`, `OpenClawCompanion-Setup-arm64.exe`, and `OpenClawCompanion-SHA256SUMS.txt` assets on the OpenClaw GitHub release. Pass the exact signed `openclaw/openclaw-windows-node` release tag as `windows_node_tag` and its candidate-approved installer digest map as `windows_node_installer_digests`; `OpenClaw Release Publish` keeps the release draft, dispatches `Windows Node Release`, and verifies all three assets before publication.
12. After publish, run the npm post-publish verifier, optional standalone published-npm Telegram E2E when you need post-publish channel proof, dist-tag promotion when needed, verify the generated GitHub release page, run the release announcement steps, then complete [Stable main closeout](#stable-main-closeout) before calling a stable release finished.
## Stable main closeout
@@ -206,7 +217,9 @@ A legacy fallback correction tag may reuse base-package evidence only when the c
Provide `release_package_spec` after publishing a beta to reuse the shipped npm package across release checks, Package Acceptance, and package Telegram E2E without rebuilding the release tarball. Provide `npm_telegram_package_spec` only when Telegram should use a different published package from the rest of release validation. Provide `package_acceptance_package_spec` when Package Acceptance should use a different published package from the release package spec. Provide `evidence_package_spec` when the release evidence report should prove that validation matches a published npm package without forcing Telegram E2E.
```bash
gh workflow run full-release-validation.yml --ref main -f ref=release/YYYY.M.PATCH
node scripts/full-release-validation-at-sha.mjs \
--sha <code-sha> \
--target-ref release/YYYY.M.PATCH
```
- Run the manual `Package Acceptance` workflow when you want side-channel proof for a package candidate while release work continues. Use `source=npm` for `openclaw@beta`, `openclaw@latest`, or an exact release version; `source=ref` to pack a trusted `package_ref` branch/tag/SHA with the current `workflow_ref` harness; `source=url` for a public HTTPS tarball with a required SHA-256 and strict public URL policy; `source=trusted-url` for a named trusted-source policy using required `trusted_source_id` and SHA-256; or `source=artifact` for a tarball uploaded by another GitHub Actions run.
@@ -274,27 +287,27 @@ A legacy fallback correction tag may reuse base-package evidence only when the c
## Release test boxes
`Full Release Validation` is how operators kick off all pre-release tests from one entrypoint. For a pinned commit proof on a fast-moving branch, use the helper so every child workflow runs from a temporary branch fixed at one trusted `main` workflow SHA while the requested commit remains the candidate under test:
`Full Release Validation` is how operators kick off the full product matrix from one entrypoint. Use the helper so every child workflow runs from a temporary branch fixed at one trusted `main` workflow SHA while the requested commit remains the candidate under test:
```bash
pnpm ci:full-release --sha <full-sha>
pnpm ci:full-release \
--sha <code-sha> \
--target-ref release/YYYY.M.PATCH
```
The helper fetches current `origin/main`, pushes `release-ci/<workflow-sha>-...` at that trusted workflow commit, dispatches `Full Release Validation` from the temporary branch with `ref=<target-sha>`, reuses strict exact-target evidence when available, verifies every child workflow `headSha` matches the pinned parent workflow SHA, then deletes the temporary branch. Pass `-f reuse_evidence=false` to force a fresh run or `--workflow-sha <trusted-main-sha>` to pin an older commit that is still reachable from current `origin/main`. The workflow itself never writes repository refs. This keeps main-only release tooling available without adding tooling commits to the candidate and avoids proving a newer `main` child run by accident.
The helper fetches current `origin/main`, pushes `release-ci/<workflow-sha>-...` at that trusted workflow commit, infers `beta` from alpha/beta package versions and `stable` otherwise, dispatches `Full Release Validation` from the temporary branch with `ref=<target-sha>`, verifies every child workflow `headSha` matches the pinned parent workflow SHA, then deletes the temporary branch. Pass `-f reuse_evidence=false` to force a fresh run, `-f release_profile=full` for the broad advisory sweep, or `--workflow-sha <trusted-main-sha>` to pin an older commit that is still reachable from current `origin/main`. The workflow itself never writes repository refs. This keeps main-only release tooling available without adding tooling commits to the candidate and avoids proving a newer `main` child run by accident.
For release branch or tag validation, run it from the trusted `main` workflow ref and pass the release branch or tag as `ref`:
After the Code SHA is green, commit only `CHANGELOG.md` and run the same helper with the Release SHA:
```bash
gh workflow run full-release-validation.yml \
--ref main \
-f ref=release/YYYY.M.PATCH \
-f provider=openai \
-f mode=both \
-f release_profile=stable \
-f evidence_package_spec=openclaw@YYYY.M.PATCH-beta.N
pnpm ci:full-release \
--sha <release-sha> \
--target-ref release/YYYY.M.PATCH
```
The workflow resolves the target ref, dispatches manual `CI` with `target_ref=<release-ref>`, then dispatches `OpenClaw Release Checks`. `OpenClaw Release Checks` fans out install smoke, cross-OS release checks, live/E2E Docker release-path coverage when soak is enabled, Package Acceptance with the canonical Telegram package E2E, QA Lab parity, live Matrix, and live Telegram. A full/all run is only acceptable when the `Full Release Validation` summary shows `normal_ci`, `plugin_prerelease`, and `release_checks` as successful, unless a focused rerun intentionally skipped the separate `Plugin Prerelease` child. Use the standalone `npm-telegram` child only for a focused published-package rerun with `release_package_spec` or `npm_telegram_package_spec`. The final verifier summary includes slowest-job tables for each child run, so the release manager can see the current critical path without downloading logs.
The second parent reuses product evidence only when GitHub proves the Release SHA descends from the Code SHA and the complete changed path set is exactly `CHANGELOG.md`. It records `changelog-only-release-v1` and dispatches no product children. Npm preflight and package/install acceptance still run on the Release SHA because its tarball bytes changed.
For a fresh Code SHA, the workflow resolves the target, dispatches manual `CI`, then dispatches `OpenClaw Release Checks`. `OpenClaw Release Checks` fans out install smoke, cross-OS release checks, live/E2E Docker release-path coverage when soak is enabled, Package Acceptance with the canonical Telegram package E2E, QA Lab parity, live Matrix, and live Telegram. A full/all run is only acceptable when the `Full Release Validation` summary shows `normal_ci`, `plugin_prerelease`, and `release_checks` as successful, unless a focused rerun intentionally skipped the separate `Plugin Prerelease` child. Use the standalone `npm-telegram` child only for a focused published-package rerun with `release_package_spec` or `npm_telegram_package_spec`. The final verifier summary includes slowest-job tables for each child run, so the release manager can see the current critical path without downloading logs.
The product-performance child is artifact-only in this release path. The
umbrella dispatches it with `publish_reports=false`, and validation is rejected
@@ -303,12 +316,12 @@ skipped.
See [Full release validation](/reference/full-release-validation) for the complete stage matrix, exact workflow job names, stable versus full profile differences, artifacts, and focused rerun handles.
Child workflows are dispatched from the trusted ref that runs `Full Release Validation`, normally `--ref main`, even when the target `ref` points at an older release branch or tag. Every child run must use the exact parent workflow SHA; if `main` advances before a child dispatch resolves, the umbrella fails closed. There is no separate Full Release Validation workflow-ref input; choose the trusted harness by choosing the workflow run ref. Do not use `--ref main -f ref=<sha>` for exact commit proof on moving `main`; raw commit SHAs cannot be workflow dispatch refs, so use `pnpm ci:full-release --sha <target-sha>` to create a temporary branch at trusted `origin/main` while keeping the target SHA as the candidate input.
Child workflows are dispatched from the SHA-pinned trusted ref that runs `Full Release Validation`. Every child run must use the exact parent workflow SHA. Do not use raw `--ref main -f ref=<sha>` dispatches for release proof; use `pnpm ci:full-release --sha <target-sha> --target-ref release/YYYY.M.PATCH`.
Use `release_profile` to select live/provider breadth:
- `minimum`: fastest release-critical OpenAI/core live and Docker path
- `stable`: minimum plus stable provider/backend coverage for release approval
- `beta`: fastest release-critical OpenAI/core live and Docker path
- `stable`: beta plus stable provider/backend coverage for release approval
- `full`: stable plus broad advisory provider/media coverage
Stable and full validation always run the exhaustive live/E2E, Docker release-path, and bounded published upgrade-survivor sweep before promotion. Use `run_release_soak=true` to request that same sweep for a beta. That sweep covers the latest four stable packages plus pinned `2026.4.23` and `2026.5.2` baselines plus older `2026.4.15` coverage, with duplicate baselines removed and each baseline sharded into its own Docker runner job.
@@ -320,28 +333,20 @@ The cross-OS OpenAI install smoke uses `OPENCLAW_CROSS_OS_OPENAI_MODEL` when the
Use these variants depending on release stage:
```bash
# Validate an unpublished release candidate branch.
gh workflow run full-release-validation.yml \
--ref main \
-f ref=release/YYYY.M.PATCH \
-f provider=openai \
-f mode=both \
-f release_profile=stable
# Validate the product-complete Code SHA.
pnpm ci:full-release \
--sha <code-sha> \
--target-ref release/YYYY.M.PATCH
# Validate an exact pushed commit.
gh workflow run full-release-validation.yml \
--ref main \
-f ref=<40-char-sha> \
-f provider=openai \
-f mode=both
# Validate the changelog-only Release SHA by reusing Code SHA product evidence.
pnpm ci:full-release \
--sha <release-sha> \
--target-ref release/YYYY.M.PATCH
# After publishing a beta, add published-package Telegram E2E.
gh workflow run full-release-validation.yml \
--ref main \
-f ref=release/YYYY.M.PATCH \
-f provider=openai \
-f mode=both \
-f release_profile=full \
pnpm ci:full-release \
--sha <release-sha> \
--target-ref release/YYYY.M.PATCH \
-f release_package_spec=openclaw@YYYY.M.PATCH-beta.N \
-f evidence_package_spec=openclaw@YYYY.M.PATCH-beta.N \
-f npm_telegram_provider_mode=mock-openai
@@ -349,14 +354,17 @@ gh workflow run full-release-validation.yml \
Do not use the full umbrella as the first rerun after a focused fix. If one box fails, use the failed child workflow, job, Docker lane, package profile, model provider, or QA lane for the next proof. Run the full umbrella again only when the fix changed shared release orchestration or made earlier all-box evidence stale. The umbrella's final verifier re-checks the recorded child workflow run ids, so after a child workflow is rerun successfully, rerun only the failed `Verify full validation` parent job.
`rerun_group=all` may reuse a prior green umbrella run only when it validated
the exact same target SHA, release profile, effective soak setting, and
validation inputs. This is bounded recovery for rerunning the same candidate,
not cross-SHA evidence reuse. For a changed candidate, including a changelog or
version-only commit, rerun every package, artifact, install, Docker, or provider
gate affected by the changed paths or artifact hashes. Newer umbrella runs for
the same `release/*`
ref and rerun group supersede in-progress ones automatically. Pass
`rerun_group=all` may reuse a prior green umbrella run when the release profile,
effective soak setting, and validation inputs match and either the target SHA
is identical or the new target is a descendant whose complete changed path set
is exactly `CHANGELOG.md`. Exact-target reuse records
`exact-target-full-validation-v1`; the post-validation Release SHA records
`changelog-only-release-v1`. The latter reuses only product validation. Npm
preflight, package bytes, release-note provenance, and install/update acceptance
must still run against the Release SHA. Any version, source, generated,
dependency, package, or workflow-owned target change requires a new Code SHA
and fresh full validation. Newer umbrella runs for the same `release/*` ref and
rerun group supersede in-progress ones automatically. Pass
`reuse_evidence=false` to force a fresh full run.
For bounded recovery, pass `rerun_group` to the umbrella. `all` is the real release-candidate run, `ci` runs only the normal CI child, `plugin-prerelease` runs only the release-only plugin child, `release-checks` runs every release box, and the narrower release groups are `install-smoke`, `cross-os`, `live-e2e`, `package`, `qa`, `qa-parity`, `qa-live`, and `npm-telegram`. Focused `npm-telegram` reruns require `release_package_spec` or `npm_telegram_package_spec`; full/all runs use the canonical package Telegram E2E inside Package Acceptance. Focused cross-OS reruns can add `cross_os_suite_filter=windows/packaged-upgrade` or another OS/suite filter. QA release-check failures block normal release validation, including required OpenClaw dynamic tool drift in the standard tier. Tideclaw alpha runs may still treat non-package-safety release-check lanes as advisory. With `release_profile=beta`, the `Run repo/live E2E validation` live-provider suites are advisory (warnings, not blockers); stable and full profiles keep them blocking. When `live_suite_filter` explicitly requests a gated QA live lane such as Discord, WhatsApp, or Slack, the matching `OPENCLAW_RELEASE_QA_*_LIVE_CI_ENABLED` repo variable must be enabled; otherwise input capture fails instead of silently skipping the lane.
+30 -30
View File
@@ -7,41 +7,38 @@ read_when:
- Debugging release validation stage failures
---
`Full Release Validation` is the release umbrella: the single manual entrypoint
for pre-release proof. Most work happens in child workflows so a failed box can
be rerun without restarting the whole release.
`Full Release Validation` is the release product-validation umbrella. Most work
happens in child workflows so a failed box can be rerun without restarting the
whole release.
Run it from a trusted workflow ref, normally `main`, and pass the release branch,
tag, or full commit SHA as `ref`:
Freeze the product-complete pre-changelog commit as the **Code SHA**, then run:
```bash
gh workflow run full-release-validation.yml \
--ref main \
-f ref=release/YYYY.M.PATCH \
-f provider=openai \
-f mode=both \
-f release_profile=stable
pnpm ci:full-release \
--sha <code-sha> \
--target-ref release/YYYY.M.PATCH
```
`provider` also accepts `anthropic` or `minimax` for cross-OS onboarding and the
end-to-end agent turn. Reusable child jobs resolve the called workflow harness
from `job.workflow_repository` and `job.workflow_sha`, while the input `ref`
selects the candidate under test. This keeps current trusted validation logic
available when validating an older release branch or tag.
end-to-end agent turn. The helper infers the `beta` profile from alpha/beta
package versions and `stable` otherwise. Pass alternate workflow inputs with
`-f key=value`; use `-f release_profile=full` only for the broad advisory sweep.
Every dispatched child must report the same workflow SHA as the parent
`Full Release Validation` run. If `main` moves between the parent and child
dispatches, the umbrella fails closed even when the child itself succeeds. For
an immutable exact-commit proof, use
`pnpm ci:full-release --sha <target-sha>`. The helper creates a temporary
`release-ci/*` ref pinned to current trusted `origin/main`, passes the target
SHA only as the candidate `ref`, reuses strict exact-target evidence when
available, and deletes the ref after validation. Pass
The helper creates a temporary `release-ci/*` ref pinned to one trusted
`origin/main` workflow SHA, passes the target SHA only as the candidate `ref`,
and deletes the temporary ref after validation. Every dispatched child must
report that same workflow SHA. Pass
`-f reuse_evidence=false` to force a fresh run or
`--workflow-sha <trusted-main-sha>` to select an older workflow commit still
reachable from current `origin/main`. The workflow never creates or updates
repository refs itself.
When the Code SHA is green, generate and commit only `CHANGELOG.md`. This new
commit is the **Release SHA**. Run the same helper for the Release SHA. Product
evidence is reused only when GitHub proves the Release SHA descends from the
Code SHA and the complete changed path set is exactly `CHANGELOG.md`; npm
preflight and package/install acceptance still run on the Release SHA.
`release_profile=stable` and `release_profile=full` always run the exhaustive
live/Docker soak. Pass `run_release_soak=true` to include the same soak lanes
with the `beta` profile. Stable publication rejects a validation manifest
@@ -63,13 +60,13 @@ that plugin, then runs Codex CLI preflight and same-session OpenAI agent turns.
## Top-level stages
For `rerun_group=all`, a `Check for reusable validation evidence` job runs
first: it looks for the newest prior green full validation for the exact same
target SHA, release profile, effective soak setting, and validation inputs.
When such evidence exists, every lane is skipped and the umbrella verifier
re-checks the immutable parent artifact, child runs, and dispatch logs. This is
same-candidate rerun recovery only; it does not authorize cross-SHA reuse. For
a changed candidate, rerun every package, artifact, install, Docker, or provider
gate affected by that delta. Pass `reuse_evidence=false` to force a fresh full
first. It looks for the newest prior green full validation with the same release
profile, effective soak setting, and validation inputs. Exact-target reruns use
`exact-target-full-validation-v1`. A descendant whose complete delta is exactly
`CHANGELOG.md` uses `changelog-only-release-v1`; every product lane is skipped
and the verifier independently rechecks the GitHub commit comparison, immutable
parent artifact, child runs, and dispatch logs. Any other target change requires
a fresh Code SHA validation. Pass `reuse_evidence=false` to force a fresh full
run. Evidence reuse runs only from `main` or a canonical SHA-pinned
`release-ci/*` ref whose workflow commit remains on trusted `main` lineage;
other workflow refs run the selected lanes fresh.
@@ -252,6 +249,9 @@ Keep the `Full Release Validation` summary as the release-level index. It links
child run ids and includes slowest-job tables. For failures, inspect the child
workflow first, then rerun the smallest matching handle above.
Record both Code SHA and Release SHA, the reuse policy and changed-path set, the
green Code SHA parent run, and the lightweight Release SHA parent run.
Useful artifacts:
- `release-package-under-test` from `OpenClaw Release Checks`
+5 -1
View File
@@ -8,11 +8,15 @@ export function parseArgs(argv: unknown): {
inputs: {
provider: string;
mode: string;
release_profile: string;
release_profile?: string;
rerun_group: string;
reuse_evidence: string;
};
};
export function releaseProfileForTarget(
targetSha: string,
readPackageJson?: (sha: string) => string,
): "beta" | "stable";
export function releaseEvidenceVerificationArgs(parentRunId: unknown): string[];
export function releaseEvidenceVerifierPath(worktreeRoot: unknown): string;
export function resolveRemoteTargetRefSha(
+27 -3
View File
@@ -13,7 +13,6 @@ const RELEASE_TAG_PATTERN = /^v[0-9]{4}\.[0-9]+\.[0-9]+(?:-(?:alpha|beta)\.[0-9]
const DEFAULT_INPUTS = {
provider: "openai",
mode: "both",
release_profile: "full",
rerun_group: "all",
reuse_evidence: "true",
};
@@ -25,8 +24,10 @@ Creates a temporary remote branch pinned to trusted main release tooling,
dispatches Full Release Validation with the target commit as its ref input,
watches the parent run, verifies all child workflow head SHAs match the trusted
workflow lineage through the release evidence manifest, then deletes the
temporary branch by default. Exact-target evidence reuse stays enabled; pass
-f reuse_evidence=false to force a fresh run.`);
temporary branch by default. Exact-target and changelog-only Release SHA
evidence reuse stay enabled; pass -f reuse_evidence=false to force a fresh
run. The release profile defaults to beta for alpha/beta package versions and
stable otherwise; pass -f release_profile=full for the broad advisory sweep.`);
}
function run(command, args, options = {}) {
@@ -135,6 +136,12 @@ export function parseArgs(argv) {
if (!["true", "false"].includes(args.inputs.reuse_evidence)) {
throw new Error("reuse_evidence must be true or false");
}
if (
args.inputs.release_profile &&
!["beta", "stable", "full"].includes(args.inputs.release_profile)
) {
throw new Error("release_profile must be beta, stable, or full");
}
if (Object.hasOwn(args.inputs, "ref")) {
throw new Error("SHA-pinned release validation reserves the ref input for --sha");
}
@@ -179,6 +186,22 @@ function resolveSha(requestedSha) {
return run("git", ["rev-parse", "--verify", `${rev}^{commit}`], { dryRun: false });
}
export function releaseProfileForTarget(
targetSha,
readPackageJson = (sha) => run("git", ["show", `${sha}:package.json`]),
) {
let version;
try {
version = JSON.parse(readPackageJson(targetSha)).version;
} catch {
throw new Error(`Could not read package.json from target SHA ${targetSha}`);
}
if (typeof version !== "string" || !/^[0-9]{4}\.[0-9]+\.[0-9]+(?:-.+)?$/u.test(version)) {
throw new Error(`Target SHA ${targetSha} has an invalid package version`);
}
return /-(?:alpha|beta)\.[1-9][0-9]*$/u.test(version) ? "beta" : "stable";
}
function resolveTrustedWorkflowSha(requestedSha) {
run("git", ["fetch", "--no-tags", "origin", "refs/heads/main:refs/remotes/origin/main"], {
stdio: "inherit",
@@ -276,6 +299,7 @@ function verifyReleaseEvidence(parentRunId, workflowSha) {
function main() {
const args = parseArgs(process.argv.slice(2));
const targetSha = resolveSha(args.sha);
args.inputs.release_profile ??= releaseProfileForTarget(targetSha);
const targetContextRef = verifyTargetRef(args.targetRef, targetSha);
const workflowSha = resolveTrustedWorkflowSha(args.workflowSha);
const shortSha = workflowSha.slice(0, 12);
@@ -1,9 +1,10 @@
#!/usr/bin/env bash
set -euo pipefail
# Finds a prior green Full Release Validation run for the exact target SHA.
# Cross-SHA evidence reuse is intentionally left to the granular delta manifest,
# which can require fresh package/install/provider closure per changed artifact.
# Finds a prior green Full Release Validation run for the exact target SHA or
# for its immediate product-equivalent predecessor. Cross-SHA reuse is limited
# to a descendant whose complete tree delta is CHANGELOG.md; package/install
# proof still runs against the release SHA after that changelog is committed.
# Always exits 0 with reuse=true/false; callers fail open to a full validation.
REPO="${GH_REPO:-}"
@@ -34,8 +35,9 @@ Scans recent successful Full Release Validation runs for an exact-target
validation manifest whose recorded lane-selection inputs match --inputs-json
and whose normalized strict-v3 evidence is accepted by the current trusted-main
verifier identified by --workflow-sha. The historical producer workflow SHA
remains independent. Writes reuse=true plus evidence_* outputs when found;
reuse=false otherwise.
remains independent. A descendant target may reuse product validation only
when GitHub proves the entire delta is CHANGELOG.md. Writes reuse=true plus
evidence_* outputs when found; reuse=false otherwise.
EOF
}
@@ -287,20 +289,42 @@ for ((index = 0; index < run_count; index += 1)); do
fi
prior_sha="$(jq -r '.root.targetSha' <<< "$validation_record")"
evidence_policy="exact-target-full-validation-v1"
changed_paths="[]"
if [[ "$prior_sha" != "$TARGET_SHA" ]]; then
echo "[evidence-reuse] run ${run_id}: target ${prior_sha} differs from ${TARGET_SHA}; cross-SHA reuse requires granular artifact evidence" >&2
continue
compare_json=""
if ! compare_json="$(
gh api "repos/${REPO}/compare/${prior_sha}...${TARGET_SHA}"
)"; then
echo "[evidence-reuse] run ${run_id}: could not compare ${prior_sha}...${TARGET_SHA}; skipping" >&2
continue
fi
if ! jq -e \
--arg prior_sha "$prior_sha" '
.status == "ahead"
and .merge_base_commit.sha == $prior_sha
and (.files | type == "array" and length == 1)
and .files[0].filename == "CHANGELOG.md"
and .files[0].status == "modified"
and ((.files[0].previous_filename // "") == "")
' <<< "$compare_json" >/dev/null; then
echo "[evidence-reuse] run ${run_id}: target ${TARGET_SHA} is not a CHANGELOG.md-only descendant of ${prior_sha}; skipping" >&2
continue
fi
evidence_policy="changelog-only-release-v1"
changed_paths='["CHANGELOG.md"]'
fi
run_url="$(jq -r '.root.url' <<< "$validation_record")"
echo "[evidence-reuse] reusing exact-target run ${run_id} (${run_url}) for ${TARGET_SHA}" >&2
echo "[evidence-reuse] reusing ${evidence_policy} run ${run_id} (${run_url}) for ${TARGET_SHA}" >&2
write_output reuse true
write_output evidence_run_id "$run_id"
write_output evidence_root_run_id "$run_id"
write_output evidence_run_url "$run_url"
write_output evidence_sha "$prior_sha"
write_output changed_path_count "0"
write_output changed_paths "[]"
write_output evidence_policy "$evidence_policy"
write_output changed_path_count "$(jq 'length' <<< "$changed_paths")"
write_output changed_paths "$changed_paths"
write_output evidence_manifest "$(jq -c '.manifest' <<< "$validation_record")"
exit 0
done
@@ -23,6 +23,7 @@ export function parseArgs(argv: unknown): {
windowsNodeInstallerDigests: string;
outputDir: string;
};
export function releaseBranchForTag(tag: string): string;
export function run(command: unknown, args: unknown, options?: Record<string, unknown>): string;
export function buildReleaseCandidateState(
options: unknown,
+10
View File
@@ -109,6 +109,14 @@ function requireValue(argv, index, flag) {
return value;
}
export function releaseBranchForTag(tag) {
if (tag.includes("-alpha.")) {
return "";
}
const version = tag.replace(/^v/u, "").split("-", 1)[0];
return `release/${version}`;
}
/**
* Parses release-candidate validation options and enforces publish-scope policy.
*/
@@ -1283,8 +1291,10 @@ async function main() {
if (!options.fullReleaseRunId && !options.skipDispatch) {
const workflowFile = "full-release-validation.yml";
const targetContextRef = releaseBranchForTag(options.tag);
options.fullReleaseRunId = dispatchWorkflow(options.repo, workflowFile, options.workflowRef, {
ref: options.tag,
...(targetContextRef ? { target_context_ref: targetContextRef } : {}),
provider: options.provider,
mode: options.mode,
release_profile: options.releaseProfile,
+1
View File
@@ -83,6 +83,7 @@ export function validateEvidenceReuseChain(
currentManifest: unknown,
selectedManifest: unknown,
rootManifest: unknown,
compareCommits?: unknown,
): unknown;
export function selectedChildKeys(parentJobs: unknown): Set<string>;
export function manifestChildEntries<Child extends { manifestKey: string; name: string }>(
+52 -11
View File
@@ -66,7 +66,12 @@ const CHILD_DISPATCHES = [
},
];
const EVIDENCE_REUSE_POLICY = "exact-target-full-validation-v1";
const EXACT_TARGET_EVIDENCE_REUSE_POLICY = "exact-target-full-validation-v1";
const CHANGELOG_ONLY_EVIDENCE_REUSE_POLICY = "changelog-only-release-v1";
const EVIDENCE_REUSE_POLICIES = new Set([
EXACT_TARGET_EVIDENCE_REUSE_POLICY,
CHANGELOG_ONLY_EVIDENCE_REUSE_POLICY,
]);
const RERUN_GROUP_CHILD_KEYS = new Map([
["all", ["normalCi", "releaseChecks", "pluginPrerelease", "productPerformance"]],
@@ -459,7 +464,7 @@ export function validateParentManifest(value, expected) {
value.evidenceReuse,
"release validation manifest evidence reuse",
);
if (reuse.policy !== EVIDENCE_REUSE_POLICY) {
if (!EVIDENCE_REUSE_POLICIES.has(reuse.policy)) {
throw new Error("release validation manifest evidence reuse policy is invalid");
}
if (!/^[a-f0-9]{40}$/u.test(String(reuse.evidenceSha))) {
@@ -502,14 +507,16 @@ export function validateParentManifest(value, expected) {
};
}
export function validateEvidenceReuseChain(currentManifest, selectedManifest, rootManifest) {
export function validateEvidenceReuseChain(
currentManifest,
selectedManifest,
rootManifest,
compareCommits,
) {
const reuse = currentManifest.evidenceReuse;
if (!reuse) {
throw new Error("release validation manifest does not authorize evidence reuse");
}
if (reuse.changedPaths.length !== 0) {
throw new Error("full release evidence reuse requires an exact target with no changed paths");
}
if (rootManifest.evidenceReuse || selectedManifest.evidenceReuse) {
throw new Error("evidence reuse must select a root execution manifest");
}
@@ -529,15 +536,43 @@ export function validateEvidenceReuseChain(currentManifest, selectedManifest, ro
if (selectedManifest.targetSha !== reuse.evidenceSha) {
throw new Error("evidence reuse selected manifest SHA mismatch");
}
if (
currentManifest.targetSha !== reuse.evidenceSha ||
rootManifest.targetSha !== reuse.evidenceSha
) {
throw new Error("full release evidence reuse target SHA mismatch");
if (rootManifest.targetSha !== reuse.evidenceSha) {
throw new Error("full release evidence reuse root SHA mismatch");
}
if (selectedManifest.runId !== rootManifest.runId) {
throw new Error("evidence reuse selected manifest is not the chain root");
}
if (reuse.policy === EXACT_TARGET_EVIDENCE_REUSE_POLICY) {
if (reuse.changedPaths.length !== 0 || currentManifest.targetSha !== reuse.evidenceSha) {
throw new Error("exact-target release evidence reuse requires no changed paths");
}
} else if (reuse.policy === CHANGELOG_ONLY_EVIDENCE_REUSE_POLICY) {
if (
reuse.changedPaths.length !== 1 ||
reuse.changedPaths[0] !== "CHANGELOG.md" ||
currentManifest.targetSha === reuse.evidenceSha
) {
throw new Error("changelog-only release evidence reuse has an invalid target delta");
}
if (typeof compareCommits !== "function") {
throw new Error("changelog-only release evidence reuse requires commit comparison");
}
const comparison = compareCommits(reuse.evidenceSha, currentManifest.targetSha);
const changedFiles = Array.isArray(comparison?.files) ? comparison.files : [];
const changelog = changedFiles[0];
if (
comparison?.status !== "ahead" ||
comparison?.merge_base_commit?.sha !== reuse.evidenceSha ||
changedFiles.length !== 1 ||
changelog?.filename !== "CHANGELOG.md" ||
changelog?.status !== "modified" ||
changelog?.previous_filename
) {
throw new Error("changelog-only release evidence reuse failed commit comparison");
}
} else {
throw new Error("release validation manifest evidence reuse policy is invalid");
}
const rootIdentity = JSON.stringify(manifestEvidenceIdentity(rootManifest));
for (const [label, manifest] of [
@@ -1311,6 +1346,7 @@ export function validateReleaseRunEvidence(
currentEvidence.manifest,
selectedEvidence.manifest,
rootEvidence.manifest,
(base, head) => evidenceClient.compareCommits(base, head),
);
}
@@ -1692,12 +1728,17 @@ async function main() {
currentManifest,
selectedManifest,
rootManifest,
(base, head) => githubRestJson(`compare/${base}...${head}`, repository),
);
sourceManifest = rootManifest;
sourceParent = rootParent;
console.log(`evidence-selected-run: ${selectedRunId}`);
console.log(`evidence-root-run: ${rootRunId}`);
console.log(`evidence-sha: ${evidenceSha}`);
console.log(`evidence-policy: ${currentManifest.evidenceReuse.policy}`);
console.log(
`evidence-changed-paths: ${JSON.stringify(currentManifest.evidenceReuse.changedPaths)}`,
);
}
const expectedChildren = expectedSelectedChildDispatches(
@@ -8,6 +8,8 @@ const FULL_RELEASE_WORKFLOW = "Full Release Validation";
const FULL_RELEASE_WORKFLOW_PATH = ".github/workflows/full-release-validation.yml";
const SHA_PATTERN = /^[a-f0-9]{40}$/u;
const PINNED_BRANCH_PATTERN = /^release-ci\/([a-f0-9]{12})-([1-9][0-9]*)$/u;
const EXACT_TARGET_EVIDENCE_REUSE_POLICY = "exact-target-full-validation-v1";
const CHANGELOG_ONLY_EVIDENCE_REUSE_POLICY = "changelog-only-release-v1";
function normalizeWorkflowPathRef(ref) {
if (!ref || ref.startsWith("refs/")) {
@@ -144,14 +146,22 @@ export function validateFullReleaseValidationEvidence({
}
if (Object.hasOwn(manifest, "evidenceReuse")) {
const reuse = manifest.evidenceReuse;
const exactTarget =
reuse?.policy === EXACT_TARGET_EVIDENCE_REUSE_POLICY &&
reuse.evidenceSha === expectedTargetSha &&
Array.isArray(reuse.changedPaths) &&
reuse.changedPaths.length === 0;
const changelogOnly =
reuse?.policy === CHANGELOG_ONLY_EVIDENCE_REUSE_POLICY &&
reuse.evidenceSha !== expectedTargetSha &&
Array.isArray(reuse.changedPaths) &&
reuse.changedPaths.length === 1 &&
reuse.changedPaths[0] === "CHANGELOG.md";
if (
!reuse ||
typeof reuse !== "object" ||
Array.isArray(reuse) ||
reuse.policy !== "exact-target-full-validation-v1" ||
reuse.evidenceSha !== expectedTargetSha ||
!Array.isArray(reuse.changedPaths) ||
reuse.changedPaths.length !== 0 ||
(!exactTarget && !changelogOnly) ||
!/^[1-9][0-9]*$/u.test(String(reuse.runId ?? "")) ||
!/^[1-9][0-9]*$/u.test(String(reuse.selectedRunId ?? ""))
) {
@@ -170,8 +180,11 @@ export function validateFullReleaseValidationEvidence({
strictEvidence.valid !== true ||
String(strictEvidence.current?.runId ?? "") !== String(expectedRunId) ||
strictEvidence.current?.targetSha !== expectedTargetSha ||
strictEvidence.root?.targetSha !== expectedTargetSha ||
strictEvidence.evidenceReuse?.evidenceSha !== expectedTargetSha ||
strictEvidence.root?.targetSha !== reuse.evidenceSha ||
strictEvidence.evidenceReuse?.evidenceSha !== reuse.evidenceSha ||
strictEvidence.evidenceReuse?.policy !== reuse.policy ||
JSON.stringify(strictEvidence.evidenceReuse?.changedPaths) !==
JSON.stringify(reuse.changedPaths) ||
String(strictEvidence.evidenceReuse?.rootRunId ?? "") !== String(reuse.runId) ||
String(strictEvidence.evidenceReuse?.selectedRunId ?? "") !== String(reuse.selectedRunId) ||
strictEvidence.conclusions?.allRequiredSucceeded !== true
@@ -415,6 +415,10 @@ function setUpFixtures(runs: RunFixture[]): {
function runResolver(args: {
binDir: string;
compareBaseSha?: string;
compareFiles?: string[];
compareRenamed?: boolean;
compareStatus?: string;
fixtures: string;
inputs?: unknown;
releaseProfile?: string;
@@ -434,6 +438,23 @@ function runResolver(args: {
status: args.verifierOnMain === false ? "diverged" : "ahead",
}),
);
if (args.compareBaseSha) {
writeFileSync(
fixtureName(
args.fixtures,
`repos/${REPOSITORY}/compare/${args.compareBaseSha}...${args.targetSha}`,
),
JSON.stringify({
files: (args.compareFiles ?? ["CHANGELOG.md"]).map((filename, index) => ({
filename,
status: args.compareRenamed && index === 0 ? "renamed" : "modified",
...(args.compareRenamed && index === 0 ? { previous_filename: "src/index.ts" } : {}),
})),
merge_base_commit: { sha: args.compareBaseSha },
status: args.compareStatus ?? "ahead",
}),
);
}
return spawnSync(
"bash",
[
@@ -829,7 +850,7 @@ describe("scripts/github/find-reusable-release-validation.sh", () => {
},
);
it("rejects cross-SHA reuse even for a changelog-only delta", () => {
it("reuses product validation for a changelog-only release delta", () => {
const { origin, priorSha } = createRepo();
const targetSha = commitFile(
origin,
@@ -843,6 +864,34 @@ describe("scripts/github/find-reusable-release-validation.sh", () => {
const result = runResolver({
binDir,
compareBaseSha: priorSha,
fixtures,
repoDir: clone,
targetSha,
validatorPath,
});
expect(result.status).toBe(0);
expect(parseOutput(result.stdout)).toMatchObject({
changed_path_count: "1",
changed_paths: '["CHANGELOG.md"]',
evidence_policy: "changelog-only-release-v1",
evidence_sha: priorSha,
reuse: "true",
});
});
it("rejects cross-SHA reuse when the release delta includes code", () => {
const { origin, priorSha } = createRepo();
const targetSha = commitFile(origin, "index.ts", "export const value = 2;\n", "fix: code");
const clone = cloneHead(origin);
const record = normalizedEvidence({ targetSha: priorSha });
const { binDir, fixtures, validatorPath } = setUpFixtures([{ record, runId: "111" }]);
const result = runResolver({
binDir,
compareBaseSha: priorSha,
compareFiles: ["index.ts"],
fixtures,
repoDir: clone,
targetSha,
@@ -851,7 +900,29 @@ describe("scripts/github/find-reusable-release-validation.sh", () => {
expect(result.status).toBe(0);
expect(parseOutput(result.stdout)).toMatchObject({ reuse: "false" });
expect(result.stderr).toContain("cross-SHA reuse requires granular artifact evidence");
expect(result.stderr).toContain("is not a CHANGELOG.md-only descendant");
});
it("rejects a source-file rename to CHANGELOG.md", () => {
const { origin, priorSha } = createRepo();
const targetSha = commitFile(origin, "CHANGELOG.md", "renamed source\n", "docs: rename");
const clone = cloneHead(origin);
const record = normalizedEvidence({ targetSha: priorSha });
const { binDir, fixtures, validatorPath } = setUpFixtures([{ record, runId: "111" }]);
const result = runResolver({
binDir,
compareBaseSha: priorSha,
compareRenamed: true,
fixtures,
repoDir: clone,
targetSha,
validatorPath,
});
expect(result.status).toBe(0);
expect(parseOutput(result.stdout)).toMatchObject({ reuse: "false" });
expect(result.stderr).toContain("is not a CHANGELOG.md-only descendant");
});
it("rejects target version metadata that is internally inconsistent", () => {
@@ -4,6 +4,7 @@ import { join } from "node:path";
import { describe, expect, it } from "vitest";
import {
parseArgs,
releaseProfileForTarget,
releaseEvidenceVerificationArgs,
releaseEvidenceVerifierPath,
resolveRemoteTargetRefSha,
@@ -40,6 +41,15 @@ describe("full-release-validation-at-sha", () => {
});
});
it("infers the release profile from the target package version", () => {
const readVersion = (version: string) => () => JSON.stringify({ version });
expect(releaseProfileForTarget("a".repeat(40), readVersion("2026.7.1-beta.4"))).toBe("beta");
expect(releaseProfileForTarget("a".repeat(40), readVersion("2026.7.1-alpha.4"))).toBe("beta");
expect(releaseProfileForTarget("a".repeat(40), readVersion("2026.7.1"))).toBe("stable");
expect(releaseProfileForTarget("a".repeat(40), readVersion("2026.7.1-1"))).toBe("stable");
});
it("keeps release context separate from the exact target SHA", () => {
const source = readFileSync("scripts/full-release-validation-at-sha.mjs", "utf8");
expect(source).toContain("ref: targetSha");
@@ -99,6 +109,9 @@ describe("full-release-validation-at-sha", () => {
expect(() => parseArgs(["-f", "reuse_evidence=maybe"])).toThrow(
"reuse_evidence must be true or false",
);
expect(() => parseArgs(["-f", "release_profile=minimum"])).toThrow(
"release_profile must be beta, stable, or full",
);
});
it("reserves the candidate ref for the resolved --sha", () => {
@@ -3931,7 +3931,7 @@ wait_for_run plugin-clawhub-new.yml 123 "${expectedSha}" || status=$?
}
expect(fullRelease.jobs?.release_checks?.["timeout-minutes"]).toBe(
"${{ inputs.release_profile != 'minimum' && 240 || 60 }}",
"${{ inputs.release_profile != 'beta' && 240 || 60 }}",
);
expect(fullRelease.jobs?.prepare_release_package).toBeUndefined();
expect(releaseChecks.jobs?.prepare_release_package?.["timeout-minutes"]).toBe(15);
@@ -603,7 +603,7 @@ describe("scripts/lib/plugin-prerelease-test-plan.mjs", () => {
expect(fullReleaseWorkflow.jobs[jobName]["runs-on"]).toBe("ubuntu-24.04");
}
expect(fullReleaseWorkflow.jobs.normal_ci["timeout-minutes"]).toBe(
"${{ inputs.release_profile != 'minimum' && 240 || 60 }}",
"${{ inputs.release_profile != 'beta' && 240 || 60 }}",
);
expect(fullReleaseWorkflow.jobs.normal_ci.needs).toEqual(["resolve_target", "evidence_reuse"]);
expect(fullReleaseWorkflow.jobs.normal_ci.if).toContain(
@@ -635,7 +635,7 @@ describe("scripts/lib/plugin-prerelease-test-plan.mjs", () => {
"${{ inputs.release_profile == 'full' && 300 || inputs.release_profile == 'stable' && 240 || 60 }}",
);
expect(fullReleaseWorkflow.jobs.release_checks["timeout-minutes"]).toBe(
"${{ inputs.release_profile != 'minimum' && 240 || 60 }}",
"${{ inputs.release_profile != 'beta' && 240 || 60 }}",
);
const fullReleaseSource = readFileSync(".github/workflows/full-release-validation.yml", "utf8");
expect(
@@ -12,6 +12,7 @@ import {
parseArgs,
parseRunIdFromDispatchOutput,
reconcileReleaseCandidateState,
releaseBranchForTag,
resolveArtifactName,
requireRunIdFromDispatchOutput,
run,
@@ -532,6 +533,16 @@ describe("release candidate checklist", () => {
).toThrow("--workflow-ref must be main");
});
it("keeps release validation context on the canonical release branch", () => {
expect(releaseBranchForTag("v2026.7.1-beta.4")).toBe("release/2026.7.1");
expect(releaseBranchForTag("v2026.7.1")).toBe("release/2026.7.1");
expect(releaseBranchForTag("v2026.7.1-1")).toBe("release/2026.7.1");
expect(releaseBranchForTag("v2026.7.1-alpha.4")).toBe("");
const source = readFileSync("scripts/release-candidate-checklist.mjs", "utf8");
expect(source).toContain("target_context_ref: targetContextRef");
});
it("preserves the matching Tideclaw alpha workflow source", () => {
const workflowRef = "tideclaw/alpha/2026-07-10-1200Z";
const options = parseArgs([
+52 -6
View File
@@ -1369,7 +1369,7 @@ describe("release CI summary child correlation", () => {
expect(current.targetSha).toBe(root.targetSha);
});
it("rejects changed paths and cross-SHA targets in Full Release reuse", () => {
it("accepts a verified changelog-only release delta", () => {
const root = validateParentManifest(rawManifest({}), {
runAttempt: 2,
runId: "29090000000",
@@ -1379,18 +1379,64 @@ describe("release CI summary child correlation", () => {
evidenceReuse: {
changedPaths: ["CHANGELOG.md"],
evidenceSha: root.targetSha,
policy: "exact-target-full-validation-v1",
policy: "changelog-only-release-v1",
runId: root.runId,
selectedRunId: root.runId,
},
runId: "29090000001",
targetSha: root.targetSha,
targetSha: "b".repeat(40),
}),
{ runAttempt: 2, runId: "29090000001" },
);
expect(() => validateEvidenceReuseChain(changedPaths, root, root)).toThrow(
"requires an exact target with no changed paths",
expect(
validateEvidenceReuseChain(changedPaths, root, root, (base: string, head: string) => ({
files: [{ filename: "CHANGELOG.md", status: "modified" }],
merge_base_commit: { sha: base },
status: head === changedPaths.targetSha ? "ahead" : "diverged",
})),
).toBe(root.targetSha);
});
it("rejects unverified changed paths and cross-SHA exact-target reuse", () => {
const root = validateParentManifest(rawManifest({}), {
runAttempt: 2,
runId: "29090000000",
});
const changedPaths = validateParentManifest(
rawManifest({
evidenceReuse: {
changedPaths: ["CHANGELOG.md"],
evidenceSha: root.targetSha,
policy: "changelog-only-release-v1",
runId: root.runId,
selectedRunId: root.runId,
},
runId: "29090000001",
targetSha: "b".repeat(40),
}),
{ runAttempt: 2, runId: "29090000001" },
);
expect(() =>
validateEvidenceReuseChain(changedPaths, root, root, (base: string) => ({
files: [{ filename: "src/index.ts" }],
merge_base_commit: { sha: base },
status: "ahead",
})),
).toThrow("failed commit comparison");
expect(() =>
validateEvidenceReuseChain(changedPaths, root, root, (base: string) => ({
files: [
{
filename: "CHANGELOG.md",
previous_filename: "src/index.ts",
status: "renamed",
},
],
merge_base_commit: { sha: base },
status: "ahead",
})),
).toThrow("failed commit comparison");
const changedTarget = validateParentManifest(
rawManifest({
@@ -1407,7 +1453,7 @@ describe("release CI summary child correlation", () => {
{ runAttempt: 2, runId: "29090000001" },
);
expect(() => validateEvidenceReuseChain(changedTarget, root, root)).toThrow(
"full release evidence reuse target SHA mismatch",
"exact-target release evidence reuse requires no changed paths",
);
});
@@ -325,6 +325,20 @@ describe("release validation no-push transport", () => {
expect(verify.run).not.toContain('"$head_sha" != "$TARGET_SHA"');
});
it("keeps the Release SHA wrapper as the durable evidence identity", () => {
const full = readWorkflow(FULL_RELEASE);
const verify = step(job(full, "summary"), "Verify child workflow results");
const dispatch = step(job(full, "summary"), "Request release evidence update");
expect(verify.run).toContain(
'echo "Dispatched ${workflow}: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}"',
);
expect(verify.run).toContain('"ci.yml"');
expect(verify.run).toContain('"openclaw-release-checks.yml"');
expect(dispatch.run).not.toContain('GITHUB_RUN_ID_VALUE="$EVIDENCE_ROOT_RUN_ID"');
expect(dispatch.run).toContain("reused green product evidence from chain-root run");
});
it("publishes an attempt-qualified canonical manifest plus a temporary legacy alias", () => {
const summary = job(readWorkflow(FULL_RELEASE), "summary");
expect(step(summary, "Upload release validation manifest").with).toMatchObject({
@@ -60,7 +60,9 @@ function strictEvidenceReuse() {
current: { runId: "123", targetSha },
root: { runId: "122", targetSha },
evidenceReuse: {
changedPaths: [],
evidenceSha: targetSha,
policy: "exact-target-full-validation-v1",
rootRunId: "122",
selectedRunId: "122",
},
@@ -208,6 +210,40 @@ describe("full release validation evidence", () => {
);
});
it("accepts changelog-only release evidence reuse on the SHA-pinned path", () => {
const codeSha = "c".repeat(40);
const reuse = {
changedPaths: ["CHANGELOG.md"],
evidenceSha: codeSha,
policy: "changelog-only-release-v1",
runId: "122",
selectedRunId: "122",
};
const result = validateFullReleaseValidationEvidence({
run: releaseRun(),
manifest: releaseManifest({ evidenceReuse: reuse }),
expectedRepository: "openclaw/openclaw",
expectedRunId: "123",
expectedTargetSha: targetSha,
expectedWorkflowBranch: "release/2026.7.1",
isTrustedMainAncestor: () => true,
validateEvidenceReuseStrictly: () => ({
...strictEvidenceReuse(),
current: { runId: "123", targetSha },
root: { runId: "122", targetSha: codeSha },
evidenceReuse: {
changedPaths: ["CHANGELOG.md"],
evidenceSha: codeSha,
policy: "changelog-only-release-v1",
rootRunId: "122",
selectedRunId: "122",
},
}),
});
expect(result.source).toBe("sha-pinned-main");
});
it("requires strict root and child validation for reused evidence", () => {
expect(() =>
validateFullReleaseValidationEvidence({
@@ -251,6 +287,19 @@ describe("full release validation evidence", () => {
{ evidenceReuse: { ...exactTargetEvidenceReuse(), evidenceSha: "c".repeat(40) } },
),
).toThrow("evidence reuse is invalid");
expect(() =>
validate(
{},
{
evidenceReuse: {
...exactTargetEvidenceReuse(),
changedPaths: ["src/a.ts"],
evidenceSha: "c".repeat(40),
policy: "changelog-only-release-v1",
},
},
),
).toThrow("evidence reuse is invalid");
});
it("keeps a pinned-shaped expected branch on the pinned trust path", () => {