diff --git a/.agents/skills/release-openclaw-ci/SKILL.md b/.agents/skills/release-openclaw-ci/SKILL.md index 2262de1962c..e2fe645422e 100644 --- a/.agents/skills/release-openclaw-ci/SKILL.md +++ b/.agents/skills/release-openclaw-ci/SKILL.md @@ -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 `. 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 -f +target_ref= -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 `. 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 diff --git a/.agents/skills/release-openclaw-maintainer/SKILL.md b/.agents/skills/release-openclaw-maintainer/SKILL.md index 80c74f97ecf..5b3c635b014 100644 --- a/.agents/skills/release-openclaw-maintainer/SKILL.md +++ b/.agents/skills/release-openclaw-maintainer/SKILL.md @@ -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 +` 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 `, 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 -f +target_ref= -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 ` 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 `. - 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 diff --git a/.agents/skills/verify-release/SKILL.md b/.agents/skills/verify-release/SKILL.md index e4f5b6c117f..f1b1fc0e9cc 100644 --- a/.agents/skills/verify-release/SKILL.md +++ b/.agents/skills/verify-release/SKILL.md @@ -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 dist-tags.latest dist.tarball dist.integrity time. --json` - `latest` must equal `` 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` @@ -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: diff --git a/.github/actions/docker-e2e-plan/action.yml b/.github/actions/docker-e2e-plan/action.yml index e1b722f324c..7fc6fd9daef 100644 --- a/.github/actions/docker-e2e-plan/action.yml +++ b/.github/actions/docker-e2e-plan/action.yml @@ -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 diff --git a/.github/actions/setup-node-env/action.yml b/.github/actions/setup-node-env/action.yml index 0b2be9ffa27..29d497ace1c 100644 --- a/.github/actions/setup-node-env/action.yml +++ b/.github/actions/setup-node-env/action.yml @@ -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 }} diff --git a/.github/actions/setup-pnpm-store-cache/action.yml b/.github/actions/setup-pnpm-store-cache/action.yml index 511adf9b06d..c4c16d80c2a 100644 --- a/.github/actions/setup-pnpm-store-cache/action.yml +++ b/.github/actions/setup-pnpm-store-cache/action.yml @@ -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) }} diff --git a/.github/workflows/auto-response.yml b/.github/workflows/auto-response.yml index 46c32083e9d..0ac185e5e95 100644 --- a/.github/workflows/auto-response.yml +++ b/.github/workflows/auto-response.yml @@ -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: | diff --git a/.github/workflows/ci-build-artifacts-testbox.yml b/.github/workflows/ci-build-artifacts-testbox.yml index c925595be4a..98cfea4ae7c 100644 --- a/.github/workflows/ci-build-artifacts-testbox.yml +++ b/.github/workflows/ci-build-artifacts-testbox.yml @@ -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/ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c76fb177319..cd4bb597f74 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/.github/workflows/control-ui-locale-refresh.yml b/.github/workflows/control-ui-locale-refresh.yml index f5c2e87c06d..186492dd0d6 100644 --- a/.github/workflows/control-ui-locale-refresh.yml +++ b/.github/workflows/control-ui-locale-refresh.yml @@ -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 diff --git a/.github/workflows/crabbox-hydrate.yml b/.github/workflows/crabbox-hydrate.yml index 522e2de474b..9e51c3f3a7a 100644 --- a/.github/workflows/crabbox-hydrate.yml +++ b/.github/workflows/crabbox-hydrate.yml @@ -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 }} diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml index 981e210cd7a..a964df8404e 100644 --- a/.github/workflows/docker-release.yml +++ b/.github/workflows/docker-release.yml @@ -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 diff --git a/.github/workflows/docs-agent.yml b/.github/workflows/docs-agent.yml index 6df42c682a6..1e0da02027e 100644 --- a/.github/workflows/docs-agent.yml +++ b/.github/workflows/docs-agent.yml @@ -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 diff --git a/.github/workflows/docs-sync-publish.yml b/.github/workflows/docs-sync-publish.yml index 8ed7e80382c..b5b7ef48bfd 100644 --- a/.github/workflows/docs-sync-publish.yml +++ b/.github/workflows/docs-sync-publish.yml @@ -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" diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index a7c731fc93b..0358e586b7a 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -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 diff --git a/.github/workflows/duplicate-after-merge.yml b/.github/workflows/duplicate-after-merge.yml index ad9e14446c5..da110bba1a0 100644 --- a/.github/workflows/duplicate-after-merge.yml +++ b/.github/workflows/duplicate-after-merge.yml @@ -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 }} diff --git a/.github/workflows/full-release-validation.yml b/.github/workflows/full-release-validation.yml index 5893f7669e9..9929f63104a 100644 --- a/.github/workflows/full-release-validation.yml +++ b/.github/workflows/full-release-validation.yml @@ -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 diff --git a/.github/workflows/install-smoke.yml b/.github/workflows/install-smoke.yml index ec5dcd191ca..319e212d406 100644 --- a/.github/workflows/install-smoke.yml +++ b/.github/workflows/install-smoke.yml @@ -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 diff --git a/.github/workflows/ios-periphery-comment.yml b/.github/workflows/ios-periphery-comment.yml index 608d2d529be..1c6af9f56c3 100644 --- a/.github/workflows/ios-periphery-comment.yml +++ b/.github/workflows/ios-periphery-comment.yml @@ -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"); diff --git a/.github/workflows/ios-periphery.yml b/.github/workflows/ios-periphery.yml index 4aa58289e73..3f22cc0d9c6 100644 --- a/.github/workflows/ios-periphery.yml +++ b/.github/workflows/ios-periphery.yml @@ -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 diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 6b4298f62c6..dc7d4070ef3 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -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: | diff --git a/.github/workflows/live-media-runner-image.yml b/.github/workflows/live-media-runner-image.yml index 02297559e37..255f10c817c 100644 --- a/.github/workflows/live-media-runner-image.yml +++ b/.github/workflows/live-media-runner-image.yml @@ -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: diff --git a/.github/workflows/macos-release.yml b/.github/workflows/macos-release.yml index c0f0bbc22b8..2ea1535e6cb 100644 --- a/.github/workflows/macos-release.yml +++ b/.github/workflows/macos-release.yml @@ -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 diff --git a/.github/workflows/maintainer-command-reactions.yml b/.github/workflows/maintainer-command-reactions.yml index 8a334faa032..b2fef4a9c9d 100644 --- a/.github/workflows/maintainer-command-reactions.yml +++ b/.github/workflows/maintainer-command-reactions.yml @@ -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; diff --git a/.github/workflows/mantis-discord-smoke.yml b/.github/workflows/mantis-discord-smoke.yml index 438d58260ff..bcb47c48282 100644 --- a/.github/workflows/mantis-discord-smoke.yml +++ b/.github/workflows/mantis-discord-smoke.yml @@ -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/ diff --git a/.github/workflows/mantis-discord-status-reactions.yml b/.github/workflows/mantis-discord-status-reactions.yml index 41e2fc421be..f0433620c79 100644 --- a/.github/workflows/mantis-discord-status-reactions.yml +++ b/.github/workflows/mantis-discord-status-reactions.yml @@ -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; diff --git a/.github/workflows/mantis-discord-thread-attachment.yml b/.github/workflows/mantis-discord-thread-attachment.yml index 49d43896f60..34d0fb5e491 100644 --- a/.github/workflows/mantis-discord-thread-attachment.yml +++ b/.github/workflows/mantis-discord-thread-attachment.yml @@ -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; diff --git a/.github/workflows/mantis-slack-desktop-smoke.yml b/.github/workflows/mantis-slack-desktop-smoke.yml index cb27624a5e1..39c131e29d2 100644 --- a/.github/workflows/mantis-slack-desktop-smoke.yml +++ b/.github/workflows/mantis-slack-desktop-smoke.yml @@ -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 }} diff --git a/.github/workflows/mantis-telegram-desktop-proof.yml b/.github/workflows/mantis-telegram-desktop-proof.yml index 390508084fa..ba8b77ce993 100644 --- a/.github/workflows/mantis-telegram-desktop-proof.yml +++ b/.github/workflows/mantis-telegram-desktop-proof.yml @@ -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; diff --git a/.github/workflows/mantis-telegram-live.yml b/.github/workflows/mantis-telegram-live.yml index 0fbb9c8eea4..407a80a0e21 100644 --- a/.github/workflows/mantis-telegram-live.yml +++ b/.github/workflows/mantis-telegram-live.yml @@ -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; diff --git a/.github/workflows/npm-telegram-beta-e2e.yml b/.github/workflows/npm-telegram-beta-e2e.yml index e88a128c638..5e3503ac8c5 100644 --- a/.github/workflows/npm-telegram-beta-e2e.yml +++ b/.github/workflows/npm-telegram-beta-e2e.yml @@ -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/ diff --git a/.github/workflows/openclaw-cross-os-release-checks-reusable.yml b/.github/workflows/openclaw-cross-os-release-checks-reusable.yml index 0a4be2e3ae6..a8ff9590f5d 100644 --- a/.github/workflows/openclaw-cross-os-release-checks-reusable.yml +++ b/.github/workflows/openclaw-cross-os-release-checks-reusable.yml @@ -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 }} diff --git a/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml b/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml index 73ca6743a97..2d16c74a499 100644 --- a/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml +++ b/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml @@ -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 diff --git a/.github/workflows/openclaw-npm-release.yml b/.github/workflows/openclaw-npm-release.yml index 1ed49cf7eb0..af851d80a59 100644 --- a/.github/workflows/openclaw-npm-release.yml +++ b/.github/workflows/openclaw-npm-release.yml @@ -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 diff --git a/.github/workflows/openclaw-performance.yml b/.github/workflows/openclaw-performance.yml index 7875cbf2f2b..63f55a4749f 100644 --- a/.github/workflows/openclaw-performance.yml +++ b/.github/workflows/openclaw-performance.yml @@ -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: | diff --git a/.github/workflows/openclaw-release-checks.yml b/.github/workflows/openclaw-release-checks.yml index 80fbadc337a..33db3f6fa57 100644 --- a/.github/workflows/openclaw-release-checks.yml +++ b/.github/workflows/openclaw-release-checks.yml @@ -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 diff --git a/.github/workflows/openclaw-release-publish.yml b/.github/workflows/openclaw-release-publish.yml index 0c71afa6009..b7a9ed594d6 100644 --- a/.github/workflows/openclaw-release-publish.yml +++ b/.github/workflows/openclaw-release-publish.yml @@ -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 diff --git a/.github/workflows/openclaw-stable-main-closeout.yml b/.github/workflows/openclaw-stable-main-closeout.yml new file mode 100644 index 00000000000..494d8dc50a7 --- /dev/null +++ b/.github/workflows/openclaw-stable-main-closeout.yml @@ -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] ?? ""}.`); + } + } + 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] ?? ""}.`); + } + } + 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 diff --git a/.github/workflows/opengrep-precise-full.yml b/.github/workflows/opengrep-precise-full.yml index 288d08cd642..240b40ddd24 100644 --- a/.github/workflows/opengrep-precise-full.yml +++ b/.github/workflows/opengrep-precise-full.yml @@ -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 diff --git a/.github/workflows/opengrep-precise.yml b/.github/workflows/opengrep-precise.yml index 49419d8662f..9eee588e96d 100644 --- a/.github/workflows/opengrep-precise.yml +++ b/.github/workflows/opengrep-precise.yml @@ -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 diff --git a/.github/workflows/package-acceptance.yml b/.github/workflows/package-acceptance.yml index 4e9df58bbe2..9123a04b028 100644 --- a/.github/workflows/package-acceptance.yml +++ b/.github/workflows/package-acceptance.yml @@ -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 diff --git a/.github/workflows/plugin-clawhub-new.yml b/.github/workflows/plugin-clawhub-new.yml index 7e2e6073a07..bc84c7d9f44 100644 --- a/.github/workflows/plugin-clawhub-new.yml +++ b/.github/workflows/plugin-clawhub-new.yml @@ -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 }} diff --git a/.github/workflows/plugin-clawhub-release.yml b/.github/workflows/plugin-clawhub-release.yml index da9645a2cdd..3978afacf8a 100644 --- a/.github/workflows/plugin-clawhub-release.yml +++ b/.github/workflows/plugin-clawhub-release.yml @@ -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 diff --git a/.github/workflows/plugin-npm-release.yml b/.github/workflows/plugin-npm-release.yml index aab8648d2a9..b67f35b8358 100644 --- a/.github/workflows/plugin-npm-release.yml +++ b/.github/workflows/plugin-npm-release.yml @@ -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 }} diff --git a/.github/workflows/plugin-prerelease.yml b/.github/workflows/plugin-prerelease.yml index 179e70b5def..abed4e2439a 100644 --- a/.github/workflows/plugin-prerelease.yml +++ b/.github/workflows/plugin-prerelease.yml @@ -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/** diff --git a/.github/workflows/qa-live-transports-convex.yml b/.github/workflows/qa-live-transports-convex.yml index 8faeccc660e..b7105036e60 100644 --- a/.github/workflows/qa-live-transports-convex.yml +++ b/.github/workflows/qa-live-transports-convex.yml @@ -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 }} diff --git a/.github/workflows/real-behavior-proof.yml b/.github/workflows/real-behavior-proof.yml index e29c5e0901e..015810240f0 100644 --- a/.github/workflows/real-behavior-proof.yml +++ b/.github/workflows/real-behavior-proof.yml @@ -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 diff --git a/.github/workflows/sandbox-common-smoke.yml b/.github/workflows/sandbox-common-smoke.yml index aa92a617cf6..94024321520 100644 --- a/.github/workflows/sandbox-common-smoke.yml +++ b/.github/workflows/sandbox-common-smoke.yml @@ -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 diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 2e40405d6e0..8e6eb8bf47c 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -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: | diff --git a/.github/workflows/test-performance-agent.yml b/.github/workflows/test-performance-agent.yml index 0ec19bd4399..54f8f25de22 100644 --- a/.github/workflows/test-performance-agent.yml +++ b/.github/workflows/test-performance-agent.yml @@ -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/ diff --git a/.github/workflows/tui-pty.yml b/.github/workflows/tui-pty.yml index 45236f604fa..61fecba03a0 100644 --- a/.github/workflows/tui-pty.yml +++ b/.github/workflows/tui-pty.yml @@ -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: diff --git a/.github/workflows/website-installer-sync.yml b/.github/workflows/website-installer-sync.yml index 6f0f675ece6..e1e6345560e 100644 --- a/.github/workflows/website-installer-sync.yml +++ b/.github/workflows/website-installer-sync.yml @@ -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" diff --git a/.github/workflows/windows-blacksmith-testbox.yml b/.github/workflows/windows-blacksmith-testbox.yml index ad49f938760..b31d0078a55 100644 --- a/.github/workflows/windows-blacksmith-testbox.yml +++ b/.github/workflows/windows-blacksmith-testbox.yml @@ -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 diff --git a/.github/workflows/windows-testbox-probe.yml b/.github/workflows/windows-testbox-probe.yml index a890bf0107c..21fec2f0c71 100644 --- a/.github/workflows/windows-testbox-probe.yml +++ b/.github/workflows/windows-testbox-probe.yml @@ -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 diff --git a/.github/workflows/workflow-sanity.yml b/.github/workflows/workflow-sanity.yml index 5fdfa58d12c..4753963e4f0 100644 --- a/.github/workflows/workflow-sanity.yml +++ b/.github/workflows/workflow-sanity.yml @@ -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" diff --git a/apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt b/apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt index f52d06ab238..8bbcbc07dab 100644 --- a/apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt +++ b/apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt @@ -1114,7 +1114,7 @@ internal fun gatewayRecoveryUiState( ready: Boolean, statusText: String, connectSettling: Boolean, - nodeCapabilityApprovalState: GatewayNodeApprovalState = GatewayNodeApprovalState.Loading, + nodeCapabilityApprovalState: GatewayNodeApprovalState, gatewayConnectionProblem: GatewayConnectionProblem? = null, ): GatewayRecoveryUiState = when { diff --git a/apps/android/app/src/test/java/ai/openclaw/app/ui/OnboardingFlowLogicTest.kt b/apps/android/app/src/test/java/ai/openclaw/app/ui/OnboardingFlowLogicTest.kt index 428fb72f317..37c8ad7978d 100644 --- a/apps/android/app/src/test/java/ai/openclaw/app/ui/OnboardingFlowLogicTest.kt +++ b/apps/android/app/src/test/java/ai/openclaw/app/ui/OnboardingFlowLogicTest.kt @@ -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, ), ) } diff --git a/docs/ci.md b/docs/ci.md index 718edcc1e00..ff6d44d2048 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -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@` derives `codex_plugin_spec=npm:@openclaw/codex@`, 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@` derives `codex_plugin_spec=npm:@openclaw/codex@`, 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. diff --git a/docs/reference/RELEASING.md b/docs/reference/RELEASING.md index e61b6cae529..00f667bd5ce 100644 --- a/docs/reference/RELEASING.md +++ b/docs/reference/RELEASING.md @@ -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--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: diff --git a/docs/reference/full-release-validation.md b/docs/reference/full-release-validation.md index af281cb12c9..ef7334fb8ce 100644 --- a/docs/reference/full-release-validation.md +++ b/docs/reference/full-release-validation.md @@ -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`
**Child workflow:** none
**Proves:** resolves the release branch, tag, or full commit SHA and records selected inputs.
**Rerun:** rerun the umbrella if this fails. | -| Vitest and normal CI | **Job:** `Run normal full CI`
**Child workflow:** `CI`
**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.
**Rerun:** `rerun_group=ci`. | -| Plugin prerelease | **Job:** `Run plugin prerelease validation`
**Child workflow:** `Plugin Prerelease`
**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.
**Rerun:** `rerun_group=plugin-prerelease`. | -| Release checks | **Job:** `Run release/live/Docker/QA validation`
**Child workflow:** `OpenClaw Release Checks`
**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.
**Rerun:** `rerun_group=release-checks` or a narrower release-checks handle. | -| Package artifact | **Job:** `Prepare release package artifact`
**Child workflow:** none
**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`.
**Rerun:** rerun the umbrella or provide `release_package_spec` for published-package reruns. | -| Package Telegram | **Job:** `Run package Telegram E2E`
**Child workflow:** `NPM Telegram Beta E2E`
**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.
**Rerun:** `rerun_group=npm-telegram` with `release_package_spec` or `npm_telegram_package_spec`. | -| Umbrella verifier | **Job:** `Verify full validation`
**Child workflow:** none
**Proves:** re-checks recorded child run conclusions and appends slowest-job tables from child workflows.
**Rerun:** rerun only this job after rerunning a failed child to green. | +| Stage | Details | +| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Target resolution | **Job:** `Resolve target ref`
**Child workflow:** none
**Proves:** resolves the release branch, tag, or full commit SHA and records selected inputs.
**Rerun:** rerun the umbrella if this fails. | +| Vitest and normal CI | **Job:** `Run normal full CI`
**Child workflow:** `CI`
**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.
**Rerun:** `rerun_group=ci`. | +| Plugin prerelease | **Job:** `Run plugin prerelease validation`
**Child workflow:** `Plugin Prerelease`
**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.
**Rerun:** `rerun_group=plugin-prerelease`. | +| Release checks | **Job:** `Run release/live/Docker/QA validation`
**Child workflow:** `OpenClaw Release Checks`
**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`.
**Rerun:** `rerun_group=release-checks` or a narrower release-checks handle. | +| Package artifact | **Job:** `Prepare release package artifact`
**Child workflow:** none
**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`.
**Rerun:** rerun the umbrella or provide `release_package_spec` for published-package reruns. | +| Package Telegram | **Job:** `Run package Telegram E2E`
**Child workflow:** `NPM Telegram Beta E2E`
**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.
**Rerun:** `rerun_group=npm-telegram` with `release_package_spec` or `npm_telegram_package_spec`. | +| Umbrella verifier | **Job:** `Verify full validation`
**Child workflow:** none
**Proves:** re-checks recorded child run conclusions and appends slowest-job tables from child workflows.
**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 | | --------- | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | diff --git a/extensions/llama-cpp/npm-shrinkwrap.json b/extensions/llama-cpp/npm-shrinkwrap.json index c6bb130f638..656e897ada1 100644 --- a/extensions/llama-cpp/npm-shrinkwrap.json +++ b/extensions/llama-cpp/npm-shrinkwrap.json @@ -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" diff --git a/extensions/msteams/npm-shrinkwrap.json b/extensions/msteams/npm-shrinkwrap.json index 3dbe03f1284..5033b6122ab 100644 --- a/extensions/msteams/npm-shrinkwrap.json +++ b/extensions/msteams/npm-shrinkwrap.json @@ -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" diff --git a/extensions/slack/npm-shrinkwrap.json b/extensions/slack/npm-shrinkwrap.json index 651e257cbce..41ca91f4452 100644 --- a/extensions/slack/npm-shrinkwrap.json +++ b/extensions/slack/npm-shrinkwrap.json @@ -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" diff --git a/extensions/zalouser/npm-shrinkwrap.json b/extensions/zalouser/npm-shrinkwrap.json index 44e4e6b8a2c..51594606e28 100644 --- a/extensions/zalouser/npm-shrinkwrap.json +++ b/extensions/zalouser/npm-shrinkwrap.json @@ -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" diff --git a/package.json b/package.json index dc7301ab262..31a453f8a5b 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 59a0da9b5a2..9022ca8c333 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/scripts/lib/release-beta-verifier.ts b/scripts/lib/release-beta-verifier.ts index 3cad45b3670..ce6c6606972 100644 --- a/scripts/lib/release-beta-verifier.ts +++ b/scripts/lib/release-beta-verifier.ts @@ -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, diff --git a/scripts/lib/stable-release-closeout.mjs b/scripts/lib/stable-release-closeout.mjs new file mode 100644 index 00000000000..82eb6b96327 --- /dev/null +++ b/scripts/lib/stable-release-closeout.mjs @@ -0,0 +1,194 @@ +import { createHash } from "node:crypto"; + +const STABLE_RELEASE_TAG_RE = /^v(?\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 ?? ""}.`); + 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 ?? "")}, 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, + })), + }, + }; +} diff --git a/scripts/openclaw-npm-postpublish-verify.ts b/scripts/openclaw-npm-postpublish-verify.ts index b90d7de37ed..003e9b9cff6 100644 --- a/scripts/openclaw-npm-postpublish-verify.ts +++ b/scripts/openclaw-npm-postpublish-verify.ts @@ -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; + +type NpmProvenanceStatement = { + predicate?: { + buildDefinition?: { + externalParameters?: { + workflow?: { + path?: string; + ref?: string; + repository?: string; + }; + }; + }; + runDetails?: { + builder?: { + id?: string; + }; + }; + }; + subject?: Array<{ + digest?: Record; + 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 { + await verifySigstoreBundle(bundle as Parameters[0], policy); +} + +export async function verifyNpmProvenanceAttestation(params: { + attestations: NpmRegistryAttestation[]; + integrity: string; + packageName: string; + verifyBundle?: VerifyNpmProvenanceBundle; + version: string; +}): Promise { + 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 { + 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( + read: () => Promise, + options: { + attempts?: number; + delay?: (delayMs: number) => Promise; + } = {}, +): Promise { + const attempts = options.attempts ?? NPM_REGISTRY_PROVENANCE_ATTEMPTS; + const delay = + options.delay ?? + ((delayMs: number) => + new Promise((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 { + 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 { + 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 { 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; diff --git a/scripts/pr b/scripts/pr index ccbe8ca8cad..5b8ee011fb2 100755 --- a/scripts/pr +++ b/scripts/pr @@ -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]}")" diff --git a/scripts/pr-lib/gates.sh b/scripts/pr-lib/gates.sh index ffae1fd745e..1148e1aa571 100644 --- a/scripts/pr-lib/gates.sh +++ b/scripts/pr-lib/gates.sh @@ -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" diff --git a/scripts/pr-lib/merge.sh b/scripts/pr-lib/merge.sh index 09f36109620..46b570e7991 100644 --- a/scripts/pr-lib/merge.sh +++ b/scripts/pr-lib/merge.sh @@ -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 diff --git a/scripts/pr-lib/prepare-core.sh b/scripts/pr-lib/prepare-core.sh index ac21111feaf..6dd567ae339 100644 --- a/scripts/pr-lib/prepare-core.sh +++ b/scripts/pr-lib/prepare-core.sh @@ -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 < .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 < .local/prep-sync.env + cat >> .local/prep.md <> .local/prep.md < .local/prep.env diff --git a/scripts/pr-lib/push.sh b/scripts/pr-lib/push.sh index af308bd1e88..b4ee1bca0d9 100644 --- a/scripts/pr-lib/push.sh +++ b/scripts/pr-lib/push.sh @@ -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" diff --git a/scripts/release-candidate-checklist.mjs b/scripts/release-candidate-checklist.mjs index 759cd29ece6..34a3f1dfead 100644 --- a/scripts/release-candidate-checklist.mjs +++ b/scripts/release-candidate-checklist.mjs @@ -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, diff --git a/scripts/test-projects.test-support.mjs b/scripts/test-projects.test-support.mjs index 90f249b8427..a3b2f5d4936 100644 --- a/scripts/test-projects.test-support.mjs +++ b/scripts/test-projects.test-support.mjs @@ -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"]], diff --git a/scripts/verify-pr-hosted-gates.mjs b/scripts/verify-pr-hosted-gates.mjs new file mode 100644 index 00000000000..032efcbc483 --- /dev/null +++ b/scripts/verify-pr-hosted-gates.mjs @@ -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} .`); + } + 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 --sha --output ", + ); + } + 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(); +} diff --git a/scripts/verify-stable-main-closeout.mjs b/scripts/verify-stable-main-closeout.mjs new file mode 100644 index 00000000000..03069921152 --- /dev/null +++ b/scripts/verify-stable-main-closeout.mjs @@ -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; +} diff --git a/test/openclaw-npm-postpublish-verify.test.ts b/test/openclaw-npm-postpublish-verify.test.ts index 67e518dccb1..92dd1efea94 100644 --- a/test/openclaw-npm-postpublish-verify.test.ts +++ b/test/openclaw-npm-postpublish-verify.test.ts @@ -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"); diff --git a/test/scripts/ci-workflow-guards.test.ts b/test/scripts/ci-workflow-guards.test.ts index a078b26558d..ec646992c19 100644 --- a/test/scripts/ci-workflow-guards.test.ts +++ b/test/scripts/ci-workflow-guards.test.ts @@ -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", diff --git a/test/scripts/openclaw-performance-workflow.test.ts b/test/scripts/openclaw-performance-workflow.test.ts index 51a4cd8c5ae..9d29150e457 100644 --- a/test/scripts/openclaw-performance-workflow.test.ts +++ b/test/scripts/openclaw-performance-workflow.test.ts @@ -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"); diff --git a/test/scripts/package-acceptance-workflow.test.ts b/test/scripts/package-acceptance-workflow.test.ts index 3c072687e61..3b4690dd892 100644 --- a/test/scripts/package-acceptance-workflow.test.ts +++ b/test/scripts/package-acceptance-workflow.test.ts @@ -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); diff --git a/test/scripts/package-openclaw-for-docker.test.ts b/test/scripts/package-openclaw-for-docker.test.ts index 037453ed2a0..13a65d6a5ce 100644 --- a/test/scripts/package-openclaw-for-docker.test.ts +++ b/test/scripts/package-openclaw-for-docker.test.ts @@ -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 { }); } -async function waitForFile(filePath: string, timeoutMs: number): Promise { +async function readPid(filePath: string, timeoutMs: number): Promise { 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 { @@ -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); diff --git a/test/scripts/plugin-prerelease-test-plan.test.ts b/test/scripts/plugin-prerelease-test-plan.test.ts index e9b156295e9..3b2648ade63 100644 --- a/test/scripts/plugin-prerelease-test-plan.test.ts +++ b/test/scripts/plugin-prerelease-test-plan.test.ts @@ -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", diff --git a/test/scripts/release-candidate-checklist.test.ts b/test/scripts/release-candidate-checklist.test.ts index 47e8711d067..b974a9b35e3 100644 --- a/test/scripts/release-candidate-checklist.test.ts +++ b/test/scripts/release-candidate-checklist.test.ts @@ -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", diff --git a/test/scripts/test-projects.test.ts b/test/scripts/test-projects.test.ts index 5b67d513bb1..0b791376f9c 100644 --- a/test/scripts/test-projects.test.ts +++ b/test/scripts/test-projects.test.ts @@ -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"], diff --git a/test/scripts/verify-pr-hosted-gates.test.ts b/test/scripts/verify-pr-hosted-gates.test.ts new file mode 100644 index 00000000000..8c29b8f4dd4 --- /dev/null +++ b/test/scripts/verify-pr-hosted-gates.test.ts @@ -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" }]); + }); +}); diff --git a/test/stable-release-closeout.test.ts b/test/stable-release-closeout.test.ts new file mode 100644 index 00000000000..5d2fa2e93a9 --- /dev/null +++ b/test/stable-release-closeout.test.ts @@ -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.", + ); + }); +});