ci(release): harden release controls

One-time maintainer-authorized bootstrap merge for the release-gate verifier policy. Exact hosted CI and all supporting workflow gates passed on 66133de419.
This commit is contained in:
Vincent Koc 2026-06-18 03:11:20 +08:00 committed by GitHub
parent 3a570f1410
commit abb6f04e0c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
88 changed files with 3403 additions and 449 deletions

View file

@ -24,6 +24,25 @@ 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.
- 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:
stop and use the repo remote changed gate or a full task worktree. When the
inputs are present and a release fix changes `package.json` or
`pnpm-lock.yaml`, rebuild only the task-owned disposable box with
`CI=true pnpm install --frozen-lockfile`, then run an explicit
`require.resolve()` probe before Docker or focused tests. The CI flag permits
pnpm to recreate a prewarmed modules directory without an interactive
confirmation. Do not weaken the lockfile or label sparse-checkout failures
as product/Docker failures.
- If the candidate is rebased or its base SHA changes after warmup, stop the
task-owned box and warm a fresh one before testing. Testbox source sync is
relative to the warmed source tree; continuing can mix an old base file with
a new candidate diff and produce false lockfile or Docker failures.
- For a committed release candidate, warm the box with
`blacksmith testbox warmup ... --ref <candidate-branch-or-sha>`. Do not rely
on source sync to overlay committed branch changes onto the workflow's
default ref.
## Preflight
@ -57,7 +76,7 @@ gh workflow run openclaw-performance.yml \
-f repeat=3 \
-f deep_profile=false \
-f live_openai_candidate=false \
-f fail_on_regression=false
-f fail_on_regression=true
```
- Do not wait for full release validation to start this early perf signal.
@ -66,8 +85,9 @@ gh workflow run openclaw-performance.yml \
- Call out any regression in the release proof. Treat a major regression as a
release blocker until it is fixed, waived by the operator, or proven to be
infrastructure noise.
- Full Release Validation also records advisory product-performance evidence;
the early standalone run is for overlap and faster regression discovery.
- Full Release Validation records blocking product-performance evidence. The
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:
@ -89,7 +109,7 @@ gh workflow run full-release-validation.yml \
-f rerun_group=all
```
Use `release_profile=stable` unless the operator explicitly asks for the broad advisory provider/media matrix. Use narrow `rerun_group` after focused fixes.
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.
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.
@ -125,6 +145,19 @@ Stop watchers before ending the turn or switching strategy.
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.
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:
`gh workflow run ci.yml --repo openclaw/openclaw --ref <pr-head-branch> -f
target_ref=<full-pr-sha> -f include_android=true -f release_gate=true`.
It runs on GitHub-hosted runners and is accepted only when its run title is
`CI release gate <full-pr-sha>`. Record the stalled Blacksmith run and the
fallback run in release evidence.
If `Blacksmith Build Artifacts Testbox` is the only remaining required gate
and remains queued without a runner, that completed exact fallback may cover
it because CI's `build-artifacts` job already builds, packages, and smoke
tests the artifacts. Do not use this coverage after the artifact workflow
starts or completes non-successfully.
## Evidence

View file

@ -17,6 +17,10 @@ Use this skill for release and publish-time workflow. Load `$release-private` if
- This skill should be sufficient to drive the normal release flow end-to-end.
- Use the private maintainer release docs for credentials, recovery steps, and mac signing/notary specifics, and use `docs/reference/RELEASING.md` for public policy.
- Core `openclaw` publish is manual `workflow_dispatch`; creating or pushing a tag does not publish by itself.
- Do not edit the root `README.md` as release prep, release closeout, or a
substitute for release notes. Package-root README validation is a hard
packaging gate, but a release only changes README content when an actual
user-facing documentation contract changed.
- Normal release work happens on a branch cut from `main`, not directly on
`main`. Use `release/YYYY.M.PATCH` for the branch name.
- If the operator asks for a release without saying stable/full, default to
@ -76,6 +80,44 @@ Use this skill for release and publish-time workflow. Load `$release-private` if
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.
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.
- Keep the canonical `scripts/pr` runner authoritative for prepare and merge
artifacts. A release-gate policy change may use focused candidate tests and
exact-SHA hosted CI for proof, but never route `prepare-*` or `merge-*`
through PR-controlled scripts or synthesize prepare artifacts to bootstrap
the change. If the current canonical gate cannot validate the new policy,
stop for explicit maintainer direction rather than weakening that boundary.
- In maintainer Testbox mode, use `OPENCLAW_TESTBOX=1 scripts/pr prepare-run
<PR>` only after the exact PR head has passed `CI` and every scheduled
hosted gate. For a workflow change, that means `Blacksmith Testbox`,
`Blacksmith ARM Testbox`, `Blacksmith Build Artifacts Testbox`, and
`Workflow Sanity`; only gates GitHub actually scheduled for that exact head
are required. This preserves the canonical prepare artifacts while avoiding
a redundant broad local suite. A
literal `CHANGELOG.md`-only head gets a clean diff check instead because
those workflows intentionally do not dispatch. Documentation and README
changes still require CI. If `merge-run` requires a mainline sync, run
`OPENCLAW_TESTBOX=1 scripts/pr prepare-sync-head <PR>`, wait for those hosted
gates on the newly pushed SHA, then run `prepare-run` again.
- If an exact PR-head CI run has no active jobs because Blacksmith capacity is
stalled, a maintainer may dispatch the explicit GitHub-hosted fallback from
the PR head branch:
`gh workflow run ci.yml --repo openclaw/openclaw --ref <pr-head-branch> -f
target_ref=<full-pr-sha> -f include_android=true -f release_gate=true`.
Use it only for an observed provider queue stall, never for failed CI or as a
routine shortcut. The run must be named `CI release gate <full-pr-sha>` and
pass on that exact SHA; the native hosted-gate verifier rejects generic manual
CI runs. If `Blacksmith Build Artifacts Testbox` is the only remaining
required gate and it is still queued without a runner, the same completed
fallback CI may cover it because its `build-artifacts` job builds, packages,
and smoke tests those artifacts. The verifier records that coverage. Never
use this coverage when the artifact workflow has started, failed, been
cancelled, or been skipped. Then rerun `OPENCLAW_TESTBOX=1 scripts/pr
prepare-run <PR>`.
- Generate the changelog before every beta, beta rerun, stable release, or
stable rerun, before version/tag preparation. Use
`$openclaw-changelog-update` for the rewrite. Do not continue release prep if
@ -119,6 +161,14 @@ Stable publication is not complete until `main` carries the actual shipped relea
`OPENCLAW_TESTBOX=1 pnpm check:changed`. Push, then verify `origin/main`
contains the shipped version and changelog before calling the stable release
done.
6. Keep repository variables `RELEASE_ROLLBACK_DRILL_ID` and
`RELEASE_ROLLBACK_DRILL_DATE` current after each private rollback drill.
`openclaw-stable-main-closeout.yml` starts from the `main` push carrying the
shipped version, changelog, and appcast after stable publication, then binds
immutable evidence to the published tag. Do not declare stable complete
until it writes the immutable closeout manifest to the GitHub release. The
drill must be within 90 days; manual dispatch is only for repair/replay, and
private rollback commands remain in the maintainer-only runbook.
## Handle versions and release files consistently

View file

@ -29,11 +29,17 @@ publish skill; use `$release-openclaw-maintainer` before changing release state.
- Confirm release body has npm, CI, plugin npm, ClawHub, mac/appcast evidence
links when expected.
- Confirm assets expected for stable mac releases are uploaded: zip, dmg,
dSYM, dependency evidence when present.
dSYM, dependency evidence, immutable full-validation manifest,
postpublish evidence, and stable-main closeout manifest.
- Download each immutable evidence asset and its `.sha256` companion, then
verify the checksum before trusting the release record.
2. Root npm:
- `npm view openclaw@<VERSION> version dist-tags.latest dist.tarball dist.integrity time.<VERSION> --json`
- `latest` must equal `<VERSION>` for stable.
- Record tarball, integrity, publish time.
- Confirm the release postpublish evidence records
`npmRegistrySignaturesVerified: true` and
`npmProvenanceAttestationMatched: true`.
3. Plugin publish set:
- Get exact tag metadata from GitHub, not the local checkout when dirty:
download `https://api.github.com/repos/openclaw/openclaw/tarball/v<VERSION>`
@ -57,6 +63,9 @@ publish skill; use `$release-openclaw-maintainer` before changing release state.
Full Release Validation, OpenClaw Release Checks, OpenClaw NPM Release,
Plugin NPM Release, Plugin ClawHub Release, mac preflight/validation/publish
when stable mac assets are expected.
- For stable, verify `OpenClaw Stable Main Closeout` succeeded and its
manifest records the matching release tag, current rollback drill, stable
soak, and blocking performance evidence.
- Summarize only relevant successful/failed jobs; ignore routine skipped
optional lanes unless the release body promised them.
6. Published package smoke:

View file

@ -113,7 +113,7 @@ runs:
- name: Download OpenClaw Docker E2E package
if: inputs.hydrate-artifacts == 'true' && steps.plan.outputs.needs_package == '1'
uses: actions/download-artifact@v8
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: ${{ inputs.package-artifact-name }}
path: .artifacts/docker-e2e-package

View file

@ -139,7 +139,7 @@ runs:
- name: Save pnpm store cache
if: ${{ inputs.install-deps == 'true' && inputs.use-actions-cache == 'true' && inputs.save-actions-cache == 'true' && runner.os != 'Windows' && steps.setup-pnpm.outputs.store-cache-hit != 'true' }}
uses: actions/cache/save@v5
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: ${{ steps.setup-pnpm.outputs.store-path }}
key: ${{ steps.setup-pnpm.outputs.store-cache-primary-key }}

View file

@ -92,7 +92,7 @@ runs:
- name: Restore pnpm store cache
id: pnpm-store-cache
if: ${{ inputs.use-actions-cache == 'true' && runner.os != 'Windows' }}
uses: actions/cache/restore@v5
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: ${{ steps.pnpm-store.outputs.path }}
key: pnpm-store-${{ runner.os }}-${{ runner.arch }}-${{ inputs.node-version }}-${{ hashFiles(inputs.package-manager-file) }}-${{ hashFiles(inputs.lockfile-path) }}

View file

@ -25,24 +25,24 @@ jobs:
pull-requests: write
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ github.sha }}
persist-credentials: false
- uses: actions/create-github-app-token@v3
- uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3
id: app-token
continue-on-error: true
with:
app-id: "2729701"
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- uses: actions/create-github-app-token@v3
- uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3
id: app-token-fallback
if: steps.app-token.outcome == 'failure'
with:
app-id: "2971289"
private-key: ${{ secrets.GH_APP_PRIVATE_KEY_FALLBACK }}
- name: Run Barnacle auto-response
uses: actions/github-script@v9
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
github-token: ${{ steps.app-token.outputs.token || steps.app-token-fallback.outputs.token }}
script: |

View file

@ -140,7 +140,7 @@ jobs:
- name: Restore dist build cache
id: dist-cache
uses: actions/cache/restore@v5
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: |
.artifacts/build-all-cache/
@ -175,7 +175,7 @@ jobs:
- name: Save dist build cache
if: steps.dist-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v5
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: |
.artifacts/build-all-cache/

View file

@ -13,6 +13,11 @@ on:
required: false
default: false
type: boolean
release_gate:
description: Run an exact-SHA maintainer release-gate fallback when PR CI is capacity-stalled.
required: false
default: false
type: boolean
push:
branches: [main]
paths-ignore:
@ -26,6 +31,8 @@ on:
permissions:
contents: read
run-name: ${{ github.event_name == 'workflow_dispatch' && inputs.release_gate && format('CI release gate {0}', inputs.target_ref) || 'CI' }}
concurrency:
group: ${{ github.event_name == 'workflow_dispatch' && format('{0}-manual-v1-{1}', github.workflow, github.run_id) || (github.event_name == 'pull_request' && format('{0}-v7-{1}', github.workflow, github.event.pull_request.number) || (github.repository == 'openclaw/openclaw' && format('{0}-v7-{1}', github.workflow, github.ref) || format('{0}-v7-{1}-{2}', github.workflow, github.ref, github.sha))) }}
cancel-in-progress: ${{ github.event_name == 'pull_request' || (github.event_name == 'push' && github.repository == 'openclaw/openclaw' && github.ref == 'refs/heads/main') }}
@ -75,6 +82,23 @@ jobs:
run_android_job: ${{ steps.manifest.outputs.run_android_job }}
android_matrix: ${{ steps.manifest.outputs.android_matrix }}
steps:
- name: Validate release-gate dispatch
if: github.event_name == 'workflow_dispatch' && inputs.release_gate
env:
TARGET_REF: ${{ inputs.target_ref }}
run: |
set -euo pipefail
if [[ ! "$TARGET_REF" =~ ^[0-9a-f]{40}$ ]]; then
echo "release_gate requires target_ref to be a full commit SHA" >&2
exit 1
fi
if [[ "$GITHUB_SHA" != "$TARGET_REF" ]]; then
echo "release_gate must run from the branch at target_ref" >&2
exit 1
fi
- name: Checkout
env:
CHECKOUT_REPO: ${{ github.repository }}
@ -159,7 +183,7 @@ jobs:
OPENCLAW_CI_DOCS_CHANGED: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.docs_scope.outputs.docs_changed }}
OPENCLAW_CI_RUN_NODE: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_node || 'false' }}
OPENCLAW_CI_RUN_MACOS: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_macos || 'false' }}
OPENCLAW_CI_RUN_ANDROID: ${{ github.event_name == 'workflow_dispatch' && inputs.include_android && 'true' || steps.changed_scope.outputs.run_android || 'false' }}
OPENCLAW_CI_RUN_ANDROID: ${{ github.event_name == 'workflow_dispatch' && (inputs.release_gate || inputs.include_android) && 'true' || steps.changed_scope.outputs.run_android || 'false' }}
OPENCLAW_CI_RUN_WINDOWS: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_windows || 'false' }}
OPENCLAW_CI_RUN_NODE_FAST_ONLY: ${{ github.event_name == 'workflow_dispatch' && 'false' || steps.changed_scope.outputs.run_node_fast_only || 'false' }}
OPENCLAW_CI_RUN_NODE_FAST_PLUGIN_CONTRACTS: ${{ github.event_name == 'workflow_dispatch' && 'false' || steps.changed_scope.outputs.run_node_fast_plugin_contracts || 'false' }}
@ -598,7 +622,7 @@ jobs:
install-bun: "false"
- name: Restore build-all step cache
uses: actions/cache@v5
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: .artifacts/build-all-cache
key: ${{ runner.os }}-build-all-v3-${{ hashFiles('package.json', 'pnpm-lock.yaml', 'npm-shrinkwrap.json', 'packages/plugin-sdk/package.json', 'packages/llm-core/package.json', 'packages/model-catalog-core/package.json', 'packages/memory-host-sdk/package.json', 'scripts/build-all.mjs', 'scripts/write-plugin-sdk-entry-dts.ts', 'scripts/lib/plugin-sdk-entries.mjs', 'tsconfig.json', 'tsconfig.plugin-sdk.dts.json', 'src/plugin-sdk/**', 'packages/llm-core/src/**', 'packages/model-catalog-core/src/**', 'packages/memory-host-sdk/src/**', 'src/types/**', 'src/video-generation/dashscope-compatible.ts', 'src/video-generation/types.ts', 'scripts/copy-export-html-templates.ts', 'scripts/lib/copy-assets.ts', 'src/auto-reply/reply/export-html/**') }}
@ -607,7 +631,7 @@ jobs:
- name: Restore dist build cache
id: dist_build_cache
uses: actions/cache/restore@v5
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: |
dist/
@ -630,14 +654,14 @@ jobs:
run: tar --posix -cf dist-runtime-build.tar.zst --use-compress-program zstdmt dist dist-runtime
- name: Upload built runtime artifacts
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: dist-runtime-build
path: dist-runtime-build.tar.zst
retention-days: 1
- name: Upload bundled plugin asset artifacts
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: bundled-plugin-assets
path: |
@ -668,7 +692,7 @@ jobs:
- name: Upload startup memory report
if: always()
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: startup-memory
path: .artifacts/startup-memory/
@ -757,7 +781,7 @@ jobs:
- name: Save dist build cache
if: steps.dist_build_cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v5
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
continue-on-error: true
with:
path: |
@ -769,7 +793,7 @@ jobs:
- name: Upload gateway watch regression artifacts
if: always() && needs.preflight.outputs.run_check_additional == 'true'
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: gateway-watch-regression
path: .local/gateway-watch-regression/
@ -1339,7 +1363,7 @@ jobs:
- name: Upload deadcode reports
if: ${{ always() && matrix.task == 'dependencies' }}
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: deadcode-reports
path: .artifacts/deadcode
@ -1428,7 +1452,7 @@ jobs:
- name: Cache extension package boundary artifacts
id: extension-package-boundary-cache
if: matrix.group == 'extension-package-boundary'
uses: actions/cache@v5
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: |
dist/plugin-sdk
@ -1696,7 +1720,7 @@ jobs:
git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout
- name: Setup Python
uses: actions/setup-python@v6
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: "3.12"
@ -1965,7 +1989,7 @@ jobs:
echo "key=$toolchain_key" >> "$GITHUB_OUTPUT"
- name: Cache SwiftPM
uses: actions/cache@v5
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: ~/Library/Caches/org.swift.swiftpm
key: ${{ runner.os }}-swiftpm-${{ hashFiles('apps/macos/Package.resolved') }}
@ -1974,7 +1998,7 @@ jobs:
- name: Cache Swift build directory
id: swift-build-cache
uses: actions/cache@v5
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: apps/macos/.build
key: ${{ runner.os }}-swift-build-v2-${{ steps.swift-toolchain.outputs.key }}-${{ hashFiles('apps/macos/Package.swift', 'apps/macos/Package.resolved', 'apps/macos/Sources/**', 'apps/macos/Tests/**', 'apps/shared/OpenClawKit/Package.swift', 'apps/shared/OpenClawKit/Sources/**', 'apps/swabble/Package.swift', 'apps/swabble/Sources/**') }}
@ -2105,7 +2129,7 @@ jobs:
exit 1
- name: Setup Java
uses: actions/setup-java@v5
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5
with:
distribution: temurin
# Keep sdkmanager on the stable JDK path for Linux CI runners.
@ -2117,7 +2141,7 @@ jobs:
apps/android/gradle/libs.versions.toml
- name: Cache Android SDK
uses: actions/cache@v5
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: ~/.android-sdk
key: ${{ runner.os }}-android-sdk-v1-cmdline-14742923-platform-37.0-build-tools-36.0.0
@ -2204,7 +2228,7 @@ jobs:
timeout-minutes: 5
steps:
- name: Checkout timing summary helper
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || needs.preflight.outputs.checkout_revision || github.sha }}
fetch-depth: 1
@ -2220,7 +2244,7 @@ jobs:
cat ci-timings-summary.txt >> "$GITHUB_STEP_SUMMARY"
- name: Upload CI timing summary
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: ci-timings-summary
path: ci-timings-summary.txt

View file

@ -35,7 +35,7 @@ jobs:
locales_json: ${{ steps.plan.outputs.locales_json }}
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
fetch-depth: 0
persist-credentials: false
@ -112,7 +112,7 @@ jobs:
name: Refresh ${{ matrix.locale }}
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: true
submodules: false

View file

@ -45,12 +45,12 @@ jobs:
runs-on: [self-hosted, "${{ inputs.crabbox_runner_label }}"]
timeout-minutes: 120
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ inputs.ref || github.ref }}
- name: Setup Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: "24"
@ -328,12 +328,12 @@ jobs:
runs-on: [self-hosted, "${{ inputs.crabbox_runner_label }}"]
timeout-minutes: 120
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ inputs.ref || github.ref }}
- name: Setup Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: "24"
@ -561,7 +561,7 @@ jobs:
runs-on: [self-hosted, "${{ inputs.crabbox_runner_label }}"]
timeout-minutes: 120
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ inputs.ref || github.ref }}

View file

@ -49,7 +49,7 @@ jobs:
fi
- name: Checkout selected tag
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: refs/tags/${{ inputs.tag }}
fetch-depth: 0
@ -83,7 +83,7 @@ jobs:
browser_digest: ${{ steps.build-browser.outputs.digest }}
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/{0}', inputs.tag) || github.ref }}
fetch-depth: 0
@ -293,7 +293,7 @@ jobs:
browser_digest: ${{ steps.build-browser.outputs.digest }}
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/{0}', inputs.tag) || github.ref }}
fetch-depth: 0
@ -500,7 +500,7 @@ jobs:
contents: read
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/{0}', inputs.tag) || github.ref }}
fetch-depth: 0
@ -595,7 +595,7 @@ jobs:
packages: read
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
fetch-depth: 1

View file

@ -33,7 +33,7 @@ jobs:
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: main
fetch-depth: 0

View file

@ -25,13 +25,13 @@ jobs:
- name: Checkout source repo
if: env.OPENCLAW_DOCS_SYNC_TOKEN != ''
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
fetch-depth: 0
- name: Checkout ClawHub docs source
if: env.OPENCLAW_DOCS_SYNC_TOKEN != ''
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
repository: openclaw/clawhub
path: clawhub-source
@ -41,7 +41,7 @@ jobs:
- name: Setup Node
if: env.OPENCLAW_DOCS_SYNC_TOKEN != ''
uses: actions/setup-node@v6
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: "24.x"

View file

@ -24,7 +24,7 @@ jobs:
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
fetch-depth: 1
fetch-tags: false
@ -37,7 +37,7 @@ jobs:
install-bun: "false"
- name: Checkout ClawHub docs source
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
repository: openclaw/clawhub
path: clawhub-source

View file

@ -35,8 +35,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- name: Close confirmed duplicates
env:
APPLY: ${{ inputs.apply }}

View file

@ -36,7 +36,7 @@ on:
- stable
- full
run_release_soak:
description: Run exhaustive live/Docker and upgrade-survivor soak lanes; forced on for release_profile=full
description: Run exhaustive live/Docker and upgrade-survivor soak lanes; forced on for stable and full release profiles
required: false
default: false
type: boolean
@ -130,7 +130,7 @@ jobs:
sha: ${{ steps.resolve.outputs.sha }}
steps:
- name: Checkout trusted workflow helper
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ github.ref_name }}
path: workflow
@ -158,7 +158,7 @@ jobs:
PACKAGE_ACCEPTANCE_PACKAGE_SPEC: ${{ inputs.package_acceptance_package_spec }}
CODEX_PLUGIN_SPEC: ${{ inputs.codex_plugin_spec }}
RELEASE_PROFILE: ${{ inputs.release_profile }}
RUN_RELEASE_SOAK: ${{ inputs.run_release_soak || inputs.release_profile == 'full' }}
RUN_RELEASE_SOAK: ${{ inputs.run_release_soak || inputs.release_profile == 'stable' || inputs.release_profile == 'full' }}
RERUN_GROUP: ${{ inputs.rerun_group }}
LIVE_SUITE_FILTER: ${{ inputs.live_suite_filter }}
CROSS_OS_SUITE_FILTER: ${{ inputs.cross_os_suite_filter }}
@ -234,7 +234,7 @@ jobs:
contents: read
steps:
- name: Checkout target SHA
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ needs.resolve_target.outputs.sha }}
fetch-depth: 1
@ -537,7 +537,7 @@ jobs:
PROVIDER: ${{ inputs.provider }}
MODE: ${{ inputs.mode }}
RELEASE_PROFILE: ${{ inputs.release_profile }}
RUN_RELEASE_SOAK: ${{ inputs.run_release_soak || inputs.release_profile == 'full' }}
RUN_RELEASE_SOAK: ${{ inputs.run_release_soak || inputs.release_profile == 'stable' || inputs.release_profile == 'full' }}
RERUN_GROUP: ${{ inputs.rerun_group }}
LIVE_SUITE_FILTER: ${{ inputs.live_suite_filter }}
CROSS_OS_SUITE_FILTER: ${{ inputs.cross_os_suite_filter }}
@ -780,7 +780,7 @@ jobs:
source_sha: ${{ steps.package.outputs.source_sha }}
steps:
- name: Checkout trusted workflow ref
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: true
ref: ${{ github.ref_name }}
@ -826,7 +826,7 @@ jobs:
} >> "$GITHUB_STEP_SUMMARY"
- name: Upload release package artifact
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: release-package-under-test
path: |
@ -1017,9 +1017,12 @@ jobs:
echo "- Repeat: \`3\`"
echo "- Deep profile: \`false\`"
echo "- Live OpenAI candidate: \`false\`"
echo "- Release impact: advisory"
echo "- Release impact: blocking"
} >> "$GITHUB_STEP_SUMMARY"
dispatch_id="full-release-validation-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
dispatch_run_name="OpenClaw Performance ${dispatch_id}"
dispatch_output="$(gh_with_retry workflow run openclaw-performance.yml \
--ref "$CHILD_WORKFLOW_REF" \
-f target_ref="$TARGET_SHA" \
@ -1027,17 +1030,27 @@ jobs:
-f repeat=3 \
-f deep_profile=false \
-f live_openai_candidate=false \
-f fail_on_regression=false)"
-f fail_on_regression=true \
-f dispatch_id="$dispatch_id")"
printf '%s\n' "$dispatch_output"
run_id="$(
printf '%s\n' "$dispatch_output" |
sed -nE 's#.*actions/runs/([0-9]+).*#\1#p' |
tail -n 1
)"
run_id=""
for _ in $(seq 1 60); do
run_id="$(
DISPATCH_RUN_NAME="$dispatch_run_name" gh_with_retry api -X GET "repos/${GITHUB_REPOSITORY}/actions/workflows/openclaw-performance.yml/runs" \
-F event=workflow_dispatch \
-F per_page=100 \
--jq '.workflow_runs | map(select(.display_title == env.DISPATCH_RUN_NAME)) | sort_by(.created_at) | reverse | .[0].id // empty'
)"
if [[ -n "$run_id" ]]; then
break
fi
sleep 5
done
if [[ -z "$run_id" ]]; then
echo "::warning::gh workflow run openclaw-performance.yml did not return an Actions run URL; refusing to guess from recent workflow_dispatch runs."
exit 0
echo "::error::Could not find dispatched run for ${dispatch_run_name}." >&2
exit 1
fi
echo "Dispatched openclaw-performance.yml: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
@ -1072,8 +1085,9 @@ jobs:
echo "url=${url}" >> "$GITHUB_OUTPUT"
echo "conclusion=${conclusion}" >> "$GITHUB_OUTPUT"
if [[ "$conclusion" != "success" ]]; then
echo "::warning::OpenClaw Performance is advisory and ended with ${conclusion}: ${url}"
echo "::error::OpenClaw Performance ended with ${conclusion}: ${url}"
gh_with_retry run view "$run_id" --json jobs --jq '.jobs[] | select(.conclusion != "success" and .conclusion != "skipped") | {name, conclusion, url}' || true
exit 1
fi
summary:
@ -1364,6 +1378,7 @@ jobs:
normal_ci_required=0
plugin_prerelease_required=0
release_checks_required=0
performance_required=0
if [[ "$RERUN_GROUP" == "all" && "$DOCKER_RUNTIME_ASSETS_PREFLIGHT_RESULT" != "success" ]]; then
echo "::error::Docker runtime-assets preflight ended with ${DOCKER_RUNTIME_ASSETS_PREFLIGHT_RESULT}."
failed=1
@ -1371,6 +1386,7 @@ jobs:
normal_ci_required=1
plugin_prerelease_required=1
release_checks_required=1
performance_required=1
else
case "$RERUN_GROUP" in
ci)
@ -1382,6 +1398,9 @@ jobs:
release-checks|install-smoke|cross-os|live-e2e|package|qa|qa-parity|qa-live)
release_checks_required=1
;;
performance)
performance_required=1
;;
esac
fi
@ -1415,6 +1434,12 @@ jobs:
check_child "npm_telegram" "$NPM_TELEGRAM_RUN_ID" 1 || failed=1
fi
if [[ "$PERFORMANCE_RESULT" == "skipped" && -z "${PERFORMANCE_RUN_ID// }" ]]; then
check_child "product_performance" "" "$performance_required" || failed=1
else
check_child "product_performance" "$PERFORMANCE_RUN_ID" "$performance_required" || failed=1
fi
summarize_child_timing "normal_ci" "$NORMAL_CI_RUN_ID"
summarize_child_timing "plugin_prerelease" "$PLUGIN_PRERELEASE_RUN_ID"
summarize_child_timing "release_checks" "$RELEASE_CHECKS_RUN_ID"
@ -1426,6 +1451,7 @@ jobs:
summarize_failed_child "plugin_prerelease" "$PLUGIN_PRERELEASE_RUN_ID"
summarize_failed_child "release_checks" "$RELEASE_CHECKS_RUN_ID"
summarize_failed_child "npm_telegram" "$NPM_TELEGRAM_RUN_ID"
summarize_failed_child "product_performance" "$PERFORMANCE_RUN_ID"
fi
exit "$failed"
@ -1512,12 +1538,13 @@ jobs:
TARGET_SHA: ${{ needs.resolve_target.outputs.sha }}
RELEASE_PROFILE: ${{ inputs.release_profile }}
RERUN_GROUP: ${{ inputs.rerun_group }}
RUN_RELEASE_SOAK: ${{ inputs.run_release_soak || inputs.release_profile == 'full' }}
RUN_RELEASE_SOAK: ${{ inputs.run_release_soak || inputs.release_profile == 'stable' || inputs.release_profile == 'full' }}
NORMAL_CI_RUN_ID: ${{ needs.normal_ci.outputs.run_id }}
PLUGIN_PRERELEASE_RUN_ID: ${{ needs.plugin_prerelease.outputs.run_id }}
RELEASE_CHECKS_RUN_ID: ${{ needs.release_checks.outputs.run_id }}
NPM_TELEGRAM_RUN_ID: ${{ needs.npm_telegram.outputs.run_id }}
PERFORMANCE_RUN_ID: ${{ needs.performance.outputs.run_id }}
PERFORMANCE_CONCLUSION: ${{ needs.performance.outputs.conclusion }}
run: |
set -euo pipefail
manifest_dir="${RUNNER_TEMP}/full-release-validation"
@ -1537,8 +1564,9 @@ jobs:
--arg releaseChecksRunId "$RELEASE_CHECKS_RUN_ID" \
--arg npmTelegramRunId "$NPM_TELEGRAM_RUN_ID" \
--arg performanceRunId "$PERFORMANCE_RUN_ID" \
--arg performanceConclusion "$PERFORMANCE_CONCLUSION" \
'{
version: 1,
version: 2,
workflowName: $workflowName,
runId: $runId,
runAttempt: $runAttempt,
@ -1548,18 +1576,26 @@ jobs:
releaseProfile: $releaseProfile,
rerunGroup: $rerunGroup,
runReleaseSoak: $runReleaseSoak,
controls: {
stableSoakRequired: ($releaseProfile == "stable" or $releaseProfile == "full"),
performanceBlocking: true
},
childRuns: {
normalCi: $normalCiRunId,
pluginPrerelease: $pluginPrereleaseRunId,
releaseChecks: $releaseChecksRunId,
npmTelegram: $npmTelegramRunId,
productPerformance: $performanceRunId
productPerformance: {
runId: $performanceRunId,
conclusion: $performanceConclusion,
blocking: true
}
}
}' > "${manifest_dir}/full-release-validation-manifest.json"
- name: Upload release validation manifest
if: ${{ success() }}
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: full-release-validation-${{ github.run_id }}
path: ${{ runner.temp }}/full-release-validation

View file

@ -56,7 +56,7 @@ jobs:
dockerfile_image: ${{ steps.manifest.outputs.dockerfile_image }}
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ inputs.ref || github.ref }}
fetch-depth: 1
@ -106,7 +106,7 @@ jobs:
DOCKER_BUILD_RECORD_UPLOAD: "false"
steps:
- name: Checkout CLI
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ inputs.ref || github.ref }}
persist-credentials: false
@ -217,7 +217,7 @@ jobs:
DOCKER_BUILD_RECORD_UPLOAD: "false"
steps:
- name: Checkout CLI
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ inputs.ref || github.ref }}
persist-credentials: false
@ -289,7 +289,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Checkout CLI
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ inputs.ref || github.ref }}
persist-credentials: false
@ -305,7 +305,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Checkout CLI
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ inputs.ref || github.ref }}
persist-credentials: false
@ -411,7 +411,7 @@ jobs:
DOCKER_BUILD_RECORD_UPLOAD: "false"
steps:
- name: Checkout CLI
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ inputs.ref || github.ref }}
persist-credentials: false
@ -499,7 +499,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Checkout CLI
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ inputs.ref || github.ref }}
persist-credentials: false
@ -538,7 +538,7 @@ jobs:
DOCKER_BUILD_RECORD_UPLOAD: "false"
steps:
- name: Checkout CLI
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ inputs.ref || github.ref }}
persist-credentials: false

View file

@ -24,7 +24,7 @@ jobs:
github.event.workflow_run.name == 'iOS Periphery Dead Code'
steps:
- name: Upsert Periphery PR comment
uses: actions/github-script@v9
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
script: |
const fs = require("node:fs");

View file

@ -25,7 +25,7 @@ jobs:
steps:
- name: Detect changed paths
id: scope
uses: actions/github-script@v9
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
script: |
if (context.eventName === "workflow_dispatch") {
@ -65,7 +65,7 @@ jobs:
timeout-minutes: 45
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
fetch-depth: 1
fetch-tags: false
@ -216,7 +216,7 @@ jobs:
- name: Upload Periphery report
if: always()
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: ios-periphery-dead-code-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/ios-periphery

View file

@ -32,25 +32,25 @@ jobs:
pull-requests: write
runs-on: ubuntu-24.04
steps:
- uses: actions/create-github-app-token@v3
- uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3
id: app-token
continue-on-error: true
with:
app-id: "2729701"
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- uses: actions/create-github-app-token@v3
- uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3
id: app-token-fallback
if: steps.app-token.outcome == 'failure'
with:
app-id: "2971289"
private-key: ${{ secrets.GH_APP_PRIVATE_KEY_FALLBACK }}
- uses: actions/labeler@v6
- uses: actions/labeler@f27b608878404679385c85cfa523b85ccb86e213 # v6
with:
configuration-path: .github/labeler.yml
repo-token: ${{ steps.app-token.outputs.token || steps.app-token-fallback.outputs.token }}
sync-labels: true
- name: Apply PR size label
uses: actions/github-script@v9
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
github-token: ${{ steps.app-token.outputs.token || steps.app-token-fallback.outputs.token }}
script: |
@ -139,7 +139,7 @@ jobs:
labels: [targetSizeLabel],
});
- name: Apply maintainer or trusted-contributor label
uses: actions/github-script@v9
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
github-token: ${{ steps.app-token.outputs.token || steps.app-token-fallback.outputs.token }}
script: |
@ -210,7 +210,7 @@ jobs:
// });
// }
- name: Apply beta-blocker title label
uses: actions/github-script@v9
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
github-token: ${{ steps.app-token.outputs.token || steps.app-token-fallback.outputs.token }}
script: |
@ -263,7 +263,7 @@ jobs:
});
}
- name: Apply too-many-prs label
uses: actions/github-script@v9
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
github-token: ${{ steps.app-token.outputs.token || steps.app-token-fallback.outputs.token }}
script: |
@ -466,20 +466,20 @@ jobs:
pull-requests: write
runs-on: ubuntu-24.04
steps:
- uses: actions/create-github-app-token@v3
- uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3
id: app-token
continue-on-error: true
with:
app-id: "2729701"
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- uses: actions/create-github-app-token@v3
- uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3
id: app-token-fallback
if: steps.app-token.outcome == 'failure'
with:
app-id: "2971289"
private-key: ${{ secrets.GH_APP_PRIVATE_KEY_FALLBACK }}
- name: Backfill PR labels
uses: actions/github-script@v9
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
github-token: ${{ steps.app-token.outputs.token || steps.app-token-fallback.outputs.token }}
script: |
@ -765,20 +765,20 @@ jobs:
issues: write
runs-on: ubuntu-24.04
steps:
- uses: actions/create-github-app-token@v3
- uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3
id: app-token
continue-on-error: true
with:
app-id: "2729701"
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- uses: actions/create-github-app-token@v3
- uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3
id: app-token-fallback
if: steps.app-token.outcome == 'failure'
with:
app-id: "2971289"
private-key: ${{ secrets.GH_APP_PRIVATE_KEY_FALLBACK }}
- name: Apply maintainer or trusted-contributor label
uses: actions/github-script@v9
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
github-token: ${{ steps.app-token.outputs.token || steps.app-token-fallback.outputs.token }}
script: |
@ -849,7 +849,7 @@ jobs:
// });
// }
- name: Apply beta-blocker title label
uses: actions/github-script@v9
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
github-token: ${{ steps.app-token.outputs.token || steps.app-token-fallback.outputs.token }}
script: |

View file

@ -26,8 +26,7 @@ jobs:
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- name: Login to GHCR
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:

View file

@ -43,7 +43,7 @@ jobs:
fi
- name: Checkout selected tag
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: refs/tags/${{ inputs.tag }}
fetch-depth: 0

View file

@ -21,7 +21,7 @@ jobs:
MAINTAINER_COMMAND_REACTIONS: ${{ vars.MAINTAINER_COMMAND_REACTIONS || '/autoclose,/clawsweeper autoclose,/clawsweeper automerge,/merge,/land,/landpr' }}
steps:
- name: React to maintainer slash command
uses: actions/github-script@v9
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
script: |
const comment = context.payload.comment;

View file

@ -37,7 +37,7 @@ jobs:
steps:
- name: Require maintainer-level repository access
id: permission
uses: actions/github-script@v9
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
script: |
const allowed = new Set(["admin", "maintain", "write"]);
@ -68,7 +68,7 @@ jobs:
trusted_reason: ${{ steps.validate.outputs.trusted_reason }}
steps:
- name: Checkout selected ref
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
ref: ${{ inputs.ref }}
@ -131,7 +131,7 @@ jobs:
environment: qa-live-shared
steps:
- name: Checkout selected ref
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
ref: ${{ needs.validate_selected_ref.outputs.selected_revision }}
@ -166,7 +166,7 @@ jobs:
- name: Upload Mantis artifacts
if: always()
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: mantis-discord-smoke-${{ github.run_id }}-${{ github.run_attempt }}
path: .artifacts/qa-e2e/mantis/

View file

@ -56,7 +56,7 @@ jobs:
steps:
- name: Require maintainer-level repository access
id: permission
uses: actions/github-script@v9
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
script: |
const allowed = new Set(["admin", "maintain", "write"]);
@ -91,7 +91,7 @@ jobs:
steps:
- name: Resolve refs and target PR
id: resolve
uses: actions/github-script@v9
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
script: |
const defaultBaseline = "0bf06e953fdda290799fc9fb9244a8f67fdae593";
@ -179,7 +179,7 @@ jobs:
candidate_revision: ${{ steps.validate.outputs.candidate_revision }}
steps:
- name: Checkout harness ref
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
fetch-depth: 0
@ -245,7 +245,7 @@ jobs:
environment: qa-live-shared
steps:
- name: Checkout harness ref
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
fetch-depth: 0
@ -260,7 +260,7 @@ jobs:
run: pnpm build
- name: Setup Go for Crabbox CLI
uses: actions/setup-go@v6
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
with:
go-version: "1.26.x"
cache: false
@ -535,7 +535,7 @@ jobs:
- name: Upload Mantis status reaction artifacts
id: upload_artifact
if: ${{ always() && steps.run_mantis.outputs.output_dir != '' }}
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: mantis-discord-status-reactions-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ steps.run_mantis.outputs.output_dir }}
@ -545,7 +545,7 @@ jobs:
- name: Create Mantis GitHub App token
id: mantis_app_token
if: ${{ always() && needs.resolve_request.outputs.pr_number != '' }}
uses: actions/create-github-app-token@v3
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3
with:
app-id: ${{ secrets.MANTIS_GITHUB_APP_ID }}
private-key: ${{ secrets.MANTIS_GITHUB_APP_PRIVATE_KEY }}
@ -590,7 +590,7 @@ jobs:
issues: write
steps:
- name: Remove workflow eyes reaction
uses: actions/github-script@v9
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
script: |
const { owner, repo } = context.repo;

View file

@ -56,7 +56,7 @@ jobs:
steps:
- name: Require maintainer-level repository access
id: permission
uses: actions/github-script@v9
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
script: |
const allowed = new Set(["admin", "maintain", "write"]);
@ -91,7 +91,7 @@ jobs:
steps:
- name: Resolve refs and target PR
id: resolve
uses: actions/github-script@v9
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
script: |
const defaultBaseline = "synthetic-reverted-thread-filepath-fix";
@ -177,7 +177,7 @@ jobs:
candidate_revision: ${{ steps.validate.outputs.candidate_revision }}
steps:
- name: Checkout harness ref
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
fetch-depth: 0
@ -235,7 +235,7 @@ jobs:
output_dir: ${{ steps.run_mantis.outputs.output_dir }}
steps:
- name: Checkout harness ref
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
fetch-depth: 0
@ -250,7 +250,7 @@ jobs:
run: pnpm build
- name: Setup Go for Crabbox CLI
uses: actions/setup-go@v6
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
with:
go-version: "1.26.x"
cache: false
@ -543,7 +543,7 @@ jobs:
- name: Upload Mantis thread attachment artifacts
id: upload_artifact
if: ${{ always() && steps.run_mantis.outputs.output_dir != '' }}
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: mantis-discord-thread-attachment-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ steps.run_mantis.outputs.output_dir }}
@ -553,7 +553,7 @@ jobs:
- name: Create Mantis GitHub App token
id: mantis_app_token
if: ${{ always() && needs.resolve_request.outputs.pr_number != '' }}
uses: actions/create-github-app-token@v3
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3
with:
app-id: ${{ secrets.MANTIS_GITHUB_APP_ID }}
private-key: ${{ secrets.MANTIS_GITHUB_APP_PRIVATE_KEY }}
@ -612,7 +612,7 @@ jobs:
issues: write
steps:
- name: Remove workflow eyes reaction
uses: actions/github-script@v9
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
script: |
const { owner, repo } = context.repo;

View file

@ -81,7 +81,7 @@ jobs:
steps:
- name: Require maintainer-level repository access
id: permission
uses: actions/github-script@v9
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
script: |
const allowed = new Set(["admin", "maintain", "write"]);
@ -111,7 +111,7 @@ jobs:
candidate_revision: ${{ steps.validate.outputs.candidate_revision }}
steps:
- name: Checkout harness ref
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
fetch-depth: 0
@ -165,7 +165,7 @@ jobs:
environment: qa-live-shared
steps:
- name: Checkout harness ref
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
fetch-depth: 0
@ -180,7 +180,7 @@ jobs:
run: pnpm build
- name: Cache Mantis candidate pnpm store
uses: actions/cache@v5
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: |
~/.local/share/pnpm/store
@ -190,7 +190,7 @@ jobs:
mantis-slack-pnpm-${{ runner.os }}-${{ env.NODE_VERSION }}-
- name: Setup Go for Crabbox CLI
uses: actions/setup-go@v6
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
with:
go-version: "1.26.x"
cache: false
@ -453,7 +453,7 @@ jobs:
- name: Upload Mantis Slack desktop artifacts
id: upload_artifact
if: ${{ always() && steps.run_mantis.outputs.output_dir != '' }}
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: mantis-slack-desktop-smoke-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ steps.run_mantis.outputs.output_dir }}
@ -463,7 +463,7 @@ jobs:
- name: Create Mantis GitHub App token
id: mantis_app_token
if: ${{ always() && inputs.pr_number != '' }}
uses: actions/create-github-app-token@v3
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3
with:
app-id: ${{ secrets.MANTIS_GITHUB_APP_ID }}
private-key: ${{ secrets.MANTIS_GITHUB_APP_PRIVATE_KEY }}

View file

@ -79,7 +79,7 @@ jobs:
steps:
- name: Require maintainer-level repository access
id: permission
uses: actions/github-script@v9
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
script: |
if (context.eventName === "pull_request_target") {
@ -125,7 +125,7 @@ jobs:
steps:
- name: Resolve refs and target PR
id: resolve
uses: actions/github-script@v9
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
script: |
const eventName = context.eventName;
@ -223,7 +223,7 @@ jobs:
candidate_trust: ${{ steps.validate.outputs.candidate_trust }}
steps:
- name: Checkout harness ref
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: main
persist-credentials: false
@ -350,7 +350,7 @@ jobs:
done
- name: Checkout harness ref
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
fetch-depth: 0
@ -362,7 +362,7 @@ jobs:
install-bun: "true"
- name: Setup Go for Crabbox CLI
uses: actions/setup-go@v6
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
with:
go-version: "1.26.x"
cache: false
@ -551,7 +551,7 @@ jobs:
- name: Upload Mantis Telegram desktop artifacts
id: upload_artifact
if: ${{ always() && steps.inspect.outputs.output_dir != '' }}
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: mantis-telegram-desktop-proof-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ steps.inspect.outputs.output_dir }}
@ -561,7 +561,7 @@ jobs:
- name: Create Mantis GitHub App token
id: mantis_app_token
if: ${{ always() && needs.resolve_request.outputs.pr_number != '' }}
uses: actions/create-github-app-token@v3
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3
with:
app-id: ${{ secrets.MANTIS_GITHUB_APP_ID }}
private-key: ${{ secrets.MANTIS_GITHUB_APP_PRIVATE_KEY }}
@ -620,7 +620,7 @@ jobs:
environment: qa-live-shared
steps:
- name: Checkout harness ref
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
@ -663,7 +663,7 @@ jobs:
- name: Create Mantis GitHub App token
id: mantis_app_token
uses: actions/create-github-app-token@v3
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3
with:
app-id: ${{ secrets.MANTIS_GITHUB_APP_ID }}
private-key: ${{ secrets.MANTIS_GITHUB_APP_PRIVATE_KEY }}
@ -709,7 +709,7 @@ jobs:
issues: write
steps:
- name: Remove workflow eyes reaction
uses: actions/github-script@v9
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
script: |
const { owner, repo } = context.repo;

View file

@ -68,7 +68,7 @@ jobs:
steps:
- name: Require maintainer-level repository access
id: permission
uses: actions/github-script@v9
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
script: |
const allowed = new Set(["admin", "maintain", "write"]);
@ -105,7 +105,7 @@ jobs:
steps:
- name: Resolve refs and target PR
id: resolve
uses: actions/github-script@v9
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
script: |
const eventName = context.eventName;
@ -209,7 +209,7 @@ jobs:
candidate_revision: ${{ steps.validate.outputs.candidate_revision }}
steps:
- name: Checkout harness ref
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
fetch-depth: 0
@ -312,7 +312,7 @@ jobs:
done
- name: Checkout harness ref
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
fetch-depth: 0
@ -327,7 +327,7 @@ jobs:
run: pnpm build
- name: Cache Mantis candidate pnpm store
uses: actions/cache@v5
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: |
~/.local/share/pnpm/store
@ -337,7 +337,7 @@ jobs:
mantis-telegram-pnpm-${{ runner.os }}-${{ env.NODE_VERSION }}-
- name: Setup Go for Crabbox CLI
uses: actions/setup-go@v6
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
with:
go-version: "1.26.x"
cache: false
@ -501,7 +501,7 @@ jobs:
- name: Upload Mantis Telegram artifacts
id: upload_artifact
if: ${{ always() && steps.run_mantis.outputs.output_dir != '' }}
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: mantis-telegram-live-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ steps.run_mantis.outputs.output_dir }}
@ -511,7 +511,7 @@ jobs:
- name: Create Mantis GitHub App token
id: mantis_app_token
if: ${{ always() && needs.resolve_request.outputs.pr_number != '' }}
uses: actions/create-github-app-token@v3
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3
with:
app-id: ${{ secrets.MANTIS_GITHUB_APP_ID }}
private-key: ${{ secrets.MANTIS_GITHUB_APP_PRIVATE_KEY }}
@ -572,7 +572,7 @@ jobs:
issues: write
steps:
- name: Remove workflow eyes reaction
uses: actions/github-script@v9
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
script: |
const { owner, repo } = context.repo;

View file

@ -120,7 +120,7 @@ jobs:
DOCKER_BUILD_RECORD_UPLOAD: "false"
steps:
- name: Checkout dispatch ref
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ inputs.harness_ref || github.sha }}
fetch-depth: 1
@ -190,14 +190,14 @@ jobs:
- name: Download package-under-test artifact
if: inputs.package_artifact_name != '' && inputs.package_artifact_run_id == ''
uses: actions/download-artifact@v8
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: ${{ inputs.package_artifact_name }}
path: .artifacts/telegram-package-under-test
- name: Download package-under-test artifact from release run
if: inputs.package_artifact_name != '' && inputs.package_artifact_run_id != ''
uses: actions/download-artifact@v8
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: ${{ inputs.package_artifact_name }}
path: .artifacts/telegram-package-under-test
@ -268,7 +268,7 @@ jobs:
- name: Upload npm Telegram E2E artifacts
if: always()
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: npm-telegram-beta-e2e-${{ github.run_id }}-${{ github.run_attempt }}
path: .artifacts/qa-e2e/

View file

@ -332,7 +332,7 @@ jobs:
esac
- name: Checkout workflow repo
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
repository: ${{ env.OPENCLAW_REPOSITORY }}
ref: ${{ steps.workflow_ref.outputs.value }}
@ -342,7 +342,7 @@ jobs:
- name: Checkout public source ref
if: inputs.candidate_artifact_name == ''
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
repository: ${{ env.OPENCLAW_REPOSITORY }}
ref: ${{ inputs.ref }}
@ -352,7 +352,7 @@ jobs:
submodules: recursive
- name: Setup Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: ${{ env.NODE_VERSION }}
@ -379,14 +379,14 @@ jobs:
- name: Download current-run candidate artifact
if: inputs.candidate_artifact_name != '' && inputs.candidate_artifact_run_id == ''
uses: actions/download-artifact@v8
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: ${{ inputs.candidate_artifact_name }}
path: ${{ runner.temp }}/openclaw-cross-os-release-checks/prepare/package
- name: Download previous-run candidate artifact
if: inputs.candidate_artifact_name != '' && inputs.candidate_artifact_run_id != ''
uses: actions/download-artifact@v8
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: ${{ inputs.candidate_artifact_name }}
run-id: ${{ inputs.candidate_artifact_run_id }}
@ -510,7 +510,7 @@ jobs:
NODE
- name: Upload candidate artifact
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: openclaw-cross-os-release-checks-candidate-${{ github.run_id }}
path: ${{ runner.temp }}/openclaw-cross-os-release-checks/prepare/package/${{ steps.candidate_metadata.outputs.file_name }}
@ -518,7 +518,7 @@ jobs:
- name: Upload baseline artifact
if: ${{ inputs.mode != 'fresh' }}
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: openclaw-cross-os-release-checks-baseline-${{ github.run_id }}
path: ${{ runner.temp }}/openclaw-cross-os-release-checks/prepare/baseline/${{ steps.baseline_metadata.outputs.file_name }}
@ -558,7 +558,7 @@ jobs:
timeout-minutes: 60
steps:
- name: Checkout workflow repo
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
repository: ${{ env.OPENCLAW_REPOSITORY }}
ref: ${{ needs.prepare.outputs.workflow_ref }}
@ -567,7 +567,7 @@ jobs:
persist-credentials: true
- name: Setup Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: ${{ env.NODE_VERSION }}
@ -582,14 +582,14 @@ jobs:
- name: Download candidate artifact
id: download_candidate
continue-on-error: true
uses: actions/download-artifact@v8
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: openclaw-cross-os-release-checks-candidate-${{ github.run_id }}
path: ${{ runner.temp }}/openclaw-cross-os-release-checks/candidate
- name: Retry candidate artifact download
if: ${{ steps.download_candidate.outcome == 'failure' }}
uses: actions/download-artifact@v8
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: openclaw-cross-os-release-checks-candidate-${{ github.run_id }}
path: ${{ runner.temp }}/openclaw-cross-os-release-checks/candidate
@ -598,14 +598,14 @@ jobs:
if: ${{ matrix.suite == 'packaged-upgrade' }}
id: download_baseline
continue-on-error: true
uses: actions/download-artifact@v8
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: openclaw-cross-os-release-checks-baseline-${{ github.run_id }}
path: ${{ runner.temp }}/openclaw-cross-os-release-checks/baseline
- name: Retry baseline artifact download
if: ${{ matrix.suite == 'packaged-upgrade' && steps.download_baseline.outcome == 'failure' }}
uses: actions/download-artifact@v8
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: openclaw-cross-os-release-checks-baseline-${{ github.run_id }}
path: ${{ runner.temp }}/openclaw-cross-os-release-checks/baseline
@ -684,7 +684,7 @@ jobs:
- name: Upload release-check artifacts
if: always()
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: openclaw-cross-os-release-checks-${{ matrix.artifact_name }}-${{ matrix.suite }}-${{ github.run_id }}
path: ${{ runner.temp }}/openclaw-cross-os-release-checks/${{ matrix.artifact_name }}-${{ matrix.suite }}

View file

@ -329,7 +329,7 @@ jobs:
trusted_reason: ${{ steps.validate.outputs.trusted_reason }}
steps:
- name: Checkout workflow repository
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
fetch-depth: 0
@ -493,7 +493,7 @@ jobs:
live_models_omitted_json: ${{ steps.plan.outputs.live_models_omitted_json }}
steps:
- name: Checkout trusted release harness
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
ref: ${{ github.sha }}
@ -523,7 +523,7 @@ jobs:
OPENCLAW_LIVE_TEST: "1"
steps:
- name: Checkout selected ref
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ needs.validate_selected_ref.outputs.selected_sha }}
fetch-depth: 1
@ -570,7 +570,7 @@ jobs:
OPENCLAW_VITEST_MAX_WORKERS: "2"
steps:
- name: Checkout selected ref
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ needs.validate_selected_ref.outputs.selected_sha }}
fetch-depth: 1
@ -614,7 +614,7 @@ jobs:
OPENCLAW_VITEST_MAX_WORKERS: "1"
steps:
- name: Checkout selected ref
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ needs.validate_selected_ref.outputs.selected_sha }}
fetch-depth: 1
@ -740,7 +740,7 @@ jobs:
steps:
- name: Checkout selected ref
if: contains(matrix.profiles, inputs.release_test_profile)
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
ref: ${{ needs.validate_selected_ref.outputs.selected_sha }}
@ -748,7 +748,7 @@ jobs:
- name: Checkout trusted release harness
if: contains(matrix.profiles, inputs.release_test_profile)
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
ref: ${{ github.sha }}
@ -801,7 +801,7 @@ jobs:
- name: Download OpenClaw Docker E2E package
if: contains(matrix.profiles, inputs.release_test_profile) && steps.plan.outputs.needs_package == '1'
uses: actions/download-artifact@v8
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: ${{ inputs.package_artifact_name || 'docker-e2e-package' }}
path: .artifacts/docker-e2e-package
@ -894,7 +894,7 @@ jobs:
- name: Upload Docker E2E chunk artifacts
if: always()
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: docker-e2e-${{ matrix.chunk_id }}
path: .artifacts/docker-tests/
@ -910,7 +910,7 @@ jobs:
groups_json: ${{ steps.groups.outputs.groups_json }}
steps:
- name: Checkout trusted release harness
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
ref: ${{ github.sha }}
@ -1002,14 +1002,14 @@ jobs:
DOCKER_E2E_LANES: ${{ matrix.group.docker_lanes }}
steps:
- name: Checkout selected ref
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
ref: ${{ needs.validate_selected_ref.outputs.selected_sha }}
fetch-depth: 1
- name: Checkout trusted release harness
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
ref: ${{ github.sha }}
@ -1062,7 +1062,7 @@ jobs:
- name: Download OpenClaw Docker E2E package
if: steps.plan.outputs.needs_package == '1'
uses: actions/download-artifact@v8
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: ${{ inputs.package_artifact_name || 'docker-e2e-package' }}
path: .artifacts/docker-e2e-package
@ -1154,7 +1154,7 @@ jobs:
- name: Upload targeted Docker E2E artifacts
if: always()
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: docker-e2e-${{ steps.plan.outputs.artifact_suffix }}
path: .artifacts/docker-tests/
@ -1179,13 +1179,13 @@ jobs:
OPENCLAW_SKIP_DOCKER_BUILD: "1"
steps:
- name: Checkout selected ref
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ needs.validate_selected_ref.outputs.selected_sha }}
fetch-depth: 1
- name: Checkout trusted release harness
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ github.sha }}
fetch-depth: 1
@ -1229,7 +1229,7 @@ jobs:
- name: Download OpenClaw Docker E2E package
if: steps.plan.outputs.needs_package == '1'
uses: actions/download-artifact@v8
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: ${{ inputs.package_artifact_name || 'docker-e2e-package' }}
path: .artifacts/docker-e2e-package
@ -1281,7 +1281,7 @@ jobs:
- name: Upload Open WebUI Docker E2E artifacts
if: always()
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: docker-e2e-openwebui
path: .artifacts/docker-tests/
@ -1312,13 +1312,13 @@ jobs:
OPENCLAW_DOCKER_E2E_REPO_ROOT: ${{ github.workspace }}
steps:
- name: Checkout selected ref
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ needs.validate_selected_ref.outputs.selected_sha }}
fetch-depth: 1
- name: Checkout trusted release harness
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ github.sha }}
fetch-depth: 1
@ -1364,14 +1364,14 @@ jobs:
- name: Download current-run OpenClaw Docker E2E package
if: steps.plan.outputs.needs_package == '1' && inputs.package_artifact_name != '' && inputs.package_artifact_run_id == ''
uses: actions/download-artifact@v8
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: ${{ inputs.package_artifact_name }}
path: .artifacts/docker-e2e-package
- name: Download previous-run OpenClaw Docker E2E package
if: steps.plan.outputs.needs_package == '1' && inputs.package_artifact_run_id != ''
uses: actions/download-artifact@v8
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: ${{ inputs.package_artifact_name || 'docker-e2e-package' }}
path: .artifacts/docker-e2e-package
@ -1421,7 +1421,7 @@ jobs:
- name: Upload OpenClaw Docker E2E package
if: steps.plan.outputs.needs_package == '1' && (inputs.package_artifact_name == '' || inputs.package_artifact_run_id != '')
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: ${{ inputs.package_artifact_name || 'docker-e2e-package' }}
path: .artifacts/docker-e2e-package/openclaw-current.tgz
@ -1581,7 +1581,7 @@ jobs:
DOCKER_BUILD_RECORD_UPLOAD: "false"
steps:
- name: Checkout selected ref
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ needs.validate_selected_ref.outputs.selected_sha }}
fetch-depth: 1
@ -1693,14 +1693,14 @@ jobs:
steps:
- name: Checkout selected ref
if: contains(matrix.profiles, inputs.release_test_profile)
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ needs.validate_selected_ref.outputs.selected_sha }}
fetch-depth: 1
- name: Checkout trusted live Docker harness
if: contains(matrix.profiles, inputs.release_test_profile)
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ github.sha }}
fetch-depth: 1
@ -1815,13 +1815,13 @@ jobs:
OPENCLAW_VITEST_MAX_WORKERS: "2"
steps:
- name: Checkout selected ref
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ needs.validate_selected_ref.outputs.selected_sha }}
fetch-depth: 1
- name: Checkout trusted live Docker harness
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ github.sha }}
fetch-depth: 1
@ -2187,14 +2187,14 @@ jobs:
steps:
- name: Checkout selected ref
if: contains(matrix.profiles, inputs.release_test_profile) && (inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id || (inputs.live_suite_filter == 'native-live-src-gateway-profiles-anthropic' && startsWith(matrix.suite_id, 'native-live-src-gateway-profiles-anthropic-')) || (inputs.live_suite_filter == 'native-live-src-gateway-profiles-opencode-go' && startsWith(matrix.suite_id, 'native-live-src-gateway-profiles-opencode-go-')))
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ needs.validate_selected_ref.outputs.selected_sha }}
fetch-depth: 1
- name: Checkout trusted live shard harness
if: contains(matrix.profiles, inputs.release_test_profile) && (inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id || (inputs.live_suite_filter == 'native-live-src-gateway-profiles-anthropic' && startsWith(matrix.suite_id, 'native-live-src-gateway-profiles-anthropic-')) || (inputs.live_suite_filter == 'native-live-src-gateway-profiles-opencode-go' && startsWith(matrix.suite_id, 'native-live-src-gateway-profiles-opencode-go-')))
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ github.sha }}
fetch-depth: 1
@ -2409,14 +2409,14 @@ jobs:
steps:
- name: Checkout selected ref
if: contains(matrix.profiles, inputs.release_test_profile) && (inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id || (inputs.live_suite_filter == 'live-gateway-advisory-docker' && startsWith(matrix.suite_id, 'live-gateway-advisory-docker-')))
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ needs.validate_selected_ref.outputs.selected_sha }}
fetch-depth: 1
- name: Checkout trusted live shard harness
if: contains(matrix.profiles, inputs.release_test_profile) && (inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id || (inputs.live_suite_filter == 'live-gateway-advisory-docker' && startsWith(matrix.suite_id, 'live-gateway-advisory-docker-')))
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ github.sha }}
fetch-depth: 1
@ -2623,14 +2623,14 @@ jobs:
steps:
- name: Checkout selected ref
if: contains(matrix.profiles, inputs.release_test_profile) && (inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id || (inputs.live_suite_filter == 'native-live-extensions-media-video' && startsWith(matrix.suite_id, 'native-live-extensions-media-video-')))
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ needs.validate_selected_ref.outputs.selected_sha }}
fetch-depth: 1
- name: Checkout trusted live shard harness
if: contains(matrix.profiles, inputs.release_test_profile) && (inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id || (inputs.live_suite_filter == 'native-live-extensions-media-video' && startsWith(matrix.suite_id, 'native-live-extensions-media-video-')))
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ github.sha }}
fetch-depth: 1

View file

@ -87,7 +87,7 @@ jobs:
exit 1
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ inputs.tag }}
fetch-depth: 0
@ -354,7 +354,7 @@ jobs:
node --import tsx scripts/openclaw-npm-prepublish-verify.ts "$TARBALL_PATH" "$PACKAGE_VERSION"
- name: Upload dependency release evidence
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: openclaw-release-dependency-evidence-${{ inputs.tag }}
path: ${{ steps.dependency_evidence.outputs.dir }}
@ -362,14 +362,14 @@ jobs:
- name: Upload dependency release evidence tag alias
if: ${{ steps.packed_tarball.outputs.release_tag != inputs.tag }}
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: openclaw-release-dependency-evidence-${{ steps.packed_tarball.outputs.release_tag }}
path: ${{ steps.dependency_evidence.outputs.dir }}
if-no-files-found: error
- name: Upload prepared npm publish bundle
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: openclaw-npm-preflight-${{ inputs.tag }}
path: ${{ steps.packed_tarball.outputs.dir }}
@ -377,7 +377,7 @@ jobs:
- name: Upload prepared npm publish bundle tag alias
if: ${{ steps.packed_tarball.outputs.release_tag != inputs.tag }}
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: openclaw-npm-preflight-${{ steps.packed_tarball.outputs.release_tag }}
path: ${{ steps.packed_tarball.outputs.dir }}
@ -391,7 +391,7 @@ jobs:
contents: read
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
@ -492,7 +492,7 @@ jobs:
fi
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: refs/tags/${{ inputs.tag }}
fetch-depth: 0
@ -611,7 +611,7 @@ jobs:
- name: Download full release validation manifest
if: ${{ inputs.full_release_validation_run_id != '' }}
uses: actions/download-artifact@v8
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: full-release-validation-${{ inputs.full_release_validation_run_id }}
path: full-release-validation
@ -677,6 +677,8 @@ jobs:
- name: Verify full release validation target
if: ${{ inputs.full_release_validation_run_id != '' }}
env:
RELEASE_TAG: ${{ inputs.tag }}
run: |
set -euo pipefail
EXPECTED_RELEASE_SHA="$(git rev-parse HEAD)"
@ -689,6 +691,8 @@ jobs:
WORKFLOW_NAME="$(jq -r '.workflowName // ""' "$MANIFEST_FILE")"
TARGET_SHA="$(jq -r '.targetSha // ""' "$MANIFEST_FILE")"
RERUN_GROUP="$(jq -r '.rerunGroup // ""' "$MANIFEST_FILE")"
RUN_RELEASE_SOAK="$(jq -r '.runReleaseSoak // ""' "$MANIFEST_FILE")"
PERFORMANCE_BLOCKING="$(jq -r '.controls.performanceBlocking // false' "$MANIFEST_FILE")"
if [[ "$WORKFLOW_NAME" != "Full Release Validation" ]]; then
echo "Full release validation manifest workflow mismatch: $WORKFLOW_NAME" >&2
exit 1
@ -701,6 +705,14 @@ jobs:
echo "Full release validation must run rerun_group=all before npm publish; got $RERUN_GROUP" >&2
exit 1
fi
if [[ "$PERFORMANCE_BLOCKING" != "true" ]]; then
echo "Full release validation manifest does not record blocking product performance evidence." >&2
exit 1
fi
if [[ "$RELEASE_TAG" != *"-alpha."* && "$RELEASE_TAG" != *"-beta."* && "$RUN_RELEASE_SOAK" != "true" ]]; then
echo "Stable releases require Full Release Validation with runReleaseSoak=true." >&2
exit 1
fi
- name: Resolve publish tarball
id: publish_tarball

View file

@ -1,5 +1,7 @@
name: OpenClaw Performance
run-name: ${{ inputs.dispatch_id != '' && format('OpenClaw Performance {0}', inputs.dispatch_id) || 'OpenClaw Performance' }}
on:
schedule:
- cron: "11 5 * * *"
@ -45,6 +47,11 @@ on:
required: false
default: b63b6f9e20efb23641df00487e982230d81a90ac
type: string
dispatch_id:
description: Optional parent workflow dispatch identifier
required: false
default: ""
type: string
permissions:
contents: read
@ -145,7 +152,7 @@ jobs:
- name: Checkout OpenClaw
if: steps.lane.outputs.run == 'true'
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ inputs.target_ref || github.ref }}
fetch-depth: 1
@ -153,7 +160,7 @@ jobs:
- name: Checkout performance workflow helpers
if: steps.lane.outputs.run == 'true'
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ github.sha }}
path: .artifacts/performance-workflow
@ -556,7 +563,7 @@ jobs:
- name: Upload Kova artifacts
if: ${{ always() && steps.lane.outputs.run == 'true' }}
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: openclaw-performance-${{ matrix.lane }}-${{ github.run_id }}-${{ github.run_attempt }}
path: |

View file

@ -40,7 +40,7 @@ on:
- stable
- full
run_release_soak:
description: Run exhaustive live/Docker and upgrade-survivor soak lanes; forced on for release_profile=full
description: Run exhaustive live/Docker and upgrade-survivor soak lanes; forced on for release_profile=stable and full
required: false
default: false
type: boolean
@ -152,7 +152,7 @@ jobs:
fi
- name: Checkout trusted workflow helper
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
ref: ${{ github.ref_name }}
@ -173,7 +173,7 @@ jobs:
- name: Checkout selected ref for reachability fallback
if: steps.fast_ref.outputs.fallback == 'true'
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
ref: ${{ inputs.ref }}
@ -330,7 +330,7 @@ jobs:
exit 1
;;
esac
if [[ "$release_profile" == "full" ]]; then
if [[ "$release_profile" == "stable" || "$release_profile" == "full" ]]; then
run_release_soak=true
fi
codex_plugin_spec="$RELEASE_CODEX_PLUGIN_SPEC_INPUT"
@ -507,7 +507,7 @@ jobs:
source_sha: ${{ steps.package.outputs.source_sha }}
steps:
- name: Checkout trusted workflow ref
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: true
ref: ${{ github.ref_name }}
@ -559,7 +559,7 @@ jobs:
} >> "$GITHUB_STEP_SUMMARY"
- name: Upload release package artifact
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: release-package-under-test
path: |
@ -798,7 +798,7 @@ jobs:
OPENCLAW_ENABLE_PRIVATE_QA_CLI: "1"
steps:
- name: Checkout selected ref
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: true
ref: ${{ needs.resolve_target.outputs.revision }}
@ -849,7 +849,7 @@ jobs:
- name: Upload parity lane artifacts
id: upload_parity_lane_artifacts
if: always()
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: release-qa-parity-${{ matrix.lane }}-${{ needs.resolve_target.outputs.revision }}
path: .artifacts/qa-e2e/
@ -895,7 +895,7 @@ jobs:
- name: Upload advisory status
if: always()
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: release-check-status-qa-parity-${{ matrix.lane }}-${{ needs.resolve_target.outputs.revision }}
path: .artifacts/release-check-status/qa_lab_parity_lane_release_checks-${{ matrix.lane }}.env
@ -917,7 +917,7 @@ jobs:
OPENCLAW_ENABLE_PRIVATE_QA_CLI: "1"
steps:
- name: Checkout selected ref
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: true
ref: ${{ needs.resolve_target.outputs.revision }}
@ -930,7 +930,7 @@ jobs:
install-bun: "true"
- name: Download parity lane artifacts
uses: actions/download-artifact@v8
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
pattern: release-qa-parity-*-${{ needs.resolve_target.outputs.revision }}
path: .artifacts/qa-e2e/
@ -955,7 +955,7 @@ jobs:
- name: Upload parity artifacts
id: upload_parity_artifacts
if: always()
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: release-qa-parity-${{ needs.resolve_target.outputs.revision }}
path: .artifacts/qa-e2e/
@ -999,7 +999,7 @@ jobs:
- name: Upload advisory status
if: always()
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: release-check-status-qa-parity-report-${{ needs.resolve_target.outputs.revision }}
path: .artifacts/release-check-status/qa_lab_parity_report_release_checks.env
@ -1028,7 +1028,7 @@ jobs:
OPENCLAW_ENABLE_PRIVATE_QA_CLI: "1"
steps:
- name: Checkout selected ref
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: true
ref: ${{ needs.resolve_target.outputs.revision }}
@ -1127,7 +1127,7 @@ jobs:
- name: Upload runtime parity artifacts
id: upload_runtime_parity_artifacts
if: always()
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: release-qa-runtime-parity-${{ needs.resolve_target.outputs.revision }}
path: .artifacts/qa-e2e/
@ -1171,7 +1171,7 @@ jobs:
- name: Upload advisory status
if: always()
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: release-check-status-qa-runtime-parity-${{ needs.resolve_target.outputs.revision }}
path: .artifacts/release-check-status/qa_lab_runtime_parity_release_checks.env
@ -1192,7 +1192,7 @@ jobs:
OPENCLAW_ENABLE_PRIVATE_QA_CLI: "1"
steps:
- name: Checkout selected ref
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: true
ref: ${{ needs.resolve_target.outputs.revision }}
@ -1205,7 +1205,7 @@ jobs:
install-bun: "true"
- name: Download runtime parity status
uses: actions/download-artifact@v8
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: release-check-status-qa-runtime-parity-${{ needs.resolve_target.outputs.revision }}
path: .artifacts/release-check-status/
@ -1226,7 +1226,7 @@ jobs:
- name: Download runtime parity artifacts
if: steps.verify_runtime_parity_status.outputs.ready == 'true'
uses: actions/download-artifact@v8
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: release-qa-runtime-parity-${{ needs.resolve_target.outputs.revision }}
path: .artifacts/qa-e2e/
@ -1243,7 +1243,7 @@ jobs:
- name: Upload runtime tool coverage artifacts
if: always()
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: release-qa-runtime-tool-coverage-${{ needs.resolve_target.outputs.revision }}
path: .artifacts/qa-e2e/runtime-parity-standard-report/
@ -1266,7 +1266,7 @@ jobs:
OPENCLAW_ENABLE_PRIVATE_QA_CLI: "1"
steps:
- name: Checkout selected ref
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: true
ref: ${{ needs.resolve_target.outputs.revision }}
@ -1323,7 +1323,7 @@ jobs:
- name: Upload Matrix QA artifacts
id: upload_matrix_qa_artifacts
if: always()
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: release-qa-live-matrix-${{ needs.resolve_target.outputs.revision }}
path: .artifacts/qa-e2e/
@ -1367,7 +1367,7 @@ jobs:
- name: Upload advisory status
if: always()
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: release-check-status-qa-live-matrix-${{ needs.resolve_target.outputs.revision }}
path: .artifacts/release-check-status/qa_live_matrix_release_checks.env
@ -1390,7 +1390,7 @@ jobs:
OPENCLAW_ENABLE_PRIVATE_QA_CLI: "1"
steps:
- name: Checkout selected ref
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: true
ref: ${{ needs.resolve_target.outputs.revision }}
@ -1463,7 +1463,7 @@ jobs:
- name: Upload Telegram QA artifacts
id: upload_telegram_qa_artifacts
if: always()
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: release-qa-live-telegram-${{ needs.resolve_target.outputs.revision }}
path: .artifacts/qa-e2e/
@ -1507,7 +1507,7 @@ jobs:
- name: Upload advisory status
if: always()
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: release-check-status-qa-live-telegram-${{ needs.resolve_target.outputs.revision }}
path: .artifacts/release-check-status/qa_live_telegram_release_checks.env
@ -1530,7 +1530,7 @@ jobs:
OPENCLAW_ENABLE_PRIVATE_QA_CLI: "1"
steps:
- name: Checkout selected ref
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: true
ref: ${{ needs.resolve_target.outputs.revision }}
@ -1603,7 +1603,7 @@ jobs:
- name: Upload Discord QA artifacts
id: upload_discord_qa_artifacts
if: always()
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: release-qa-live-discord-${{ needs.resolve_target.outputs.revision }}
path: .artifacts/qa-e2e/
@ -1647,7 +1647,7 @@ jobs:
- name: Upload advisory status
if: always()
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: release-check-status-qa-live-discord-${{ needs.resolve_target.outputs.revision }}
path: .artifacts/release-check-status/qa_live_discord_release_checks.env
@ -1673,7 +1673,7 @@ jobs:
OPENCLAW_ENABLE_PRIVATE_QA_CLI: "1"
steps:
- name: Checkout selected ref
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: true
ref: ${{ needs.resolve_target.outputs.revision }}
@ -1746,7 +1746,7 @@ jobs:
- name: Upload WhatsApp QA artifacts
id: upload_whatsapp_qa_artifacts
if: always()
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: release-qa-live-whatsapp-${{ needs.resolve_target.outputs.revision }}
path: .artifacts/qa-e2e/
@ -1790,7 +1790,7 @@ jobs:
- name: Upload advisory status
if: always()
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: release-check-status-qa-live-whatsapp-${{ needs.resolve_target.outputs.revision }}
path: .artifacts/release-check-status/qa_live_whatsapp_release_checks.env
@ -1813,7 +1813,7 @@ jobs:
OPENCLAW_ENABLE_PRIVATE_QA_CLI: "1"
steps:
- name: Checkout selected ref
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: true
ref: ${{ needs.resolve_target.outputs.revision }}
@ -1886,7 +1886,7 @@ jobs:
- name: Upload Slack QA artifacts
id: upload_slack_qa_artifacts
if: always()
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: release-qa-live-slack-${{ needs.resolve_target.outputs.revision }}
path: .artifacts/qa-e2e/
@ -1930,7 +1930,7 @@ jobs:
- name: Upload advisory status
if: always()
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: release-check-status-qa-live-slack-${{ needs.resolve_target.outputs.revision }}
path: .artifacts/release-check-status/qa_live_slack_release_checks.env
@ -1964,7 +1964,7 @@ jobs:
- name: Download advisory status artifacts
if: always()
continue-on-error: true
uses: actions/download-artifact@v8
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
pattern: release-check-status-*
path: .artifacts/release-check-status

View file

@ -290,7 +290,7 @@ jobs:
- name: Download full release validation manifest
if: ${{ inputs.publish_openclaw_npm }}
uses: actions/download-artifact@v8
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: full-release-validation-${{ inputs.full_release_validation_run_id }}
path: ${{ runner.temp }}/full-release-validation-manifest
@ -299,7 +299,7 @@ jobs:
github-token: ${{ github.token }}
- name: Checkout release tag
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: refs/tags/${{ inputs.tag }}
fetch-depth: 0
@ -359,6 +359,7 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
FULL_RELEASE_VALIDATION_RUN_ID: ${{ inputs.full_release_validation_run_id }}
RELEASE_TAG: ${{ inputs.tag }}
EXPECTED_SHA: ${{ steps.ref.outputs.sha }}
EXPECTED_RELEASE_PROFILE: ${{ inputs.release_profile }}
EXPECTED_WORKFLOW_BRANCH: ${{ github.ref_name }}
@ -377,6 +378,8 @@ jobs:
target_sha="$(jq -r '.targetSha // ""' "$manifest")"
release_profile="$(jq -r '.releaseProfile // ""' "$manifest")"
rerun_group="$(jq -r '.rerunGroup // ""' "$manifest")"
run_release_soak="$(jq -r '.runReleaseSoak // ""' "$manifest")"
performance_blocking="$(jq -r '.controls.performanceBlocking // false' "$manifest")"
if [[ "$workflow_name" != "Full Release Validation" ]]; then
echo "Full release validation manifest workflow mismatch: $workflow_name" >&2
exit 1
@ -393,6 +396,14 @@ jobs:
echo "Full release validation must run rerun_group=all before npm publish; got $rerun_group" >&2
exit 1
fi
if [[ "$performance_blocking" != "true" ]]; then
echo "Full release validation manifest does not record blocking product performance evidence." >&2
exit 1
fi
if [[ "$RELEASE_TAG" != *"-alpha."* && "$RELEASE_TAG" != *"-beta."* && "$run_release_soak" != "true" ]]; then
echo "Stable releases require Full Release Validation with runReleaseSoak=true." >&2
exit 1
fi
echo "release_profile=$release_profile" >> "$GITHUB_OUTPUT"
- name: Validate release tag is reachable from a trusted release branch
@ -455,12 +466,22 @@ jobs:
environment: npm-release
steps:
- name: Checkout release SHA
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ needs.resolve_release_target.outputs.sha }}
fetch-depth: 1
persist-credentials: false
- name: Download full release validation manifest
if: ${{ inputs.publish_openclaw_npm }}
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: full-release-validation-${{ inputs.full_release_validation_run_id }}
path: ${{ runner.temp }}/full-release-validation-manifest
repository: ${{ github.repository }}
run-id: ${{ inputs.full_release_validation_run_id }}
github-token: ${{ github.token }}
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
@ -484,6 +505,7 @@ jobs:
WINDOWS_NODE_TAG: ${{ inputs.windows_node_tag }}
WINDOWS_NODE_INSTALLER_DIGESTS: ${{ needs.resolve_release_target.outputs.windows_node_installer_digests }}
POSTPUBLISH_EVIDENCE_DIR: ${{ runner.temp }}/openclaw-release-postpublish-evidence
FULL_RELEASE_VALIDATION_MANIFEST_DIR: ${{ runner.temp }}/full-release-validation-manifest
run: |
set -euo pipefail
@ -1060,13 +1082,75 @@ jobs:
exit 1
fi
(cd "${download_dir}" && zip -qr "${asset_path}" dependency-evidence)
gh release upload "${RELEASE_TAG}" "${asset_path}#${asset_name}" \
--repo "${GITHUB_REPOSITORY}" \
--clobber
(
cd "${download_dir}"
find dependency-evidence -type f -print | LC_ALL=C sort | zip -X -q "${asset_path}" -@
)
attach_or_verify_release_asset "${asset_path}" "${asset_name}"
echo "- Dependency evidence asset: \`${asset_name}\`" >> "$GITHUB_STEP_SUMMARY"
}
attach_or_verify_release_asset() {
local source_path="$1"
local asset_name="$2"
local existing_dir="${RUNNER_TEMP}/openclaw-release-existing-assets/${asset_name}"
local existing_path="${existing_dir}/${asset_name}"
if gh release view "${RELEASE_TAG}" --repo "${GITHUB_REPOSITORY}" --json assets |
jq -e --arg name "${asset_name}" 'any(.assets[]?; .name == $name)' >/dev/null; then
rm -rf "${existing_dir}"
mkdir -p "${existing_dir}"
gh release download "${RELEASE_TAG}" --repo "${GITHUB_REPOSITORY}" \
--pattern "${asset_name}" --dir "${existing_dir}"
cmp --silent "${source_path}" "${existing_path}" || {
echo "Existing release evidence asset ${asset_name} differs from this release run." >&2
exit 1
}
return
fi
gh release upload "${RELEASE_TAG}" "${source_path}#${asset_name}" --repo "${GITHUB_REPOSITORY}"
}
upload_release_evidence_assets() {
local release_version manifest_path evidence_path manifest_asset evidence_asset
release_version="${RELEASE_TAG#v}"
manifest_path="${FULL_RELEASE_VALIDATION_MANIFEST_DIR}/full-release-validation-manifest.json"
evidence_path="${POSTPUBLISH_EVIDENCE_DIR}/release-postpublish-evidence.json"
manifest_asset="openclaw-${release_version}-release-manifest.json"
evidence_asset="openclaw-${release_version}-postpublish-evidence.json"
if [[ ! -f "${manifest_path}" ]]; then
echo "Full release validation manifest is missing from ${FULL_RELEASE_VALIDATION_MANIFEST_DIR}." >&2
exit 1
fi
if [[ ! -f "${evidence_path}" ]]; then
echo "Postpublish release evidence is missing from ${POSTPUBLISH_EVIDENCE_DIR}." >&2
exit 1
fi
cp "${manifest_path}" "${RUNNER_TEMP}/${manifest_asset}"
cp "${evidence_path}" "${RUNNER_TEMP}/${evidence_asset}"
(
cd "${RUNNER_TEMP}"
sha256sum "${manifest_asset}" > "${manifest_asset}.sha256"
sha256sum "${evidence_asset}" > "${evidence_asset}.sha256"
)
attach_or_verify_release_asset "${RUNNER_TEMP}/${manifest_asset}" "${manifest_asset}"
attach_or_verify_release_asset \
"${RUNNER_TEMP}/${manifest_asset}.sha256" \
"${manifest_asset}.sha256"
attach_or_verify_release_asset "${RUNNER_TEMP}/${evidence_asset}" "${evidence_asset}"
attach_or_verify_release_asset \
"${RUNNER_TEMP}/${evidence_asset}.sha256" \
"${evidence_asset}.sha256"
{
echo "- Immutable release manifest: \`${manifest_asset}\`"
echo "- Immutable postpublish evidence: \`${evidence_asset}\`"
} >> "$GITHUB_STEP_SUMMARY"
}
verify_published_release() {
local release_version evidence_path skip_clawhub clawhub_runtime_state_path
local -a verify_args
@ -1105,6 +1189,10 @@ jobs:
fi
pnpm "${verify_args[@]}"
jq --arg release_publish_run_id "$GITHUB_RUN_ID" \
'.releasePublishRunId = $release_publish_run_id' \
"${evidence_path}" > "${evidence_path}.next"
mv "${evidence_path}.next" "${evidence_path}"
{
echo "- Postpublish verification: passed"
echo "- Postpublish evidence: \`${evidence_path}\`"
@ -1382,6 +1470,7 @@ jobs:
fi
create_or_update_github_release
upload_dependency_evidence_release_asset
upload_release_evidence_assets
if ! promote_windows_release_assets; then
failed=1
fi
@ -1398,7 +1487,7 @@ jobs:
- name: Upload postpublish evidence
if: ${{ always() }}
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: openclaw-release-postpublish-evidence-${{ inputs.tag }}
path: ${{ runner.temp }}/openclaw-release-postpublish-evidence

View file

@ -0,0 +1,384 @@
name: OpenClaw Stable Main Closeout
on:
push:
branches: [main]
workflow_dispatch:
inputs:
tag:
description: Stable OpenClaw tag to replay or repair, for example v2026.6.8 or v2026.6.8-2
required: false
type: string
rollback_drill_id:
description: Opaque identifier for the current private rollback drill record
required: false
type: string
rollback_drill_date:
description: UTC date of the private rollback drill in YYYY-MM-DD form; must be within 90 days
required: false
type: string
permissions:
actions: read
contents: write
concurrency:
group: openclaw-stable-main-closeout
cancel-in-progress: false
jobs:
resolve:
name: Resolve stable release closeout inputs
runs-on: ubuntu-24.04
timeout-minutes: 10
outputs:
full_release_validation_run_id: ${{ steps.inputs.outputs.full_release_validation_run_id }}
release_publish_run_id: ${{ steps.inputs.outputs.release_publish_run_id }}
rollback_drill_date: ${{ steps.inputs.outputs.rollback_drill_date }}
rollback_drill_id: ${{ steps.inputs.outputs.rollback_drill_id }}
evidence_tag: ${{ steps.inputs.outputs.evidence_tag }}
fallback_correction: ${{ steps.inputs.outputs.fallback_correction }}
main_ref: ${{ steps.inputs.outputs.main_ref }}
repair_partial_closeout: ${{ steps.inputs.outputs.repair_partial_closeout }}
should_closeout: ${{ steps.inputs.outputs.should_closeout }}
tag: ${{ steps.inputs.outputs.tag }}
steps:
- name: Checkout pushed main
if: ${{ github.event_name == 'push' }}
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ github.sha }}
fetch-depth: 1
persist-credentials: false
- name: Resolve published stable release evidence
id: inputs
env:
EVENT_NAME: ${{ github.event_name }}
GH_TOKEN: ${{ github.token }}
MANUAL_TAG: ${{ inputs.tag }}
ROLLBACK_DRILL_DATE: ${{ inputs.rollback_drill_date || vars.RELEASE_ROLLBACK_DRILL_DATE }}
ROLLBACK_DRILL_ID: ${{ inputs.rollback_drill_id || vars.RELEASE_ROLLBACK_DRILL_ID }}
TRIGGER_SHA: ${{ github.sha }}
run: |
set -euo pipefail
if [[ "$EVENT_NAME" == "push" ]]; then
main_ref="$TRIGGER_SHA"
tag="$(gh release list --repo "$GITHUB_REPOSITORY" --exclude-drafts --limit 100 \
--json tagName,isPrerelease,publishedAt \
--jq '[.[] | select(.isPrerelease | not) | select(.tagName | test("^v[0-9]{4}\\.[0-9]+\\.[0-9]+(-[0-9]+)?$"))] | sort_by(.publishedAt) | last | .tagName // empty')"
if [[ -z "$tag" ]]; then
echo "should_closeout=false" >> "$GITHUB_OUTPUT"
exit 0
fi
else
tag="$MANUAL_TAG"
fi
if [[ ! "$tag" =~ ^v[0-9]{4}\.[0-9]+\.[0-9]+(-[0-9]+)?$ ]]; then
if [[ "$EVENT_NAME" == "push" ]]; then
echo "should_closeout=false" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "Stable main closeout accepts only a stable vYYYY.M.PATCH or vYYYY.M.PATCH-N tag, got $tag." >&2
exit 1
fi
release_asset_version="${tag#v}"
release_package_version="$release_asset_version"
fallback_package_version="$release_asset_version"
if [[ "$release_package_version" =~ ^(.+)-[0-9]+$ ]]; then
fallback_package_version="${BASH_REMATCH[1]}"
fi
tag_package_version="$(gh api "repos/$GITHUB_REPOSITORY/contents/package.json?ref=$tag" \
--jq '.content' | tr -d '\n' | base64 --decode | jq -r '.version // empty')"
fallback_correction=false
evidence_source_tag="$tag"
if [[ "$release_package_version" != "$fallback_package_version" &&
"$tag_package_version" == "$fallback_package_version" ]]; then
fallback_correction=true
evidence_source_tag="v$fallback_package_version"
elif [[ "$tag_package_version" != "$release_package_version" ]]; then
echo "Stable closeout requires $tag package.json to match $release_package_version, or the legacy fallback package version $fallback_package_version." >&2
exit 1
fi
evidence_version="${evidence_source_tag#v}"
evidence_asset="openclaw-${evidence_version}-postpublish-evidence.json"
evidence_checksum_asset="${evidence_asset}.sha256"
closeout_asset="openclaw-${release_asset_version}-stable-main-closeout.json"
closeout_checksum_asset="${closeout_asset}.sha256"
closeout_dir="$RUNNER_TEMP/release-closeout-evidence"
mkdir -p "$closeout_dir"
gh release download "$tag" --repo "$GITHUB_REPOSITORY" \
--pattern "$closeout_asset" --pattern "$closeout_checksum_asset" --dir "$closeout_dir" || true
closeout_json_path="$closeout_dir/$closeout_asset"
closeout_checksum_path="$closeout_dir/$closeout_checksum_asset"
repair_partial_closeout=false
existing_closeout_full_release_validation_run_id=""
existing_closeout_release_publish_run_id=""
if [[ -f "$closeout_json_path" && -f "$closeout_checksum_path" ]]; then
expected_closeout_digest="$(awk 'NF { print $1; exit }' "$closeout_checksum_path")"
actual_closeout_digest="$(sha256sum "$closeout_json_path" | awk '{print $1}')"
if [[ ! "$expected_closeout_digest" =~ ^[0-9a-f]{64}$ ||
"$expected_closeout_digest" != "$actual_closeout_digest" ]]; then
echo "Stable closeout evidence for $tag has an invalid checksum; refusing to repair it." >&2
exit 1
fi
fi
if [[ -f "$closeout_checksum_path" && ! -f "$closeout_json_path" ]]; then
echo "Stable closeout evidence for $tag has a checksum without its manifest; refusing to repair it." >&2
exit 1
fi
if [[ -f "$closeout_json_path" ]]; then
existing_closeout_tag="$(jq -r '.releaseTag // empty' "$closeout_json_path")"
existing_closeout_version="$(jq -r '.releaseVersion // empty' "$closeout_json_path")"
existing_closeout_release_tag_sha="$(jq -r '.releaseTagSha // empty' "$closeout_json_path")"
existing_closeout_main_ref="$(jq -r '.mainSha // empty' "$closeout_json_path")"
existing_closeout_full_release_validation_run_id="$(jq -r '.fullReleaseValidationRunId // empty' "$closeout_json_path")"
existing_closeout_release_publish_run_id="$(jq -r '.releasePublishRunId // empty' "$closeout_json_path")"
existing_closeout_rollback_drill_id="$(jq -r '.rollbackDrill.id // empty' "$closeout_json_path")"
existing_closeout_rollback_drill_date="$(jq -r '.rollbackDrill.date // empty' "$closeout_json_path")"
if [[ "$existing_closeout_tag" != "$tag" ||
"$existing_closeout_version" != "$tag_package_version" ||
! "$existing_closeout_release_tag_sha" =~ ^[0-9a-f]{40}$ ||
! "$existing_closeout_main_ref" =~ ^[0-9a-f]{40}$ ||
-z "$existing_closeout_full_release_validation_run_id" ||
-z "$existing_closeout_release_publish_run_id" ||
-z "$existing_closeout_rollback_drill_id" ||
! "$existing_closeout_rollback_drill_date" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
echo "Stable closeout manifest for $tag is incomplete; refusing to repair it." >&2
exit 1
fi
main_ref="$existing_closeout_main_ref"
ROLLBACK_DRILL_ID="$existing_closeout_rollback_drill_id"
ROLLBACK_DRILL_DATE="$existing_closeout_rollback_drill_date"
repair_partial_closeout=true
elif [[ "$EVENT_NAME" == "push" ]]; then
main_version="$(jq -r '.version // empty' package.json)"
if [[ "$main_version" != "$release_package_version" &&
"$main_version" != "$fallback_package_version" ]]; then
echo "should_closeout=false" >> "$GITHUB_OUTPUT"
exit 0
fi
else
main_ref="main"
fi
evidence_dir="$RUNNER_TEMP/release-postpublish-evidence"
mkdir -p "$evidence_dir"
if ! gh release download "$evidence_source_tag" --repo "$GITHUB_REPOSITORY" \
--pattern "$evidence_asset" --pattern "$evidence_checksum_asset" --dir "$evidence_dir"; then
if [[ "$EVENT_NAME" == "push" ]]; then
echo "Stable closeout skipped: $evidence_source_tag predates immutable postpublish evidence." >&2
echo "should_closeout=false" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "Stable closeout is required for $tag, but immutable postpublish evidence from $evidence_source_tag is missing." >&2
exit 1
fi
evidence_path="$evidence_dir/$evidence_asset"
if ! (
cd "$evidence_dir"
sha256sum --strict --status -c "$evidence_checksum_asset"
); then
echo "Postpublish evidence checksum failed for $tag." >&2
exit 1
fi
evidence_release_tag="$(jq -r '.releaseTag // empty' "$evidence_path")"
full_release_validation_run_id="$(jq -r '[.workflowRuns[]? | select(.label == "Full Release Validation") | .id] | if length == 1 then .[0] else empty end' "$evidence_path")"
release_publish_run_id="$(jq -r '.releasePublishRunId // empty' "$evidence_path")"
if [[ "$evidence_release_tag" != "$evidence_source_tag" || -z "$full_release_validation_run_id" || -z "$release_publish_run_id" ]]; then
echo "Stable closeout is required for $tag, but postpublish evidence does not bind $evidence_source_tag to exactly one Full Release Validation run and its Publish run." >&2
exit 1
fi
if [[ -n "$existing_closeout_full_release_validation_run_id" &&
( "$existing_closeout_full_release_validation_run_id" != "$full_release_validation_run_id" ||
"$existing_closeout_release_publish_run_id" != "$release_publish_run_id" ) ]]; then
echo "Stable closeout manifest for $tag does not match immutable postpublish evidence; refusing to accept it." >&2
exit 1
fi
if [[ -z "$ROLLBACK_DRILL_ID" || -z "$ROLLBACK_DRILL_DATE" ]]; then
echo "Stable closeout requires repository variables RELEASE_ROLLBACK_DRILL_ID and RELEASE_ROLLBACK_DRILL_DATE, or explicit manual overrides." >&2
exit 1
fi
{
echo "full_release_validation_run_id=$full_release_validation_run_id"
echo "release_publish_run_id=$release_publish_run_id"
echo "rollback_drill_date=$ROLLBACK_DRILL_DATE"
echo "rollback_drill_id=$ROLLBACK_DRILL_ID"
echo "evidence_tag=$evidence_source_tag"
echo "fallback_correction=$fallback_correction"
echo "main_ref=$main_ref"
echo "repair_partial_closeout=$repair_partial_closeout"
echo "should_closeout=true"
echo "tag=$tag"
} >> "$GITHUB_OUTPUT"
verify:
name: Verify stable main closeout
needs: resolve
if: ${{ needs.resolve.outputs.should_closeout == 'true' }}
runs-on: ubuntu-24.04
timeout-minutes: 20
steps:
- name: Checkout resolved main state
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ needs.resolve.outputs.main_ref }}
fetch-depth: 1
persist-credentials: false
- name: Checkout shipped release tag
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: refs/tags/${{ needs.resolve.outputs.tag }}
path: release-tag
fetch-depth: 1
persist-credentials: false
- name: Checkout fallback evidence tag
if: ${{ needs.resolve.outputs.fallback_correction == 'true' }}
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: refs/tags/${{ needs.resolve.outputs.evidence_tag }}
path: evidence-tag
fetch-depth: 1
persist-credentials: false
- name: Bind fallback correction to the published package source
if: ${{ needs.resolve.outputs.fallback_correction == 'true' }}
run: |
set -euo pipefail
correction_sha="$(git -C "$GITHUB_WORKSPACE/release-tag" rev-parse HEAD)"
evidence_sha="$(git -C "$GITHUB_WORKSPACE/evidence-tag" rev-parse HEAD)"
if [[ "$correction_sha" != "$evidence_sha" ]]; then
echo "Fallback correction ${{ needs.resolve.outputs.tag }} must point to the same source commit as ${{ needs.resolve.outputs.evidence_tag }} to reuse immutable package evidence." >&2
exit 1
fi
- name: Verify release workflow evidence
env:
GH_TOKEN: ${{ github.token }}
FULL_RELEASE_VALIDATION_RUN_ID: ${{ needs.resolve.outputs.full_release_validation_run_id }}
RELEASE_PUBLISH_RUN_ID: ${{ needs.resolve.outputs.release_publish_run_id }}
run: |
set -euo pipefail
gh run view "$FULL_RELEASE_VALIDATION_RUN_ID" --repo "$GITHUB_REPOSITORY" \
--json workflowName,event,status,conclusion \
> "$RUNNER_TEMP/full-release-validation-run.json"
node --input-type=module - "$RUNNER_TEMP/full-release-validation-run.json" <<'NODE'
import { readFileSync } from "node:fs";
const run = JSON.parse(readFileSync(process.argv[2], "utf8"));
for (const [key, expected] of [
["workflowName", "Full Release Validation"],
["event", "workflow_dispatch"],
["status", "completed"],
["conclusion", "success"],
]) {
if (run[key] !== expected) {
throw new Error(`Full Release Validation must have ${key}=${expected}, got ${run[key] ?? "<missing>"}.`);
}
}
NODE
gh run view "$RELEASE_PUBLISH_RUN_ID" --repo "$GITHUB_REPOSITORY" \
--json workflowName,event,status,conclusion \
> "$RUNNER_TEMP/release-publish-run.json"
node --input-type=module - "$RUNNER_TEMP/release-publish-run.json" <<'NODE'
import { readFileSync } from "node:fs";
const run = JSON.parse(readFileSync(process.argv[2], "utf8"));
for (const [key, expected] of [
["workflowName", "OpenClaw Release Publish"],
["event", "workflow_dispatch"],
["status", "completed"],
["conclusion", "success"],
]) {
if (run[key] !== expected) {
throw new Error(`OpenClaw Release Publish must have ${key}=${expected}, got ${run[key] ?? "<missing>"}.`);
}
}
NODE
manifest_dir="$RUNNER_TEMP/full-release-validation-manifest"
rm -rf "$manifest_dir"
mkdir -p "$manifest_dir"
gh run download "$FULL_RELEASE_VALIDATION_RUN_ID" --repo "$GITHUB_REPOSITORY" \
--name "full-release-validation-${FULL_RELEASE_VALIDATION_RUN_ID}" \
--dir "$manifest_dir"
tag_sha="$(git -C "$GITHUB_WORKSPACE/release-tag" rev-parse HEAD)"
jq -e --arg tag_sha "$tag_sha" '
.workflowName == "Full Release Validation" and
.targetSha == $tag_sha and
.rerunGroup == "all" and
.runReleaseSoak == "true" and
.controls.performanceBlocking == true and
.childRuns.productPerformance.conclusion == "success"
' "$manifest_dir/full-release-validation-manifest.json" >/dev/null || {
echo "Full Release Validation manifest does not contain the required stable release controls." >&2
exit 1
}
- name: Verify stable state and write closeout manifest
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ needs.resolve.outputs.tag }}
FULL_RELEASE_VALIDATION_RUN_ID: ${{ needs.resolve.outputs.full_release_validation_run_id }}
RELEASE_PUBLISH_RUN_ID: ${{ needs.resolve.outputs.release_publish_run_id }}
ROLLBACK_DRILL_ID: ${{ needs.resolve.outputs.rollback_drill_id }}
ROLLBACK_DRILL_DATE: ${{ needs.resolve.outputs.rollback_drill_date }}
REPAIR_PARTIAL_CLOSEOUT: ${{ needs.resolve.outputs.repair_partial_closeout }}
CLOSEOUT_DIR: ${{ runner.temp }}/openclaw-stable-main-closeout
run: |
set -euo pipefail
mkdir -p "$CLOSEOUT_DIR"
gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" \
--json tagName,isDraft,isPrerelease,assets \
> "$CLOSEOUT_DIR/github-release.json"
node scripts/verify-stable-main-closeout.mjs \
--tag "$RELEASE_TAG" \
--main-dir "$GITHUB_WORKSPACE" \
--tag-dir "$GITHUB_WORKSPACE/release-tag" \
--release-json "$CLOSEOUT_DIR/github-release.json" \
--full-release-validation-run-id "$FULL_RELEASE_VALIDATION_RUN_ID" \
--release-publish-run-id "$RELEASE_PUBLISH_RUN_ID" \
--rollback-drill-id "$ROLLBACK_DRILL_ID" \
--rollback-drill-date "$ROLLBACK_DRILL_DATE" \
--allow-stale-rollback-drill "$REPAIR_PARTIAL_CLOSEOUT" \
--output "$CLOSEOUT_DIR/stable-main-closeout.json"
release_version="${RELEASE_TAG#v}"
sha256sum "$CLOSEOUT_DIR/stable-main-closeout.json" | awk -v asset="openclaw-${release_version}-stable-main-closeout.json" \
'{print $1 " " asset}' \
> "$CLOSEOUT_DIR/stable-main-closeout.json.sha256"
- name: Attach immutable closeout evidence
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ needs.resolve.outputs.tag }}
CLOSEOUT_DIR: ${{ runner.temp }}/openclaw-stable-main-closeout
run: |
set -euo pipefail
release_version="${RELEASE_TAG#v}"
attach_or_verify() {
local source_path="$1"
local asset_name="$2"
local existing_dir="$CLOSEOUT_DIR/existing-${asset_name}"
mkdir -p "$existing_dir"
if gh release download "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" \
--pattern "$asset_name" --dir "$existing_dir"; then
cmp --silent "$source_path" "$existing_dir/$asset_name" || {
echo "Existing release asset $asset_name differs from closeout evidence." >&2
exit 1
}
return
fi
gh release upload "$RELEASE_TAG" "$source_path#$asset_name" --repo "$GITHUB_REPOSITORY"
}
attach_or_verify \
"$CLOSEOUT_DIR/stable-main-closeout.json" \
"openclaw-${release_version}-stable-main-closeout.json"
attach_or_verify \
"$CLOSEOUT_DIR/stable-main-closeout.json.sha256" \
"openclaw-${release_version}-stable-main-closeout.json.sha256"
- name: Upload closeout workflow evidence
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: openclaw-stable-main-closeout-${{ needs.resolve.outputs.tag }}
path: ${{ runner.temp }}/openclaw-stable-main-closeout
if-no-files-found: error

View file

@ -25,7 +25,7 @@ jobs:
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
@ -53,7 +53,7 @@ jobs:
scripts/run-opengrep.sh --sarif --error
- name: Upload SARIF to GitHub Code Scanning
uses: github/codeql-action/upload-sarif@v4.36.2
uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e
# Only upload if the scan actually produced a SARIF file.
if: always() && hashFiles('.opengrep-out/precise.sarif') != ''
with:
@ -62,7 +62,7 @@ jobs:
- name: Upload SARIF as workflow artifact
if: always()
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: opengrep-full-sarif
path: .opengrep-out/precise.sarif

View file

@ -41,7 +41,7 @@ jobs:
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ github.sha }}
fetch-depth: 2
@ -84,7 +84,7 @@ jobs:
scripts/run-opengrep.sh --changed --sarif --error
- name: Upload SARIF to GitHub Code Scanning
uses: github/codeql-action/upload-sarif@v4.36.2
uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e
# Only upload if the scan actually produced a SARIF file.
if: always() && hashFiles('.opengrep-out/precise.sarif') != ''
with:
@ -93,7 +93,7 @@ jobs:
- name: Upload SARIF as workflow artifact
if: always()
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: opengrep-pr-diff-sarif
path: .opengrep-out/precise.sarif

View file

@ -325,7 +325,7 @@ jobs:
telegram_mode: ${{ steps.profile.outputs.telegram_mode }}
steps:
- name: Checkout package workflow ref
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ inputs.workflow_ref }}
fetch-depth: 0
@ -339,7 +339,7 @@ jobs:
- name: Download current-run package artifact input
if: inputs.source == 'artifact' && inputs.artifact_run_id == ''
uses: actions/download-artifact@v8
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: ${{ inputs.artifact_name }}
path: .artifacts/package-candidate-input
@ -492,7 +492,7 @@ jobs:
node scripts/resolve-upgrade-survivor-baselines.mjs "${args[@]}" >/dev/null
- name: Upload package-under-test artifact
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: ${{ env.PACKAGE_ARTIFACT_NAME }}
path: |
@ -541,13 +541,13 @@ jobs:
timeout-minutes: 10
steps:
- name: Checkout package workflow ref
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ inputs.workflow_ref }}
fetch-depth: 1
- name: Download package-under-test artifact
uses: actions/download-artifact@v8
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: ${{ needs.resolve_package.outputs.package_artifact_name }}
path: .artifacts/docker-e2e-package

View file

@ -48,7 +48,7 @@ jobs:
matrix: ${{ steps.plan.outputs.matrix }}
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
ref: ${{ github.ref }}
@ -229,7 +229,7 @@ jobs:
contents: read
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
@ -303,7 +303,7 @@ jobs:
plugin: ${{ fromJson(needs.resolve_bootstrap_plan.outputs.matrix) }}
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
ref: ${{ github.ref }}

View file

@ -63,7 +63,7 @@ jobs:
missing_trusted_publisher_matrix: ${{ steps.plan.outputs.missing_trusted_publisher_matrix }}
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
ref: ${{ github.ref }}
@ -275,7 +275,7 @@ jobs:
contents: read
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
@ -315,7 +315,7 @@ jobs:
plugin: ${{ fromJson(needs.preview_plugins_clawhub.outputs.matrix) }}
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
ref: ${{ github.ref }}
@ -364,7 +364,7 @@ jobs:
run: bash scripts/plugin-clawhub-publish.sh --pack "${PACKAGE_DIR}"
- name: Upload ClawHub package artifact
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: ${{ matrix.plugin.artifactName }}
path: ${{ runner.temp }}/clawhub-package-artifact/*.tgz

View file

@ -57,7 +57,7 @@ jobs:
matrix: ${{ steps.plan.outputs.matrix }}
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.sha }}
@ -185,7 +185,7 @@ jobs:
contents: read
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
@ -224,7 +224,7 @@ jobs:
plugin: ${{ fromJson(needs.preview_plugins_npm.outputs.matrix) }}
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
ref: ${{ needs.preview_plugins_npm.outputs.ref_revision }}
@ -257,7 +257,7 @@ jobs:
plugin: ${{ fromJson(needs.preview_plugins_npm.outputs.matrix) }}
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
ref: ${{ needs.preview_plugins_npm.outputs.ref_revision }}

View file

@ -47,7 +47,7 @@ jobs:
plugin_prerelease_docker_lanes: ${{ steps.manifest.outputs.plugin_prerelease_docker_lanes }}
steps:
- name: Checkout target
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ inputs.target_ref }}
fetch-depth: 1
@ -216,7 +216,7 @@ jobs:
matrix: ${{ fromJson(needs.preflight.outputs.plugin_prerelease_static_matrix) }}
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ needs.preflight.outputs.checkout_revision }}
fetch-depth: 1
@ -252,7 +252,7 @@ jobs:
matrix: ${{ fromJson(needs.preflight.outputs.plugin_prerelease_node_matrix) }}
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ needs.preflight.outputs.checkout_revision }}
fetch-depth: 1
@ -325,7 +325,7 @@ jobs:
matrix: ${{ fromJson(needs.preflight.outputs.plugin_prerelease_extension_matrix) }}
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ needs.preflight.outputs.checkout_revision }}
fetch-depth: 1
@ -357,7 +357,7 @@ jobs:
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ needs.preflight.outputs.checkout_revision }}
fetch-depth: 1
@ -519,7 +519,7 @@ jobs:
- name: Upload plugin inspector advisory artifacts
if: always()
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: plugin-inspector-advisory
path: .artifacts/plugin-inspector/**

View file

@ -65,7 +65,7 @@ jobs:
steps:
- name: Require maintainer-level repository access
id: permission
uses: actions/github-script@v9
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
script: |
if (context.eventName === "schedule") {
@ -101,7 +101,7 @@ jobs:
trusted_reason: ${{ steps.validate.outputs.trusted_reason }}
steps:
- name: Checkout selected ref
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.sha }}
@ -172,7 +172,7 @@ jobs:
OPENCLAW_LIVE_SETUP_TOKEN_VALUE: ""
steps:
- name: Checkout selected ref
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
ref: ${{ needs.validate_selected_ref.outputs.selected_revision }}
@ -221,7 +221,7 @@ jobs:
- name: Upload parity artifacts
if: always()
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: qa-parity-${{ github.run_id }}-${{ github.run_attempt }}
path: .artifacts/qa-e2e/
@ -241,7 +241,7 @@ jobs:
OPENCLAW_QA_REDACT_PUBLIC_METADATA: "1"
steps:
- name: Checkout selected ref
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
ref: ${{ needs.validate_selected_ref.outputs.selected_revision }}
@ -310,7 +310,7 @@ jobs:
- name: Upload live runtime token-efficiency artifacts
if: always()
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: qa-live-runtime-token-efficiency-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ steps.run_lane.outputs.output_dir }}
@ -326,7 +326,7 @@ jobs:
environment: qa-live-shared
steps:
- name: Checkout selected ref
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
ref: ${{ needs.validate_selected_ref.outputs.selected_revision }}
@ -386,7 +386,7 @@ jobs:
- name: Upload Matrix QA artifacts
if: always()
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: qa-live-matrix-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ steps.run_lane.outputs.output_dir }}
@ -411,7 +411,7 @@ jobs:
- e2ee-cli
steps:
- name: Checkout selected ref
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
ref: ${{ needs.validate_selected_ref.outputs.selected_revision }}
@ -470,7 +470,7 @@ jobs:
- name: Upload Matrix QA shard artifacts
if: always()
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: qa-live-matrix-${{ matrix.profile }}-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ steps.run_lane.outputs.output_dir }}
@ -485,7 +485,7 @@ jobs:
environment: qa-live-shared
steps:
- name: Checkout selected ref
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
ref: ${{ needs.validate_selected_ref.outputs.selected_revision }}
@ -564,7 +564,7 @@ jobs:
- name: Upload Telegram QA artifacts
if: always()
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: qa-live-telegram-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ steps.run_lane.outputs.output_dir }}
@ -579,7 +579,7 @@ jobs:
environment: qa-live-shared
steps:
- name: Checkout selected ref
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
ref: ${{ needs.validate_selected_ref.outputs.selected_revision }}
@ -658,7 +658,7 @@ jobs:
- name: Upload Discord QA artifacts
if: always()
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: qa-live-discord-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ steps.run_lane.outputs.output_dir }}
@ -676,7 +676,7 @@ jobs:
environment: qa-live-shared
steps:
- name: Checkout selected ref
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
ref: ${{ needs.validate_selected_ref.outputs.selected_revision }}
@ -755,7 +755,7 @@ jobs:
- name: Upload WhatsApp QA artifacts
if: always()
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: qa-live-whatsapp-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ steps.run_lane.outputs.output_dir }}
@ -770,7 +770,7 @@ jobs:
environment: qa-live-shared
steps:
- name: Checkout selected ref
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
ref: ${{ needs.validate_selected_ref.outputs.selected_revision }}
@ -850,7 +850,7 @@ jobs:
- name: Upload Slack QA artifacts
if: always()
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: qa-live-slack-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ steps.run_lane.outputs.output_dir }}

View file

@ -22,11 +22,11 @@ jobs:
pull-requests: read
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ github.event.pull_request.base.sha }}
persist-credentials: false
- uses: actions/create-github-app-token@v3
- uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3
id: app-token
continue-on-error: true
with:
@ -34,7 +34,7 @@ jobs:
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
permission-issues: read
permission-members: read
- uses: actions/create-github-app-token@v3
- uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3
id: app-token-fallback
if: steps.app-token.outcome == 'failure'
continue-on-error: true

View file

@ -30,7 +30,7 @@ jobs:
runs-on: blacksmith-16vcpu-ubuntu-2404
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
submodules: false

View file

@ -44,13 +44,13 @@ jobs:
pull-requests: write
runs-on: blacksmith-16vcpu-ubuntu-2404
steps:
- uses: actions/create-github-app-token@v3
- uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3
id: app-token
continue-on-error: true
with:
app-id: "2729701"
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- uses: actions/create-github-app-token@v3
- uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3
id: app-token-fallback
continue-on-error: true
with:
@ -59,7 +59,7 @@ jobs:
- name: Mark stale unassigned issues and pull requests (primary)
id: stale-primary
continue-on-error: true
uses: actions/stale@v10
uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10
with:
repo-token: ${{ steps.app-token.outputs.token || steps.app-token-fallback.outputs.token }}
days-before-issue-stale: 14
@ -92,7 +92,7 @@ jobs:
- name: Mark stale assigned issues (primary)
id: assigned-issue-stale-primary
continue-on-error: true
uses: actions/stale@v10
uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10
with:
repo-token: ${{ steps.app-token.outputs.token || steps.app-token-fallback.outputs.token }}
days-before-issue-stale: 30
@ -116,7 +116,7 @@ jobs:
- name: Mark stale assigned pull requests (primary)
id: assigned-stale-primary
continue-on-error: true
uses: actions/stale@v10
uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10
with:
repo-token: ${{ steps.app-token.outputs.token || steps.app-token-fallback.outputs.token }}
days-before-issue-stale: -1
@ -140,7 +140,7 @@ jobs:
- name: Check stale state cache
id: stale-state
if: always()
uses: actions/github-script@v9
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
github-token: ${{ steps.app-token-fallback.outputs.token || steps.app-token.outputs.token }}
script: |
@ -163,7 +163,7 @@ jobs:
}
- name: Mark stale unassigned issues and pull requests (fallback)
if: (steps.stale-primary.outcome == 'failure' || steps.stale-state.outputs.has_state == 'true') && steps.app-token-fallback.outputs.token != ''
uses: actions/stale@v10
uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10
with:
repo-token: ${{ steps.app-token-fallback.outputs.token }}
days-before-issue-stale: 14
@ -195,7 +195,7 @@ jobs:
That channel is the escape hatch for high-quality PRs that get auto-closed.
- name: Mark stale assigned issues (fallback)
if: (steps.assigned-issue-stale-primary.outcome == 'failure' || steps.stale-state.outputs.has_state == 'true') && steps.app-token-fallback.outputs.token != ''
uses: actions/stale@v10
uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10
with:
repo-token: ${{ steps.app-token-fallback.outputs.token }}
days-before-issue-stale: 30
@ -218,7 +218,7 @@ jobs:
close-issue-reason: not_planned
- name: Mark stale assigned pull requests (fallback)
if: (steps.assigned-stale-primary.outcome == 'failure' || steps.stale-state.outputs.has_state == 'true') && steps.app-token-fallback.outputs.token != ''
uses: actions/stale@v10
uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10
with:
repo-token: ${{ steps.app-token-fallback.outputs.token }}
days-before-issue-stale: -1
@ -247,13 +247,13 @@ jobs:
pull-requests: write
runs-on: blacksmith-16vcpu-ubuntu-2404
steps:
- uses: actions/create-github-app-token@v3
- uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3
id: app-token
with:
app-id: "2971289"
private-key: ${{ secrets.GH_APP_PRIVATE_KEY_FALLBACK }}
- name: Backfill stale closures
uses: actions/github-script@v9
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
env:
DRY_RUN: ${{ inputs.dry_run }}
INCLUDE_ISSUES: ${{ inputs.include_issues }}
@ -494,13 +494,13 @@ jobs:
issues: write
runs-on: blacksmith-16vcpu-ubuntu-2404
steps:
- uses: actions/create-github-app-token@v3
- uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3
id: app-token
with:
app-id: "2729701"
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- name: Lock closed issues after 48h of no comments
uses: actions/github-script@v9
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
github-token: ${{ steps.app-token.outputs.token }}
script: |

View file

@ -35,7 +35,7 @@ jobs:
timeout-minutes: 240
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: main
fetch-depth: 0
@ -271,7 +271,7 @@ jobs:
- name: Upload test performance artifacts
if: steps.gate.outputs.run_agent == 'true' && always()
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: test-performance-agent-${{ github.run_id }}
path: .artifacts/test-perf/

View file

@ -32,8 +32,7 @@ jobs:
OPENCLAW_TUI_PTY_INCLUDE_LOCAL: "1"
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:

View file

@ -37,8 +37,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- name: Install ShellCheck
run: sudo apt-get update -y && sudo apt-get install -y shellcheck
@ -71,8 +70,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- name: install.sh in Docker
run: |
timeout --kill-after=30s 20m docker run --rm \
@ -93,10 +91,9 @@ jobs:
runs-on: macos-15
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- name: Setup Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: 24
@ -115,10 +112,9 @@ jobs:
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- name: Setup Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: 24
@ -141,13 +137,13 @@ jobs:
- name: Checkout OpenClaw
if: env.OPENCLAW_GH_TOKEN != ''
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
path: openclaw
- name: Checkout openclaw.ai
if: env.OPENCLAW_GH_TOKEN != ''
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
repository: openclaw/openclaw.ai
token: ${{ env.OPENCLAW_GH_TOKEN }}
@ -175,13 +171,13 @@ jobs:
- name: Setup Bun
if: steps.changes.outputs.changed == 'true'
uses: oven-sh/setup-bun@v2
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: latest
- name: Setup Node.js
if: steps.changes.outputs.changed == 'true'
uses: actions/setup-node@v6
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: "24"

View file

@ -120,7 +120,7 @@ jobs:
chmod 600 ~/.ssh/authorized_keys
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
submodules: false

View file

@ -54,7 +54,7 @@ jobs:
shell: pwsh
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ inputs.target_ref || github.ref }}
persist-credentials: false

View file

@ -115,7 +115,7 @@ jobs:
git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout
- name: Setup Python
uses: actions/setup-python@v6
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: "3.12"

View file

@ -1114,7 +1114,7 @@ internal fun gatewayRecoveryUiState(
ready: Boolean,
statusText: String,
connectSettling: Boolean,
nodeCapabilityApprovalState: GatewayNodeApprovalState = GatewayNodeApprovalState.Loading,
nodeCapabilityApprovalState: GatewayNodeApprovalState,
gatewayConnectionProblem: GatewayConnectionProblem? = null,
): GatewayRecoveryUiState =
when {

View file

@ -112,6 +112,7 @@ class OnboardingFlowLogicTest {
ready = false,
statusText = "Gateway error: pairing required; approval in progress",
connectSettling = false,
nodeCapabilityApprovalState = GatewayNodeApprovalState.Approved,
),
)
}
@ -124,6 +125,7 @@ class OnboardingFlowLogicTest {
ready = true,
statusText = "Gateway error: pairing required",
connectSettling = false,
nodeCapabilityApprovalState = GatewayNodeApprovalState.Approved,
),
)
}
@ -162,6 +164,7 @@ class OnboardingFlowLogicTest {
ready = false,
statusText = "Connecting…",
connectSettling = false,
nodeCapabilityApprovalState = GatewayNodeApprovalState.Approved,
gatewayConnectionProblem =
GatewayConnectionProblem(
code = "PAIRING_REQUIRED",
@ -184,6 +187,7 @@ class OnboardingFlowLogicTest {
ready = false,
statusText = "Connecting…",
connectSettling = false,
nodeCapabilityApprovalState = GatewayNodeApprovalState.Approved,
gatewayConnectionProblem =
GatewayConnectionProblem(
code = "PAIRING_REQUIRED",
@ -206,6 +210,7 @@ class OnboardingFlowLogicTest {
ready = false,
statusText = "Offline",
connectSettling = true,
nodeCapabilityApprovalState = GatewayNodeApprovalState.Approved,
),
)
}
@ -218,6 +223,7 @@ class OnboardingFlowLogicTest {
ready = false,
statusText = "Connected (node offline)",
connectSettling = false,
nodeCapabilityApprovalState = GatewayNodeApprovalState.Approved,
),
)
}
@ -230,6 +236,7 @@ class OnboardingFlowLogicTest {
ready = false,
statusText = "Gateway error: connection refused",
connectSettling = false,
nodeCapabilityApprovalState = GatewayNodeApprovalState.Approved,
),
)
}

View file

@ -189,7 +189,7 @@ Every lane uploads GitHub artifacts. When `CLAWGRIT_REPORTS_TOKEN` is configured
## Full Release Validation
`Full Release Validation` is the manual umbrella workflow for "run everything before release." It accepts a branch, tag, or full commit SHA, dispatches the manual `CI` workflow with that target, dispatches `Plugin Prerelease` for release-only plugin/package/static/Docker proof, and dispatches `OpenClaw Release Checks` for install smoke, package acceptance, cross-OS package checks, QA Lab parity, Matrix, and Telegram lanes. Stable/default runs keep exhaustive live/E2E and Docker release-path coverage behind `run_release_soak=true`; `release_profile=full` forces that soak coverage on so broad advisory validation remains broad. With `rerun_group=all` and `release_profile=full`, it also runs `NPM Telegram Beta E2E` against the `release-package-under-test` artifact from release checks. After publishing, pass `release_package_spec` to reuse the shipped npm package across release checks, Package Acceptance, Docker, cross-OS, and Telegram without rebuilding. Use `npm_telegram_package_spec` only when Telegram must prove a different package. The Codex plugin live package lane uses the same selected state by default: published `release_package_spec=openclaw@<tag>` derives `codex_plugin_spec=npm:@openclaw/codex@<tag>`, while SHA/artifact runs pack `extensions/codex` from the selected ref. Set `codex_plugin_spec` explicitly for custom plugin sources such as `npm:`, `npm-pack:`, or `git:` specs.
`Full Release Validation` is the manual umbrella workflow for "run everything before release." It accepts a branch, tag, or full commit SHA, dispatches the manual `CI` workflow with that target, dispatches `Plugin Prerelease` for release-only plugin/package/static/Docker proof, and dispatches `OpenClaw Release Checks` for install smoke, package acceptance, cross-OS package checks, QA Lab parity, Matrix, and Telegram lanes. Stable and full profiles always include exhaustive live/E2E and Docker release-path soak coverage; the beta profile can opt in with `run_release_soak=true`. With `rerun_group=all` and `release_profile=full`, it also runs `NPM Telegram Beta E2E` against the `release-package-under-test` artifact from release checks. After publishing, pass `release_package_spec` to reuse the shipped npm package across release checks, Package Acceptance, Docker, cross-OS, and Telegram without rebuilding. Use `npm_telegram_package_spec` only when Telegram must prove a different package. The Codex plugin live package lane uses the same selected state by default: published `release_package_spec=openclaw@<tag>` derives `codex_plugin_spec=npm:@openclaw/codex@<tag>`, while SHA/artifact runs pack `extensions/codex` from the selected ref. Set `codex_plugin_spec` explicitly for custom plugin sources such as `npm:`, `npm-pack:`, or `git:` specs.
See [Full release validation](/reference/full-release-validation) for the
stage matrix, exact workflow job names, profile differences, artifacts, and
@ -232,9 +232,9 @@ different SHA.
`release_profile` controls live/provider breadth passed into release checks. The
manual release workflows default to `stable`; use `full` only when you
intentionally want the broad advisory provider/media matrix. `run_release_soak`
controls whether stable/default release checks run the exhaustive live/E2E and
Docker release-path soak; `full` forces soak on.
intentionally want the broad advisory provider/media matrix. Stable and full
release checks always run the exhaustive live/E2E and Docker release-path soak;
the beta profile can opt in with `run_release_soak=true`.
- `minimum` keeps the fastest OpenAI/core release-critical lanes.
- `stable` adds the stable provider/backend set.

View file

@ -118,8 +118,8 @@ the maintainer-only release runbook.
`CHANGELOG.md` section. Stable releases published to npm `latest` become the
GitHub latest release; stable maintenance releases kept on npm `beta` are
created with GitHub `latest=false`. The workflow also uploads the preflight
dependency evidence to the GitHub release as
`openclaw-<version>-dependency-evidence.zip` for post-release incident
dependency evidence, the full-validation manifest, and postpublish registry
verification evidence to the GitHub release for post-release incident
response. The publish workflow prints child run IDs immediately, auto-approves
release environment gates the workflow token is allowed to approve, summarizes
failed child jobs with log tails, closes out the GitHub release and dependency
@ -178,6 +178,27 @@ release state.
`OPENCLAW_TESTBOX=1 pnpm check:changed`. Push, then verify `origin/main`
contains the shipped version and changelog before calling the stable release
done.
6. Keep the repository variables `RELEASE_ROLLBACK_DRILL_ID` and
`RELEASE_ROLLBACK_DRILL_DATE` current after each private rollback drill.
`OpenClaw Stable Main Closeout` starts from the `main` push that carries the
shipped version, changelog, and appcast after stable publication. It reads
immutable postpublish evidence to bind the shipped tag to its Full Release
Validation and Publish runs, then verifies the stable main state, release,
mandatory stable soak, and blocking performance evidence. It attaches an
immutable closeout manifest and checksum to the GitHub release. The automatic
push trigger skips legacy releases that predate immutable postpublish
evidence; it never treats that skip as a completed closeout. A complete
closeout requires both assets and a matching checksum. A partial manifest
replays its recorded `main` SHA and rollback drill to regenerate identical
bytes, then attaches the missing checksum; an invalid pair, or a checksum
without a manifest, stays blocking. A missing or more-than-90-day-old drill
record blocks a new evidence-backed closeout; private recovery commands
remain in the maintainer-only runbook. Use manual dispatch only to repair or
replay an evidence-backed stable closeout.
A legacy fallback correction tag may reuse base-package evidence only when
the correction tag resolves to the same source commit as the base stable tag.
A correction with different source must publish and verify its own package
evidence.
## Release preflight
@ -205,9 +226,9 @@ release state.
kick off all pre-release test boxes from one entrypoint. It accepts a branch,
tag, or full commit SHA, dispatches manual `CI`, and dispatches
`OpenClaw Release Checks` for install smoke, package acceptance, cross-OS
package checks, QA Lab parity, Matrix, and Telegram lanes. Stable/default runs
keep exhaustive live/E2E and Docker release-path soak behind
`run_release_soak=true`; `release_profile=full` forces soak on. With
package checks, QA Lab parity, Matrix, and Telegram lanes. Stable and full
runs always include exhaustive live/E2E and Docker release-path soak;
`run_release_soak=true` is retained for an explicit beta soak. With
`release_profile=full` and `rerun_group=all`, it also runs package Telegram
E2E against the `release-package-under-test` artifact from release checks.
Provide `release_package_spec` after publishing a beta to reuse the shipped
@ -472,13 +493,12 @@ Use `release_profile` to select live/provider breadth:
- `stable`: minimum plus stable provider/backend coverage for release approval
- `full`: stable plus broad advisory provider/media coverage
Use `run_release_soak=true` with `stable` when the release-blocking lanes are
green and you want the exhaustive live/E2E, Docker release-path, and
bounded published upgrade-survivor sweep before promotion. That sweep covers
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. `full` implies
`run_release_soak=true`.
each baseline sharded into its own Docker runner job.
`OpenClaw Release Checks` uses the trusted workflow ref to resolve the target
ref once as `release-package-under-test` and reuses that artifact in cross-OS,
@ -669,7 +689,7 @@ prepared release package artifact, `suite_profile=custom`,
configured-auth update restart, live ClawHub skill install, stale plugin dependency cleanup, offline plugin
fixtures, plugin update, and Telegram package QA against the same resolved
tarball. Blocking release checks use the default latest published package
baseline; `run_release_soak=true` or
baseline; the beta profile with `run_release_soak=true`, `release_profile=stable`, or
`release_profile=full` expands to every stable npm-published baseline from
`2026.4.23` through `latest` plus reported-issue fixtures. Use
Package Acceptance with `source=npm` for an already shipped candidate,
@ -835,8 +855,8 @@ package cannot ship without every publishable official plugin, including
require the resolved commit to be reachable from an OpenClaw branch or
release tag.
- `run_release_soak`: opt into exhaustive live/E2E, Docker release-path, and
all-since upgrade-survivor soak on stable/default release checks. It is forced
on by `release_profile=full`.
all-since upgrade-survivor soak for beta release checks. It is forced on by
`release_profile=stable` and `release_profile=full`.
Rules:

View file

@ -27,10 +27,10 @@ Child workflows use the trusted workflow ref for the harness and the input
`ref` for the candidate under test. That keeps new validation logic available
when validating an older release branch or tag.
By default, `release_profile=stable` runs the release-blocking lanes and skips
the exhaustive live/Docker soak. Pass `run_release_soak=true` to include the
soak lanes on a stable run. `release_profile=full` always enables soak lanes so
the broad advisory profile never drops coverage silently.
`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 without this
soak and blocking product-performance evidence.
Package Acceptance normally builds the candidate tarball from the resolved
`ref`, including full-SHA runs dispatched with `pnpm ci:full-release`. After a
@ -47,15 +47,15 @@ that plugin, then runs Codex CLI preflight and same-session OpenAI agent turns.
## Top-level stages
| Stage | Details |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Target resolution | **Job:** `Resolve target ref`<br />**Child workflow:** none<br />**Proves:** resolves the release branch, tag, or full commit SHA and records selected inputs.<br />**Rerun:** rerun the umbrella if this fails. |
| Vitest and normal CI | **Job:** `Run normal full CI`<br />**Child workflow:** `CI`<br />**Proves:** manual full CI graph against the target ref, including Linux Node lanes, bundled plugin shards, plugin and channel contract shards, Node 22 compatibility, `check-*`, `check-additional-*`, built-artifact smoke checks, docs checks, Python skills, Windows, macOS, Control UI i18n, and Android via the umbrella.<br />**Rerun:** `rerun_group=ci`. |
| Plugin prerelease | **Job:** `Run plugin prerelease validation`<br />**Child workflow:** `Plugin Prerelease`<br />**Proves:** release-only plugin static checks, agentic plugin coverage, full extension batch shards, plugin prerelease Docker lanes, and a non-blocking `plugin-inspector-advisory` artifact for compatibility triage.<br />**Rerun:** `rerun_group=plugin-prerelease`. |
| Release checks | **Job:** `Run release/live/Docker/QA validation`<br />**Child workflow:** `OpenClaw Release Checks`<br />**Proves:** install smoke, cross-OS package checks, Package Acceptance, QA Lab parity, live Matrix, and live Telegram. With `run_release_soak=true` or `release_profile=full`, also runs exhaustive live/E2E suites and Docker release-path chunks.<br />**Rerun:** `rerun_group=release-checks` or a narrower release-checks handle. |
| Package artifact | **Job:** `Prepare release package artifact`<br />**Child workflow:** none<br />**Proves:** creates the parent `release-package-under-test` tarball early enough for package-facing checks that do not need to wait for `OpenClaw Release Checks`.<br />**Rerun:** rerun the umbrella or provide `release_package_spec` for published-package reruns. |
| Package Telegram | **Job:** `Run package Telegram E2E`<br />**Child workflow:** `NPM Telegram Beta E2E`<br />**Proves:** parent-artifact-backed Telegram package proof for `rerun_group=all` with `release_profile=full`, or published-package Telegram proof when `release_package_spec` or `npm_telegram_package_spec` is set.<br />**Rerun:** `rerun_group=npm-telegram` with `release_package_spec` or `npm_telegram_package_spec`. |
| Umbrella verifier | **Job:** `Verify full validation`<br />**Child workflow:** none<br />**Proves:** re-checks recorded child run conclusions and appends slowest-job tables from child workflows.<br />**Rerun:** rerun only this job after rerunning a failed child to green. |
| Stage | Details |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Target resolution | **Job:** `Resolve target ref`<br />**Child workflow:** none<br />**Proves:** resolves the release branch, tag, or full commit SHA and records selected inputs.<br />**Rerun:** rerun the umbrella if this fails. |
| Vitest and normal CI | **Job:** `Run normal full CI`<br />**Child workflow:** `CI`<br />**Proves:** manual full CI graph against the target ref, including Linux Node lanes, bundled plugin shards, plugin and channel contract shards, Node 22 compatibility, `check-*`, `check-additional-*`, built-artifact smoke checks, docs checks, Python skills, Windows, macOS, Control UI i18n, and Android via the umbrella.<br />**Rerun:** `rerun_group=ci`. |
| Plugin prerelease | **Job:** `Run plugin prerelease validation`<br />**Child workflow:** `Plugin Prerelease`<br />**Proves:** release-only plugin static checks, agentic plugin coverage, full extension batch shards, plugin prerelease Docker lanes, and a non-blocking `plugin-inspector-advisory` artifact for compatibility triage.<br />**Rerun:** `rerun_group=plugin-prerelease`. |
| Release checks | **Job:** `Run release/live/Docker/QA validation`<br />**Child workflow:** `OpenClaw Release Checks`<br />**Proves:** install smoke, cross-OS package checks, Package Acceptance, QA Lab parity, live Matrix, and live Telegram. Stable and full profiles also run exhaustive live/E2E suites and Docker release-path chunks; beta can opt in with `run_release_soak=true`.<br />**Rerun:** `rerun_group=release-checks` or a narrower release-checks handle. |
| Package artifact | **Job:** `Prepare release package artifact`<br />**Child workflow:** none<br />**Proves:** creates the parent `release-package-under-test` tarball early enough for package-facing checks that do not need to wait for `OpenClaw Release Checks`.<br />**Rerun:** rerun the umbrella or provide `release_package_spec` for published-package reruns. |
| Package Telegram | **Job:** `Run package Telegram E2E`<br />**Child workflow:** `NPM Telegram Beta E2E`<br />**Proves:** parent-artifact-backed Telegram package proof for `rerun_group=all` with `release_profile=full`, or published-package Telegram proof when `release_package_spec` or `npm_telegram_package_spec` is set.<br />**Rerun:** `rerun_group=npm-telegram` with `release_package_spec` or `npm_telegram_package_spec`. |
| Umbrella verifier | **Job:** `Verify full validation`<br />**Child workflow:** none<br />**Proves:** re-checks recorded child run conclusions and appends slowest-job tables from child workflows.<br />**Rerun:** rerun only this job after rerunning a failed child to green. |
For `ref=main` and `rerun_group=all`, a newer umbrella supersedes an older one.
When the parent is cancelled, its monitor cancels any child workflow it already
@ -105,11 +105,11 @@ commands with package artifact and image reuse inputs when available.
`release_profile` mostly controls live/provider breadth inside release checks.
It does not remove normal full CI, Plugin Prerelease, install smoke, package
acceptance, or QA Lab. For `stable`, exhaustive repo/live E2E and Docker
release-path chunks are soak coverage and run when `run_release_soak=true`.
`full` forces soak coverage on and also makes the umbrella run package Telegram
E2E against the parent release package artifact when `rerun_group=all`, so a full
pre-publish candidate does not silently skip that Telegram package lane.
acceptance, or QA Lab. Stable and full profiles always run exhaustive repo/live
E2E and Docker release-path soak coverage. The beta profile can opt in with
`run_release_soak=true`. The full profile also makes the umbrella run package
Telegram E2E against the parent release package artifact when `rerun_group=all`,
so a full pre-publish candidate does not silently skip that Telegram package lane.
| Profile | Intended use | Included live/provider coverage |
| --------- | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |

View file

@ -1363,9 +1363,9 @@
}
},
"node_modules/semver": {
"version": "7.8.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.2.tgz",
"integrity": "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==",
"version": "7.8.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz",
"integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"

View file

@ -1467,9 +1467,9 @@
"license": "MIT"
},
"node_modules/semver": {
"version": "7.8.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.2.tgz",
"integrity": "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==",
"version": "7.8.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz",
"integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"

View file

@ -1156,9 +1156,9 @@
"license": "MIT"
},
"node_modules/semver": {
"version": "7.8.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.2.tgz",
"integrity": "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==",
"version": "7.8.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz",
"integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"

View file

@ -348,9 +348,9 @@
"license": "MIT"
},
"node_modules/semver": {
"version": "7.8.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.2.tgz",
"integrity": "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==",
"version": "7.8.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz",
"integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"

View file

@ -1982,6 +1982,7 @@
"oxlint-tsgolint": "0.23.0",
"shiki": "4.1.0",
"signal-utils": "0.21.1",
"sigstore": "4.1.1",
"tsdown": "0.22.1",
"tsx": "4.22.3",
"unrun": "0.3.0",

293
pnpm-lock.yaml generated
View file

@ -284,6 +284,9 @@ importers:
signal-utils:
specifier: 0.21.1
version: 0.21.1(signal-polyfill@0.2.2)
sigstore:
specifier: 4.1.1
version: 4.1.1
tsdown:
specifier: 0.22.1
version: 0.22.1(@typescript/native-preview@7.0.0-dev.20260527.2)(tsx@4.22.3)(typescript@6.0.3)(unrun@0.3.0)
@ -7706,6 +7709,131 @@ packages:
zwitch@2.0.4:
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
'@gar/promise-retry@1.0.3':
resolution: {integrity: sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA==}
engines: {node: ^20.17.0 || >=22.9.0}
'@npmcli/agent@4.0.2':
resolution: {integrity: sha512-EUEuWAxnL07Sp5/iC/1X6Xj+XThUvnbei9zfRWZdEXa7lss9RTHMhAHBeg+MZ5To9s/gGaSI+UwZTPdYMvKSeg==}
engines: {node: ^20.17.0 || >=22.9.0}
'@npmcli/fs@5.0.0':
resolution: {integrity: sha512-7OsC1gNORBEawOa5+j2pXN9vsicaIOH5cPXxoR6fJOmH6/EXpJB2CajXOu1fPRFun2m1lktEFX11+P89hqO/og==}
engines: {node: ^20.17.0 || >=22.9.0}
'@npmcli/redact@4.0.0':
resolution: {integrity: sha512-gOBg5YHMfZy+TfHArfVogwgfBeQnKbbGo3pSUyK/gSI0AVu+pEiDVcKlQb0D8Mg1LNRZILZ6XG8I5dJ4KuAd9Q==}
engines: {node: ^20.17.0 || >=22.9.0}
'@sigstore/bundle@4.0.0':
resolution: {integrity: sha512-NwCl5Y0V6Di0NexvkTqdoVfmjTaQwoLM236r89KEojGmq/jMls8S+zb7yOwAPdXvbwfKDlP+lmXgAL4vKSQT+A==}
engines: {node: ^20.17.0 || >=22.9.0}
'@sigstore/core@3.2.1':
resolution: {integrity: sha512-qRsxPnCrbC/puegGxKuynfnxgLiHqWStrSjxkoB4YKqq3Z3s4cyZyj42ZdWFAEblNP65C+rBH8EuREHIXoi83g==}
engines: {node: ^20.17.0 || >=22.9.0}
'@sigstore/protobuf-specs@0.5.1':
resolution: {integrity: sha512-/ScWUhhoFasJsSRGTVBwId1loQjjnjAfE4djL6ZhrXRpNCmPTnUKF5Jokd58ILseOMjzET3UrMOtJPS9sYeI0g==}
engines: {node: ^18.17.0 || >=20.5.0}
'@sigstore/sign@4.1.1':
resolution: {integrity: sha512-Hf4xglukg0XXQ2RiD5vSoLjdPe8OBUPA8XeVjUObheuDcWdYWrnH/BNmxZCzkAy68MzmNCxXLeurJvs6hcP2OQ==}
engines: {node: ^20.17.0 || >=22.9.0}
'@sigstore/tuf@4.0.2':
resolution: {integrity: sha512-TCAzTy0xzdP79EnxSjq9KQ3eaR7+FmudLC6eRKknVKZbV7ZNlGLClAAQb/HMNJ5n2OBNk2GT1tEmU0xuPr+SLQ==}
engines: {node: ^20.17.0 || >=22.9.0}
'@sigstore/verify@3.1.1':
resolution: {integrity: sha512-qv7+G3J2cc6wwFj3yKvXOamzqhMwSk1ogPGmhpS8iXllcPrJaIIBA+4HbttlHVu1pqWTdmaCH/WE7UOC51kdoA==}
engines: {node: ^20.17.0 || >=22.9.0}
'@tufjs/canonical-json@2.0.0':
resolution: {integrity: sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==}
engines: {node: ^16.14.0 || >=18.0.0}
'@tufjs/models@4.1.0':
resolution: {integrity: sha512-Y8cK9aggNRsqJVaKUlEYs4s7CvQ1b1ta2DVPyAimb0I2qhzjNk+A+mxvll/klL0RlfuIUei8BF7YWiua4kQqww==}
engines: {node: ^20.17.0 || >=22.9.0}
cacache@20.0.4:
resolution: {integrity: sha512-M3Lab8NPYlZU2exsL3bMVvMrMqgwCnMWfdZbK28bn3pK6APT/Te/I8hjRPNu1uwORY9a1eEQoifXbKPQMfMTOA==}
engines: {node: ^20.17.0 || >=22.9.0}
fs-minipass@3.0.3:
resolution: {integrity: sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
http-cache-semantics@4.2.0:
resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==}
make-fetch-happen@15.0.6:
resolution: {integrity: sha512-Je0fLJ0F5atA7F+eIlLzk+Wkcl57JDf4kf+EW8xiP5E31xOQxkIxTbgf1Oi1Lw9tRI9UEMRdI5Vz2xTzoNU1Jw==}
engines: {node: ^20.17.0 || >=22.9.0}
minipass-collect@2.0.1:
resolution: {integrity: sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==}
engines: {node: '>=16 || 14 >=14.17'}
minipass-fetch@5.0.2:
resolution: {integrity: sha512-2d0q2a8eCi2IRg/IGubCNRJoYbA1+YPXAzQVRFmB45gdGZafyivnZ5YSEfo3JikbjGxOdntGFvBQGqaSMXlAFQ==}
engines: {node: ^20.17.0 || >=22.9.0}
minipass-flush@1.0.7:
resolution: {integrity: sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA==}
engines: {node: '>= 8'}
minipass-pipeline@1.2.4:
resolution: {integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==}
engines: {node: '>=8'}
minipass-sized@2.0.0:
resolution: {integrity: sha512-zSsHhto5BcUVM2m1LurnXY6M//cGhVaegT71OfOXoprxT6o780GZd792ea6FfrQkuU4usHZIUczAQMRUE2plzA==}
engines: {node: '>=8'}
minipass@3.3.6:
resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==}
engines: {node: '>=8'}
p-map@7.0.4:
resolution: {integrity: sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==}
engines: {node: '>=18'}
proc-log@6.1.0:
resolution: {integrity: sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==}
engines: {node: ^20.17.0 || >=22.9.0}
semver@7.8.4:
resolution: {integrity: sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==}
engines: {node: '>=10'}
hasBin: true
sigstore@4.1.1:
resolution: {integrity: sha512-endqECJkfhozrXMK5ngu/UAA0xVcVEFdnHJCElGaExypjW+HK5i6zu3NteLoaX/iFbRUbC3+DjttQs0GARr+5w==}
engines: {node: ^20.17.0 || >=22.9.0}
smart-buffer@4.2.0:
resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==}
engines: {node: '>= 6.0.0', npm: '>= 3.0.0'}
socks-proxy-agent@8.0.5:
resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==}
engines: {node: '>= 14'}
socks@2.8.9:
resolution: {integrity: sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==}
engines: {node: '>= 10.0.0', npm: '>= 3.0.0'}
ssri@13.0.1:
resolution: {integrity: sha512-QUiRf1+u9wPTL/76GTYlKttDEBWV1ga9ZXW8BG6kfdeyyM8LGPix9gROyg9V2+P0xNyF3X2Go526xKFdMZrHSQ==}
engines: {node: ^20.17.0 || >=22.9.0}
tuf-js@4.1.0:
resolution: {integrity: sha512-50QV99kCKH5P/Vs4E2Gzp7BopNV+KzTXqWeaxrfu5IQJBOULRsTIS9seSsOVT8ZnGXzCyx55nYWAi4qJzpZKEQ==}
engines: {node: ^20.17.0 || >=22.9.0}
snapshots:
'@a2ui/lit@0.10.0(signal-polyfill@0.2.2)':
@ -14080,3 +14208,168 @@ snapshots:
zod@4.4.3: {}
zwitch@2.0.4: {}
'@gar/promise-retry@1.0.3': {}
'@npmcli/agent@4.0.2':
dependencies:
agent-base: 7.1.4
http-proxy-agent: 7.0.2
https-proxy-agent: 7.0.6
lru-cache: 11.5.1
socks-proxy-agent: 8.0.5
transitivePeerDependencies:
- supports-color
'@npmcli/fs@5.0.0':
dependencies:
semver: 7.8.4
'@npmcli/redact@4.0.0': {}
'@sigstore/bundle@4.0.0':
dependencies:
'@sigstore/protobuf-specs': 0.5.1
'@sigstore/core@3.2.1': {}
'@sigstore/protobuf-specs@0.5.1': {}
'@sigstore/sign@4.1.1':
dependencies:
'@gar/promise-retry': 1.0.3
'@sigstore/bundle': 4.0.0
'@sigstore/core': 3.2.1
'@sigstore/protobuf-specs': 0.5.1
make-fetch-happen: 15.0.6
proc-log: 6.1.0
transitivePeerDependencies:
- supports-color
'@sigstore/tuf@4.0.2':
dependencies:
'@sigstore/protobuf-specs': 0.5.1
tuf-js: 4.1.0
transitivePeerDependencies:
- supports-color
'@sigstore/verify@3.1.1':
dependencies:
'@sigstore/bundle': 4.0.0
'@sigstore/core': 3.2.1
'@sigstore/protobuf-specs': 0.5.1
'@tufjs/canonical-json@2.0.0': {}
'@tufjs/models@4.1.0':
dependencies:
'@tufjs/canonical-json': 2.0.0
minimatch: 10.2.5
cacache@20.0.4:
dependencies:
'@npmcli/fs': 5.0.0
fs-minipass: 3.0.3
glob: 13.0.6
lru-cache: 11.5.1
minipass: 7.1.3
minipass-collect: 2.0.1
minipass-flush: 1.0.7
minipass-pipeline: 1.2.4
p-map: 7.0.4
ssri: 13.0.1
fs-minipass@3.0.3:
dependencies:
minipass: 7.1.3
http-cache-semantics@4.2.0: {}
make-fetch-happen@15.0.6:
dependencies:
'@gar/promise-retry': 1.0.3
'@npmcli/agent': 4.0.2
'@npmcli/redact': 4.0.0
cacache: 20.0.4
http-cache-semantics: 4.2.0
minipass: 7.1.3
minipass-fetch: 5.0.2
minipass-flush: 1.0.7
minipass-pipeline: 1.2.4
negotiator: 1.0.0
proc-log: 6.1.0
ssri: 13.0.1
transitivePeerDependencies:
- supports-color
minipass-collect@2.0.1:
dependencies:
minipass: 7.1.3
minipass-fetch@5.0.2:
dependencies:
minipass: 7.1.3
minipass-sized: 2.0.0
minizlib: 3.1.0
optionalDependencies:
iconv-lite: 0.7.2
minipass-flush@1.0.7:
dependencies:
minipass: 3.3.6
minipass-pipeline@1.2.4:
dependencies:
minipass: 3.3.6
minipass-sized@2.0.0:
dependencies:
minipass: 7.1.3
minipass@3.3.6:
dependencies:
yallist: 4.0.0
p-map@7.0.4: {}
proc-log@6.1.0: {}
semver@7.8.4: {}
sigstore@4.1.1:
dependencies:
'@sigstore/bundle': 4.0.0
'@sigstore/core': 3.2.1
'@sigstore/protobuf-specs': 0.5.1
'@sigstore/sign': 4.1.1
'@sigstore/tuf': 4.0.2
'@sigstore/verify': 3.1.1
transitivePeerDependencies:
- supports-color
smart-buffer@4.2.0: {}
socks-proxy-agent@8.0.5:
dependencies:
agent-base: 7.1.4
debug: 4.4.3
socks: 2.8.9
transitivePeerDependencies:
- supports-color
socks@2.8.9:
dependencies:
ip-address: 10.2.0
smart-buffer: 4.2.0
ssri@13.0.1:
dependencies:
minipass: 7.1.3
tuf-js@4.1.0:
dependencies:
'@tufjs/models': 4.1.0
debug: 4.4.3
make-fetch-happen: 15.0.6
transitivePeerDependencies:
- supports-color

View file

@ -692,6 +692,8 @@ export async function verifyBetaRelease(
pluginSelection: args.pluginSelection,
openclawNpmIntegrity: openclawNpm.integrity,
openclawNpmTarball: openclawNpm.tarball,
npmRegistrySignaturesVerified: args.skipPostpublish ? null : true,
npmProvenanceAttestationMatched: args.skipPostpublish ? null : true,
githubReleaseUrl: releaseUrl ?? null,
pluginNpmPackageCount: npmPlugins.length,
clawHubPackageCount: clawHubPlugins.length,

View file

@ -0,0 +1,194 @@
import { createHash } from "node:crypto";
const STABLE_RELEASE_TAG_RE = /^v(?<version>\d{4}\.\d{1,2}\.\d{1,2})(?:-\d+)?$/u;
const MAX_ROLLBACK_DRILL_AGE_MS = 90 * 24 * 60 * 60 * 1000;
function parseStableReleaseTagDetails(tag) {
const match = STABLE_RELEASE_TAG_RE.exec(tag);
if (!match?.groups?.version) {
throw new Error(`expected a stable release tag, got ${tag}`);
}
return {
baseVersion: match.groups.version,
tagVersion: tag.slice(1),
};
}
function escapeRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
}
function sha256(value) {
return createHash("sha256").update(value).digest("hex");
}
export function parseStableReleaseTag(tag) {
return parseStableReleaseTagDetails(tag).baseVersion;
}
export function extractStableChangelogSection(changelog, version) {
const heading = new RegExp(`^## ${escapeRegExp(version)}\\n`, "mu").exec(changelog);
if (!heading || heading.index === undefined) {
return null;
}
const section = changelog.slice(heading.index);
const nextHeading = section.slice(heading[0].length).search(/^## /mu);
return (
nextHeading === -1 ? section : section.slice(0, heading[0].length + nextHeading)
).trimEnd();
}
function readVersion(packageJson, label, errors) {
const value = packageJson?.version;
if (typeof value !== "string" || value.length === 0) {
errors.push(`${label} package.json is missing a version.`);
return "";
}
return value;
}
function readReleaseAssets(release) {
return Array.isArray(release?.assets)
? release.assets.filter((asset) => asset && typeof asset.name === "string")
: [];
}
function isCloseoutEvidenceAsset(assetName, tag) {
const releaseVersion = tag.slice(1);
return (
assetName === `openclaw-${releaseVersion}-stable-main-closeout.json` ||
assetName === `openclaw-${releaseVersion}-stable-main-closeout.json.sha256`
);
}
function parseRollbackDrillDate(value) {
if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}$/u.test(value)) {
return null;
}
const parsed = new Date(`${value}T00:00:00.000Z`);
return Number.isFinite(parsed.getTime()) && parsed.toISOString().slice(0, 10) === value
? parsed.getTime()
: null;
}
function verifyRollbackDrill(params, errors) {
if (!params.rollbackDrillId?.trim()) {
errors.push("rollback drill id is required.");
}
const drillDateMs = parseRollbackDrillDate(params.rollbackDrillDate);
if (drillDateMs === null) {
errors.push(`rollback drill date is invalid: ${params.rollbackDrillDate ?? "<missing>"}.`);
return;
}
const ageMs = params.nowMs - drillDateMs;
if (ageMs < 0) {
errors.push(`rollback drill date is in the future: ${params.rollbackDrillDate}.`);
} else if (!params.allowStaleRollbackDrill && ageMs > MAX_ROLLBACK_DRILL_AGE_MS) {
errors.push(
`rollback drill is older than 90 days: ${params.rollbackDrillDate}. Run the private rollback drill before stable closeout.`,
);
}
}
export function verifyStableMainCloseout(params) {
const { baseVersion, tagVersion } = parseStableReleaseTagDetails(params.tag);
const errors = [];
const mainVersion = readVersion(params.mainPackageJson, "main", errors);
const tagPackageVersion = readVersion(params.tagPackageJson, "release tag", errors);
const fallbackCorrection =
tagVersion !== baseVersion && mainVersion === baseVersion && tagPackageVersion === baseVersion;
const version = fallbackCorrection ? baseVersion : tagVersion;
if (mainVersion && mainVersion !== version) {
errors.push(
`main package.json version is ${mainVersion}, expected shipped version ${version}.`,
);
}
if (tagPackageVersion && tagPackageVersion !== version) {
errors.push(
`release tag package.json version is ${tagPackageVersion}, expected shipped version ${version}.`,
);
}
const mainChangelog = extractStableChangelogSection(params.mainChangelog, version);
const tagChangelog = extractStableChangelogSection(params.tagChangelog, version);
if (!mainChangelog) {
errors.push(`main CHANGELOG.md is missing the ## ${version} section.`);
}
if (!tagChangelog) {
errors.push(`release tag CHANGELOG.md is missing the ## ${version} section.`);
}
if (mainChangelog && tagChangelog && mainChangelog !== tagChangelog) {
errors.push(
`main CHANGELOG.md ## ${version} does not exactly match the shipped release section.`,
);
}
if (params.release?.tagName !== params.tag) {
errors.push(
`GitHub release tag is ${String(params.release?.tagName ?? "<missing>")}, expected ${params.tag}.`,
);
}
if (params.release?.isDraft === true) {
errors.push(`GitHub release ${params.tag} is still a draft.`);
}
if (params.release?.isPrerelease === true) {
errors.push(`GitHub release ${params.tag} is marked as a prerelease.`);
}
const macAssetVersion = version;
const expectedMacAssets = [
`OpenClaw-${macAssetVersion}.zip`,
`OpenClaw-${macAssetVersion}.dmg`,
`OpenClaw-${macAssetVersion}.dSYM.zip`,
];
const assetNames = new Set(readReleaseAssets(params.release).map((asset) => asset.name));
const missingMacAssets = expectedMacAssets.filter((asset) => !assetNames.has(asset));
if (missingMacAssets.length > 0) {
errors.push(
`GitHub release ${params.tag} is missing required macOS asset(s): ${missingMacAssets.join(", ")}.`,
);
} else {
const macZip = expectedMacAssets[0];
if (!params.mainAppcast.includes(`/releases/download/${params.tag}/${macZip}`)) {
errors.push(`main appcast.xml does not point at ${macZip} from ${params.tag}.`);
}
}
verifyRollbackDrill(params, errors);
if (errors.length > 0) {
return { errors, manifest: null };
}
return {
errors,
manifest: {
version: 1,
releaseTag: params.tag,
releaseVersion: version,
releaseTagSha: params.releaseTagSha,
mainSha: params.mainSha,
mainPackageVersion: mainVersion,
releaseTagPackageVersion: tagPackageVersion,
changelogSha256: sha256(mainChangelog),
appcastSha256: sha256(params.mainAppcast),
fullReleaseValidationRunId: params.fullReleaseValidationRunId,
releasePublishRunId: params.releasePublishRunId,
rollbackDrill: {
id: params.rollbackDrillId,
date: params.rollbackDrillDate,
},
githubReleaseAssets: readReleaseAssets(params.release)
.filter((asset) => !isCloseoutEvidenceAsset(asset.name, params.tag))
.map((asset) => ({
name: asset.name,
digest: typeof asset.digest === "string" ? asset.digest : null,
})),
},
};
}

View file

@ -1,6 +1,7 @@
#!/usr/bin/env -S node --import tsx
// Openclaw Npm Postpublish Verify script supports OpenClaw repository automation.
import { createPublicKey, verify as verifySignature } from "node:crypto";
import {
existsSync,
lstatSync,
@ -23,6 +24,7 @@ import {
win32 as pathWin32,
} from "node:path";
import { pathToFileURL } from "node:url";
import { verify as verifySigstoreBundle } from "sigstore";
import { formatErrorMessage } from "../src/infra/errors.ts";
import { BUNDLED_RUNTIME_SIDECAR_PATHS } from "../src/plugins/runtime-sidecar-paths.ts";
import { listBundledPluginPackArtifacts } from "./lib/bundled-plugin-build-entries.mjs";
@ -123,6 +125,226 @@ export function buildPublishedInstallScenarios(version: string): PublishedInstal
return scenarios;
}
type NpmRegistryKey = {
key: string;
keyid: string;
};
type NpmRegistrySignature = {
keyid: string;
sig: string;
};
type NpmRegistryAttestation = {
bundle?: {
dsseEnvelope?: {
payload?: string;
};
};
predicateType?: string;
};
type NpmProvenanceVerificationPolicy = {
certificateIdentityURI: string;
certificateIssuer: string;
};
type VerifyNpmProvenanceBundle = (
bundle: unknown,
policy: NpmProvenanceVerificationPolicy,
) => Promise<void>;
type NpmProvenanceStatement = {
predicate?: {
buildDefinition?: {
externalParameters?: {
workflow?: {
path?: string;
ref?: string;
repository?: string;
};
};
};
runDetails?: {
builder?: {
id?: string;
};
};
};
subject?: Array<{
digest?: Record<string, string>;
name?: string;
}>;
};
const NPM_PROVENANCE_PREDICATE_TYPE = "https://slsa.dev/provenance/v1";
const NPM_PROVENANCE_REPOSITORY = "https://github.com/openclaw/openclaw";
const NPM_PROVENANCE_WORKFLOW_PATH = ".github/workflows/openclaw-npm-release.yml";
const NPM_PROVENANCE_CERTIFICATE_ISSUER = "https://token.actions.githubusercontent.com";
const NPM_PROVENANCE_BUILDER_ID = "https://github.com/actions/runner/github-hosted";
const NPM_REGISTRY_REQUEST_TIMEOUT_MS = 30_000;
const NPM_REGISTRY_PROVENANCE_ATTEMPTS = 30;
const NPM_REGISTRY_PROVENANCE_RETRY_MAX_DELAY_MS = 10_000;
export function verifyNpmRegistrySignatures(params: {
integrity: string;
keys: NpmRegistryKey[];
packageName: string;
signatures: NpmRegistrySignature[];
version: string;
}): void {
if (!params.integrity.startsWith("sha512-")) {
throw new Error(`npm registry integrity is missing a sha512 digest for ${params.packageName}.`);
}
if (params.signatures.length === 0) {
throw new Error(
`npm registry returned no signatures for ${params.packageName}@${params.version}.`,
);
}
const payload = `${params.packageName}@${params.version}:${params.integrity}`;
for (const signature of params.signatures) {
const key = params.keys.find((candidate) => candidate.keyid === signature.keyid);
if (!key) {
continue;
}
const publicKey = createPublicKey({
key: Buffer.from(key.key, "base64"),
format: "der",
type: "spki",
});
if (
verifySignature(
"sha256",
Buffer.from(payload, "utf8"),
publicKey,
Buffer.from(signature.sig, "base64"),
)
) {
return;
}
}
throw new Error(
`npm registry signatures did not verify for ${params.packageName}@${params.version}.`,
);
}
function resolveNpmProvenanceVerificationPolicy(
statement: NpmProvenanceStatement,
version: string,
): NpmProvenanceVerificationPolicy {
const parsedVersion = parseReleaseVersion(version);
if (parsedVersion === null) {
throw new Error(`Unsupported release version "${version}".`);
}
const workflow = statement.predicate?.buildDefinition?.externalParameters?.workflow;
const workflowRef = workflow?.ref;
const expectedReleaseRef = `refs/heads/release/${parsedVersion.baseVersion}`;
const isTrustedRef =
workflowRef === "refs/heads/main" ||
workflowRef === expectedReleaseRef ||
(parsedVersion.channel === "alpha" &&
/^refs\/heads\/tideclaw\/alpha\/[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{4}Z$/u.test(
workflowRef ?? "",
));
if (
workflow?.repository !== NPM_PROVENANCE_REPOSITORY ||
workflow?.path !== NPM_PROVENANCE_WORKFLOW_PATH ||
!isTrustedRef ||
statement.predicate?.runDetails?.builder?.id !== NPM_PROVENANCE_BUILDER_ID
) {
throw new Error(
`npm provenance attestation does not bind ${version} to the trusted OpenClaw GitHub release workflow.`,
);
}
return {
certificateIssuer: NPM_PROVENANCE_CERTIFICATE_ISSUER,
certificateIdentityURI: `${NPM_PROVENANCE_REPOSITORY}/${NPM_PROVENANCE_WORKFLOW_PATH}@${workflowRef}`,
};
}
async function verifySigstoreNpmProvenanceBundle(
bundle: unknown,
policy: NpmProvenanceVerificationPolicy,
): Promise<void> {
await verifySigstoreBundle(bundle as Parameters<typeof verifySigstoreBundle>[0], policy);
}
export async function verifyNpmProvenanceAttestation(params: {
attestations: NpmRegistryAttestation[];
integrity: string;
packageName: string;
verifyBundle?: VerifyNpmProvenanceBundle;
version: string;
}): Promise<void> {
const expectedSubject = `pkg:npm/${params.packageName}@${params.version}`;
const expectedSha512 = Buffer.from(params.integrity.slice("sha512-".length), "base64").toString(
"hex",
);
const verifyBundle = params.verifyBundle ?? verifySigstoreNpmProvenanceBundle;
let verificationError: unknown;
let policyError: unknown;
for (const attestation of params.attestations) {
if (attestation.predicateType !== NPM_PROVENANCE_PREDICATE_TYPE) {
continue;
}
const payload = attestation.bundle?.dsseEnvelope?.payload;
if (!payload) {
continue;
}
try {
const statement = JSON.parse(
Buffer.from(payload, "base64").toString("utf8"),
) as NpmProvenanceStatement;
if (
statement.subject?.some(
(subject) =>
subject.name === expectedSubject && subject.digest?.sha512 === expectedSha512,
)
) {
let policy: NpmProvenanceVerificationPolicy;
try {
policy = resolveNpmProvenanceVerificationPolicy(statement, params.version);
} catch (error) {
policyError = error;
continue;
}
try {
await verifyBundle(attestation.bundle, policy);
return;
} catch (error) {
verificationError = error;
}
}
} catch {
// Try the remaining attestations before reporting the missing match.
}
}
if (verificationError) {
throw new Error(
`npm provenance attestation failed Sigstore verification for ${params.packageName}@${params.version}: ${formatErrorMessage(verificationError)}`,
);
}
if (policyError instanceof Error) {
throw policyError;
}
if (policyError) {
throw new Error(
`npm provenance attestation policy evaluation failed for ${params.packageName}@${params.version}: ${formatErrorMessage(policyError)}`,
);
}
throw new Error(
`npm provenance attestation does not match ${params.packageName}@${params.version} and its registry integrity.`,
);
}
export function collectInstalledPackageErrors(params: {
expectedVersion: string;
installedVersion: string;
@ -698,6 +920,149 @@ function installSpec(prefixDir: string, spec: string, cwd: string): void {
npmExec(buildPublishedInstallCommandArgs(prefixDir, spec), cwd);
}
async function fetchRegistryJson(url: string): Promise<unknown> {
const response = await fetch(url, {
headers: {
Accept: "application/json",
},
redirect: "error",
signal: AbortSignal.timeout(NPM_REGISTRY_REQUEST_TIMEOUT_MS),
});
if (!response.ok) {
throw new Error(`npm registry request failed (${response.status}): ${url}`);
}
return response.json();
}
function isRetryableRegistryProvenanceError(error: unknown): boolean {
const message = error instanceof Error ? error.message : String(error);
return (
/npm registry request failed \((?:404|408|425|429|5\d\d)\)/u.test(message) ||
message.includes("npm registry metadata is incomplete") ||
message.includes("npm registry provenance metadata is incomplete") ||
/aborted|fetch failed|network|timeout|timed out/u.test(message)
);
}
export async function retryNpmRegistryProvenanceRead<T>(
read: () => Promise<T>,
options: {
attempts?: number;
delay?: (delayMs: number) => Promise<void>;
} = {},
): Promise<T> {
const attempts = options.attempts ?? NPM_REGISTRY_PROVENANCE_ATTEMPTS;
const delay =
options.delay ??
((delayMs: number) =>
new Promise<void>((resolveDelay) => {
setTimeout(resolveDelay, delayMs);
}));
let lastError: unknown;
for (let attempt = 1; attempt <= attempts; attempt += 1) {
try {
return await read();
} catch (error) {
lastError = error;
if (!isRetryableRegistryProvenanceError(error) || attempt === attempts) {
throw error;
}
await delay(Math.min(attempt * 1000, NPM_REGISTRY_PROVENANCE_RETRY_MAX_DELAY_MS));
}
}
throw lastError;
}
async function verifyPublishedRegistryProvenanceOnce(version: string): Promise<void> {
const registry = new URL(process.env.npm_config_registry ?? "https://registry.npmjs.org");
if (registry.protocol !== "https:") {
throw new Error(`npm registry must use HTTPS: ${registry}`);
}
if (!registry.pathname.endsWith("/")) {
registry.pathname = `${registry.pathname}/`;
}
const packageName = "openclaw";
const packageDocument = (await fetchRegistryJson(
new URL(
`${encodeURIComponent(packageName)}/${encodeURIComponent(version)}`,
registry,
).toString(),
)) as {
dist?: {
attestations?: {
provenance?: {
predicateType?: string;
};
url?: string;
};
integrity?: string;
signatures?: NpmRegistrySignature[];
};
};
const keysDocument = (await fetchRegistryJson(new URL("-/npm/v1/keys", registry).toString())) as {
keys?: NpmRegistryKey[];
};
const integrity = packageDocument.dist?.integrity;
const signatures = packageDocument.dist?.signatures;
const provenance = packageDocument.dist?.attestations?.provenance;
const attestationUrl = packageDocument.dist?.attestations?.url;
if (!integrity || !signatures || !keysDocument.keys) {
throw new Error(`npm registry metadata is incomplete for ${packageName}@${version}.`);
}
if (
provenance?.predicateType !== NPM_PROVENANCE_PREDICATE_TYPE ||
typeof attestationUrl !== "string" ||
attestationUrl.length === 0
) {
throw new Error(
`npm registry provenance metadata is incomplete for ${packageName}@${version}.`,
);
}
const parsedAttestationUrl = new URL(attestationUrl);
const attestationPathPrefix = new URL("-/npm/v1/attestations/", registry).pathname;
if (
parsedAttestationUrl.protocol !== "https:" ||
parsedAttestationUrl.origin !== registry.origin ||
!parsedAttestationUrl.pathname.startsWith(attestationPathPrefix)
) {
throw new Error(
`npm registry returned an untrusted provenance attestation URL for ${packageName}@${version}.`,
);
}
verifyNpmRegistrySignatures({
packageName,
version,
integrity,
signatures,
keys: keysDocument.keys,
});
const attestationDocument = (await fetchRegistryJson(parsedAttestationUrl.toString())) as {
attestations?: NpmRegistryAttestation[];
};
const attestations = attestationDocument.attestations ?? [];
if (attestations.length === 0) {
throw new Error(
`npm registry provenance metadata is incomplete for ${packageName}@${version}.`,
);
}
await verifyNpmProvenanceAttestation({
packageName,
version,
integrity,
attestations,
});
console.log(
`openclaw-npm-postpublish-verify: registry signature and provenance attestation verified (${version})`,
);
}
async function verifyPublishedRegistryProvenance(version: string): Promise<void> {
await retryNpmRegistryProvenanceRead(() => verifyPublishedRegistryProvenanceOnce(version));
}
function readInstalledBinaryVersion(prefixDir: string, cwd: string): string {
const invocation = resolveInstalledBinaryCommandInvocation(prefixDir, ["--version"]);
return runNpmVerifyCommand(invocation, cwd);
@ -744,7 +1109,7 @@ function verifyScenario(version: string, scenario: PublishedInstallScenario): vo
}
}
function main(): void {
async function main(): Promise<void> {
const version = process.argv[2]?.trim();
if (!version) {
throw new Error(
@ -753,6 +1118,7 @@ function main(): void {
}
const scenarios = buildPublishedInstallScenarios(version);
await verifyPublishedRegistryProvenance(version);
for (const scenario of scenarios) {
verifyScenario(version, scenario);
}
@ -765,7 +1131,7 @@ function main(): void {
const entrypoint = process.argv[1] ? pathToFileURL(process.argv[1]).href : null;
if (entrypoint !== null && import.meta.url === entrypoint) {
try {
main();
await main();
} catch (error) {
console.error(`openclaw-npm-postpublish-verify: ${formatErrorMessage(error)}`);
process.exitCode = 1;

View file

@ -2,6 +2,13 @@
set -euo pipefail
# This wrapper parses GitHub CLI JSON. Caller shells may force ANSI color globally.
export NO_COLOR=1
export CLICOLOR=0
export CLICOLOR_FORCE=0
export FORCE_COLOR=0
unset COLORTERM
# If invoked from a linked worktree copy of this script, re-exec the canonical
# script from the repository root so behavior stays consistent across worktrees.
script_self="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$(basename "${BASH_SOURCE[0]}")"

View file

@ -1,6 +1,37 @@
run_hosted_prepare_gates() {
local pr="$1"
local current_head="$2"
local changelog_only="$3"
local remote_head
remote_head=$(gh pr view "$pr" --json headRefOid --jq .headRefOid)
if [ "$remote_head" != "$current_head" ]; then
echo "PR head changed before hosted gate verification (expected $current_head, got $remote_head). Re-run prepare-init."
return 1
fi
local repo
repo=$(gh repo view --json nameWithOwner --jq .nameWithOwner)
local args=(
scripts/verify-pr-hosted-gates.mjs
--repo "$repo"
--sha "$current_head"
--output ".local/gates-hosted-checks.json"
)
if [ "$changelog_only" = "true" ]; then
args+=(--changelog-only)
fi
run_quiet_logged "exact-head hosted CI/Testbox gates" ".local/gates-hosted-checks.log" node "${args[@]}"
}
run_prepare_push_retry_gates() {
local docs_only="${1:-false}"
if [ "${OPENCLAW_TESTBOX:-}" = "1" ]; then
echo "A lease retry changed the prepared head, so its exact-head hosted evidence no longer applies."
echo "Stop here, wait for CI/Testbox on the pushed head, then re-run prepare-run."
return 1
fi
bootstrap_deps_if_needed
run_quiet_logged "pnpm build (lease-retry)" ".local/lease-retry-build.log" pnpm build
run_quiet_logged "pnpm check (lease-retry)" ".local/lease-retry-check.log" pnpm check
@ -14,7 +45,6 @@ prepare_gates() {
enter_worktree "$pr" false
checkout_prep_branch "$pr"
bootstrap_deps_if_needed
require_artifact .local/pr-meta.env
# shellcheck disable=SC1091
source .local/pr-meta.env
@ -33,6 +63,10 @@ prepare_gates() {
if [ -n "$changed_files" ] && [ -z "$non_docs" ]; then
docs_only=true
fi
local changelog_only=false
if [ "$changed_files" = "CHANGELOG.md" ]; then
changelog_only=true
fi
local changelog_required=false
if changelog_required_for_changed_files "$changed_files"; then
@ -85,8 +119,9 @@ prepare_gates() {
fi
local gates_mode="full"
local hosted_gates_head=""
local reuse_gates=false
if [ "$docs_only" = "true" ] && [ -n "$previous_last_verified_head" ] && git merge-base --is-ancestor "$previous_last_verified_head" HEAD 2>/dev/null; then
if [ "${OPENCLAW_TESTBOX:-}" != "1" ] && [ "$docs_only" = "true" ] && [ -n "$previous_last_verified_head" ] && git merge-base --is-ancestor "$previous_last_verified_head" HEAD 2>/dev/null; then
local delta_since_verified
delta_since_verified=$(git diff --name-only "$previous_last_verified_head"..HEAD)
if [ -z "$delta_since_verified" ] || file_list_is_docsish_only "$delta_since_verified"; then
@ -94,10 +129,18 @@ prepare_gates() {
fi
fi
if [ "$reuse_gates" = "true" ]; then
if [ "${OPENCLAW_TESTBOX:-}" = "1" ]; then
gates_mode="hosted_exact_head"
if [ "$changelog_only" = "true" ]; then
run_quiet_logged "git diff --check" ".local/gates-diff-check.log" git diff --check origin/main...HEAD
fi
run_hosted_prepare_gates "$pr" "$current_head" "$changelog_only"
hosted_gates_head="$current_head"
elif [ "$reuse_gates" = "true" ]; then
gates_mode="reused_docs_only"
echo "Docs/changelog-only delta since last verified head $previous_last_verified_head; reusing prior gates."
else
bootstrap_deps_if_needed
run_quiet_logged "pnpm build" ".local/gates-build.log" pnpm build
run_quiet_logged "pnpm check" ".local/gates-check.log" pnpm check
@ -128,10 +171,12 @@ prepare_gates() {
GATES_MODE "$gates_mode" \
LAST_VERIFIED_HEAD_SHA "$current_head" \
FULL_GATES_HEAD_SHA "${previous_full_gates_head:-}" \
HOSTED_GATES_HEAD_SHA "$hosted_gates_head" \
GATES_PASSED_AT "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
> .local/gates.env
echo "docs_only=$docs_only"
echo "changelog_only=$changelog_only"
echo "changelog_required=$changelog_required"
echo "gates_mode=$gates_mode"
echo "wrote=.local/gates.env"

View file

@ -27,27 +27,33 @@ print_file_list_with_limit() {
}
mainline_drift_requires_sync() {
local prep_head_sha="$1"
local mainline_base="$1"
local prepared_head_sha="$2"
require_artifact .local/pr-meta.json
if ! git cat-file -e "${prep_head_sha}^{commit}" 2>/dev/null; then
echo "Mainline drift relevance: prep head $prep_head_sha is missing locally; require sync."
if ! git cat-file -e "${mainline_base}^{commit}" 2>/dev/null; then
echo "Mainline drift relevance: mainline base $mainline_base is missing locally; require sync."
return 0
fi
if ! git cat-file -e "${prepared_head_sha}^{commit}" 2>/dev/null; then
echo "Mainline drift relevance: prepared head $prepared_head_sha is missing locally; require sync."
return 0
fi
local delta_file
local pr_files_file
local prepared_files_file
local overlap_file
local critical_file
delta_file=$(mktemp)
pr_files_file=$(mktemp)
prepared_files_file=$(mktemp)
overlap_file=$(mktemp)
critical_file=$(mktemp)
git diff --name-only "${prep_head_sha}..origin/main" | sed '/^$/d' | sort -u > "$delta_file"
jq -r '.files[]?.path // empty' .local/pr-meta.json | sed '/^$/d' | sort -u > "$pr_files_file"
comm -12 "$delta_file" "$pr_files_file" > "$overlap_file" || true
# Compare only mainline commits since the prepared lineage base. The remote
# GraphQL commit has a different parent but its verified tree shares this
# lineage, so its PR files must not look like incoming mainline drift.
git diff --name-only "${mainline_base}..origin/main" | sed '/^$/d' | sort -u > "$delta_file"
git diff --name-only "${mainline_base}..${prepared_head_sha}" | sed '/^$/d' | sort -u > "$prepared_files_file"
comm -12 "$delta_file" "$prepared_files_file" > "$overlap_file" || true
local path
while IFS= read -r path; do
@ -65,22 +71,22 @@ mainline_drift_requires_sync() {
critical_count=$(wc -l < "$critical_file" | tr -d ' ')
if [ "$delta_count" -eq 0 ]; then
echo "Mainline drift relevance: unable to enumerate drift files; require sync."
rm -f "$delta_file" "$pr_files_file" "$overlap_file" "$critical_file"
return 0
echo "Mainline drift relevance: no mainline changes since the prepared base."
rm -f "$delta_file" "$prepared_files_file" "$overlap_file" "$critical_file"
return 1
fi
if [ "$overlap_count" -gt 0 ] || [ "$critical_count" -gt 0 ]; then
echo "Mainline drift relevance: sync required before merge."
print_file_list_with_limit "Mainline files overlapping PR touched files" "$overlap_file"
print_file_list_with_limit "Mainline files overlapping prepared files" "$overlap_file"
print_file_list_with_limit "Mainline files touching merge-critical infrastructure" "$critical_file"
rm -f "$delta_file" "$pr_files_file" "$overlap_file" "$critical_file"
rm -f "$delta_file" "$prepared_files_file" "$overlap_file" "$critical_file"
return 0
fi
echo "Mainline drift relevance: no overlap with PR files and no critical infra drift."
echo "Mainline drift relevance: no overlap with prepared files and no critical infra drift."
print_file_list_with_limit "Mainline-only drift files" "$delta_file"
rm -f "$delta_file" "$pr_files_file" "$overlap_file" "$critical_file"
rm -f "$delta_file" "$prepared_files_file" "$overlap_file" "$critical_file"
return 1
}
@ -91,7 +97,7 @@ merge_verify() {
require_artifact .local/prep.env
# shellcheck disable=SC1091
source .local/prep.env
verify_prep_branch_matches_prepared_head "$pr" "$PREP_HEAD_SHA"
verify_prep_branch_matches_prepared_head "$pr" "${LOCAL_PREP_HEAD_SHA:-$PREP_HEAD_SHA}"
local json
json=$(pr_meta_json "$pr")
@ -154,7 +160,10 @@ merge_verify() {
git fetch origin "pull/$pr/head:pr-$pr" --force
if ! git merge-base --is-ancestor origin/main "pr-$pr"; then
echo "PR branch is behind main."
if mainline_drift_requires_sync "$PREP_HEAD_SHA"; then
if mainline_drift_requires_sync \
"${PREP_MAINLINE_BASE_SHA:-${LOCAL_PREP_HEAD_SHA:-$PREP_HEAD_SHA}}" \
"$PREP_HEAD_SHA"
then
echo "Merge verify failed: mainline drift is relevant to this PR; run scripts/pr prepare-sync-head $pr before merge."
exit 1
fi

View file

@ -146,6 +146,7 @@ prepare_push() {
local prep_head_sha
prep_head_sha=$(git rev-parse HEAD)
local local_prep_head_sha
local lease_sha
lease_sha=$(gh pr view "$pr" --json headRefOid --jq .headRefOid)
@ -156,6 +157,24 @@ prepare_push() {
# shellcheck disable=SC1090
source "$push_result_env"
prep_head_sha="$PUSH_PREP_HEAD_SHA"
local_prep_head_sha="$PUSH_LOCAL_PREP_HEAD_SHA"
local mainline_base_sha
mainline_base_sha=$(git merge-base "$local_prep_head_sha" origin/main) || {
echo "Unable to resolve the prepared mainline base."
exit 1
}
if [ -s .local/prep-sync.env ]; then
# shellcheck disable=SC1091
source .local/prep-sync.env
local current_prep_tree
current_prep_tree=$(git rev-parse "${local_prep_head_sha}^{tree}")
if [ "${PREP_SYNC_TREE:-}" != "$current_prep_tree" ] || [ -z "${PREP_SYNC_MAINLINE_BASE_SHA:-}" ]; then
echo "Prepared PR head no longer matches the verified sync tree."
exit 1
fi
mainline_base_sha="$PREP_SYNC_MAINLINE_BASE_SHA"
rm -f .local/prep-sync.env
fi
local pushed_from_sha="$PUSHED_FROM_SHA"
local pr_head_sha_after="$PR_HEAD_SHA_AFTER_PUSH"
@ -173,8 +192,7 @@ prepare_push() {
cat >> .local/prep.md <<EOF_PREP
- Gates passed and push succeeded to branch $PR_HEAD.
- Gate mode: ${GATES_MODE:-unknown}.
- Verified PR head SHA matches local prep HEAD.
- Verified PR head contains origin/main.
- Verified the remote PR head tree matches the local prep head.
EOF_PREP
# Security: shell-escape values to prevent command injection via propagated PR_HEAD.
@ -185,6 +203,8 @@ EOF_PREP
PR_HEAD "$PR_HEAD" \
PR_HEAD_SHA_BEFORE "$pushed_from_sha" \
PREP_HEAD_SHA "$prep_head_sha" \
LOCAL_PREP_HEAD_SHA "$local_prep_head_sha" \
PREP_MAINLINE_BASE_SHA "$mainline_base_sha" \
COAUTHOR_EMAIL "$coauthor_email" \
> .local/prep.env
@ -217,12 +237,18 @@ prepare_sync_head() {
if ! git merge-base --is-ancestor origin/main HEAD; then
git rebase origin/main
rebased=true
prepare_gates "$pr"
checkout_prep_branch "$pr"
if [ "${OPENCLAW_TESTBOX:-}" = "1" ]; then
rm -f .local/gates.env .local/prep.env
echo "Rebased head requires fresh exact-head hosted CI/Testbox evidence after push."
else
prepare_gates "$pr"
checkout_prep_branch "$pr"
fi
fi
local prep_head_sha
prep_head_sha=$(git rev-parse HEAD)
local local_prep_head_sha
local lease_sha
lease_sha=$(gh pr view "$pr" --json headRefOid --jq .headRefOid)
@ -233,6 +259,12 @@ prepare_sync_head() {
# shellcheck disable=SC1090
source "$push_result_env"
prep_head_sha="$PUSH_PREP_HEAD_SHA"
local_prep_head_sha="$PUSH_LOCAL_PREP_HEAD_SHA"
local mainline_base_sha
mainline_base_sha=$(git merge-base "$local_prep_head_sha" origin/main) || {
echo "Unable to resolve the prepared mainline base."
exit 1
}
local pushed_from_sha="$PUSHED_FROM_SHA"
local pr_head_sha_after="$PR_HEAD_SHA_AFTER_PUSH"
@ -250,8 +282,28 @@ prepare_sync_head() {
cat >> .local/prep.md <<EOF_PREP
- Prep head sync completed to branch $PR_HEAD.
- Rebased onto origin/main: $rebased.
- Verified PR head SHA matches local prep HEAD.
- Verified PR head contains origin/main.
- Verified the remote PR head tree matches the local prep head.
EOF_PREP
if [ "$rebased" = "true" ] && [ "${OPENCLAW_TESTBOX:-}" = "1" ]; then
local prep_sync_tree
prep_sync_tree=$(git rev-parse "${local_prep_head_sha}^{tree}")
# Preserve the verified local lineage because GraphQL creates a remote
# commit with the same tree but the old branch parent.
printf '%s=%q\n' \
PREP_SYNC_MAINLINE_BASE_SHA "$mainline_base_sha" \
PREP_SYNC_TREE "$prep_sync_tree" \
> .local/prep-sync.env
cat >> .local/prep.md <<EOF_PREP
- Cleared stale prepare artifacts. Wait for hosted CI/Testbox on $prep_head_sha, then run prepare-run again.
EOF_PREP
echo "prepare-sync-head complete"
echo "prep_head_sha=$prep_head_sha"
echo "Hosted CI/Testbox must pass for this exact head before prepare-run can continue."
return
fi
cat >> .local/prep.md <<EOF_PREP
- Prepare gates reran automatically when the sync rebase changed the prep head.
EOF_PREP
@ -263,6 +315,8 @@ EOF_PREP
PR_HEAD "$PR_HEAD" \
PR_HEAD_SHA_BEFORE "$pushed_from_sha" \
PREP_HEAD_SHA "$prep_head_sha" \
LOCAL_PREP_HEAD_SHA "$local_prep_head_sha" \
PREP_MAINLINE_BASE_SHA "$mainline_base_sha" \
COAUTHOR_EMAIL "$coauthor_email" \
> .local/prep.env

View file

@ -247,6 +247,7 @@ push_prep_head_to_pr_branch() {
local rerun_gates_on_lease_retry="${5:-false}"
local docs_only="${6:-false}"
local result_env_path="${7:-.local/push-result.env}"
local local_prep_head_sha="$prep_head_sha"
setup_prhead_remote
@ -277,6 +278,10 @@ push_prep_head_to_pr_branch() {
exit 1
fi
else
if [ "$rerun_gates_on_lease_retry" != "true" ]; then
echo "PR head changed during sync; re-run prepare-sync-head from the refreshed branch."
exit 1
fi
echo "Lease push failed, retrying once with fresh PR head..."
lease_sha=$(gh pr view "$pr" --json headRefOid --jq .headRefOid)
pushed_from_sha="$lease_sha"
@ -285,6 +290,7 @@ push_prep_head_to_pr_branch() {
git fetch origin "pull/$pr/head:pr-$pr-latest" --force
git rebase "pr-$pr-latest"
prep_head_sha=$(git rev-parse HEAD)
local_prep_head_sha="$prep_head_sha"
run_prepare_push_retry_gates "$docs_only"
fi
@ -318,16 +324,24 @@ push_prep_head_to_pr_branch() {
local pr_head_sha_after
pr_head_sha_after=$(gh pr view "$pr" --json headRefOid --jq .headRefOid)
git fetch origin main
git fetch origin "pull/$pr/head:pr-$pr-verify" --force
git merge-base --is-ancestor origin/main "pr-$pr-verify" || {
echo "PR branch is behind main after push."
exit 1
}
local local_prep_tree
local remote_prep_tree
local_prep_tree=$(git rev-parse "${local_prep_head_sha}^{tree}")
remote_prep_tree=$(git rev-parse "pr-$pr-verify^{tree}")
git branch -D "pr-$pr-verify" 2>/dev/null || true
if [ "$local_prep_tree" != "$remote_prep_tree" ]; then
echo "Pushed PR head tree differs from the prepared local tree."
exit 1
fi
# merge-verify owns relevance-aware mainline drift checks. Requiring every
# prepared head to contain main here forces needless rebases, while GraphQL
# createCommitOnBranch cannot move a rebased branch's commit ancestry.
# Security: shell-escape values to prevent command injection when sourced.
printf '%s=%q\n' \
PUSH_PREP_HEAD_SHA "$prep_head_sha" \
PUSH_LOCAL_PREP_HEAD_SHA "$local_prep_head_sha" \
PUSHED_FROM_SHA "$pushed_from_sha" \
PR_HEAD_SHA_AFTER_PUSH "$pr_head_sha_after" \
> "$result_env_path"

View file

@ -641,7 +641,7 @@ function validatePreflightManifest(manifest, params) {
}
}
function validateFullManifest(manifest, params) {
export function validateFullManifest(manifest, params) {
if (manifest.workflowName !== "Full Release Validation") {
throw new Error(`full validation workflow mismatch: ${manifest.workflowName}`);
}
@ -658,6 +658,17 @@ function validateFullManifest(manifest, params) {
if (manifest.rerunGroup !== "all") {
throw new Error(`full validation must use rerun_group=all, got ${manifest.rerunGroup}`);
}
if (
(params.releaseProfile === "stable" || params.releaseProfile === "full") &&
manifest.runReleaseSoak !== "true"
) {
throw new Error(
`full validation must record runReleaseSoak=true for ${params.releaseProfile} release candidates`,
);
}
if (manifest.controls?.performanceBlocking !== true) {
throw new Error("full validation manifest must record blocking product performance evidence");
}
}
export function candidateParallelsArgs(tarballPath) {
@ -746,7 +757,8 @@ async function main() {
provider: options.provider,
mode: options.mode,
release_profile: options.releaseProfile,
run_release_soak: options.releaseProfile === "full" ? "true" : "false",
run_release_soak:
options.releaseProfile === "stable" || options.releaseProfile === "full" ? "true" : "false",
rerun_group: "all",
});
options.fullReleaseRunId =
@ -846,6 +858,7 @@ async function main() {
windowsNodeTag: options.windowsNodeTag || undefined,
windowsNodeSourceRelease,
fullReleaseValidationUrl: fullRun.url,
fullReleaseValidationControls: fullManifest.controls,
npmPreflightUrl: npmRun.url,
artifacts: {
npmPreflight: npmArtifactName,

View file

@ -604,6 +604,7 @@ const TOOLING_SOURCE_TEST_TARGETS = new Map([
["scripts/test-live.mjs", ["test/scripts/test-live.test.ts"]],
["scripts/tsdown-build.mjs", ["test/scripts/tsdown-build.test.ts"]],
["scripts/verify.mjs", ["test/scripts/verify.test.ts"]],
["scripts/verify-pr-hosted-gates.mjs", ["test/scripts/verify-pr-hosted-gates.test.ts"]],
["scripts/zai-fallback-repro.ts", ["test/scripts/zai-fallback-repro.test.ts"]],
["scripts/repro/code-mode-namespace-live.ts", ["test/scripts/code-mode-namespace-live.test.ts"]],
["scripts/lib/extension-test-plan.mjs", ["test/scripts/test-extension.test.ts"]],

View file

@ -0,0 +1,244 @@
#!/usr/bin/env node
import { execFileSync } from "node:child_process";
import { mkdirSync, writeFileSync } from "node:fs";
import path from "node:path";
import { isDirectRunUrl } from "./lib/direct-run.mjs";
export const SCHEDULED_HOSTED_WORKFLOWS = [
"Blacksmith Testbox",
"Blacksmith ARM Testbox",
"Blacksmith Build Artifacts Testbox",
"Workflow Sanity",
];
const CI_WORKFLOW_PATH = ".github/workflows/ci.yml";
const BUILD_ARTIFACTS_WORKFLOW = "Blacksmith Build Artifacts Testbox";
const ARTIFACT_FALLBACK_REQUIRED_WORKFLOWS = [
"Blacksmith Testbox",
"Blacksmith ARM Testbox",
"Workflow Sanity",
];
function readOptionValue(argv, index, optionName) {
const value = argv[index + 1];
if (!value || value.startsWith("--")) {
throw new Error(`Expected ${optionName} <value>.`);
}
return value;
}
export function parseArgs(argv) {
const args = { repo: "", sha: "", output: "", changelogOnly: false };
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
switch (arg) {
case "--repo":
args.repo = readOptionValue(argv, index, arg);
index += 1;
break;
case "--sha":
args.sha = readOptionValue(argv, index, arg);
index += 1;
break;
case "--output":
args.output = readOptionValue(argv, index, arg);
index += 1;
break;
case "--changelog-only":
args.changelogOnly = true;
break;
default:
throw new Error(`Unknown option: ${arg}`);
}
}
if (!args.repo || !args.sha || !args.output) {
throw new Error(
"Usage: node scripts/verify-pr-hosted-gates.mjs --repo <owner/repo> --sha <sha> --output <path>",
);
}
return args;
}
function formatObservedRuns(runs) {
if (runs.length === 0) {
return "none";
}
return runs
.map(
(run) => `${run.id ?? "unknown"}:${run.status ?? "unknown"}/${run.conclusion ?? "unknown"}`,
)
.join(", ");
}
function isReleaseGateCiRun(run, sha) {
return (
run?.event === "workflow_dispatch" &&
run?.head_sha === sha &&
String(run?.path ?? "").split("@", 1)[0] === CI_WORKFLOW_PATH &&
run?.display_title === `CI release gate ${sha}`
);
}
function matchingAuthoritativeRuns(runs, workflowName, sha) {
return runs.filter((run) => {
if (run?.head_sha !== sha) {
return false;
}
if (run?.event === "pull_request") {
return run.name === workflowName;
}
return workflowName === "CI" && isReleaseGateCiRun(run, sha);
});
}
function latestRun(runs) {
return runs.toSorted((left, right) =>
String(right.updated_at ?? "").localeCompare(String(left.updated_at ?? "")),
)[0];
}
function preferredCiRun(runs) {
const scheduledRuns = runs.filter((run) => run.event === "pull_request");
const failedScheduledRun = scheduledRuns.find(
(run) => run.status === "completed" && run.conclusion !== "success",
);
if (failedScheduledRun) {
return failedScheduledRun;
}
const latestScheduledRun = latestRun(scheduledRuns);
if (latestScheduledRun?.status === "completed") {
return latestScheduledRun;
}
return latestRun(runs.filter((run) => run.event === "workflow_dispatch")) ?? latestScheduledRun;
}
function successfulRunOrThrow(runs, workflowName, sha) {
const matchingRuns = matchingAuthoritativeRuns(runs, workflowName, sha);
const run = workflowName === "CI" ? preferredCiRun(matchingRuns) : latestRun(matchingRuns);
if (!run || run.status !== "completed" || run.conclusion !== "success") {
throw new Error(
`Missing successful exact-head ${workflowName} workflow for ${sha}. Observed: ${formatObservedRuns(matchingRuns)}`,
);
}
return run;
}
function successfulReleaseGateFallback(workflowRuns, sha) {
const fallback = latestRun(workflowRuns.filter((run) => isReleaseGateCiRun(run, sha)));
if (fallback?.status !== "completed" || fallback.conclusion !== "success") {
return null;
}
return fallback;
}
function canCoverQueuedBuildArtifacts(workflowRuns, sha) {
if (!successfulReleaseGateFallback(workflowRuns, sha)) {
return false;
}
const supportingGatesPassed = ARTIFACT_FALLBACK_REQUIRED_WORKFLOWS.every((workflowName) => {
const run = latestRun(matchingAuthoritativeRuns(workflowRuns, workflowName, sha));
return run?.status === "completed" && run.conclusion === "success";
});
if (!supportingGatesPassed) {
return false;
}
const buildArtifactRuns = matchingAuthoritativeRuns(workflowRuns, BUILD_ARTIFACTS_WORKFLOW, sha);
const latestBuildArtifactRun = latestRun(buildArtifactRuns);
return (
latestBuildArtifactRun?.status === "queued" &&
buildArtifactRuns.every(
(run) =>
run.status === "queued" || (run.status === "completed" && run.conclusion === "success"),
)
);
}
function stripAnsi(raw) {
const escape = String.fromCharCode(27);
return raw.replace(new RegExp(`${escape}\\[[0-?]*[ -/]*[@-~]`, "gu"), "");
}
export function parseWorkflowRunPages(raw) {
return JSON.parse(stripAnsi(raw)).flatMap((page) => page.workflow_runs ?? []);
}
export function collectHostedGateEvidence({ sha, workflowRuns, changelogOnly = false }) {
if (!Array.isArray(workflowRuns)) {
throw new Error("workflowRuns must be an array.");
}
const workflows = [];
const fallbackCoveredWorkflows = [];
let ciRun;
if (!changelogOnly) {
ciRun = successfulRunOrThrow(workflowRuns, "CI", sha);
workflows.push(ciRun);
}
for (const workflowName of SCHEDULED_HOSTED_WORKFLOWS) {
const matchingRuns = matchingAuthoritativeRuns(workflowRuns, workflowName, sha);
if (matchingRuns.length > 0) {
if (
workflowName === BUILD_ARTIFACTS_WORKFLOW &&
canCoverQueuedBuildArtifacts(workflowRuns, sha)
) {
fallbackCoveredWorkflows.push({
name: workflowName,
coveredBy: "CI release gate",
reason: "scheduled workflow is queued",
});
continue;
}
workflows.push(successfulRunOrThrow(workflowRuns, workflowName, sha));
}
}
const evidence = {
headSha: sha,
workflows: workflows.map((run) => ({
id: run.id,
name: run.name,
event: run.event,
status: run.status,
conclusion: run.conclusion,
createdAt: run.created_at,
updatedAt: run.updated_at,
url: run.html_url,
})),
};
if (fallbackCoveredWorkflows.length > 0) {
evidence.fallbackCoveredWorkflows = fallbackCoveredWorkflows;
}
return evidence;
}
function loadWorkflowRuns(repo, sha) {
const raw = execFileSync(
"gh",
["api", `repos/${repo}/actions/runs?head_sha=${sha}&per_page=100`, "--paginate", "--slurp"],
{ encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] },
);
return parseWorkflowRunPages(raw);
}
export function main(argv = process.argv.slice(2)) {
const args = parseArgs(argv);
const evidence = collectHostedGateEvidence({
sha: args.sha,
workflowRuns: loadWorkflowRuns(args.repo, args.sha),
changelogOnly: args.changelogOnly,
});
const manifest = {
schemaVersion: 1,
generatedAt: new Date().toISOString(),
repo: args.repo,
...evidence,
};
mkdirSync(path.dirname(args.output), { recursive: true });
writeFileSync(args.output, `${JSON.stringify(manifest, null, 2)}\n`);
console.log(
`Exact-head hosted gates passed for ${args.sha}: ${manifest.workflows
.map((workflow) => `${workflow.name}#${workflow.id}`)
.join(", ")}`,
);
}
if (isDirectRunUrl(process.argv[1], import.meta.url)) {
main();
}

View file

@ -0,0 +1,86 @@
#!/usr/bin/env node
import { execFileSync } from "node:child_process";
import { readFileSync, writeFileSync } from "node:fs";
import { resolve } from "node:path";
import { verifyStableMainCloseout } from "./lib/stable-release-closeout.mjs";
function parseArgs(argv) {
const values = new Map();
for (let index = 0; index < argv.length; index += 1) {
const key = argv[index];
if (!key.startsWith("--")) {
throw new Error(`unexpected argument: ${key}`);
}
const value = argv[index + 1];
if (!value || value.startsWith("--")) {
throw new Error(`${key} requires a value.`);
}
values.set(key.slice(2), value);
index += 1;
}
const required = [
"tag",
"main-dir",
"tag-dir",
"release-json",
"full-release-validation-run-id",
"release-publish-run-id",
"rollback-drill-id",
"rollback-drill-date",
"output",
];
for (const key of required) {
if (!values.has(key)) {
throw new Error(`--${key} is required.`);
}
}
return Object.fromEntries(values);
}
function readJson(path) {
return JSON.parse(readFileSync(path, "utf8"));
}
function gitSha(dir) {
return execFileSync("git", ["-C", dir, "rev-parse", "HEAD"], {
encoding: "utf8",
}).trim();
}
function main() {
const args = parseArgs(process.argv.slice(2));
const mainDir = resolve(args["main-dir"]);
const tagDir = resolve(args["tag-dir"]);
const result = verifyStableMainCloseout({
tag: args.tag,
mainPackageJson: readJson(resolve(mainDir, "package.json")),
tagPackageJson: readJson(resolve(tagDir, "package.json")),
mainChangelog: readFileSync(resolve(mainDir, "CHANGELOG.md"), "utf8"),
tagChangelog: readFileSync(resolve(tagDir, "CHANGELOG.md"), "utf8"),
mainAppcast: readFileSync(resolve(mainDir, "appcast.xml"), "utf8"),
release: readJson(resolve(args["release-json"])),
releaseTagSha: gitSha(tagDir),
mainSha: gitSha(mainDir),
fullReleaseValidationRunId: args["full-release-validation-run-id"],
releasePublishRunId: args["release-publish-run-id"],
rollbackDrillId: args["rollback-drill-id"],
rollbackDrillDate: args["rollback-drill-date"],
allowStaleRollbackDrill: args["allow-stale-rollback-drill"] === "true",
nowMs: Date.now(),
});
if (result.errors.length > 0 || !result.manifest) {
throw new Error(`stable main closeout failed:\n- ${result.errors.join("\n- ")}`);
}
writeFileSync(resolve(args.output), `${JSON.stringify(result.manifest, null, 2)}\n`);
console.log(`stable main closeout verified: ${args.tag}`);
}
try {
main();
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exitCode = 1;
}

View file

@ -1,3 +1,4 @@
import { generateKeyPairSync, sign } from "node:crypto";
// OpenClaw npm postpublish tests validate postpublish verification behavior.
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
@ -14,6 +15,9 @@ import {
normalizeInstalledBinaryVersion,
resolveInstalledBinaryCommandInvocation,
resolveInstalledBinaryPath,
retryNpmRegistryProvenanceRead,
verifyNpmProvenanceAttestation,
verifyNpmRegistrySignatures,
} from "../scripts/openclaw-npm-postpublish-verify.ts";
const INSTALLED_ROOT_DIST_JS_FILE_SCAN_LIMIT = 10_000;
@ -53,6 +57,214 @@ describe("buildPublishedInstallScenarios", () => {
});
});
describe("npm registry provenance verification", () => {
const packageName = "openclaw";
const version = "2026.3.23";
const integrity = `sha512-${Buffer.from("registry integrity", "utf8").toString("base64")}`;
const provenancePayload = {
subject: [
{
name: `pkg:npm/${packageName}@${version}`,
digest: {
sha512: Buffer.from(integrity.slice("sha512-".length), "base64").toString("hex"),
},
},
],
predicate: {
buildDefinition: {
externalParameters: {
workflow: {
repository: "https://github.com/openclaw/openclaw",
path: ".github/workflows/openclaw-npm-release.yml",
ref: "refs/heads/release/2026.3.23",
},
},
},
runDetails: {
builder: {
id: "https://github.com/actions/runner/github-hosted",
},
},
},
};
it("verifies an npm registry signature against the matching public key", () => {
const keys = generateKeyPairSync("ec", { namedCurve: "prime256v1" });
const payload = `${packageName}@${version}:${integrity}`;
const signature = sign("sha256", Buffer.from(payload, "utf8"), keys.privateKey).toString(
"base64",
);
expect(() =>
verifyNpmRegistrySignatures({
packageName,
version,
integrity,
signatures: [{ keyid: "test-key", sig: signature }],
keys: [
{
keyid: "test-key",
key: keys.publicKey.export({ format: "der", type: "spki" }).toString("base64"),
},
],
}),
).not.toThrow();
});
it("requires a trusted GitHub release identity for the exact SLSA provenance attestation", async () => {
let verificationPolicy:
| {
certificateIdentityURI: string;
certificateIssuer: string;
}
| undefined;
await expect(
verifyNpmProvenanceAttestation({
packageName,
version,
integrity,
attestations: [
{
predicateType: "https://slsa.dev/provenance/v1",
bundle: {
dsseEnvelope: {
payload: Buffer.from(JSON.stringify(provenancePayload), "utf8").toString("base64"),
},
},
},
],
verifyBundle: async (_bundle, policy) => {
verificationPolicy = policy;
},
}),
).resolves.toBeUndefined();
expect(verificationPolicy).toEqual({
certificateIssuer: "https://token.actions.githubusercontent.com",
certificateIdentityURI:
"https://github.com/openclaw/openclaw/.github/workflows/openclaw-npm-release.yml@refs/heads/release/2026.3.23",
});
await expect(
verifyNpmProvenanceAttestation({
packageName,
version,
integrity,
attestations: [
{
predicateType: "https://slsa.dev/provenance/v1",
bundle: {
dsseEnvelope: {
payload: Buffer.from(
JSON.stringify({
...provenancePayload,
subject: [{ name: "pkg:npm/openclaw@2026.3.24", digest: {} }],
}),
"utf8",
).toString("base64"),
},
},
},
],
verifyBundle: async () => undefined,
}),
).rejects.toThrow("does not match");
});
it("rejects matching provenance from an untrusted source before Sigstore verification", async () => {
let verificationCalls = 0;
await expect(
verifyNpmProvenanceAttestation({
packageName,
version,
integrity,
attestations: [
{
predicateType: "https://slsa.dev/provenance/v1",
bundle: {
dsseEnvelope: {
payload: Buffer.from(
JSON.stringify({
...provenancePayload,
predicate: {
...provenancePayload.predicate,
buildDefinition: {
externalParameters: {
workflow: {
...provenancePayload.predicate.buildDefinition.externalParameters
.workflow,
ref: "refs/heads/feature/untrusted",
},
},
},
},
}),
"utf8",
).toString("base64"),
},
},
},
],
verifyBundle: async () => {
verificationCalls += 1;
},
}),
).rejects.toThrow("does not bind 2026.3.23 to the trusted OpenClaw GitHub release workflow");
expect(verificationCalls).toBe(0);
});
it("rejects a matching provenance payload when Sigstore cannot verify its bundle", async () => {
await expect(
verifyNpmProvenanceAttestation({
packageName,
version,
integrity,
attestations: [
{
predicateType: "https://slsa.dev/provenance/v1",
bundle: {
dsseEnvelope: {
payload: Buffer.from(JSON.stringify(provenancePayload), "utf8").toString("base64"),
},
},
},
],
verifyBundle: async () => {
throw new Error("forged bundle");
},
}),
).rejects.toThrow("failed Sigstore verification");
});
it("retries incomplete registry metadata while npm publish propagates", async () => {
let attempts = 0;
const delays: number[] = [];
await expect(
retryNpmRegistryProvenanceRead(
async () => {
attempts += 1;
if (attempts < 3) {
throw new Error(
"npm registry provenance metadata is incomplete for openclaw@2026.3.23.",
);
}
return "verified";
},
{
attempts: 3,
delay: async (delayMs) => {
delays.push(delayMs);
},
},
),
).resolves.toBe("verified");
expect(attempts).toBe(3);
expect(delays).toEqual([1000, 2000]);
});
});
describe("buildPublishedInstallCommandArgs", () => {
it("runs lifecycle scripts for published install verification", () => {
const args = buildPublishedInstallCommandArgs("/tmp/openclaw-prefix", "openclaw@2026.4.10");

View file

@ -1,8 +1,12 @@
// Ci Workflow Guards tests cover ci workflow guards script behavior.
import { readFileSync } from "node:fs";
import { readdirSync, readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
import { parse } from "yaml";
const CHECKOUT_V6 = "actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10";
const CACHE_V5 = "actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae";
const UPLOAD_ARTIFACT_V7 = "actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a";
function readCiWorkflow() {
return parse(readFileSync(".github/workflows/ci.yml", "utf8"));
}
@ -15,7 +19,71 @@ function readCriticalQualityWorkflow() {
return readFileSync(".github/workflows/codeql-critical-quality.yml", "utf8");
}
function findYamlFiles(directory: string): string[] {
return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
const path = `${directory}/${entry.name}`;
if (entry.isDirectory()) {
return findYamlFiles(path);
}
return entry.isFile() && /\.ya?ml$/u.test(entry.name) ? [path] : [];
});
}
function findUnpinnedExternalActions(): string[] {
const violations: string[] = [];
for (const workflowPath of [
...findYamlFiles(".github/workflows"),
...findYamlFiles(".github/actions"),
]) {
for (const [index, line] of readFileSync(workflowPath, "utf8").split("\n").entries()) {
const uses = line.match(/^\s*(?:-\s*)?uses:\s*([^#\s]+)/u)?.[1];
if (!uses || uses.startsWith("./") || uses.startsWith("docker://")) {
continue;
}
const at = uses.lastIndexOf("@");
if (at < 1 || !/^[a-f0-9]{40}$/u.test(uses.slice(at + 1))) {
violations.push(`${workflowPath}:${index + 1}: ${uses}`);
}
}
}
return violations;
}
describe("ci workflow guards", () => {
it("makes the hosted release-gate fallback explicit and exact-SHA only", () => {
const workflow = readCiWorkflow();
const releaseGate = workflow.on.workflow_dispatch.inputs.release_gate;
expect(releaseGate).toEqual({
description:
"Run an exact-SHA maintainer release-gate fallback when PR CI is capacity-stalled.",
required: false,
default: false,
type: "boolean",
});
expect(readFileSync(".github/workflows/ci.yml", "utf8")).toContain(
"run-name: ${{ github.event_name == 'workflow_dispatch' && inputs.release_gate && format('CI release gate {0}', inputs.target_ref) || 'CI' }}",
);
const preflightSteps = workflow.jobs.preflight.steps;
const validationStep = preflightSteps.find(
(step) => step.name === "Validate release-gate dispatch",
);
expect(validationStep.if).toBe(
"github.event_name == 'workflow_dispatch' && inputs.release_gate",
);
expect(validationStep.run).toContain(
"release_gate requires target_ref to be a full commit SHA",
);
expect(validationStep.run).toContain("release_gate must run from the branch at target_ref");
expect(readFileSync(".github/workflows/ci.yml", "utf8")).toContain(
"OPENCLAW_CI_RUN_ANDROID: ${{ github.event_name == 'workflow_dispatch' && (inputs.release_gate || inputs.include_android) && 'true' || steps.changed_scope.outputs.run_android || 'false' }}",
);
});
it("pins every external GitHub Action reference to a full commit SHA", () => {
expect(findUnpinnedExternalActions()).toEqual([]);
});
it("runs the session accessor ratchet as a visible additional check", () => {
const workflow = readCiWorkflow();
const additionalJob = workflow.jobs["check-additional-shard"];
@ -270,9 +338,9 @@ describe("ci workflow guards", () => {
expect(stepNames.indexOf("Run built artifact checks")).toBeLessThan(
stepNames.indexOf("Save dist build cache"),
);
expect(restoreStep.uses).toBe("actions/cache/restore@v5");
expect(restoreStep.uses).toBe(CACHE_V5);
expect(buildDistStep.if).toBe("steps.dist_build_cache.outputs.cache-hit != 'true'");
expect(saveStep.uses).toBe("actions/cache/save@v5");
expect(saveStep.uses).toBe("actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae");
expect(saveStep.if).toBe("steps.dist_build_cache.outputs.cache-hit != 'true'");
expect(saveStep.with.key).toBe("${{ steps.dist_build_cache.outputs.cache-primary-key }}");
expect(restoreStep.with.path).toContain("dist/");
@ -323,7 +391,7 @@ describe("ci workflow guards", () => {
const checkoutStep = timingJob.steps.find(
(step) => step.name === "Checkout timing summary helper",
);
expect(checkoutStep.uses).toBe("actions/checkout@v6");
expect(checkoutStep.uses).toBe(CHECKOUT_V6);
expect(checkoutStep.with.ref).toBe(
"${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || needs.preflight.outputs.checkout_revision || github.sha }}",
);
@ -337,7 +405,7 @@ describe("ci workflow guards", () => {
expect(writeStep.run).toContain('cat ci-timings-summary.txt >> "$GITHUB_STEP_SUMMARY"');
const uploadStep = timingJob.steps.find((step) => step.name === "Upload CI timing summary");
expect(uploadStep.uses).toBe("actions/upload-artifact@v7");
expect(uploadStep.uses).toBe(UPLOAD_ARTIFACT_V7);
expect(uploadStep.with).toMatchObject({
name: "ci-timings-summary",
path: "ci-timings-summary.txt",

View file

@ -33,6 +33,16 @@ function findStep(name: string): WorkflowStep {
}
describe("OpenClaw performance workflow", () => {
it("uses an optional dispatch identifier to name parent-owned runs", () => {
const workflow = readFileSync(WORKFLOW, "utf8");
expect(workflow).toContain(
"run-name: ${{ inputs.dispatch_id != '' && format('OpenClaw Performance {0}', inputs.dispatch_id) || 'OpenClaw Performance' }}",
);
expect(workflow).toContain("dispatch_id:");
expect(workflow).toContain("Optional parent workflow dispatch identifier");
});
it("uses the clawgrit reports token for every report repo push path", () => {
const prepare = findStep("Prepare clawgrit reports checkout");
const publish = findStep("Publish to clawgrit reports");

View file

@ -11,6 +11,7 @@ const SETUP_PNPM_STORE_CACHE_ACTION = ".github/actions/setup-pnpm-store-cache/ac
const DOCKER_E2E_PLAN_ACTION = ".github/actions/docker-e2e-plan/action.yml";
const RELEASE_CHECKS_WORKFLOW = ".github/workflows/openclaw-release-checks.yml";
const RELEASE_PUBLISH_WORKFLOW = ".github/workflows/openclaw-release-publish.yml";
const STABLE_MAIN_CLOSEOUT_WORKFLOW = ".github/workflows/openclaw-stable-main-closeout.yml";
const WINDOWS_NODE_RELEASE_WORKFLOW = ".github/workflows/windows-node-release.yml";
const FULL_RELEASE_VALIDATION_WORKFLOW = ".github/workflows/full-release-validation.yml";
const QA_LIVE_TRANSPORTS_WORKFLOW = ".github/workflows/qa-live-transports-convex.yml";
@ -24,6 +25,9 @@ const CI_HYDRATE_LIVE_AUTH_SCRIPT = "scripts/ci-hydrate-live-auth.sh";
const VERIFY_PROVIDER_SECRETS_SCRIPT =
".agents/skills/release-openclaw-ci/scripts/verify-provider-secrets.mjs";
const UPGRADE_SURVIVOR_RUN_SCRIPT = "scripts/e2e/lib/upgrade-survivor/run.sh";
const SETUP_NODE_V6 = "actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e";
const DOWNLOAD_ARTIFACT_V8 = "actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c";
const UPLOAD_ARTIFACT_V7 = "actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a";
type WorkflowStep = {
"continue-on-error"?: boolean | string;
@ -95,6 +99,80 @@ function expectTextToIncludeAll(text: string | undefined, snippets: string[]): v
}
describe("package acceptance workflow", () => {
it("verifies immutable postpublish evidence before stable closeout reads it", () => {
const workflow = readFileSync(STABLE_MAIN_CLOSEOUT_WORKFLOW, "utf8");
const checksumIndex = workflow.indexOf(
'sha256sum --strict --status -c "$evidence_checksum_asset"',
);
const evidenceReadIndex = workflow.indexOf('evidence_release_tag="$(jq -r');
const releaseVersionGateIndex = workflow.indexOf(
'if [[ "$main_version" != "$release_package_version" &&',
);
const evidenceDownloadIndex = workflow.indexOf(
'if ! gh release download "$evidence_source_tag"',
);
const partialRepairIndex = workflow.indexOf('if [[ -f "$closeout_json_path" ]]; then');
const existingCloseoutEvidenceMatchIndex = workflow.indexOf(
'if [[ -n "$existing_closeout_full_release_validation_run_id" &&',
);
expect(workflow).toContain('evidence_checksum_asset="${evidence_asset}.sha256"');
expect(workflow).toContain('--pattern "$evidence_checksum_asset"');
expect(workflow).toContain('fallback_package_version="${BASH_REMATCH[1]}"');
expect(workflow).toContain(
'tag_package_version="$(gh api "repos/$GITHUB_REPOSITORY/contents/package.json?ref=$tag"',
);
expect(workflow).toContain('evidence_source_tag="v$fallback_package_version"');
expect(workflow).toContain('gh release download "$evidence_source_tag"');
expect(workflow).toContain("Checkout fallback evidence tag");
expect(workflow).toContain("Bind fallback correction to the published package source");
expect(workflow).toContain(
"Fallback correction ${{ needs.resolve.outputs.tag }} must point to the same source commit",
);
expect(workflow).toContain("main_ref: ${{ steps.inputs.outputs.main_ref }}");
expect(workflow).toContain("TRIGGER_SHA: ${{ github.sha }}");
expect(workflow).toContain('main_ref="$TRIGGER_SHA"');
expect(workflow).toContain("ref: ${{ needs.resolve.outputs.main_ref }}");
expect(workflow).toContain(
"Stable closeout skipped: $evidence_source_tag predates immutable postpublish evidence.",
);
expect(workflow).toContain("Stable closeout is required for $tag");
expect(workflow).toContain('closeout_checksum_asset="${closeout_asset}.sha256"');
expect(workflow).toContain('expected_closeout_digest="$(awk');
expect(workflow).toContain('actual_closeout_digest="$(sha256sum "$closeout_json_path"');
expect(workflow).toContain(
"Stable closeout manifest for $tag is incomplete; refusing to repair it.",
);
expect(workflow).toContain(
'if [[ -f "$closeout_checksum_path" && ! -f "$closeout_json_path" ]]; then',
);
expect(workflow).toContain(
"Stable closeout evidence for $tag has an invalid checksum; refusing to repair it.",
);
expect(workflow).toContain("repair_partial_closeout=false");
expect(workflow).toContain(
"Stable closeout manifest for $tag does not match immutable postpublish evidence; refusing to accept it.",
);
expect(workflow).toContain(
"REPAIR_PARTIAL_CLOSEOUT: ${{ needs.resolve.outputs.repair_partial_closeout }}",
);
expect(workflow).toContain('--allow-stale-rollback-drill "$REPAIR_PARTIAL_CLOSEOUT"');
expect(workflow).toContain(
'awk -v asset="openclaw-${release_version}-stable-main-closeout.json"',
);
expect(workflow).toContain("attach_or_verify \\");
expect(checksumIndex).toBeGreaterThan(-1);
expect(evidenceReadIndex).toBeGreaterThan(checksumIndex);
expect(existingCloseoutEvidenceMatchIndex).toBeGreaterThan(evidenceReadIndex);
expect(workflow.slice(checksumIndex, existingCloseoutEvidenceMatchIndex)).not.toContain(
'echo "should_closeout=false"',
);
expect(releaseVersionGateIndex).toBeGreaterThan(-1);
expect(partialRepairIndex).toBeGreaterThan(-1);
expect(partialRepairIndex).toBeLessThan(releaseVersionGateIndex);
expect(evidenceDownloadIndex).toBeGreaterThan(releaseVersionGateIndex);
});
it("keeps pnpm version selection sourced from packageManager", () => {
const packageJson = JSON.parse(readFileSync(PACKAGE_JSON, "utf8")) as {
packageManager?: string;
@ -145,7 +223,7 @@ describe("package acceptance workflow", () => {
expect(hydrate.if).toBe(
"${{ inputs.crabbox_job != 'hydrate-github' && inputs.crabbox_job != 'hydrate-windows-daemon' }}",
);
expect(workflowStep(hydrate, "Setup Node.js").uses).toBe("actions/setup-node@v6");
expect(workflowStep(hydrate, "Setup Node.js").uses).toBe(SETUP_NODE_V6);
expect(workflowStep(hydrate, "Setup Node.js").with?.["node-version"]).toBe("24");
const hydratePnpm = workflowStep(hydrate, "Setup pnpm and dependencies");
expect(hydratePnpm.if).toBeUndefined();
@ -184,7 +262,7 @@ describe("package acceptance workflow", () => {
expect(workflowStep(hydrate, "Hydrate provider env helper").env).toBeUndefined();
expect(hydrateWindowsDaemon.if).toBe("${{ inputs.crabbox_job == 'hydrate-windows-daemon' }}");
expect(workflowStep(hydrateWindowsDaemon, "Setup Node.js").uses).toBe("actions/setup-node@v6");
expect(workflowStep(hydrateWindowsDaemon, "Setup Node.js").uses).toBe(SETUP_NODE_V6);
const hydrateWindowsPnpm = workflowStep(hydrateWindowsDaemon, "Setup pnpm and dependencies");
expect(hydrateWindowsPnpm.shell).toBe("powershell");
expect(hydrateWindowsPnpm.run).toContain(
@ -395,6 +473,10 @@ describe("package acceptance workflow", () => {
it("requires pinned full release child workflows to run at the resolved target SHA", () => {
const workflow = readFileSync(FULL_RELEASE_VALIDATION_WORKFLOW, "utf8");
const releaseChecksWorkflow = readFileSync(RELEASE_CHECKS_WORKFLOW, "utf8");
const performanceJob = workflow.slice(
workflow.indexOf(" performance:\n"),
workflow.indexOf("\n summary:"),
);
expect(workflow).toContain("TARGET_SHA: ${{ needs.resolve_target.outputs.sha }}");
expect(workflow).toContain("CHILD_WORKFLOW_REF: ${{ github.ref_name }}");
@ -419,10 +501,19 @@ describe("package acceptance workflow", () => {
expect(workflow).toContain(
'gh_with_retry workflow run "$workflow" --ref "$CHILD_WORKFLOW_REF" "$@"',
);
expect(workflow).toContain(
expect(performanceJob).toContain(
'dispatch_id="full-release-validation-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"',
);
expect(performanceJob).toContain('-f dispatch_id="$dispatch_id"');
expect(performanceJob).toContain(
'DISPATCH_RUN_NAME="$dispatch_run_name" gh_with_retry api -X GET',
);
expect(performanceJob).toContain(".display_title == env.DISPATCH_RUN_NAME");
expect(performanceJob).toContain("Could not find dispatched run for ${dispatch_run_name}.");
expect(performanceJob).not.toContain("BEFORE_IDS=");
expect(performanceJob).not.toContain(
"did not return an Actions run URL; refusing to guess from recent workflow_dispatch runs",
);
expect(workflow).not.toContain("BEFORE_IDS=");
expect(workflow).toContain("child run used ${head_sha}, expected ${TARGET_SHA}");
expect(workflow).toContain(
"Dispatch Full Release Validation from a ref pinned to the target SHA",
@ -1130,6 +1221,10 @@ describe("package artifact reuse", () => {
expect(workflow).toContain(
"(needs.resolve_target.outputs.rerun_group == 'live-e2e' || (needs.resolve_target.outputs.rerun_group == 'all' && needs.resolve_target.outputs.run_release_soak == 'true')) && needs.resolve_target.outputs.live_suite_filter == ''",
);
expect(workflow).toContain(
'if [[ "$release_profile" == "stable" || "$release_profile" == "full" ]]; then\n run_release_soak=true',
);
expect(workflow).toContain("forced on for release_profile=stable and full");
expect(workflow).toContain("- live-e2e");
expect(workflow).toContain("- qa-live");
expect(workflow).toContain("disabled_required_lanes=()");
@ -1357,7 +1452,7 @@ describe("package artifact reuse", () => {
expect(currentRunDownload).toEqual({
if: "inputs.package_artifact_name != '' && inputs.package_artifact_run_id == ''",
name: "Download package-under-test artifact",
uses: "actions/download-artifact@v8",
uses: DOWNLOAD_ARTIFACT_V8,
with: {
name: "${{ inputs.package_artifact_name }}",
path: ".artifacts/telegram-package-under-test",
@ -1366,7 +1461,7 @@ describe("package artifact reuse", () => {
expect(releaseRunDownload).toEqual({
if: "inputs.package_artifact_name != '' && inputs.package_artifact_run_id != ''",
name: "Download package-under-test artifact from release run",
uses: "actions/download-artifact@v8",
uses: DOWNLOAD_ARTIFACT_V8,
with: {
"github-token": "${{ github.token }}",
name: "${{ inputs.package_artifact_name }}",
@ -1462,7 +1557,7 @@ describe("package artifact reuse", () => {
const uploadStep = workflowStep(job, "Upload advisory status");
expect(uploadStep.if, jobName).toBe("always()");
expect(uploadStep.uses, jobName).toBe("actions/upload-artifact@v7");
expect(uploadStep.uses, jobName).toBe(UPLOAD_ARTIFACT_V7);
expect(uploadStep.with?.name, jobName).toContain("release-check-status-");
expect(uploadStep.with?.path, jobName).toMatch(
/^\.artifacts\/release-check-status\/.+\.env$/u,
@ -1474,7 +1569,7 @@ describe("package artifact reuse", () => {
expect(summary.permissions?.actions).toBe("read");
const downloadStep = workflowStep(summary, "Download advisory status artifacts");
expect(downloadStep["continue-on-error"]).toBe(true);
expect(downloadStep.uses).toBe("actions/download-artifact@v8");
expect(downloadStep.uses).toBe(DOWNLOAD_ARTIFACT_V8);
expect(downloadStep.with?.pattern).toBe("release-check-status-*");
expect(downloadStep.with?.["merge-multiple"]).toBe(true);

View file

@ -13,6 +13,9 @@ import {
} from "../../scripts/package-openclaw-for-docker.mjs";
function isProcessAlive(pid: number): boolean {
if (!Number.isSafeInteger(pid) || pid <= 0) {
return false;
}
try {
process.kill(pid, 0);
return true;
@ -27,15 +30,18 @@ async function sleep(ms: number): Promise<void> {
});
}
async function waitForFile(filePath: string, timeoutMs: number): Promise<void> {
async function readPid(filePath: string, timeoutMs: number): Promise<number> {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (fs.existsSync(filePath)) {
return;
const pid = Number(fs.readFileSync(filePath, "utf8").trim());
if (Number.isSafeInteger(pid) && pid > 0) {
return pid;
}
}
await sleep(25);
}
throw new Error(`timeout waiting for ${filePath}`);
throw new Error(`timeout waiting for a positive pid in ${filePath}`);
}
async function waitForDead(pid: number, timeoutMs: number): Promise<void> {
@ -308,8 +314,7 @@ describe("package-openclaw-for-docker", () => {
timeoutMs: 500,
});
const timeoutAssertion = expect(runPromise).rejects.toThrow(/timed out after 500ms/u);
await waitForFile(childPidPath, 2000);
childPid = Number(fs.readFileSync(childPidPath, "utf8"));
childPid = await readPid(childPidPath, 2000);
await timeoutAssertion;
await waitForDead(childPid, 2000);
} finally {
@ -348,8 +353,7 @@ describe("package-openclaw-for-docker", () => {
}),
).rejects.toThrow(/timed out after 500ms/u);
await waitForFile(childPidPath, 2000);
childPid = Number(fs.readFileSync(childPidPath, "utf8"));
childPid = await readPid(childPidPath, 2000);
await waitForDead(childPid, 2000);
} finally {
if (childPid && isProcessAlive(childPid)) {
@ -437,8 +441,7 @@ describe("package-openclaw-for-docker", () => {
});
runnerPid = runner.pid ?? 0;
await waitForFile(childPidPath, 2000);
childPid = Number(fs.readFileSync(childPidPath, "utf8"));
childPid = await readPid(childPidPath, 2000);
runner.kill("SIGTERM");
const result = await waitForExit(runner, 5000);

View file

@ -11,6 +11,9 @@ import {
createPluginPrereleaseTestPlan,
} from "../../scripts/lib/plugin-prerelease-test-plan.mjs";
const CHECKOUT_V6 = "actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10";
const UPLOAD_ARTIFACT_V7 = "actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a";
function readCiWorkflow() {
return parse(readFileSync(".github/workflows/ci.yml", "utf8"));
}
@ -280,7 +283,7 @@ describe("scripts/lib/plugin-prerelease-test-plan.mjs", () => {
steps: [
{
name: "Checkout",
uses: "actions/checkout@v6",
uses: CHECKOUT_V6,
with: {
"fetch-depth": 1,
"fetch-tags": false,
@ -333,7 +336,7 @@ describe("scripts/lib/plugin-prerelease-test-plan.mjs", () => {
OPENCLAW_CI_EVENT_NAME: "${{ github.event_name }}",
OPENCLAW_CI_REPOSITORY: "${{ github.repository }}",
OPENCLAW_CI_RUN_ANDROID:
"${{ github.event_name == 'workflow_dispatch' && inputs.include_android && 'true' || steps.changed_scope.outputs.run_android || 'false' }}",
"${{ github.event_name == 'workflow_dispatch' && (inputs.release_gate || inputs.include_android) && 'true' || steps.changed_scope.outputs.run_android || 'false' }}",
OPENCLAW_CI_RUN_CONTROL_UI_I18N:
"${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_control_ui_i18n || 'false' }}",
OPENCLAW_CI_RUN_MACOS:
@ -457,7 +460,7 @@ describe("scripts/lib/plugin-prerelease-test-plan.mjs", () => {
).toEqual({
if: "always()",
name: "Upload plugin inspector advisory artifacts",
uses: "actions/upload-artifact@v7",
uses: UPLOAD_ARTIFACT_V7,
with: {
"if-no-files-found": "warn",
name: "plugin-inspector-advisory",

View file

@ -8,6 +8,7 @@ import {
parseArgs,
parseRunIdFromDispatchOutput,
resolveArtifactName,
validateFullManifest,
validateWindowsSourceRelease,
} from "../../scripts/release-candidate-checklist.mjs";
@ -59,6 +60,49 @@ describe("release candidate checklist", () => {
);
});
it("requires stable validation evidence to include soak and blocking performance", () => {
const stableManifest = {
workflowName: "Full Release Validation",
targetSha: "candidate-sha",
releaseProfile: "stable",
rerunGroup: "all",
runReleaseSoak: "true",
controls: { performanceBlocking: true },
};
expect(() =>
validateFullManifest(stableManifest, {
targetSha: "candidate-sha",
releaseProfile: "stable",
}),
).not.toThrow();
expect(() =>
validateFullManifest(
{
...stableManifest,
runReleaseSoak: "false",
},
{
targetSha: "candidate-sha",
releaseProfile: "stable",
},
),
).toThrow("runReleaseSoak=true");
expect(() =>
validateFullManifest(
{
...stableManifest,
controls: { performanceBlocking: false },
},
{
targetSha: "candidate-sha",
releaseProfile: "stable",
},
),
).toThrow("blocking product performance");
});
it("stops parsing options after the argument terminator", () => {
const options = parseArgs([
"--tag",

View file

@ -621,6 +621,7 @@ describe("scripts/test-projects changed-target routing", () => {
"scripts/openclaw-npm-postpublish-verify.ts",
["test/openclaw-npm-postpublish-verify.test.ts"],
],
["scripts/verify-pr-hosted-gates.mjs", ["test/scripts/verify-pr-hosted-gates.test.ts"]],
[
"scripts/postinstall-bundled-plugins.mjs",
["test/scripts/postinstall-bundled-plugins.test.ts"],

View file

@ -0,0 +1,314 @@
import { describe, expect, it } from "vitest";
import {
collectHostedGateEvidence,
parseArgs,
parseWorkflowRunPages,
SCHEDULED_HOSTED_WORKFLOWS,
} from "../../scripts/verify-pr-hosted-gates.mjs";
const sha = "773ffd87a1e1e34451ad6e38fda37380c2569a50";
const BUILD_ARTIFACTS_WORKFLOW = "Blacksmith Build Artifacts Testbox";
function successfulRun(name: string, id: number, updatedAt: string) {
return {
id,
name,
event: "pull_request",
status: "completed",
conclusion: "success",
head_sha: sha,
path: ".github/workflows/ci.yml",
created_at: "2026-06-17T10:46:24Z",
updated_at: updatedAt,
html_url: `https://github.com/openclaw/openclaw/actions/runs/${id}`,
};
}
describe("verify-pr-hosted-gates", () => {
it("requires the latest scheduled workflow run to pass", () => {
const evidence = collectHostedGateEvidence({
sha,
workflowRuns: [
successfulRun("CI", 1, "2026-06-17T10:47:00Z"),
{
...successfulRun("Blacksmith Testbox", 2, "2026-06-17T10:47:30Z"),
event: "workflow_dispatch",
},
successfulRun("Blacksmith Testbox", 3, "2026-06-17T10:48:00Z"),
successfulRun("Blacksmith ARM Testbox", 4, "2026-06-17T10:49:00Z"),
successfulRun("Blacksmith Build Artifacts Testbox", 5, "2026-06-17T10:50:00Z"),
successfulRun("Workflow Sanity", 6, "2026-06-17T10:51:00Z"),
],
});
expect(evidence).toEqual({
headSha: sha,
workflows: [
expect.objectContaining({ name: "CI", id: 1 }),
expect.objectContaining({ name: "Blacksmith Testbox", id: 3 }),
expect.objectContaining({ name: "Blacksmith ARM Testbox", id: 4 }),
expect.objectContaining({ name: "Blacksmith Build Artifacts Testbox", id: 5 }),
expect.objectContaining({ name: "Workflow Sanity", id: 6 }),
],
});
});
it("rejects a failed rerun of a workflow that was scheduled for the exact head", () => {
const workflowRuns = ["CI", ...SCHEDULED_HOSTED_WORKFLOWS].map((name, index) =>
successfulRun(name, index + 1, `2026-06-17T10:4${index}:00Z`),
);
workflowRuns[2] = {
...workflowRuns[2],
conclusion: "failure",
updated_at: "2026-06-17T10:50:00Z",
};
expect(() => collectHostedGateEvidence({ sha, workflowRuns })).toThrow(
"Missing successful exact-head Blacksmith ARM Testbox workflow",
);
});
it("accepts a non-docs PR when CI is the only scheduled authoritative workflow", () => {
expect(
collectHostedGateEvidence({
sha,
workflowRuns: [successfulRun("CI", 1, "2026-06-17T10:47:00Z")],
}),
).toEqual({
headSha: sha,
workflows: [expect.objectContaining({ name: "CI", id: 1 })],
});
});
it("accepts the explicit exact-SHA manual CI release gate", () => {
expect(
collectHostedGateEvidence({
sha,
workflowRuns: [
{
...successfulRun(`CI release gate ${sha}`, 1, "2026-06-17T10:47:00Z"),
event: "workflow_dispatch",
path: ".github/workflows/ci.yml@refs/heads/release-controls",
display_title: `CI release gate ${sha}`,
},
],
}),
).toEqual({
headSha: sha,
workflows: [expect.objectContaining({ name: `CI release gate ${sha}`, id: 1 })],
});
});
it("prefers the exact release-gate fallback while scheduled CI remains queued", () => {
expect(
collectHostedGateEvidence({
sha,
workflowRuns: [
{
...successfulRun("CI", 1, "2026-06-17T10:47:00Z"),
status: "queued",
conclusion: null,
updated_at: "2026-06-17T10:50:00Z",
},
{
...successfulRun(`CI release gate ${sha}`, 2, "2026-06-17T10:49:00Z"),
event: "workflow_dispatch",
display_title: `CI release gate ${sha}`,
},
],
}),
).toEqual({
headSha: sha,
workflows: [expect.objectContaining({ name: `CI release gate ${sha}`, id: 2 })],
});
});
it("rejects a completed scheduled CI failure even when a fallback passed", () => {
expect(() =>
collectHostedGateEvidence({
sha,
workflowRuns: [
{
...successfulRun("CI", 1, "2026-06-17T10:50:00Z"),
conclusion: "failure",
},
{
...successfulRun(`CI release gate ${sha}`, 2, "2026-06-17T10:49:00Z"),
event: "workflow_dispatch",
display_title: `CI release gate ${sha}`,
},
],
}),
).toThrow("Missing successful exact-head CI workflow");
});
it("covers a queued artifact Testbox only with a completed exact CI fallback", () => {
expect(
collectHostedGateEvidence({
sha,
workflowRuns: [
{
...successfulRun(`CI release gate ${sha}`, 1, "2026-06-17T10:49:00Z"),
event: "workflow_dispatch",
display_title: `CI release gate ${sha}`,
},
successfulRun("CI", 3, "2026-06-17T10:51:00Z"),
successfulRun("Blacksmith Testbox", 4, "2026-06-17T10:52:00Z"),
successfulRun("Blacksmith ARM Testbox", 5, "2026-06-17T10:53:00Z"),
successfulRun("Workflow Sanity", 6, "2026-06-17T10:54:00Z"),
{
...successfulRun(BUILD_ARTIFACTS_WORKFLOW, 2, "2026-06-17T10:50:00Z"),
status: "queued",
conclusion: null,
},
],
}),
).toEqual({
headSha: sha,
workflows: [
expect.objectContaining({ name: "CI", id: 3 }),
expect.objectContaining({ name: "Blacksmith Testbox", id: 4 }),
expect.objectContaining({ name: "Blacksmith ARM Testbox", id: 5 }),
expect.objectContaining({ name: "Workflow Sanity", id: 6 }),
],
fallbackCoveredWorkflows: [
{
name: BUILD_ARTIFACTS_WORKFLOW,
coveredBy: "CI release gate",
reason: "scheduled workflow is queued",
},
],
});
});
it("does not cover queued artifacts until all supporting workflow gates pass", () => {
expect(() =>
collectHostedGateEvidence({
sha,
workflowRuns: [
{
...successfulRun(`CI release gate ${sha}`, 1, "2026-06-17T10:49:00Z"),
event: "workflow_dispatch",
display_title: `CI release gate ${sha}`,
},
{
...successfulRun(BUILD_ARTIFACTS_WORKFLOW, 2, "2026-06-17T10:50:00Z"),
status: "queued",
conclusion: null,
},
],
}),
).toThrow("Missing successful exact-head Blacksmith Build Artifacts Testbox workflow");
});
it("keeps active or terminal non-successful artifact Testboxes blocking", () => {
const ciFallback = {
...successfulRun(`CI release gate ${sha}`, 1, "2026-06-17T10:49:00Z"),
event: "workflow_dispatch",
display_title: `CI release gate ${sha}`,
};
for (const artifactRun of [
{
...successfulRun(BUILD_ARTIFACTS_WORKFLOW, 2, "2026-06-17T10:50:00Z"),
status: "in_progress",
conclusion: null,
},
{
...successfulRun(BUILD_ARTIFACTS_WORKFLOW, 3, "2026-06-17T10:51:00Z"),
conclusion: "failure",
},
]) {
expect(() =>
collectHostedGateEvidence({
sha,
workflowRuns: [ciFallback, artifactRun],
}),
).toThrow("Missing successful exact-head Blacksmith Build Artifacts Testbox workflow");
}
expect(() =>
collectHostedGateEvidence({
sha,
workflowRuns: [
ciFallback,
{
...successfulRun(BUILD_ARTIFACTS_WORKFLOW, 4, "2026-06-17T10:52:00Z"),
conclusion: "failure",
},
{
...successfulRun(BUILD_ARTIFACTS_WORKFLOW, 5, "2026-06-17T10:53:00Z"),
status: "queued",
conclusion: null,
},
],
}),
).toThrow("Missing successful exact-head Blacksmith Build Artifacts Testbox workflow");
});
it("rejects an unmarked manual CI run", () => {
expect(() =>
collectHostedGateEvidence({
sha,
workflowRuns: [
{
...successfulRun(`CI release gate ${sha}`, 1, "2026-06-17T10:47:00Z"),
event: "workflow_dispatch",
display_title: "CI",
},
],
}),
).toThrow("Missing successful exact-head CI workflow");
});
it("rejects a manual release-gate title from another workflow", () => {
expect(() =>
collectHostedGateEvidence({
sha,
workflowRuns: [
{
...successfulRun(`CI release gate ${sha}`, 1, "2026-06-17T10:47:00Z"),
event: "workflow_dispatch",
path: ".github/workflows/something-else.yml",
display_title: `CI release gate ${sha}`,
},
],
}),
).toThrow("Missing successful exact-head CI workflow");
});
it("requires CI for docs unless the head changes only CHANGELOG.md", () => {
expect(() => collectHostedGateEvidence({ sha, workflowRuns: [] })).toThrow(
"Missing successful exact-head CI workflow",
);
expect(collectHostedGateEvidence({ sha, workflowRuns: [], changelogOnly: true })).toEqual({
headSha: sha,
workflows: [],
});
});
it("parses required CLI arguments", () => {
expect(
parseArgs([
"--repo",
"openclaw/openclaw",
"--sha",
sha,
"--output",
".local/gates-hosted-checks.json",
]),
).toEqual({
repo: "openclaw/openclaw",
sha,
output: ".local/gates-hosted-checks.json",
changelogOnly: false,
});
expect(() => parseArgs(["--repo", "openclaw/openclaw"])).toThrow("Usage:");
});
it("accepts JSON emitted through a colorizing GitHub CLI shim", () => {
expect(
parseWorkflowRunPages('\u001B[1;37m[{"workflow_runs":[{"id":1,"name":"CI"}]}]\u001B[0m'),
).toEqual([{ id: 1, name: "CI" }]);
});
});

View file

@ -0,0 +1,211 @@
import { describe, expect, it } from "vitest";
import {
extractStableChangelogSection,
parseStableReleaseTag,
verifyStableMainCloseout,
} from "../scripts/lib/stable-release-closeout.mjs";
const release = {
tagName: "v2026.6.8",
isDraft: false,
isPrerelease: false,
assets: [
{ name: "OpenClaw-2026.6.8.zip", digest: `sha256:${"a".repeat(64)}` },
{ name: "OpenClaw-2026.6.8.dmg", digest: `sha256:${"b".repeat(64)}` },
{ name: "OpenClaw-2026.6.8.dSYM.zip", digest: `sha256:${"c".repeat(64)}` },
],
};
const changelog =
"# Changelog\n\n## 2026.6.8\n\n### Fixes\n\n- Shipped fix.\n\n## 2026.6.7\n\n- Old.\n";
const validCloseoutParams = {
tag: "v2026.6.8",
mainPackageJson: { version: "2026.6.8" },
tagPackageJson: { version: "2026.6.8" },
mainChangelog: changelog,
tagChangelog: changelog,
mainAppcast:
"https://github.com/openclaw/openclaw/releases/download/v2026.6.8/OpenClaw-2026.6.8.zip\n",
release,
releaseTagSha: "tag-sha",
mainSha: "main-sha",
fullReleaseValidationRunId: "11",
releasePublishRunId: "12",
rollbackDrillId: "rollback-drill-2026-q2",
rollbackDrillDate: "2026-06-01",
};
describe("stable release closeout", () => {
it("parses stable and correction tags", () => {
expect(parseStableReleaseTag("v2026.6.8")).toBe("2026.6.8");
expect(parseStableReleaseTag("v2026.6.8-2")).toBe("2026.6.8");
expect(() => parseStableReleaseTag("v2026.6.8-beta.1")).toThrow(
"expected a stable release tag",
);
});
it("extracts only the requested stable changelog section", () => {
expect(extractStableChangelogSection(changelog, "2026.6.8")).toBe(
"## 2026.6.8\n\n### Fixes\n\n- Shipped fix.",
);
});
it("accepts an exact stable closeout with a current rollback drill", () => {
const result = verifyStableMainCloseout({
...validCloseoutParams,
nowMs: Date.parse("2026-06-17T00:00:00Z"),
});
expect(result.errors).toEqual([]);
expect(result.manifest).toMatchObject({
releaseTag: "v2026.6.8",
releaseVersion: "2026.6.8",
rollbackDrill: { id: "rollback-drill-2026-q2", date: "2026-06-01" },
});
expect(result.manifest).not.toHaveProperty("verifiedAt");
});
it("writes identical closeout evidence when replayed", () => {
const first = verifyStableMainCloseout({
...validCloseoutParams,
nowMs: Date.parse("2026-06-17T00:00:00Z"),
});
const replay = verifyStableMainCloseout({
...validCloseoutParams,
release: {
...release,
assets: [
...release.assets,
{
name: "openclaw-2026.6.8-stable-main-closeout.json",
digest: `sha256:${"d".repeat(64)}`,
},
{
name: "openclaw-2026.6.8-stable-main-closeout.json.sha256",
digest: `sha256:${"e".repeat(64)}`,
},
],
},
nowMs: Date.parse("2026-06-18T00:00:00Z"),
});
expect(replay.manifest).toEqual(first.manifest);
});
it("replays an existing partial closeout using its recorded rollback drill", () => {
const first = verifyStableMainCloseout({
...validCloseoutParams,
nowMs: Date.parse("2026-06-17T00:00:00Z"),
});
const replay = verifyStableMainCloseout({
...validCloseoutParams,
allowStaleRollbackDrill: true,
nowMs: Date.parse("2026-10-01T00:00:00Z"),
});
expect(replay.errors).toEqual([]);
expect(replay.manifest).toEqual(first.manifest);
});
it("requires the canonical macOS zip, dmg, and dSYM assets", () => {
const result = verifyStableMainCloseout({
...validCloseoutParams,
release: {
...release,
assets: [{ name: "openclaw-2026.6.8-dependency-evidence.zip" }],
},
mainAppcast:
"https://github.com/openclaw/openclaw/releases/download/v2026.6.8/openclaw-2026.6.8-dependency-evidence.zip\n",
nowMs: Date.parse("2026-06-17T00:00:00Z"),
});
expect(result.errors).toContain(
"GitHub release v2026.6.8 is missing required macOS asset(s): OpenClaw-2026.6.8.zip, OpenClaw-2026.6.8.dmg, OpenClaw-2026.6.8.dSYM.zip.",
);
});
it("uses exact correction versions for correction-release state and assets", () => {
const correctionRelease = {
...release,
tagName: "v2026.6.8-2",
assets: release.assets.map((asset) => ({
...asset,
name: asset.name.replaceAll("2026.6.8", "2026.6.8-2"),
})),
};
const result = verifyStableMainCloseout({
...validCloseoutParams,
tag: "v2026.6.8-2",
mainPackageJson: { version: "2026.6.8-2" },
tagPackageJson: { version: "2026.6.8-2" },
mainChangelog: changelog.replaceAll("2026.6.8", "2026.6.8-2"),
tagChangelog: changelog.replaceAll("2026.6.8", "2026.6.8-2"),
release: correctionRelease,
mainAppcast:
"https://github.com/openclaw/openclaw/releases/download/v2026.6.8-2/OpenClaw-2026.6.8-2.zip\n",
nowMs: Date.parse("2026-06-17T00:00:00Z"),
});
expect(result.errors).toEqual([]);
expect(result.manifest).toMatchObject({
releaseVersion: "2026.6.8-2",
mainPackageVersion: "2026.6.8-2",
releaseTagPackageVersion: "2026.6.8-2",
});
});
it("allows a fallback correction tag for an existing base stable package", () => {
const result = verifyStableMainCloseout({
...validCloseoutParams,
tag: "v2026.6.8-2",
release: {
...release,
tagName: "v2026.6.8-2",
},
mainAppcast:
"https://github.com/openclaw/openclaw/releases/download/v2026.6.8-2/OpenClaw-2026.6.8.zip\n",
nowMs: Date.parse("2026-06-17T00:00:00Z"),
});
expect(result.errors).toEqual([]);
expect(result.manifest).toMatchObject({
releaseVersion: "2026.6.8",
mainPackageVersion: "2026.6.8",
releaseTagPackageVersion: "2026.6.8",
});
});
it("rejects calendar-normalized rollback drill dates", () => {
const result = verifyStableMainCloseout({
...validCloseoutParams,
rollbackDrillDate: "2026-02-31",
nowMs: Date.parse("2026-06-17T00:00:00Z"),
});
expect(result.errors).toContain("rollback drill date is invalid: 2026-02-31.");
});
it("rejects speculative main state, appcast drift, and stale rollback drills", () => {
const result = verifyStableMainCloseout({
...validCloseoutParams,
mainPackageJson: { version: "2026.6.9" },
mainChangelog: changelog.replace("Shipped fix.", "Different fix."),
mainAppcast: "https://example.test/old.zip\n",
rollbackDrillId: "rollback-drill-2026-q1",
rollbackDrillDate: "2026-03-01",
nowMs: Date.parse("2026-06-17T00:00:00Z"),
});
expect(result.errors).toContain(
"main package.json version is 2026.6.9, expected shipped version 2026.6.8.",
);
expect(result.errors).toContain(
"main CHANGELOG.md ## 2026.6.8 does not exactly match the shipped release section.",
);
expect(result.errors).toContain(
"main appcast.xml does not point at OpenClaw-2026.6.8.zip from v2026.6.8.",
);
expect(result.errors).toContain(
"rollback drill is older than 90 days: 2026-03-01. Run the private rollback drill before stable closeout.",
);
});
});