diff --git a/.agents/skills/openclaw-changelog-update/SKILL.md b/.agents/skills/openclaw-changelog-update/SKILL.md index baac5d21e31f..05c91819215d 100644 --- a/.agents/skills/openclaw-changelog-update/SKILL.md +++ b/.agents/skills/openclaw-changelog-update/SKILL.md @@ -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' ..` - `git log --first-parent --grep='(#' --date=short --pretty=format:'%h%x09%ad%x09%s' ..` @@ -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 ..` 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 diff --git a/.agents/skills/openclaw-testing/SKILL.md b/.agents/skills/openclaw-testing/SKILL.md index 0f1335e7d67a..8d565e0f92a6 100644 --- a/.agents/skills/openclaw-testing/SKILL.md +++ b/.agents/skills/openclaw-testing/SKILL.md @@ -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= \ - -f provider=openai \ - -f mode=both \ - -f release_profile=stable +node scripts/full-release-validation-at-sha.mjs \ + --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=`. Supported umbrella groups are `all`, `ci`, `plugin-prerelease`, `release-checks`, `install-smoke`, `cross-os`, `live-e2e`, `package`, `qa`, diff --git a/.agents/skills/release-openclaw-ci/SKILL.md b/.agents/skills/release-openclaw-ci/SKILL.md index 5f93e8d5988d..34bc45c8fdb3 100644 --- a/.agents/skills/release-openclaw-ci/SKILL.md +++ b/.agents/skills/release-openclaw-ci/SKILL.md @@ -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= \ + -f target_ref= \ -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= \ - -f provider=openai \ - -f mode=both \ - -f release_profile=full \ - -f rerun_group=all +node scripts/full-release-validation-at-sha.mjs \ + --sha \ + --target-ref release/YYYY.M.PATCH ``` For immutable workflow proof on a moving `main`, use -`pnpm ci:full-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 --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=`, 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= -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 diff --git a/.agents/skills/release-openclaw-maintainer/SKILL.md b/.agents/skills/release-openclaw-maintainer/SKILL.md index 1a5b90a32f3e..ea1b2b95ab8f 100644 --- a/.agents/skills/release-openclaw-maintainer/SKILL.md +++ b/.agents/skills/release-openclaw-maintainer/SKILL.md @@ -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=` 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=` 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= -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 `. -- 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 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=`, `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=`, `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 --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 + `..` 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 --full-release-run + --npm-preflight-run +--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 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 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 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 `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-` from the successful +34. For stable releases, download `macos-appcast-` 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 diff --git a/.github/workflows/full-release-validation.yml b/.github/workflows/full-release-validation.yml index 33197f5bcb3d..4b5c85d8f7c0 100644 --- a/.github/workflows/full-release-validation.yml +++ b/.github/workflows/full-release-validation.yml @@ -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, diff --git a/AGENTS.md b/AGENTS.md index 08109296d877..68960d1c9934 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 --target-ref release/`; 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 --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`. diff --git a/docs/reference/RELEASING.md b/docs/reference/RELEASING.md index d2bb899982e0..c822dfe961ed 100644 --- a/docs/reference/RELEASING.md +++ b/docs/reference/RELEASING.md @@ -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//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 --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 \ + --npm-preflight-run \ + --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 \ + --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 +pnpm ci:full-release \ + --sha \ + --target-ref release/YYYY.M.PATCH ``` -The helper fetches current `origin/main`, pushes `release-ci/-...` at that trusted workflow commit, dispatches `Full Release Validation` from the temporary branch with `ref=`, 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 ` 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/-...` 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=`, 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 ` 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 \ + --target-ref release/YYYY.M.PATCH ``` -The workflow resolves the target ref, dispatches manual `CI` with `target_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=` for exact commit proof on moving `main`; raw commit SHAs cannot be workflow dispatch refs, so use `pnpm ci:full-release --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=` dispatches for release proof; use `pnpm ci:full-release --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 \ + --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 \ + --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 \ + --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. diff --git a/docs/reference/full-release-validation.md b/docs/reference/full-release-validation.md index 95ac15c52980..d27bb4b2e068 100644 --- a/docs/reference/full-release-validation.md +++ b/docs/reference/full-release-validation.md @@ -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 \ + --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 `. 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 ` 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` diff --git a/scripts/full-release-validation-at-sha.d.mts b/scripts/full-release-validation-at-sha.d.mts index 127e47bd5d1a..87c51d15c24d 100644 --- a/scripts/full-release-validation-at-sha.d.mts +++ b/scripts/full-release-validation-at-sha.d.mts @@ -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( diff --git a/scripts/full-release-validation-at-sha.mjs b/scripts/full-release-validation-at-sha.mjs index 09b3e7f5afb1..9c33e17a076a 100755 --- a/scripts/full-release-validation-at-sha.mjs +++ b/scripts/full-release-validation-at-sha.mjs @@ -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); diff --git a/scripts/github/find-reusable-release-validation.sh b/scripts/github/find-reusable-release-validation.sh index f7ffeb747e82..806e93bdf39a 100755 --- a/scripts/github/find-reusable-release-validation.sh +++ b/scripts/github/find-reusable-release-validation.sh @@ -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 diff --git a/scripts/release-candidate-checklist.d.mts b/scripts/release-candidate-checklist.d.mts index 75088a514b15..2cf5a38fa96c 100644 --- a/scripts/release-candidate-checklist.d.mts +++ b/scripts/release-candidate-checklist.d.mts @@ -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; export function buildReleaseCandidateState( options: unknown, diff --git a/scripts/release-candidate-checklist.mjs b/scripts/release-candidate-checklist.mjs index 94743d3a8825..dfdcfd2835b5 100644 --- a/scripts/release-candidate-checklist.mjs +++ b/scripts/release-candidate-checklist.mjs @@ -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, diff --git a/scripts/release-ci-summary.d.mts b/scripts/release-ci-summary.d.mts index c3b7159c1c3e..b183bd4e88bf 100644 --- a/scripts/release-ci-summary.d.mts +++ b/scripts/release-ci-summary.d.mts @@ -83,6 +83,7 @@ export function validateEvidenceReuseChain( currentManifest: unknown, selectedManifest: unknown, rootManifest: unknown, + compareCommits?: unknown, ): unknown; export function selectedChildKeys(parentJobs: unknown): Set; export function manifestChildEntries( diff --git a/scripts/release-ci-summary.mjs b/scripts/release-ci-summary.mjs index a5d526dc4823..5260627166be 100755 --- a/scripts/release-ci-summary.mjs +++ b/scripts/release-ci-summary.mjs @@ -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( diff --git a/scripts/validate-full-release-validation-evidence.mjs b/scripts/validate-full-release-validation-evidence.mjs index 683b6268d439..6dfe35e9eab1 100755 --- a/scripts/validate-full-release-validation-evidence.mjs +++ b/scripts/validate-full-release-validation-evidence.mjs @@ -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 diff --git a/test/scripts/find-reusable-release-validation.test.ts b/test/scripts/find-reusable-release-validation.test.ts index 45cf6d769335..df1c3b0db70e 100644 --- a/test/scripts/find-reusable-release-validation.test.ts +++ b/test/scripts/find-reusable-release-validation.test.ts @@ -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", () => { diff --git a/test/scripts/full-release-validation-at-sha.test.ts b/test/scripts/full-release-validation-at-sha.test.ts index 8ed1e68d1981..85f3a6ef61bb 100644 --- a/test/scripts/full-release-validation-at-sha.test.ts +++ b/test/scripts/full-release-validation-at-sha.test.ts @@ -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", () => { diff --git a/test/scripts/package-acceptance-workflow.test.ts b/test/scripts/package-acceptance-workflow.test.ts index f0e2e08d3ec8..d08cab8b43f2 100644 --- a/test/scripts/package-acceptance-workflow.test.ts +++ b/test/scripts/package-acceptance-workflow.test.ts @@ -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); diff --git a/test/scripts/plugin-prerelease-test-plan.test.ts b/test/scripts/plugin-prerelease-test-plan.test.ts index e65aa93c90d9..34f4f058e35f 100644 --- a/test/scripts/plugin-prerelease-test-plan.test.ts +++ b/test/scripts/plugin-prerelease-test-plan.test.ts @@ -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( diff --git a/test/scripts/release-candidate-checklist.test.ts b/test/scripts/release-candidate-checklist.test.ts index bb190a351cb1..f3c2b046b1ed 100644 --- a/test/scripts/release-candidate-checklist.test.ts +++ b/test/scripts/release-candidate-checklist.test.ts @@ -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([ diff --git a/test/scripts/release-ci-summary.test.ts b/test/scripts/release-ci-summary.test.ts index b07f09814e66..87d886bb6d6c 100644 --- a/test/scripts/release-ci-summary.test.ts +++ b/test/scripts/release-ci-summary.test.ts @@ -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", ); }); diff --git a/test/scripts/release-no-push-workflow.test.ts b/test/scripts/release-no-push-workflow.test.ts index b19575da14fe..05a69ea0606b 100644 --- a/test/scripts/release-no-push-workflow.test.ts +++ b/test/scripts/release-no-push-workflow.test.ts @@ -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({ diff --git a/test/scripts/validate-full-release-validation-evidence.test.ts b/test/scripts/validate-full-release-validation-evidence.test.ts index 660ceb8f22a0..c5476ec9e0da 100644 --- a/test/scripts/validate-full-release-validation-evidence.test.ts +++ b/test/scripts/validate-full-release-validation-evidence.test.ts @@ -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", () => {