mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-09 15:59:30 +00:00
ci(release): harden release controls
One-time maintainer-authorized bootstrap merge for the release-gate verifier policy. Exact hosted CI and all supporting workflow gates passed on 66133de419.
This commit is contained in:
parent
3a570f1410
commit
abb6f04e0c
88 changed files with 3403 additions and 449 deletions
|
|
@ -24,6 +24,25 @@ Use this with `$release-openclaw-maintainer` and `$openclaw-testing` when a rele
|
|||
fails, the parent cancels the remaining child matrix and prints the failed
|
||||
job summary. Inspect that first red job instead of waiting for unrelated
|
||||
matrix tails.
|
||||
- In a sparse worktree or Testbox source sync, first confirm `package.json`,
|
||||
`pnpm-lock.yaml`, and every source path the selected check reads. If any are
|
||||
absent, that checkout cannot validate a release dependency or Docker lane:
|
||||
stop and use the repo remote changed gate or a full task worktree. When the
|
||||
inputs are present and a release fix changes `package.json` or
|
||||
`pnpm-lock.yaml`, rebuild only the task-owned disposable box with
|
||||
`CI=true pnpm install --frozen-lockfile`, then run an explicit
|
||||
`require.resolve()` probe before Docker or focused tests. The CI flag permits
|
||||
pnpm to recreate a prewarmed modules directory without an interactive
|
||||
confirmation. Do not weaken the lockfile or label sparse-checkout failures
|
||||
as product/Docker failures.
|
||||
- If the candidate is rebased or its base SHA changes after warmup, stop the
|
||||
task-owned box and warm a fresh one before testing. Testbox source sync is
|
||||
relative to the warmed source tree; continuing can mix an old base file with
|
||||
a new candidate diff and produce false lockfile or Docker failures.
|
||||
- For a committed release candidate, warm the box with
|
||||
`blacksmith testbox warmup ... --ref <candidate-branch-or-sha>`. Do not rely
|
||||
on source sync to overlay committed branch changes onto the workflow's
|
||||
default ref.
|
||||
|
||||
## Preflight
|
||||
|
||||
|
|
@ -57,7 +76,7 @@ gh workflow run openclaw-performance.yml \
|
|||
-f repeat=3 \
|
||||
-f deep_profile=false \
|
||||
-f live_openai_candidate=false \
|
||||
-f fail_on_regression=false
|
||||
-f fail_on_regression=true
|
||||
```
|
||||
|
||||
- Do not wait for full release validation to start this early perf signal.
|
||||
|
|
@ -66,8 +85,9 @@ gh workflow run openclaw-performance.yml \
|
|||
- Call out any regression in the release proof. Treat a major regression as a
|
||||
release blocker until it is fixed, waived by the operator, or proven to be
|
||||
infrastructure noise.
|
||||
- Full Release Validation also records advisory product-performance evidence;
|
||||
the early standalone run is for overlap and faster regression discovery.
|
||||
- Full Release Validation records blocking product-performance evidence. The
|
||||
early standalone run is for overlap and faster regression discovery, but a
|
||||
regression or missing child run blocks the parent validation.
|
||||
|
||||
Prefer the trusted workflow on `main`, target the exact release SHA:
|
||||
|
||||
|
|
@ -89,7 +109,7 @@ gh workflow run full-release-validation.yml \
|
|||
-f rerun_group=all
|
||||
```
|
||||
|
||||
Use `release_profile=stable` unless the operator explicitly asks for the broad advisory provider/media matrix. Use narrow `rerun_group` after focused fixes.
|
||||
Use `release_profile=stable` unless the operator explicitly asks for the broad advisory provider/media matrix. Stable and full profiles force the release soak; the beta profile may opt in with `run_release_soak=true`. Use narrow `rerun_group` after focused fixes.
|
||||
Publish with `openclaw-release-publish.yml` using `release_profile=from-validation`
|
||||
unless a maintainer intentionally wants to cross-check a specific profile; the
|
||||
publish workflow reads the effective profile from the full-validation manifest.
|
||||
|
|
@ -125,6 +145,19 @@ Stop watchers before ending the turn or switching strategy.
|
|||
Anthropic API-key lane.
|
||||
5. For live-cache failures, inspect whether it is missing/invalid key, empty text, provider refusal, timeout, or baseline miss. Do not weaken release gates without clear provider evidence.
|
||||
6. Fix narrowly, run local/changed proof, commit, push, rerun the smallest matching group.
|
||||
7. If a required PR CI run is capacity-stalled with queued jobs and no active
|
||||
jobs, do not cancel unrelated work or accept a generic manual dispatch.
|
||||
From the PR head branch, dispatch the explicit exact-SHA fallback:
|
||||
`gh workflow run ci.yml --repo openclaw/openclaw --ref <pr-head-branch> -f
|
||||
target_ref=<full-pr-sha> -f include_android=true -f release_gate=true`.
|
||||
It runs on GitHub-hosted runners and is accepted only when its run title is
|
||||
`CI release gate <full-pr-sha>`. Record the stalled Blacksmith run and the
|
||||
fallback run in release evidence.
|
||||
If `Blacksmith Build Artifacts Testbox` is the only remaining required gate
|
||||
and remains queued without a runner, that completed exact fallback may cover
|
||||
it because CI's `build-artifacts` job already builds, packages, and smoke
|
||||
tests the artifacts. Do not use this coverage after the artifact workflow
|
||||
starts or completes non-successfully.
|
||||
|
||||
## Evidence
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,10 @@ Use this skill for release and publish-time workflow. Load `$release-private` if
|
|||
- This skill should be sufficient to drive the normal release flow end-to-end.
|
||||
- Use the private maintainer release docs for credentials, recovery steps, and mac signing/notary specifics, and use `docs/reference/RELEASING.md` for public policy.
|
||||
- Core `openclaw` publish is manual `workflow_dispatch`; creating or pushing a tag does not publish by itself.
|
||||
- Do not edit the root `README.md` as release prep, release closeout, or a
|
||||
substitute for release notes. Package-root README validation is a hard
|
||||
packaging gate, but a release only changes README content when an actual
|
||||
user-facing documentation contract changed.
|
||||
- Normal release work happens on a branch cut from `main`, not directly on
|
||||
`main`. Use `release/YYYY.M.PATCH` for the branch name.
|
||||
- If the operator asks for a release without saying stable/full, default to
|
||||
|
|
@ -76,6 +80,44 @@ Use this skill for release and publish-time workflow. Load `$release-private` if
|
|||
or clawgrit reports. Report regressions explicitly. A major regression is a
|
||||
release blocker unless the operator waives it or the data clearly proves
|
||||
infrastructure noise.
|
||||
- Heal CI before tagging or publishing. The exact candidate SHA must have green
|
||||
`Full Release Validation`, including the root Dockerfile/install-smoke path.
|
||||
Treat a red Docker, package, or release workflow lane as a release-branch
|
||||
defect until the smallest correct fix is landed and proven; do not waive it
|
||||
because npm preflight or another sibling lane passed.
|
||||
- Keep the canonical `scripts/pr` runner authoritative for prepare and merge
|
||||
artifacts. A release-gate policy change may use focused candidate tests and
|
||||
exact-SHA hosted CI for proof, but never route `prepare-*` or `merge-*`
|
||||
through PR-controlled scripts or synthesize prepare artifacts to bootstrap
|
||||
the change. If the current canonical gate cannot validate the new policy,
|
||||
stop for explicit maintainer direction rather than weakening that boundary.
|
||||
- In maintainer Testbox mode, use `OPENCLAW_TESTBOX=1 scripts/pr prepare-run
|
||||
<PR>` only after the exact PR head has passed `CI` and every scheduled
|
||||
hosted gate. For a workflow change, that means `Blacksmith Testbox`,
|
||||
`Blacksmith ARM Testbox`, `Blacksmith Build Artifacts Testbox`, and
|
||||
`Workflow Sanity`; only gates GitHub actually scheduled for that exact head
|
||||
are required. This preserves the canonical prepare artifacts while avoiding
|
||||
a redundant broad local suite. A
|
||||
literal `CHANGELOG.md`-only head gets a clean diff check instead because
|
||||
those workflows intentionally do not dispatch. Documentation and README
|
||||
changes still require CI. If `merge-run` requires a mainline sync, run
|
||||
`OPENCLAW_TESTBOX=1 scripts/pr prepare-sync-head <PR>`, wait for those hosted
|
||||
gates on the newly pushed SHA, then run `prepare-run` again.
|
||||
- If an exact PR-head CI run has no active jobs because Blacksmith capacity is
|
||||
stalled, a maintainer may dispatch the explicit GitHub-hosted fallback from
|
||||
the PR head branch:
|
||||
`gh workflow run ci.yml --repo openclaw/openclaw --ref <pr-head-branch> -f
|
||||
target_ref=<full-pr-sha> -f include_android=true -f release_gate=true`.
|
||||
Use it only for an observed provider queue stall, never for failed CI or as a
|
||||
routine shortcut. The run must be named `CI release gate <full-pr-sha>` and
|
||||
pass on that exact SHA; the native hosted-gate verifier rejects generic manual
|
||||
CI runs. If `Blacksmith Build Artifacts Testbox` is the only remaining
|
||||
required gate and it is still queued without a runner, the same completed
|
||||
fallback CI may cover it because its `build-artifacts` job builds, packages,
|
||||
and smoke tests those artifacts. The verifier records that coverage. Never
|
||||
use this coverage when the artifact workflow has started, failed, been
|
||||
cancelled, or been skipped. Then rerun `OPENCLAW_TESTBOX=1 scripts/pr
|
||||
prepare-run <PR>`.
|
||||
- Generate the changelog before every beta, beta rerun, stable release, or
|
||||
stable rerun, before version/tag preparation. Use
|
||||
`$openclaw-changelog-update` for the rewrite. Do not continue release prep if
|
||||
|
|
@ -119,6 +161,14 @@ Stable publication is not complete until `main` carries the actual shipped relea
|
|||
`OPENCLAW_TESTBOX=1 pnpm check:changed`. Push, then verify `origin/main`
|
||||
contains the shipped version and changelog before calling the stable release
|
||||
done.
|
||||
6. Keep repository variables `RELEASE_ROLLBACK_DRILL_ID` and
|
||||
`RELEASE_ROLLBACK_DRILL_DATE` current after each private rollback drill.
|
||||
`openclaw-stable-main-closeout.yml` starts from the `main` push carrying the
|
||||
shipped version, changelog, and appcast after stable publication, then binds
|
||||
immutable evidence to the published tag. Do not declare stable complete
|
||||
until it writes the immutable closeout manifest to the GitHub release. The
|
||||
drill must be within 90 days; manual dispatch is only for repair/replay, and
|
||||
private rollback commands remain in the maintainer-only runbook.
|
||||
|
||||
## Handle versions and release files consistently
|
||||
|
||||
|
|
|
|||
|
|
@ -29,11 +29,17 @@ publish skill; use `$release-openclaw-maintainer` before changing release state.
|
|||
- Confirm release body has npm, CI, plugin npm, ClawHub, mac/appcast evidence
|
||||
links when expected.
|
||||
- Confirm assets expected for stable mac releases are uploaded: zip, dmg,
|
||||
dSYM, dependency evidence when present.
|
||||
dSYM, dependency evidence, immutable full-validation manifest,
|
||||
postpublish evidence, and stable-main closeout manifest.
|
||||
- Download each immutable evidence asset and its `.sha256` companion, then
|
||||
verify the checksum before trusting the release record.
|
||||
2. Root npm:
|
||||
- `npm view openclaw@<VERSION> version dist-tags.latest dist.tarball dist.integrity time.<VERSION> --json`
|
||||
- `latest` must equal `<VERSION>` for stable.
|
||||
- Record tarball, integrity, publish time.
|
||||
- Confirm the release postpublish evidence records
|
||||
`npmRegistrySignaturesVerified: true` and
|
||||
`npmProvenanceAttestationMatched: true`.
|
||||
3. Plugin publish set:
|
||||
- Get exact tag metadata from GitHub, not the local checkout when dirty:
|
||||
download `https://api.github.com/repos/openclaw/openclaw/tarball/v<VERSION>`
|
||||
|
|
@ -57,6 +63,9 @@ publish skill; use `$release-openclaw-maintainer` before changing release state.
|
|||
Full Release Validation, OpenClaw Release Checks, OpenClaw NPM Release,
|
||||
Plugin NPM Release, Plugin ClawHub Release, mac preflight/validation/publish
|
||||
when stable mac assets are expected.
|
||||
- For stable, verify `OpenClaw Stable Main Closeout` succeeded and its
|
||||
manifest records the matching release tag, current rollback drill, stable
|
||||
soak, and blocking performance evidence.
|
||||
- Summarize only relevant successful/failed jobs; ignore routine skipped
|
||||
optional lanes unless the release body promised them.
|
||||
6. Published package smoke:
|
||||
|
|
|
|||
2
.github/actions/docker-e2e-plan/action.yml
vendored
2
.github/actions/docker-e2e-plan/action.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
2
.github/actions/setup-node-env/action.yml
vendored
2
.github/actions/setup-node-env/action.yml
vendored
|
|
@ -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 }}
|
||||
|
|
|
|||
|
|
@ -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) }}
|
||||
|
|
|
|||
8
.github/workflows/auto-response.yml
vendored
8
.github/workflows/auto-response.yml
vendored
|
|
@ -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: |
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
|
|
|
|||
58
.github/workflows/ci.yml
vendored
58
.github/workflows/ci.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
10
.github/workflows/crabbox-hydrate.yml
vendored
10
.github/workflows/crabbox-hydrate.yml
vendored
|
|
@ -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 }}
|
||||
|
||||
|
|
|
|||
10
.github/workflows/docker-release.yml
vendored
10
.github/workflows/docker-release.yml
vendored
|
|
@ -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
|
||||
|
||||
|
|
|
|||
2
.github/workflows/docs-agent.yml
vendored
2
.github/workflows/docs-agent.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
6
.github/workflows/docs-sync-publish.yml
vendored
6
.github/workflows/docs-sync-publish.yml
vendored
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
4
.github/workflows/docs.yml
vendored
4
.github/workflows/docs.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
3
.github/workflows/duplicate-after-merge.yml
vendored
3
.github/workflows/duplicate-after-merge.yml
vendored
|
|
@ -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 }}
|
||||
|
|
|
|||
78
.github/workflows/full-release-validation.yml
vendored
78
.github/workflows/full-release-validation.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
16
.github/workflows/install-smoke.yml
vendored
16
.github/workflows/install-smoke.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
2
.github/workflows/ios-periphery-comment.yml
vendored
2
.github/workflows/ios-periphery-comment.yml
vendored
|
|
@ -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");
|
||||
|
|
|
|||
6
.github/workflows/ios-periphery.yml
vendored
6
.github/workflows/ios-periphery.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
28
.github/workflows/labeler.yml
vendored
28
.github/workflows/labeler.yml
vendored
|
|
@ -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: |
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
2
.github/workflows/macos-release.yml
vendored
2
.github/workflows/macos-release.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
8
.github/workflows/mantis-discord-smoke.yml
vendored
8
.github/workflows/mantis-discord-smoke.yml
vendored
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
14
.github/workflows/mantis-slack-desktop-smoke.yml
vendored
14
.github/workflows/mantis-slack-desktop-smoke.yml
vendored
|
|
@ -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 }}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
18
.github/workflows/mantis-telegram-live.yml
vendored
18
.github/workflows/mantis-telegram-live.yml
vendored
|
|
@ -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;
|
||||
|
|
|
|||
8
.github/workflows/npm-telegram-beta-e2e.yml
vendored
8
.github/workflows/npm-telegram-beta-e2e.yml
vendored
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -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 }}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
28
.github/workflows/openclaw-npm-release.yml
vendored
28
.github/workflows/openclaw-npm-release.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
13
.github/workflows/openclaw-performance.yml
vendored
13
.github/workflows/openclaw-performance.yml
vendored
|
|
@ -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: |
|
||||
|
|
|
|||
72
.github/workflows/openclaw-release-checks.yml
vendored
72
.github/workflows/openclaw-release-checks.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
105
.github/workflows/openclaw-release-publish.yml
vendored
105
.github/workflows/openclaw-release-publish.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
384
.github/workflows/openclaw-stable-main-closeout.yml
vendored
Normal file
384
.github/workflows/openclaw-stable-main-closeout.yml
vendored
Normal file
|
|
@ -0,0 +1,384 @@
|
|||
name: OpenClaw Stable Main Closeout
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: Stable OpenClaw tag to replay or repair, for example v2026.6.8 or v2026.6.8-2
|
||||
required: false
|
||||
type: string
|
||||
rollback_drill_id:
|
||||
description: Opaque identifier for the current private rollback drill record
|
||||
required: false
|
||||
type: string
|
||||
rollback_drill_date:
|
||||
description: UTC date of the private rollback drill in YYYY-MM-DD form; must be within 90 days
|
||||
required: false
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
contents: write
|
||||
|
||||
concurrency:
|
||||
group: openclaw-stable-main-closeout
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
resolve:
|
||||
name: Resolve stable release closeout inputs
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
outputs:
|
||||
full_release_validation_run_id: ${{ steps.inputs.outputs.full_release_validation_run_id }}
|
||||
release_publish_run_id: ${{ steps.inputs.outputs.release_publish_run_id }}
|
||||
rollback_drill_date: ${{ steps.inputs.outputs.rollback_drill_date }}
|
||||
rollback_drill_id: ${{ steps.inputs.outputs.rollback_drill_id }}
|
||||
evidence_tag: ${{ steps.inputs.outputs.evidence_tag }}
|
||||
fallback_correction: ${{ steps.inputs.outputs.fallback_correction }}
|
||||
main_ref: ${{ steps.inputs.outputs.main_ref }}
|
||||
repair_partial_closeout: ${{ steps.inputs.outputs.repair_partial_closeout }}
|
||||
should_closeout: ${{ steps.inputs.outputs.should_closeout }}
|
||||
tag: ${{ steps.inputs.outputs.tag }}
|
||||
steps:
|
||||
- name: Checkout pushed main
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
with:
|
||||
ref: ${{ github.sha }}
|
||||
fetch-depth: 1
|
||||
persist-credentials: false
|
||||
|
||||
- name: Resolve published stable release evidence
|
||||
id: inputs
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
MANUAL_TAG: ${{ inputs.tag }}
|
||||
ROLLBACK_DRILL_DATE: ${{ inputs.rollback_drill_date || vars.RELEASE_ROLLBACK_DRILL_DATE }}
|
||||
ROLLBACK_DRILL_ID: ${{ inputs.rollback_drill_id || vars.RELEASE_ROLLBACK_DRILL_ID }}
|
||||
TRIGGER_SHA: ${{ github.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ "$EVENT_NAME" == "push" ]]; then
|
||||
main_ref="$TRIGGER_SHA"
|
||||
tag="$(gh release list --repo "$GITHUB_REPOSITORY" --exclude-drafts --limit 100 \
|
||||
--json tagName,isPrerelease,publishedAt \
|
||||
--jq '[.[] | select(.isPrerelease | not) | select(.tagName | test("^v[0-9]{4}\\.[0-9]+\\.[0-9]+(-[0-9]+)?$"))] | sort_by(.publishedAt) | last | .tagName // empty')"
|
||||
if [[ -z "$tag" ]]; then
|
||||
echo "should_closeout=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
else
|
||||
tag="$MANUAL_TAG"
|
||||
fi
|
||||
if [[ ! "$tag" =~ ^v[0-9]{4}\.[0-9]+\.[0-9]+(-[0-9]+)?$ ]]; then
|
||||
if [[ "$EVENT_NAME" == "push" ]]; then
|
||||
echo "should_closeout=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
echo "Stable main closeout accepts only a stable vYYYY.M.PATCH or vYYYY.M.PATCH-N tag, got $tag." >&2
|
||||
exit 1
|
||||
fi
|
||||
release_asset_version="${tag#v}"
|
||||
release_package_version="$release_asset_version"
|
||||
fallback_package_version="$release_asset_version"
|
||||
if [[ "$release_package_version" =~ ^(.+)-[0-9]+$ ]]; then
|
||||
fallback_package_version="${BASH_REMATCH[1]}"
|
||||
fi
|
||||
tag_package_version="$(gh api "repos/$GITHUB_REPOSITORY/contents/package.json?ref=$tag" \
|
||||
--jq '.content' | tr -d '\n' | base64 --decode | jq -r '.version // empty')"
|
||||
fallback_correction=false
|
||||
evidence_source_tag="$tag"
|
||||
if [[ "$release_package_version" != "$fallback_package_version" &&
|
||||
"$tag_package_version" == "$fallback_package_version" ]]; then
|
||||
fallback_correction=true
|
||||
evidence_source_tag="v$fallback_package_version"
|
||||
elif [[ "$tag_package_version" != "$release_package_version" ]]; then
|
||||
echo "Stable closeout requires $tag package.json to match $release_package_version, or the legacy fallback package version $fallback_package_version." >&2
|
||||
exit 1
|
||||
fi
|
||||
evidence_version="${evidence_source_tag#v}"
|
||||
evidence_asset="openclaw-${evidence_version}-postpublish-evidence.json"
|
||||
evidence_checksum_asset="${evidence_asset}.sha256"
|
||||
closeout_asset="openclaw-${release_asset_version}-stable-main-closeout.json"
|
||||
closeout_checksum_asset="${closeout_asset}.sha256"
|
||||
closeout_dir="$RUNNER_TEMP/release-closeout-evidence"
|
||||
mkdir -p "$closeout_dir"
|
||||
gh release download "$tag" --repo "$GITHUB_REPOSITORY" \
|
||||
--pattern "$closeout_asset" --pattern "$closeout_checksum_asset" --dir "$closeout_dir" || true
|
||||
closeout_json_path="$closeout_dir/$closeout_asset"
|
||||
closeout_checksum_path="$closeout_dir/$closeout_checksum_asset"
|
||||
repair_partial_closeout=false
|
||||
existing_closeout_full_release_validation_run_id=""
|
||||
existing_closeout_release_publish_run_id=""
|
||||
if [[ -f "$closeout_json_path" && -f "$closeout_checksum_path" ]]; then
|
||||
expected_closeout_digest="$(awk 'NF { print $1; exit }' "$closeout_checksum_path")"
|
||||
actual_closeout_digest="$(sha256sum "$closeout_json_path" | awk '{print $1}')"
|
||||
if [[ ! "$expected_closeout_digest" =~ ^[0-9a-f]{64}$ ||
|
||||
"$expected_closeout_digest" != "$actual_closeout_digest" ]]; then
|
||||
echo "Stable closeout evidence for $tag has an invalid checksum; refusing to repair it." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
if [[ -f "$closeout_checksum_path" && ! -f "$closeout_json_path" ]]; then
|
||||
echo "Stable closeout evidence for $tag has a checksum without its manifest; refusing to repair it." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ -f "$closeout_json_path" ]]; then
|
||||
existing_closeout_tag="$(jq -r '.releaseTag // empty' "$closeout_json_path")"
|
||||
existing_closeout_version="$(jq -r '.releaseVersion // empty' "$closeout_json_path")"
|
||||
existing_closeout_release_tag_sha="$(jq -r '.releaseTagSha // empty' "$closeout_json_path")"
|
||||
existing_closeout_main_ref="$(jq -r '.mainSha // empty' "$closeout_json_path")"
|
||||
existing_closeout_full_release_validation_run_id="$(jq -r '.fullReleaseValidationRunId // empty' "$closeout_json_path")"
|
||||
existing_closeout_release_publish_run_id="$(jq -r '.releasePublishRunId // empty' "$closeout_json_path")"
|
||||
existing_closeout_rollback_drill_id="$(jq -r '.rollbackDrill.id // empty' "$closeout_json_path")"
|
||||
existing_closeout_rollback_drill_date="$(jq -r '.rollbackDrill.date // empty' "$closeout_json_path")"
|
||||
if [[ "$existing_closeout_tag" != "$tag" ||
|
||||
"$existing_closeout_version" != "$tag_package_version" ||
|
||||
! "$existing_closeout_release_tag_sha" =~ ^[0-9a-f]{40}$ ||
|
||||
! "$existing_closeout_main_ref" =~ ^[0-9a-f]{40}$ ||
|
||||
-z "$existing_closeout_full_release_validation_run_id" ||
|
||||
-z "$existing_closeout_release_publish_run_id" ||
|
||||
-z "$existing_closeout_rollback_drill_id" ||
|
||||
! "$existing_closeout_rollback_drill_date" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
|
||||
echo "Stable closeout manifest for $tag is incomplete; refusing to repair it." >&2
|
||||
exit 1
|
||||
fi
|
||||
main_ref="$existing_closeout_main_ref"
|
||||
ROLLBACK_DRILL_ID="$existing_closeout_rollback_drill_id"
|
||||
ROLLBACK_DRILL_DATE="$existing_closeout_rollback_drill_date"
|
||||
repair_partial_closeout=true
|
||||
elif [[ "$EVENT_NAME" == "push" ]]; then
|
||||
main_version="$(jq -r '.version // empty' package.json)"
|
||||
if [[ "$main_version" != "$release_package_version" &&
|
||||
"$main_version" != "$fallback_package_version" ]]; then
|
||||
echo "should_closeout=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
else
|
||||
main_ref="main"
|
||||
fi
|
||||
evidence_dir="$RUNNER_TEMP/release-postpublish-evidence"
|
||||
mkdir -p "$evidence_dir"
|
||||
if ! gh release download "$evidence_source_tag" --repo "$GITHUB_REPOSITORY" \
|
||||
--pattern "$evidence_asset" --pattern "$evidence_checksum_asset" --dir "$evidence_dir"; then
|
||||
if [[ "$EVENT_NAME" == "push" ]]; then
|
||||
echo "Stable closeout skipped: $evidence_source_tag predates immutable postpublish evidence." >&2
|
||||
echo "should_closeout=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
echo "Stable closeout is required for $tag, but immutable postpublish evidence from $evidence_source_tag is missing." >&2
|
||||
exit 1
|
||||
fi
|
||||
evidence_path="$evidence_dir/$evidence_asset"
|
||||
if ! (
|
||||
cd "$evidence_dir"
|
||||
sha256sum --strict --status -c "$evidence_checksum_asset"
|
||||
); then
|
||||
echo "Postpublish evidence checksum failed for $tag." >&2
|
||||
exit 1
|
||||
fi
|
||||
evidence_release_tag="$(jq -r '.releaseTag // empty' "$evidence_path")"
|
||||
full_release_validation_run_id="$(jq -r '[.workflowRuns[]? | select(.label == "Full Release Validation") | .id] | if length == 1 then .[0] else empty end' "$evidence_path")"
|
||||
release_publish_run_id="$(jq -r '.releasePublishRunId // empty' "$evidence_path")"
|
||||
if [[ "$evidence_release_tag" != "$evidence_source_tag" || -z "$full_release_validation_run_id" || -z "$release_publish_run_id" ]]; then
|
||||
echo "Stable closeout is required for $tag, but postpublish evidence does not bind $evidence_source_tag to exactly one Full Release Validation run and its Publish run." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ -n "$existing_closeout_full_release_validation_run_id" &&
|
||||
( "$existing_closeout_full_release_validation_run_id" != "$full_release_validation_run_id" ||
|
||||
"$existing_closeout_release_publish_run_id" != "$release_publish_run_id" ) ]]; then
|
||||
echo "Stable closeout manifest for $tag does not match immutable postpublish evidence; refusing to accept it." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ -z "$ROLLBACK_DRILL_ID" || -z "$ROLLBACK_DRILL_DATE" ]]; then
|
||||
echo "Stable closeout requires repository variables RELEASE_ROLLBACK_DRILL_ID and RELEASE_ROLLBACK_DRILL_DATE, or explicit manual overrides." >&2
|
||||
exit 1
|
||||
fi
|
||||
{
|
||||
echo "full_release_validation_run_id=$full_release_validation_run_id"
|
||||
echo "release_publish_run_id=$release_publish_run_id"
|
||||
echo "rollback_drill_date=$ROLLBACK_DRILL_DATE"
|
||||
echo "rollback_drill_id=$ROLLBACK_DRILL_ID"
|
||||
echo "evidence_tag=$evidence_source_tag"
|
||||
echo "fallback_correction=$fallback_correction"
|
||||
echo "main_ref=$main_ref"
|
||||
echo "repair_partial_closeout=$repair_partial_closeout"
|
||||
echo "should_closeout=true"
|
||||
echo "tag=$tag"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
verify:
|
||||
name: Verify stable main closeout
|
||||
needs: resolve
|
||||
if: ${{ needs.resolve.outputs.should_closeout == 'true' }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: Checkout resolved main state
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
with:
|
||||
ref: ${{ needs.resolve.outputs.main_ref }}
|
||||
fetch-depth: 1
|
||||
persist-credentials: false
|
||||
|
||||
- name: Checkout shipped release tag
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
with:
|
||||
ref: refs/tags/${{ needs.resolve.outputs.tag }}
|
||||
path: release-tag
|
||||
fetch-depth: 1
|
||||
persist-credentials: false
|
||||
|
||||
- name: Checkout fallback evidence tag
|
||||
if: ${{ needs.resolve.outputs.fallback_correction == 'true' }}
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
with:
|
||||
ref: refs/tags/${{ needs.resolve.outputs.evidence_tag }}
|
||||
path: evidence-tag
|
||||
fetch-depth: 1
|
||||
persist-credentials: false
|
||||
|
||||
- name: Bind fallback correction to the published package source
|
||||
if: ${{ needs.resolve.outputs.fallback_correction == 'true' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
correction_sha="$(git -C "$GITHUB_WORKSPACE/release-tag" rev-parse HEAD)"
|
||||
evidence_sha="$(git -C "$GITHUB_WORKSPACE/evidence-tag" rev-parse HEAD)"
|
||||
if [[ "$correction_sha" != "$evidence_sha" ]]; then
|
||||
echo "Fallback correction ${{ needs.resolve.outputs.tag }} must point to the same source commit as ${{ needs.resolve.outputs.evidence_tag }} to reuse immutable package evidence." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Verify release workflow evidence
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
FULL_RELEASE_VALIDATION_RUN_ID: ${{ needs.resolve.outputs.full_release_validation_run_id }}
|
||||
RELEASE_PUBLISH_RUN_ID: ${{ needs.resolve.outputs.release_publish_run_id }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
gh run view "$FULL_RELEASE_VALIDATION_RUN_ID" --repo "$GITHUB_REPOSITORY" \
|
||||
--json workflowName,event,status,conclusion \
|
||||
> "$RUNNER_TEMP/full-release-validation-run.json"
|
||||
node --input-type=module - "$RUNNER_TEMP/full-release-validation-run.json" <<'NODE'
|
||||
import { readFileSync } from "node:fs";
|
||||
const run = JSON.parse(readFileSync(process.argv[2], "utf8"));
|
||||
for (const [key, expected] of [
|
||||
["workflowName", "Full Release Validation"],
|
||||
["event", "workflow_dispatch"],
|
||||
["status", "completed"],
|
||||
["conclusion", "success"],
|
||||
]) {
|
||||
if (run[key] !== expected) {
|
||||
throw new Error(`Full Release Validation must have ${key}=${expected}, got ${run[key] ?? "<missing>"}.`);
|
||||
}
|
||||
}
|
||||
NODE
|
||||
gh run view "$RELEASE_PUBLISH_RUN_ID" --repo "$GITHUB_REPOSITORY" \
|
||||
--json workflowName,event,status,conclusion \
|
||||
> "$RUNNER_TEMP/release-publish-run.json"
|
||||
node --input-type=module - "$RUNNER_TEMP/release-publish-run.json" <<'NODE'
|
||||
import { readFileSync } from "node:fs";
|
||||
const run = JSON.parse(readFileSync(process.argv[2], "utf8"));
|
||||
for (const [key, expected] of [
|
||||
["workflowName", "OpenClaw Release Publish"],
|
||||
["event", "workflow_dispatch"],
|
||||
["status", "completed"],
|
||||
["conclusion", "success"],
|
||||
]) {
|
||||
if (run[key] !== expected) {
|
||||
throw new Error(`OpenClaw Release Publish must have ${key}=${expected}, got ${run[key] ?? "<missing>"}.`);
|
||||
}
|
||||
}
|
||||
NODE
|
||||
|
||||
manifest_dir="$RUNNER_TEMP/full-release-validation-manifest"
|
||||
rm -rf "$manifest_dir"
|
||||
mkdir -p "$manifest_dir"
|
||||
gh run download "$FULL_RELEASE_VALIDATION_RUN_ID" --repo "$GITHUB_REPOSITORY" \
|
||||
--name "full-release-validation-${FULL_RELEASE_VALIDATION_RUN_ID}" \
|
||||
--dir "$manifest_dir"
|
||||
tag_sha="$(git -C "$GITHUB_WORKSPACE/release-tag" rev-parse HEAD)"
|
||||
jq -e --arg tag_sha "$tag_sha" '
|
||||
.workflowName == "Full Release Validation" and
|
||||
.targetSha == $tag_sha and
|
||||
.rerunGroup == "all" and
|
||||
.runReleaseSoak == "true" and
|
||||
.controls.performanceBlocking == true and
|
||||
.childRuns.productPerformance.conclusion == "success"
|
||||
' "$manifest_dir/full-release-validation-manifest.json" >/dev/null || {
|
||||
echo "Full Release Validation manifest does not contain the required stable release controls." >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
- name: Verify stable state and write closeout manifest
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
RELEASE_TAG: ${{ needs.resolve.outputs.tag }}
|
||||
FULL_RELEASE_VALIDATION_RUN_ID: ${{ needs.resolve.outputs.full_release_validation_run_id }}
|
||||
RELEASE_PUBLISH_RUN_ID: ${{ needs.resolve.outputs.release_publish_run_id }}
|
||||
ROLLBACK_DRILL_ID: ${{ needs.resolve.outputs.rollback_drill_id }}
|
||||
ROLLBACK_DRILL_DATE: ${{ needs.resolve.outputs.rollback_drill_date }}
|
||||
REPAIR_PARTIAL_CLOSEOUT: ${{ needs.resolve.outputs.repair_partial_closeout }}
|
||||
CLOSEOUT_DIR: ${{ runner.temp }}/openclaw-stable-main-closeout
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p "$CLOSEOUT_DIR"
|
||||
gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" \
|
||||
--json tagName,isDraft,isPrerelease,assets \
|
||||
> "$CLOSEOUT_DIR/github-release.json"
|
||||
node scripts/verify-stable-main-closeout.mjs \
|
||||
--tag "$RELEASE_TAG" \
|
||||
--main-dir "$GITHUB_WORKSPACE" \
|
||||
--tag-dir "$GITHUB_WORKSPACE/release-tag" \
|
||||
--release-json "$CLOSEOUT_DIR/github-release.json" \
|
||||
--full-release-validation-run-id "$FULL_RELEASE_VALIDATION_RUN_ID" \
|
||||
--release-publish-run-id "$RELEASE_PUBLISH_RUN_ID" \
|
||||
--rollback-drill-id "$ROLLBACK_DRILL_ID" \
|
||||
--rollback-drill-date "$ROLLBACK_DRILL_DATE" \
|
||||
--allow-stale-rollback-drill "$REPAIR_PARTIAL_CLOSEOUT" \
|
||||
--output "$CLOSEOUT_DIR/stable-main-closeout.json"
|
||||
release_version="${RELEASE_TAG#v}"
|
||||
sha256sum "$CLOSEOUT_DIR/stable-main-closeout.json" | awk -v asset="openclaw-${release_version}-stable-main-closeout.json" \
|
||||
'{print $1 " " asset}' \
|
||||
> "$CLOSEOUT_DIR/stable-main-closeout.json.sha256"
|
||||
|
||||
- name: Attach immutable closeout evidence
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
RELEASE_TAG: ${{ needs.resolve.outputs.tag }}
|
||||
CLOSEOUT_DIR: ${{ runner.temp }}/openclaw-stable-main-closeout
|
||||
run: |
|
||||
set -euo pipefail
|
||||
release_version="${RELEASE_TAG#v}"
|
||||
attach_or_verify() {
|
||||
local source_path="$1"
|
||||
local asset_name="$2"
|
||||
local existing_dir="$CLOSEOUT_DIR/existing-${asset_name}"
|
||||
mkdir -p "$existing_dir"
|
||||
if gh release download "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" \
|
||||
--pattern "$asset_name" --dir "$existing_dir"; then
|
||||
cmp --silent "$source_path" "$existing_dir/$asset_name" || {
|
||||
echo "Existing release asset $asset_name differs from closeout evidence." >&2
|
||||
exit 1
|
||||
}
|
||||
return
|
||||
fi
|
||||
gh release upload "$RELEASE_TAG" "$source_path#$asset_name" --repo "$GITHUB_REPOSITORY"
|
||||
}
|
||||
attach_or_verify \
|
||||
"$CLOSEOUT_DIR/stable-main-closeout.json" \
|
||||
"openclaw-${release_version}-stable-main-closeout.json"
|
||||
attach_or_verify \
|
||||
"$CLOSEOUT_DIR/stable-main-closeout.json.sha256" \
|
||||
"openclaw-${release_version}-stable-main-closeout.json.sha256"
|
||||
|
||||
- name: Upload closeout workflow evidence
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: openclaw-stable-main-closeout-${{ needs.resolve.outputs.tag }}
|
||||
path: ${{ runner.temp }}/openclaw-stable-main-closeout
|
||||
if-no-files-found: error
|
||||
6
.github/workflows/opengrep-precise-full.yml
vendored
6
.github/workflows/opengrep-precise-full.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
6
.github/workflows/opengrep-precise.yml
vendored
6
.github/workflows/opengrep-precise.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
10
.github/workflows/package-acceptance.yml
vendored
10
.github/workflows/package-acceptance.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
6
.github/workflows/plugin-clawhub-new.yml
vendored
6
.github/workflows/plugin-clawhub-new.yml
vendored
|
|
@ -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 }}
|
||||
|
|
|
|||
8
.github/workflows/plugin-clawhub-release.yml
vendored
8
.github/workflows/plugin-clawhub-release.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
8
.github/workflows/plugin-npm-release.yml
vendored
8
.github/workflows/plugin-npm-release.yml
vendored
|
|
@ -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 }}
|
||||
|
|
|
|||
12
.github/workflows/plugin-prerelease.yml
vendored
12
.github/workflows/plugin-prerelease.yml
vendored
|
|
@ -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/**
|
||||
|
|
|
|||
36
.github/workflows/qa-live-transports-convex.yml
vendored
36
.github/workflows/qa-live-transports-convex.yml
vendored
|
|
@ -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 }}
|
||||
|
|
|
|||
6
.github/workflows/real-behavior-proof.yml
vendored
6
.github/workflows/real-behavior-proof.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
2
.github/workflows/sandbox-common-smoke.yml
vendored
2
.github/workflows/sandbox-common-smoke.yml
vendored
|
|
@ -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
|
||||
|
||||
|
|
|
|||
26
.github/workflows/stale.yml
vendored
26
.github/workflows/stale.yml
vendored
|
|
@ -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: |
|
||||
|
|
|
|||
4
.github/workflows/test-performance-agent.yml
vendored
4
.github/workflows/test-performance-agent.yml
vendored
|
|
@ -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/
|
||||
|
|
|
|||
3
.github/workflows/tui-pty.yml
vendored
3
.github/workflows/tui-pty.yml
vendored
|
|
@ -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:
|
||||
|
|
|
|||
24
.github/workflows/website-installer-sync.yml
vendored
24
.github/workflows/website-installer-sync.yml
vendored
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
2
.github/workflows/windows-testbox-probe.yml
vendored
2
.github/workflows/windows-testbox-probe.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
2
.github/workflows/workflow-sanity.yml
vendored
2
.github/workflows/workflow-sanity.yml
vendored
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -1114,7 +1114,7 @@ internal fun gatewayRecoveryUiState(
|
|||
ready: Boolean,
|
||||
statusText: String,
|
||||
connectSettling: Boolean,
|
||||
nodeCapabilityApprovalState: GatewayNodeApprovalState = GatewayNodeApprovalState.Loading,
|
||||
nodeCapabilityApprovalState: GatewayNodeApprovalState,
|
||||
gatewayConnectionProblem: GatewayConnectionProblem? = null,
|
||||
): GatewayRecoveryUiState =
|
||||
when {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -189,7 +189,7 @@ Every lane uploads GitHub artifacts. When `CLAWGRIT_REPORTS_TOKEN` is configured
|
|||
|
||||
## Full Release Validation
|
||||
|
||||
`Full Release Validation` is the manual umbrella workflow for "run everything before release." It accepts a branch, tag, or full commit SHA, dispatches the manual `CI` workflow with that target, dispatches `Plugin Prerelease` for release-only plugin/package/static/Docker proof, and dispatches `OpenClaw Release Checks` for install smoke, package acceptance, cross-OS package checks, QA Lab parity, Matrix, and Telegram lanes. Stable/default runs keep exhaustive live/E2E and Docker release-path coverage behind `run_release_soak=true`; `release_profile=full` forces that soak coverage on so broad advisory validation remains broad. With `rerun_group=all` and `release_profile=full`, it also runs `NPM Telegram Beta E2E` against the `release-package-under-test` artifact from release checks. After publishing, pass `release_package_spec` to reuse the shipped npm package across release checks, Package Acceptance, Docker, cross-OS, and Telegram without rebuilding. Use `npm_telegram_package_spec` only when Telegram must prove a different package. The Codex plugin live package lane uses the same selected state by default: published `release_package_spec=openclaw@<tag>` derives `codex_plugin_spec=npm:@openclaw/codex@<tag>`, while SHA/artifact runs pack `extensions/codex` from the selected ref. Set `codex_plugin_spec` explicitly for custom plugin sources such as `npm:`, `npm-pack:`, or `git:` specs.
|
||||
`Full Release Validation` is the manual umbrella workflow for "run everything before release." It accepts a branch, tag, or full commit SHA, dispatches the manual `CI` workflow with that target, dispatches `Plugin Prerelease` for release-only plugin/package/static/Docker proof, and dispatches `OpenClaw Release Checks` for install smoke, package acceptance, cross-OS package checks, QA Lab parity, Matrix, and Telegram lanes. Stable and full profiles always include exhaustive live/E2E and Docker release-path soak coverage; the beta profile can opt in with `run_release_soak=true`. With `rerun_group=all` and `release_profile=full`, it also runs `NPM Telegram Beta E2E` against the `release-package-under-test` artifact from release checks. After publishing, pass `release_package_spec` to reuse the shipped npm package across release checks, Package Acceptance, Docker, cross-OS, and Telegram without rebuilding. Use `npm_telegram_package_spec` only when Telegram must prove a different package. The Codex plugin live package lane uses the same selected state by default: published `release_package_spec=openclaw@<tag>` derives `codex_plugin_spec=npm:@openclaw/codex@<tag>`, while SHA/artifact runs pack `extensions/codex` from the selected ref. Set `codex_plugin_spec` explicitly for custom plugin sources such as `npm:`, `npm-pack:`, or `git:` specs.
|
||||
|
||||
See [Full release validation](/reference/full-release-validation) for the
|
||||
stage matrix, exact workflow job names, profile differences, artifacts, and
|
||||
|
|
@ -232,9 +232,9 @@ different SHA.
|
|||
|
||||
`release_profile` controls live/provider breadth passed into release checks. The
|
||||
manual release workflows default to `stable`; use `full` only when you
|
||||
intentionally want the broad advisory provider/media matrix. `run_release_soak`
|
||||
controls whether stable/default release checks run the exhaustive live/E2E and
|
||||
Docker release-path soak; `full` forces soak on.
|
||||
intentionally want the broad advisory provider/media matrix. Stable and full
|
||||
release checks always run the exhaustive live/E2E and Docker release-path soak;
|
||||
the beta profile can opt in with `run_release_soak=true`.
|
||||
|
||||
- `minimum` keeps the fastest OpenAI/core release-critical lanes.
|
||||
- `stable` adds the stable provider/backend set.
|
||||
|
|
|
|||
|
|
@ -118,8 +118,8 @@ the maintainer-only release runbook.
|
|||
`CHANGELOG.md` section. Stable releases published to npm `latest` become the
|
||||
GitHub latest release; stable maintenance releases kept on npm `beta` are
|
||||
created with GitHub `latest=false`. The workflow also uploads the preflight
|
||||
dependency evidence to the GitHub release as
|
||||
`openclaw-<version>-dependency-evidence.zip` for post-release incident
|
||||
dependency evidence, the full-validation manifest, and postpublish registry
|
||||
verification evidence to the GitHub release for post-release incident
|
||||
response. The publish workflow prints child run IDs immediately, auto-approves
|
||||
release environment gates the workflow token is allowed to approve, summarizes
|
||||
failed child jobs with log tails, closes out the GitHub release and dependency
|
||||
|
|
@ -178,6 +178,27 @@ release state.
|
|||
`OPENCLAW_TESTBOX=1 pnpm check:changed`. Push, then verify `origin/main`
|
||||
contains the shipped version and changelog before calling the stable release
|
||||
done.
|
||||
6. Keep the repository variables `RELEASE_ROLLBACK_DRILL_ID` and
|
||||
`RELEASE_ROLLBACK_DRILL_DATE` current after each private rollback drill.
|
||||
`OpenClaw Stable Main Closeout` starts from the `main` push that carries the
|
||||
shipped version, changelog, and appcast after stable publication. It reads
|
||||
immutable postpublish evidence to bind the shipped tag to its Full Release
|
||||
Validation and Publish runs, then verifies the stable main state, release,
|
||||
mandatory stable soak, and blocking performance evidence. It attaches an
|
||||
immutable closeout manifest and checksum to the GitHub release. The automatic
|
||||
push trigger skips legacy releases that predate immutable postpublish
|
||||
evidence; it never treats that skip as a completed closeout. A complete
|
||||
closeout requires both assets and a matching checksum. A partial manifest
|
||||
replays its recorded `main` SHA and rollback drill to regenerate identical
|
||||
bytes, then attaches the missing checksum; an invalid pair, or a checksum
|
||||
without a manifest, stays blocking. A missing or more-than-90-day-old drill
|
||||
record blocks a new evidence-backed closeout; private recovery commands
|
||||
remain in the maintainer-only runbook. Use manual dispatch only to repair or
|
||||
replay an evidence-backed stable closeout.
|
||||
A legacy fallback correction tag may reuse base-package evidence only when
|
||||
the correction tag resolves to the same source commit as the base stable tag.
|
||||
A correction with different source must publish and verify its own package
|
||||
evidence.
|
||||
|
||||
## Release preflight
|
||||
|
||||
|
|
@ -205,9 +226,9 @@ release state.
|
|||
kick off all pre-release test boxes from one entrypoint. It accepts a branch,
|
||||
tag, or full commit SHA, dispatches manual `CI`, and dispatches
|
||||
`OpenClaw Release Checks` for install smoke, package acceptance, cross-OS
|
||||
package checks, QA Lab parity, Matrix, and Telegram lanes. Stable/default runs
|
||||
keep exhaustive live/E2E and Docker release-path soak behind
|
||||
`run_release_soak=true`; `release_profile=full` forces soak on. With
|
||||
package checks, QA Lab parity, Matrix, and Telegram lanes. Stable and full
|
||||
runs always include exhaustive live/E2E and Docker release-path soak;
|
||||
`run_release_soak=true` is retained for an explicit beta soak. With
|
||||
`release_profile=full` and `rerun_group=all`, it also runs package Telegram
|
||||
E2E against the `release-package-under-test` artifact from release checks.
|
||||
Provide `release_package_spec` after publishing a beta to reuse the shipped
|
||||
|
|
@ -472,13 +493,12 @@ Use `release_profile` to select live/provider breadth:
|
|||
- `stable`: minimum plus stable provider/backend coverage for release approval
|
||||
- `full`: stable plus broad advisory provider/media coverage
|
||||
|
||||
Use `run_release_soak=true` with `stable` when the release-blocking lanes are
|
||||
green and you want the exhaustive live/E2E, Docker release-path, and
|
||||
bounded published upgrade-survivor sweep before promotion. That sweep covers
|
||||
Stable and full validation always run the exhaustive live/E2E, Docker
|
||||
release-path, and bounded published upgrade-survivor sweep before promotion.
|
||||
Use `run_release_soak=true` to request that same sweep for a beta. That sweep covers
|
||||
the latest four stable packages plus pinned `2026.4.23` and `2026.5.2`
|
||||
baselines plus older `2026.4.15` coverage, with duplicate baselines removed and
|
||||
each baseline sharded into its own Docker runner job. `full` implies
|
||||
`run_release_soak=true`.
|
||||
each baseline sharded into its own Docker runner job.
|
||||
|
||||
`OpenClaw Release Checks` uses the trusted workflow ref to resolve the target
|
||||
ref once as `release-package-under-test` and reuses that artifact in cross-OS,
|
||||
|
|
@ -669,7 +689,7 @@ prepared release package artifact, `suite_profile=custom`,
|
|||
configured-auth update restart, live ClawHub skill install, stale plugin dependency cleanup, offline plugin
|
||||
fixtures, plugin update, and Telegram package QA against the same resolved
|
||||
tarball. Blocking release checks use the default latest published package
|
||||
baseline; `run_release_soak=true` or
|
||||
baseline; the beta profile with `run_release_soak=true`, `release_profile=stable`, or
|
||||
`release_profile=full` expands to every stable npm-published baseline from
|
||||
`2026.4.23` through `latest` plus reported-issue fixtures. Use
|
||||
Package Acceptance with `source=npm` for an already shipped candidate,
|
||||
|
|
@ -835,8 +855,8 @@ package cannot ship without every publishable official plugin, including
|
|||
require the resolved commit to be reachable from an OpenClaw branch or
|
||||
release tag.
|
||||
- `run_release_soak`: opt into exhaustive live/E2E, Docker release-path, and
|
||||
all-since upgrade-survivor soak on stable/default release checks. It is forced
|
||||
on by `release_profile=full`.
|
||||
all-since upgrade-survivor soak for beta release checks. It is forced on by
|
||||
`release_profile=stable` and `release_profile=full`.
|
||||
|
||||
Rules:
|
||||
|
||||
|
|
|
|||
|
|
@ -27,10 +27,10 @@ Child workflows use the trusted workflow ref for the harness and the input
|
|||
`ref` for the candidate under test. That keeps new validation logic available
|
||||
when validating an older release branch or tag.
|
||||
|
||||
By default, `release_profile=stable` runs the release-blocking lanes and skips
|
||||
the exhaustive live/Docker soak. Pass `run_release_soak=true` to include the
|
||||
soak lanes on a stable run. `release_profile=full` always enables soak lanes so
|
||||
the broad advisory profile never drops coverage silently.
|
||||
`release_profile=stable` and `release_profile=full` always run the exhaustive
|
||||
live/Docker soak. Pass `run_release_soak=true` to include the same soak lanes
|
||||
with the beta profile. Stable publication rejects a validation manifest without this
|
||||
soak and blocking product-performance evidence.
|
||||
|
||||
Package Acceptance normally builds the candidate tarball from the resolved
|
||||
`ref`, including full-SHA runs dispatched with `pnpm ci:full-release`. After a
|
||||
|
|
@ -47,15 +47,15 @@ that plugin, then runs Codex CLI preflight and same-session OpenAI agent turns.
|
|||
|
||||
## Top-level stages
|
||||
|
||||
| Stage | Details |
|
||||
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Target resolution | **Job:** `Resolve target ref`<br />**Child workflow:** none<br />**Proves:** resolves the release branch, tag, or full commit SHA and records selected inputs.<br />**Rerun:** rerun the umbrella if this fails. |
|
||||
| Vitest and normal CI | **Job:** `Run normal full CI`<br />**Child workflow:** `CI`<br />**Proves:** manual full CI graph against the target ref, including Linux Node lanes, bundled plugin shards, plugin and channel contract shards, Node 22 compatibility, `check-*`, `check-additional-*`, built-artifact smoke checks, docs checks, Python skills, Windows, macOS, Control UI i18n, and Android via the umbrella.<br />**Rerun:** `rerun_group=ci`. |
|
||||
| Plugin prerelease | **Job:** `Run plugin prerelease validation`<br />**Child workflow:** `Plugin Prerelease`<br />**Proves:** release-only plugin static checks, agentic plugin coverage, full extension batch shards, plugin prerelease Docker lanes, and a non-blocking `plugin-inspector-advisory` artifact for compatibility triage.<br />**Rerun:** `rerun_group=plugin-prerelease`. |
|
||||
| Release checks | **Job:** `Run release/live/Docker/QA validation`<br />**Child workflow:** `OpenClaw Release Checks`<br />**Proves:** install smoke, cross-OS package checks, Package Acceptance, QA Lab parity, live Matrix, and live Telegram. With `run_release_soak=true` or `release_profile=full`, also runs exhaustive live/E2E suites and Docker release-path chunks.<br />**Rerun:** `rerun_group=release-checks` or a narrower release-checks handle. |
|
||||
| Package artifact | **Job:** `Prepare release package artifact`<br />**Child workflow:** none<br />**Proves:** creates the parent `release-package-under-test` tarball early enough for package-facing checks that do not need to wait for `OpenClaw Release Checks`.<br />**Rerun:** rerun the umbrella or provide `release_package_spec` for published-package reruns. |
|
||||
| Package Telegram | **Job:** `Run package Telegram E2E`<br />**Child workflow:** `NPM Telegram Beta E2E`<br />**Proves:** parent-artifact-backed Telegram package proof for `rerun_group=all` with `release_profile=full`, or published-package Telegram proof when `release_package_spec` or `npm_telegram_package_spec` is set.<br />**Rerun:** `rerun_group=npm-telegram` with `release_package_spec` or `npm_telegram_package_spec`. |
|
||||
| Umbrella verifier | **Job:** `Verify full validation`<br />**Child workflow:** none<br />**Proves:** re-checks recorded child run conclusions and appends slowest-job tables from child workflows.<br />**Rerun:** rerun only this job after rerunning a failed child to green. |
|
||||
| Stage | Details |
|
||||
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| Target resolution | **Job:** `Resolve target ref`<br />**Child workflow:** none<br />**Proves:** resolves the release branch, tag, or full commit SHA and records selected inputs.<br />**Rerun:** rerun the umbrella if this fails. |
|
||||
| Vitest and normal CI | **Job:** `Run normal full CI`<br />**Child workflow:** `CI`<br />**Proves:** manual full CI graph against the target ref, including Linux Node lanes, bundled plugin shards, plugin and channel contract shards, Node 22 compatibility, `check-*`, `check-additional-*`, built-artifact smoke checks, docs checks, Python skills, Windows, macOS, Control UI i18n, and Android via the umbrella.<br />**Rerun:** `rerun_group=ci`. |
|
||||
| Plugin prerelease | **Job:** `Run plugin prerelease validation`<br />**Child workflow:** `Plugin Prerelease`<br />**Proves:** release-only plugin static checks, agentic plugin coverage, full extension batch shards, plugin prerelease Docker lanes, and a non-blocking `plugin-inspector-advisory` artifact for compatibility triage.<br />**Rerun:** `rerun_group=plugin-prerelease`. |
|
||||
| Release checks | **Job:** `Run release/live/Docker/QA validation`<br />**Child workflow:** `OpenClaw Release Checks`<br />**Proves:** install smoke, cross-OS package checks, Package Acceptance, QA Lab parity, live Matrix, and live Telegram. Stable and full profiles also run exhaustive live/E2E suites and Docker release-path chunks; beta can opt in with `run_release_soak=true`.<br />**Rerun:** `rerun_group=release-checks` or a narrower release-checks handle. |
|
||||
| Package artifact | **Job:** `Prepare release package artifact`<br />**Child workflow:** none<br />**Proves:** creates the parent `release-package-under-test` tarball early enough for package-facing checks that do not need to wait for `OpenClaw Release Checks`.<br />**Rerun:** rerun the umbrella or provide `release_package_spec` for published-package reruns. |
|
||||
| Package Telegram | **Job:** `Run package Telegram E2E`<br />**Child workflow:** `NPM Telegram Beta E2E`<br />**Proves:** parent-artifact-backed Telegram package proof for `rerun_group=all` with `release_profile=full`, or published-package Telegram proof when `release_package_spec` or `npm_telegram_package_spec` is set.<br />**Rerun:** `rerun_group=npm-telegram` with `release_package_spec` or `npm_telegram_package_spec`. |
|
||||
| Umbrella verifier | **Job:** `Verify full validation`<br />**Child workflow:** none<br />**Proves:** re-checks recorded child run conclusions and appends slowest-job tables from child workflows.<br />**Rerun:** rerun only this job after rerunning a failed child to green. |
|
||||
|
||||
For `ref=main` and `rerun_group=all`, a newer umbrella supersedes an older one.
|
||||
When the parent is cancelled, its monitor cancels any child workflow it already
|
||||
|
|
@ -105,11 +105,11 @@ commands with package artifact and image reuse inputs when available.
|
|||
|
||||
`release_profile` mostly controls live/provider breadth inside release checks.
|
||||
It does not remove normal full CI, Plugin Prerelease, install smoke, package
|
||||
acceptance, or QA Lab. For `stable`, exhaustive repo/live E2E and Docker
|
||||
release-path chunks are soak coverage and run when `run_release_soak=true`.
|
||||
`full` forces soak coverage on and also makes the umbrella run package Telegram
|
||||
E2E against the parent release package artifact when `rerun_group=all`, so a full
|
||||
pre-publish candidate does not silently skip that Telegram package lane.
|
||||
acceptance, or QA Lab. Stable and full profiles always run exhaustive repo/live
|
||||
E2E and Docker release-path soak coverage. The beta profile can opt in with
|
||||
`run_release_soak=true`. The full profile also makes the umbrella run package
|
||||
Telegram E2E against the parent release package artifact when `rerun_group=all`,
|
||||
so a full pre-publish candidate does not silently skip that Telegram package lane.
|
||||
|
||||
| Profile | Intended use | Included live/provider coverage |
|
||||
| --------- | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
|
|
|
|||
6
extensions/llama-cpp/npm-shrinkwrap.json
generated
6
extensions/llama-cpp/npm-shrinkwrap.json
generated
|
|
@ -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"
|
||||
|
|
|
|||
6
extensions/msteams/npm-shrinkwrap.json
generated
6
extensions/msteams/npm-shrinkwrap.json
generated
|
|
@ -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"
|
||||
|
|
|
|||
6
extensions/slack/npm-shrinkwrap.json
generated
6
extensions/slack/npm-shrinkwrap.json
generated
|
|
@ -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"
|
||||
|
|
|
|||
6
extensions/zalouser/npm-shrinkwrap.json
generated
6
extensions/zalouser/npm-shrinkwrap.json
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -1982,6 +1982,7 @@
|
|||
"oxlint-tsgolint": "0.23.0",
|
||||
"shiki": "4.1.0",
|
||||
"signal-utils": "0.21.1",
|
||||
"sigstore": "4.1.1",
|
||||
"tsdown": "0.22.1",
|
||||
"tsx": "4.22.3",
|
||||
"unrun": "0.3.0",
|
||||
|
|
|
|||
293
pnpm-lock.yaml
generated
293
pnpm-lock.yaml
generated
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
194
scripts/lib/stable-release-closeout.mjs
Normal file
194
scripts/lib/stable-release-closeout.mjs
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
import { createHash } from "node:crypto";
|
||||
|
||||
const STABLE_RELEASE_TAG_RE = /^v(?<version>\d{4}\.\d{1,2}\.\d{1,2})(?:-\d+)?$/u;
|
||||
const MAX_ROLLBACK_DRILL_AGE_MS = 90 * 24 * 60 * 60 * 1000;
|
||||
|
||||
function parseStableReleaseTagDetails(tag) {
|
||||
const match = STABLE_RELEASE_TAG_RE.exec(tag);
|
||||
if (!match?.groups?.version) {
|
||||
throw new Error(`expected a stable release tag, got ${tag}`);
|
||||
}
|
||||
return {
|
||||
baseVersion: match.groups.version,
|
||||
tagVersion: tag.slice(1),
|
||||
};
|
||||
}
|
||||
|
||||
function escapeRegExp(value) {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
||||
}
|
||||
|
||||
function sha256(value) {
|
||||
return createHash("sha256").update(value).digest("hex");
|
||||
}
|
||||
|
||||
export function parseStableReleaseTag(tag) {
|
||||
return parseStableReleaseTagDetails(tag).baseVersion;
|
||||
}
|
||||
|
||||
export function extractStableChangelogSection(changelog, version) {
|
||||
const heading = new RegExp(`^## ${escapeRegExp(version)}\\n`, "mu").exec(changelog);
|
||||
if (!heading || heading.index === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const section = changelog.slice(heading.index);
|
||||
const nextHeading = section.slice(heading[0].length).search(/^## /mu);
|
||||
return (
|
||||
nextHeading === -1 ? section : section.slice(0, heading[0].length + nextHeading)
|
||||
).trimEnd();
|
||||
}
|
||||
|
||||
function readVersion(packageJson, label, errors) {
|
||||
const value = packageJson?.version;
|
||||
if (typeof value !== "string" || value.length === 0) {
|
||||
errors.push(`${label} package.json is missing a version.`);
|
||||
return "";
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function readReleaseAssets(release) {
|
||||
return Array.isArray(release?.assets)
|
||||
? release.assets.filter((asset) => asset && typeof asset.name === "string")
|
||||
: [];
|
||||
}
|
||||
|
||||
function isCloseoutEvidenceAsset(assetName, tag) {
|
||||
const releaseVersion = tag.slice(1);
|
||||
return (
|
||||
assetName === `openclaw-${releaseVersion}-stable-main-closeout.json` ||
|
||||
assetName === `openclaw-${releaseVersion}-stable-main-closeout.json.sha256`
|
||||
);
|
||||
}
|
||||
|
||||
function parseRollbackDrillDate(value) {
|
||||
if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}$/u.test(value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parsed = new Date(`${value}T00:00:00.000Z`);
|
||||
return Number.isFinite(parsed.getTime()) && parsed.toISOString().slice(0, 10) === value
|
||||
? parsed.getTime()
|
||||
: null;
|
||||
}
|
||||
|
||||
function verifyRollbackDrill(params, errors) {
|
||||
if (!params.rollbackDrillId?.trim()) {
|
||||
errors.push("rollback drill id is required.");
|
||||
}
|
||||
|
||||
const drillDateMs = parseRollbackDrillDate(params.rollbackDrillDate);
|
||||
if (drillDateMs === null) {
|
||||
errors.push(`rollback drill date is invalid: ${params.rollbackDrillDate ?? "<missing>"}.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const ageMs = params.nowMs - drillDateMs;
|
||||
if (ageMs < 0) {
|
||||
errors.push(`rollback drill date is in the future: ${params.rollbackDrillDate}.`);
|
||||
} else if (!params.allowStaleRollbackDrill && ageMs > MAX_ROLLBACK_DRILL_AGE_MS) {
|
||||
errors.push(
|
||||
`rollback drill is older than 90 days: ${params.rollbackDrillDate}. Run the private rollback drill before stable closeout.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function verifyStableMainCloseout(params) {
|
||||
const { baseVersion, tagVersion } = parseStableReleaseTagDetails(params.tag);
|
||||
const errors = [];
|
||||
const mainVersion = readVersion(params.mainPackageJson, "main", errors);
|
||||
const tagPackageVersion = readVersion(params.tagPackageJson, "release tag", errors);
|
||||
const fallbackCorrection =
|
||||
tagVersion !== baseVersion && mainVersion === baseVersion && tagPackageVersion === baseVersion;
|
||||
const version = fallbackCorrection ? baseVersion : tagVersion;
|
||||
|
||||
if (mainVersion && mainVersion !== version) {
|
||||
errors.push(
|
||||
`main package.json version is ${mainVersion}, expected shipped version ${version}.`,
|
||||
);
|
||||
}
|
||||
if (tagPackageVersion && tagPackageVersion !== version) {
|
||||
errors.push(
|
||||
`release tag package.json version is ${tagPackageVersion}, expected shipped version ${version}.`,
|
||||
);
|
||||
}
|
||||
|
||||
const mainChangelog = extractStableChangelogSection(params.mainChangelog, version);
|
||||
const tagChangelog = extractStableChangelogSection(params.tagChangelog, version);
|
||||
if (!mainChangelog) {
|
||||
errors.push(`main CHANGELOG.md is missing the ## ${version} section.`);
|
||||
}
|
||||
if (!tagChangelog) {
|
||||
errors.push(`release tag CHANGELOG.md is missing the ## ${version} section.`);
|
||||
}
|
||||
if (mainChangelog && tagChangelog && mainChangelog !== tagChangelog) {
|
||||
errors.push(
|
||||
`main CHANGELOG.md ## ${version} does not exactly match the shipped release section.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (params.release?.tagName !== params.tag) {
|
||||
errors.push(
|
||||
`GitHub release tag is ${String(params.release?.tagName ?? "<missing>")}, expected ${params.tag}.`,
|
||||
);
|
||||
}
|
||||
if (params.release?.isDraft === true) {
|
||||
errors.push(`GitHub release ${params.tag} is still a draft.`);
|
||||
}
|
||||
if (params.release?.isPrerelease === true) {
|
||||
errors.push(`GitHub release ${params.tag} is marked as a prerelease.`);
|
||||
}
|
||||
|
||||
const macAssetVersion = version;
|
||||
const expectedMacAssets = [
|
||||
`OpenClaw-${macAssetVersion}.zip`,
|
||||
`OpenClaw-${macAssetVersion}.dmg`,
|
||||
`OpenClaw-${macAssetVersion}.dSYM.zip`,
|
||||
];
|
||||
const assetNames = new Set(readReleaseAssets(params.release).map((asset) => asset.name));
|
||||
const missingMacAssets = expectedMacAssets.filter((asset) => !assetNames.has(asset));
|
||||
if (missingMacAssets.length > 0) {
|
||||
errors.push(
|
||||
`GitHub release ${params.tag} is missing required macOS asset(s): ${missingMacAssets.join(", ")}.`,
|
||||
);
|
||||
} else {
|
||||
const macZip = expectedMacAssets[0];
|
||||
if (!params.mainAppcast.includes(`/releases/download/${params.tag}/${macZip}`)) {
|
||||
errors.push(`main appcast.xml does not point at ${macZip} from ${params.tag}.`);
|
||||
}
|
||||
}
|
||||
|
||||
verifyRollbackDrill(params, errors);
|
||||
|
||||
if (errors.length > 0) {
|
||||
return { errors, manifest: null };
|
||||
}
|
||||
|
||||
return {
|
||||
errors,
|
||||
manifest: {
|
||||
version: 1,
|
||||
releaseTag: params.tag,
|
||||
releaseVersion: version,
|
||||
releaseTagSha: params.releaseTagSha,
|
||||
mainSha: params.mainSha,
|
||||
mainPackageVersion: mainVersion,
|
||||
releaseTagPackageVersion: tagPackageVersion,
|
||||
changelogSha256: sha256(mainChangelog),
|
||||
appcastSha256: sha256(params.mainAppcast),
|
||||
fullReleaseValidationRunId: params.fullReleaseValidationRunId,
|
||||
releasePublishRunId: params.releasePublishRunId,
|
||||
rollbackDrill: {
|
||||
id: params.rollbackDrillId,
|
||||
date: params.rollbackDrillDate,
|
||||
},
|
||||
githubReleaseAssets: readReleaseAssets(params.release)
|
||||
.filter((asset) => !isCloseoutEvidenceAsset(asset.name, params.tag))
|
||||
.map((asset) => ({
|
||||
name: asset.name,
|
||||
digest: typeof asset.digest === "string" ? asset.digest : null,
|
||||
})),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
#!/usr/bin/env -S node --import tsx
|
||||
// Openclaw Npm Postpublish Verify script supports OpenClaw repository automation.
|
||||
|
||||
import { createPublicKey, verify as verifySignature } from "node:crypto";
|
||||
import {
|
||||
existsSync,
|
||||
lstatSync,
|
||||
|
|
@ -23,6 +24,7 @@ import {
|
|||
win32 as pathWin32,
|
||||
} from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { verify as verifySigstoreBundle } from "sigstore";
|
||||
import { formatErrorMessage } from "../src/infra/errors.ts";
|
||||
import { BUNDLED_RUNTIME_SIDECAR_PATHS } from "../src/plugins/runtime-sidecar-paths.ts";
|
||||
import { listBundledPluginPackArtifacts } from "./lib/bundled-plugin-build-entries.mjs";
|
||||
|
|
@ -123,6 +125,226 @@ export function buildPublishedInstallScenarios(version: string): PublishedInstal
|
|||
return scenarios;
|
||||
}
|
||||
|
||||
type NpmRegistryKey = {
|
||||
key: string;
|
||||
keyid: string;
|
||||
};
|
||||
|
||||
type NpmRegistrySignature = {
|
||||
keyid: string;
|
||||
sig: string;
|
||||
};
|
||||
|
||||
type NpmRegistryAttestation = {
|
||||
bundle?: {
|
||||
dsseEnvelope?: {
|
||||
payload?: string;
|
||||
};
|
||||
};
|
||||
predicateType?: string;
|
||||
};
|
||||
|
||||
type NpmProvenanceVerificationPolicy = {
|
||||
certificateIdentityURI: string;
|
||||
certificateIssuer: string;
|
||||
};
|
||||
|
||||
type VerifyNpmProvenanceBundle = (
|
||||
bundle: unknown,
|
||||
policy: NpmProvenanceVerificationPolicy,
|
||||
) => Promise<void>;
|
||||
|
||||
type NpmProvenanceStatement = {
|
||||
predicate?: {
|
||||
buildDefinition?: {
|
||||
externalParameters?: {
|
||||
workflow?: {
|
||||
path?: string;
|
||||
ref?: string;
|
||||
repository?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
runDetails?: {
|
||||
builder?: {
|
||||
id?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
subject?: Array<{
|
||||
digest?: Record<string, string>;
|
||||
name?: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
const NPM_PROVENANCE_PREDICATE_TYPE = "https://slsa.dev/provenance/v1";
|
||||
const NPM_PROVENANCE_REPOSITORY = "https://github.com/openclaw/openclaw";
|
||||
const NPM_PROVENANCE_WORKFLOW_PATH = ".github/workflows/openclaw-npm-release.yml";
|
||||
const NPM_PROVENANCE_CERTIFICATE_ISSUER = "https://token.actions.githubusercontent.com";
|
||||
const NPM_PROVENANCE_BUILDER_ID = "https://github.com/actions/runner/github-hosted";
|
||||
const NPM_REGISTRY_REQUEST_TIMEOUT_MS = 30_000;
|
||||
const NPM_REGISTRY_PROVENANCE_ATTEMPTS = 30;
|
||||
const NPM_REGISTRY_PROVENANCE_RETRY_MAX_DELAY_MS = 10_000;
|
||||
|
||||
export function verifyNpmRegistrySignatures(params: {
|
||||
integrity: string;
|
||||
keys: NpmRegistryKey[];
|
||||
packageName: string;
|
||||
signatures: NpmRegistrySignature[];
|
||||
version: string;
|
||||
}): void {
|
||||
if (!params.integrity.startsWith("sha512-")) {
|
||||
throw new Error(`npm registry integrity is missing a sha512 digest for ${params.packageName}.`);
|
||||
}
|
||||
if (params.signatures.length === 0) {
|
||||
throw new Error(
|
||||
`npm registry returned no signatures for ${params.packageName}@${params.version}.`,
|
||||
);
|
||||
}
|
||||
|
||||
const payload = `${params.packageName}@${params.version}:${params.integrity}`;
|
||||
for (const signature of params.signatures) {
|
||||
const key = params.keys.find((candidate) => candidate.keyid === signature.keyid);
|
||||
if (!key) {
|
||||
continue;
|
||||
}
|
||||
const publicKey = createPublicKey({
|
||||
key: Buffer.from(key.key, "base64"),
|
||||
format: "der",
|
||||
type: "spki",
|
||||
});
|
||||
if (
|
||||
verifySignature(
|
||||
"sha256",
|
||||
Buffer.from(payload, "utf8"),
|
||||
publicKey,
|
||||
Buffer.from(signature.sig, "base64"),
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`npm registry signatures did not verify for ${params.packageName}@${params.version}.`,
|
||||
);
|
||||
}
|
||||
|
||||
function resolveNpmProvenanceVerificationPolicy(
|
||||
statement: NpmProvenanceStatement,
|
||||
version: string,
|
||||
): NpmProvenanceVerificationPolicy {
|
||||
const parsedVersion = parseReleaseVersion(version);
|
||||
if (parsedVersion === null) {
|
||||
throw new Error(`Unsupported release version "${version}".`);
|
||||
}
|
||||
const workflow = statement.predicate?.buildDefinition?.externalParameters?.workflow;
|
||||
const workflowRef = workflow?.ref;
|
||||
const expectedReleaseRef = `refs/heads/release/${parsedVersion.baseVersion}`;
|
||||
const isTrustedRef =
|
||||
workflowRef === "refs/heads/main" ||
|
||||
workflowRef === expectedReleaseRef ||
|
||||
(parsedVersion.channel === "alpha" &&
|
||||
/^refs\/heads\/tideclaw\/alpha\/[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{4}Z$/u.test(
|
||||
workflowRef ?? "",
|
||||
));
|
||||
|
||||
if (
|
||||
workflow?.repository !== NPM_PROVENANCE_REPOSITORY ||
|
||||
workflow?.path !== NPM_PROVENANCE_WORKFLOW_PATH ||
|
||||
!isTrustedRef ||
|
||||
statement.predicate?.runDetails?.builder?.id !== NPM_PROVENANCE_BUILDER_ID
|
||||
) {
|
||||
throw new Error(
|
||||
`npm provenance attestation does not bind ${version} to the trusted OpenClaw GitHub release workflow.`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
certificateIssuer: NPM_PROVENANCE_CERTIFICATE_ISSUER,
|
||||
certificateIdentityURI: `${NPM_PROVENANCE_REPOSITORY}/${NPM_PROVENANCE_WORKFLOW_PATH}@${workflowRef}`,
|
||||
};
|
||||
}
|
||||
|
||||
async function verifySigstoreNpmProvenanceBundle(
|
||||
bundle: unknown,
|
||||
policy: NpmProvenanceVerificationPolicy,
|
||||
): Promise<void> {
|
||||
await verifySigstoreBundle(bundle as Parameters<typeof verifySigstoreBundle>[0], policy);
|
||||
}
|
||||
|
||||
export async function verifyNpmProvenanceAttestation(params: {
|
||||
attestations: NpmRegistryAttestation[];
|
||||
integrity: string;
|
||||
packageName: string;
|
||||
verifyBundle?: VerifyNpmProvenanceBundle;
|
||||
version: string;
|
||||
}): Promise<void> {
|
||||
const expectedSubject = `pkg:npm/${params.packageName}@${params.version}`;
|
||||
const expectedSha512 = Buffer.from(params.integrity.slice("sha512-".length), "base64").toString(
|
||||
"hex",
|
||||
);
|
||||
const verifyBundle = params.verifyBundle ?? verifySigstoreNpmProvenanceBundle;
|
||||
let verificationError: unknown;
|
||||
let policyError: unknown;
|
||||
|
||||
for (const attestation of params.attestations) {
|
||||
if (attestation.predicateType !== NPM_PROVENANCE_PREDICATE_TYPE) {
|
||||
continue;
|
||||
}
|
||||
const payload = attestation.bundle?.dsseEnvelope?.payload;
|
||||
if (!payload) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const statement = JSON.parse(
|
||||
Buffer.from(payload, "base64").toString("utf8"),
|
||||
) as NpmProvenanceStatement;
|
||||
if (
|
||||
statement.subject?.some(
|
||||
(subject) =>
|
||||
subject.name === expectedSubject && subject.digest?.sha512 === expectedSha512,
|
||||
)
|
||||
) {
|
||||
let policy: NpmProvenanceVerificationPolicy;
|
||||
try {
|
||||
policy = resolveNpmProvenanceVerificationPolicy(statement, params.version);
|
||||
} catch (error) {
|
||||
policyError = error;
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
await verifyBundle(attestation.bundle, policy);
|
||||
return;
|
||||
} catch (error) {
|
||||
verificationError = error;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Try the remaining attestations before reporting the missing match.
|
||||
}
|
||||
}
|
||||
|
||||
if (verificationError) {
|
||||
throw new Error(
|
||||
`npm provenance attestation failed Sigstore verification for ${params.packageName}@${params.version}: ${formatErrorMessage(verificationError)}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (policyError instanceof Error) {
|
||||
throw policyError;
|
||||
}
|
||||
if (policyError) {
|
||||
throw new Error(
|
||||
`npm provenance attestation policy evaluation failed for ${params.packageName}@${params.version}: ${formatErrorMessage(policyError)}`,
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`npm provenance attestation does not match ${params.packageName}@${params.version} and its registry integrity.`,
|
||||
);
|
||||
}
|
||||
|
||||
export function collectInstalledPackageErrors(params: {
|
||||
expectedVersion: string;
|
||||
installedVersion: string;
|
||||
|
|
@ -698,6 +920,149 @@ function installSpec(prefixDir: string, spec: string, cwd: string): void {
|
|||
npmExec(buildPublishedInstallCommandArgs(prefixDir, spec), cwd);
|
||||
}
|
||||
|
||||
async function fetchRegistryJson(url: string): Promise<unknown> {
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
},
|
||||
redirect: "error",
|
||||
signal: AbortSignal.timeout(NPM_REGISTRY_REQUEST_TIMEOUT_MS),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`npm registry request failed (${response.status}): ${url}`);
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
function isRetryableRegistryProvenanceError(error: unknown): boolean {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return (
|
||||
/npm registry request failed \((?:404|408|425|429|5\d\d)\)/u.test(message) ||
|
||||
message.includes("npm registry metadata is incomplete") ||
|
||||
message.includes("npm registry provenance metadata is incomplete") ||
|
||||
/aborted|fetch failed|network|timeout|timed out/u.test(message)
|
||||
);
|
||||
}
|
||||
|
||||
export async function retryNpmRegistryProvenanceRead<T>(
|
||||
read: () => Promise<T>,
|
||||
options: {
|
||||
attempts?: number;
|
||||
delay?: (delayMs: number) => Promise<void>;
|
||||
} = {},
|
||||
): Promise<T> {
|
||||
const attempts = options.attempts ?? NPM_REGISTRY_PROVENANCE_ATTEMPTS;
|
||||
const delay =
|
||||
options.delay ??
|
||||
((delayMs: number) =>
|
||||
new Promise<void>((resolveDelay) => {
|
||||
setTimeout(resolveDelay, delayMs);
|
||||
}));
|
||||
let lastError: unknown;
|
||||
|
||||
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
||||
try {
|
||||
return await read();
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
if (!isRetryableRegistryProvenanceError(error) || attempt === attempts) {
|
||||
throw error;
|
||||
}
|
||||
await delay(Math.min(attempt * 1000, NPM_REGISTRY_PROVENANCE_RETRY_MAX_DELAY_MS));
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
async function verifyPublishedRegistryProvenanceOnce(version: string): Promise<void> {
|
||||
const registry = new URL(process.env.npm_config_registry ?? "https://registry.npmjs.org");
|
||||
if (registry.protocol !== "https:") {
|
||||
throw new Error(`npm registry must use HTTPS: ${registry}`);
|
||||
}
|
||||
if (!registry.pathname.endsWith("/")) {
|
||||
registry.pathname = `${registry.pathname}/`;
|
||||
}
|
||||
const packageName = "openclaw";
|
||||
const packageDocument = (await fetchRegistryJson(
|
||||
new URL(
|
||||
`${encodeURIComponent(packageName)}/${encodeURIComponent(version)}`,
|
||||
registry,
|
||||
).toString(),
|
||||
)) as {
|
||||
dist?: {
|
||||
attestations?: {
|
||||
provenance?: {
|
||||
predicateType?: string;
|
||||
};
|
||||
url?: string;
|
||||
};
|
||||
integrity?: string;
|
||||
signatures?: NpmRegistrySignature[];
|
||||
};
|
||||
};
|
||||
const keysDocument = (await fetchRegistryJson(new URL("-/npm/v1/keys", registry).toString())) as {
|
||||
keys?: NpmRegistryKey[];
|
||||
};
|
||||
const integrity = packageDocument.dist?.integrity;
|
||||
const signatures = packageDocument.dist?.signatures;
|
||||
const provenance = packageDocument.dist?.attestations?.provenance;
|
||||
const attestationUrl = packageDocument.dist?.attestations?.url;
|
||||
if (!integrity || !signatures || !keysDocument.keys) {
|
||||
throw new Error(`npm registry metadata is incomplete for ${packageName}@${version}.`);
|
||||
}
|
||||
if (
|
||||
provenance?.predicateType !== NPM_PROVENANCE_PREDICATE_TYPE ||
|
||||
typeof attestationUrl !== "string" ||
|
||||
attestationUrl.length === 0
|
||||
) {
|
||||
throw new Error(
|
||||
`npm registry provenance metadata is incomplete for ${packageName}@${version}.`,
|
||||
);
|
||||
}
|
||||
const parsedAttestationUrl = new URL(attestationUrl);
|
||||
const attestationPathPrefix = new URL("-/npm/v1/attestations/", registry).pathname;
|
||||
if (
|
||||
parsedAttestationUrl.protocol !== "https:" ||
|
||||
parsedAttestationUrl.origin !== registry.origin ||
|
||||
!parsedAttestationUrl.pathname.startsWith(attestationPathPrefix)
|
||||
) {
|
||||
throw new Error(
|
||||
`npm registry returned an untrusted provenance attestation URL for ${packageName}@${version}.`,
|
||||
);
|
||||
}
|
||||
|
||||
verifyNpmRegistrySignatures({
|
||||
packageName,
|
||||
version,
|
||||
integrity,
|
||||
signatures,
|
||||
keys: keysDocument.keys,
|
||||
});
|
||||
const attestationDocument = (await fetchRegistryJson(parsedAttestationUrl.toString())) as {
|
||||
attestations?: NpmRegistryAttestation[];
|
||||
};
|
||||
const attestations = attestationDocument.attestations ?? [];
|
||||
if (attestations.length === 0) {
|
||||
throw new Error(
|
||||
`npm registry provenance metadata is incomplete for ${packageName}@${version}.`,
|
||||
);
|
||||
}
|
||||
await verifyNpmProvenanceAttestation({
|
||||
packageName,
|
||||
version,
|
||||
integrity,
|
||||
attestations,
|
||||
});
|
||||
console.log(
|
||||
`openclaw-npm-postpublish-verify: registry signature and provenance attestation verified (${version})`,
|
||||
);
|
||||
}
|
||||
|
||||
async function verifyPublishedRegistryProvenance(version: string): Promise<void> {
|
||||
await retryNpmRegistryProvenanceRead(() => verifyPublishedRegistryProvenanceOnce(version));
|
||||
}
|
||||
|
||||
function readInstalledBinaryVersion(prefixDir: string, cwd: string): string {
|
||||
const invocation = resolveInstalledBinaryCommandInvocation(prefixDir, ["--version"]);
|
||||
return runNpmVerifyCommand(invocation, cwd);
|
||||
|
|
@ -744,7 +1109,7 @@ function verifyScenario(version: string, scenario: PublishedInstallScenario): vo
|
|||
}
|
||||
}
|
||||
|
||||
function main(): void {
|
||||
async function main(): Promise<void> {
|
||||
const version = process.argv[2]?.trim();
|
||||
if (!version) {
|
||||
throw new Error(
|
||||
|
|
@ -753,6 +1118,7 @@ function main(): void {
|
|||
}
|
||||
|
||||
const scenarios = buildPublishedInstallScenarios(version);
|
||||
await verifyPublishedRegistryProvenance(version);
|
||||
for (const scenario of scenarios) {
|
||||
verifyScenario(version, scenario);
|
||||
}
|
||||
|
|
@ -765,7 +1131,7 @@ function main(): void {
|
|||
const entrypoint = process.argv[1] ? pathToFileURL(process.argv[1]).href : null;
|
||||
if (entrypoint !== null && import.meta.url === entrypoint) {
|
||||
try {
|
||||
main();
|
||||
await main();
|
||||
} catch (error) {
|
||||
console.error(`openclaw-npm-postpublish-verify: ${formatErrorMessage(error)}`);
|
||||
process.exitCode = 1;
|
||||
|
|
|
|||
|
|
@ -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]}")"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -146,6 +146,7 @@ prepare_push() {
|
|||
|
||||
local prep_head_sha
|
||||
prep_head_sha=$(git rev-parse HEAD)
|
||||
local local_prep_head_sha
|
||||
|
||||
local lease_sha
|
||||
lease_sha=$(gh pr view "$pr" --json headRefOid --jq .headRefOid)
|
||||
|
|
@ -156,6 +157,24 @@ prepare_push() {
|
|||
# shellcheck disable=SC1090
|
||||
source "$push_result_env"
|
||||
prep_head_sha="$PUSH_PREP_HEAD_SHA"
|
||||
local_prep_head_sha="$PUSH_LOCAL_PREP_HEAD_SHA"
|
||||
local mainline_base_sha
|
||||
mainline_base_sha=$(git merge-base "$local_prep_head_sha" origin/main) || {
|
||||
echo "Unable to resolve the prepared mainline base."
|
||||
exit 1
|
||||
}
|
||||
if [ -s .local/prep-sync.env ]; then
|
||||
# shellcheck disable=SC1091
|
||||
source .local/prep-sync.env
|
||||
local current_prep_tree
|
||||
current_prep_tree=$(git rev-parse "${local_prep_head_sha}^{tree}")
|
||||
if [ "${PREP_SYNC_TREE:-}" != "$current_prep_tree" ] || [ -z "${PREP_SYNC_MAINLINE_BASE_SHA:-}" ]; then
|
||||
echo "Prepared PR head no longer matches the verified sync tree."
|
||||
exit 1
|
||||
fi
|
||||
mainline_base_sha="$PREP_SYNC_MAINLINE_BASE_SHA"
|
||||
rm -f .local/prep-sync.env
|
||||
fi
|
||||
local pushed_from_sha="$PUSHED_FROM_SHA"
|
||||
local pr_head_sha_after="$PR_HEAD_SHA_AFTER_PUSH"
|
||||
|
||||
|
|
@ -173,8 +192,7 @@ prepare_push() {
|
|||
cat >> .local/prep.md <<EOF_PREP
|
||||
- Gates passed and push succeeded to branch $PR_HEAD.
|
||||
- Gate mode: ${GATES_MODE:-unknown}.
|
||||
- Verified PR head SHA matches local prep HEAD.
|
||||
- Verified PR head contains origin/main.
|
||||
- Verified the remote PR head tree matches the local prep head.
|
||||
EOF_PREP
|
||||
|
||||
# Security: shell-escape values to prevent command injection via propagated PR_HEAD.
|
||||
|
|
@ -185,6 +203,8 @@ EOF_PREP
|
|||
PR_HEAD "$PR_HEAD" \
|
||||
PR_HEAD_SHA_BEFORE "$pushed_from_sha" \
|
||||
PREP_HEAD_SHA "$prep_head_sha" \
|
||||
LOCAL_PREP_HEAD_SHA "$local_prep_head_sha" \
|
||||
PREP_MAINLINE_BASE_SHA "$mainline_base_sha" \
|
||||
COAUTHOR_EMAIL "$coauthor_email" \
|
||||
> .local/prep.env
|
||||
|
||||
|
|
@ -217,12 +237,18 @@ prepare_sync_head() {
|
|||
if ! git merge-base --is-ancestor origin/main HEAD; then
|
||||
git rebase origin/main
|
||||
rebased=true
|
||||
prepare_gates "$pr"
|
||||
checkout_prep_branch "$pr"
|
||||
if [ "${OPENCLAW_TESTBOX:-}" = "1" ]; then
|
||||
rm -f .local/gates.env .local/prep.env
|
||||
echo "Rebased head requires fresh exact-head hosted CI/Testbox evidence after push."
|
||||
else
|
||||
prepare_gates "$pr"
|
||||
checkout_prep_branch "$pr"
|
||||
fi
|
||||
fi
|
||||
|
||||
local prep_head_sha
|
||||
prep_head_sha=$(git rev-parse HEAD)
|
||||
local local_prep_head_sha
|
||||
|
||||
local lease_sha
|
||||
lease_sha=$(gh pr view "$pr" --json headRefOid --jq .headRefOid)
|
||||
|
|
@ -233,6 +259,12 @@ prepare_sync_head() {
|
|||
# shellcheck disable=SC1090
|
||||
source "$push_result_env"
|
||||
prep_head_sha="$PUSH_PREP_HEAD_SHA"
|
||||
local_prep_head_sha="$PUSH_LOCAL_PREP_HEAD_SHA"
|
||||
local mainline_base_sha
|
||||
mainline_base_sha=$(git merge-base "$local_prep_head_sha" origin/main) || {
|
||||
echo "Unable to resolve the prepared mainline base."
|
||||
exit 1
|
||||
}
|
||||
local pushed_from_sha="$PUSHED_FROM_SHA"
|
||||
local pr_head_sha_after="$PR_HEAD_SHA_AFTER_PUSH"
|
||||
|
||||
|
|
@ -250,8 +282,28 @@ prepare_sync_head() {
|
|||
cat >> .local/prep.md <<EOF_PREP
|
||||
- Prep head sync completed to branch $PR_HEAD.
|
||||
- Rebased onto origin/main: $rebased.
|
||||
- Verified PR head SHA matches local prep HEAD.
|
||||
- Verified PR head contains origin/main.
|
||||
- Verified the remote PR head tree matches the local prep head.
|
||||
EOF_PREP
|
||||
|
||||
if [ "$rebased" = "true" ] && [ "${OPENCLAW_TESTBOX:-}" = "1" ]; then
|
||||
local prep_sync_tree
|
||||
prep_sync_tree=$(git rev-parse "${local_prep_head_sha}^{tree}")
|
||||
# Preserve the verified local lineage because GraphQL creates a remote
|
||||
# commit with the same tree but the old branch parent.
|
||||
printf '%s=%q\n' \
|
||||
PREP_SYNC_MAINLINE_BASE_SHA "$mainline_base_sha" \
|
||||
PREP_SYNC_TREE "$prep_sync_tree" \
|
||||
> .local/prep-sync.env
|
||||
cat >> .local/prep.md <<EOF_PREP
|
||||
- Cleared stale prepare artifacts. Wait for hosted CI/Testbox on $prep_head_sha, then run prepare-run again.
|
||||
EOF_PREP
|
||||
echo "prepare-sync-head complete"
|
||||
echo "prep_head_sha=$prep_head_sha"
|
||||
echo "Hosted CI/Testbox must pass for this exact head before prepare-run can continue."
|
||||
return
|
||||
fi
|
||||
|
||||
cat >> .local/prep.md <<EOF_PREP
|
||||
- Prepare gates reran automatically when the sync rebase changed the prep head.
|
||||
EOF_PREP
|
||||
|
||||
|
|
@ -263,6 +315,8 @@ EOF_PREP
|
|||
PR_HEAD "$PR_HEAD" \
|
||||
PR_HEAD_SHA_BEFORE "$pushed_from_sha" \
|
||||
PREP_HEAD_SHA "$prep_head_sha" \
|
||||
LOCAL_PREP_HEAD_SHA "$local_prep_head_sha" \
|
||||
PREP_MAINLINE_BASE_SHA "$mainline_base_sha" \
|
||||
COAUTHOR_EMAIL "$coauthor_email" \
|
||||
> .local/prep.env
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"]],
|
||||
|
|
|
|||
244
scripts/verify-pr-hosted-gates.mjs
Normal file
244
scripts/verify-pr-hosted-gates.mjs
Normal file
|
|
@ -0,0 +1,244 @@
|
|||
#!/usr/bin/env node
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { mkdirSync, writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { isDirectRunUrl } from "./lib/direct-run.mjs";
|
||||
|
||||
export const SCHEDULED_HOSTED_WORKFLOWS = [
|
||||
"Blacksmith Testbox",
|
||||
"Blacksmith ARM Testbox",
|
||||
"Blacksmith Build Artifacts Testbox",
|
||||
"Workflow Sanity",
|
||||
];
|
||||
const CI_WORKFLOW_PATH = ".github/workflows/ci.yml";
|
||||
const BUILD_ARTIFACTS_WORKFLOW = "Blacksmith Build Artifacts Testbox";
|
||||
const ARTIFACT_FALLBACK_REQUIRED_WORKFLOWS = [
|
||||
"Blacksmith Testbox",
|
||||
"Blacksmith ARM Testbox",
|
||||
"Workflow Sanity",
|
||||
];
|
||||
|
||||
function readOptionValue(argv, index, optionName) {
|
||||
const value = argv[index + 1];
|
||||
if (!value || value.startsWith("--")) {
|
||||
throw new Error(`Expected ${optionName} <value>.`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function parseArgs(argv) {
|
||||
const args = { repo: "", sha: "", output: "", changelogOnly: false };
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const arg = argv[index];
|
||||
switch (arg) {
|
||||
case "--repo":
|
||||
args.repo = readOptionValue(argv, index, arg);
|
||||
index += 1;
|
||||
break;
|
||||
case "--sha":
|
||||
args.sha = readOptionValue(argv, index, arg);
|
||||
index += 1;
|
||||
break;
|
||||
case "--output":
|
||||
args.output = readOptionValue(argv, index, arg);
|
||||
index += 1;
|
||||
break;
|
||||
case "--changelog-only":
|
||||
args.changelogOnly = true;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown option: ${arg}`);
|
||||
}
|
||||
}
|
||||
if (!args.repo || !args.sha || !args.output) {
|
||||
throw new Error(
|
||||
"Usage: node scripts/verify-pr-hosted-gates.mjs --repo <owner/repo> --sha <sha> --output <path>",
|
||||
);
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
function formatObservedRuns(runs) {
|
||||
if (runs.length === 0) {
|
||||
return "none";
|
||||
}
|
||||
return runs
|
||||
.map(
|
||||
(run) => `${run.id ?? "unknown"}:${run.status ?? "unknown"}/${run.conclusion ?? "unknown"}`,
|
||||
)
|
||||
.join(", ");
|
||||
}
|
||||
|
||||
function isReleaseGateCiRun(run, sha) {
|
||||
return (
|
||||
run?.event === "workflow_dispatch" &&
|
||||
run?.head_sha === sha &&
|
||||
String(run?.path ?? "").split("@", 1)[0] === CI_WORKFLOW_PATH &&
|
||||
run?.display_title === `CI release gate ${sha}`
|
||||
);
|
||||
}
|
||||
|
||||
function matchingAuthoritativeRuns(runs, workflowName, sha) {
|
||||
return runs.filter((run) => {
|
||||
if (run?.head_sha !== sha) {
|
||||
return false;
|
||||
}
|
||||
if (run?.event === "pull_request") {
|
||||
return run.name === workflowName;
|
||||
}
|
||||
return workflowName === "CI" && isReleaseGateCiRun(run, sha);
|
||||
});
|
||||
}
|
||||
|
||||
function latestRun(runs) {
|
||||
return runs.toSorted((left, right) =>
|
||||
String(right.updated_at ?? "").localeCompare(String(left.updated_at ?? "")),
|
||||
)[0];
|
||||
}
|
||||
|
||||
function preferredCiRun(runs) {
|
||||
const scheduledRuns = runs.filter((run) => run.event === "pull_request");
|
||||
const failedScheduledRun = scheduledRuns.find(
|
||||
(run) => run.status === "completed" && run.conclusion !== "success",
|
||||
);
|
||||
if (failedScheduledRun) {
|
||||
return failedScheduledRun;
|
||||
}
|
||||
const latestScheduledRun = latestRun(scheduledRuns);
|
||||
if (latestScheduledRun?.status === "completed") {
|
||||
return latestScheduledRun;
|
||||
}
|
||||
return latestRun(runs.filter((run) => run.event === "workflow_dispatch")) ?? latestScheduledRun;
|
||||
}
|
||||
|
||||
function successfulRunOrThrow(runs, workflowName, sha) {
|
||||
const matchingRuns = matchingAuthoritativeRuns(runs, workflowName, sha);
|
||||
const run = workflowName === "CI" ? preferredCiRun(matchingRuns) : latestRun(matchingRuns);
|
||||
if (!run || run.status !== "completed" || run.conclusion !== "success") {
|
||||
throw new Error(
|
||||
`Missing successful exact-head ${workflowName} workflow for ${sha}. Observed: ${formatObservedRuns(matchingRuns)}`,
|
||||
);
|
||||
}
|
||||
return run;
|
||||
}
|
||||
|
||||
function successfulReleaseGateFallback(workflowRuns, sha) {
|
||||
const fallback = latestRun(workflowRuns.filter((run) => isReleaseGateCiRun(run, sha)));
|
||||
if (fallback?.status !== "completed" || fallback.conclusion !== "success") {
|
||||
return null;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function canCoverQueuedBuildArtifacts(workflowRuns, sha) {
|
||||
if (!successfulReleaseGateFallback(workflowRuns, sha)) {
|
||||
return false;
|
||||
}
|
||||
const supportingGatesPassed = ARTIFACT_FALLBACK_REQUIRED_WORKFLOWS.every((workflowName) => {
|
||||
const run = latestRun(matchingAuthoritativeRuns(workflowRuns, workflowName, sha));
|
||||
return run?.status === "completed" && run.conclusion === "success";
|
||||
});
|
||||
if (!supportingGatesPassed) {
|
||||
return false;
|
||||
}
|
||||
const buildArtifactRuns = matchingAuthoritativeRuns(workflowRuns, BUILD_ARTIFACTS_WORKFLOW, sha);
|
||||
const latestBuildArtifactRun = latestRun(buildArtifactRuns);
|
||||
return (
|
||||
latestBuildArtifactRun?.status === "queued" &&
|
||||
buildArtifactRuns.every(
|
||||
(run) =>
|
||||
run.status === "queued" || (run.status === "completed" && run.conclusion === "success"),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function stripAnsi(raw) {
|
||||
const escape = String.fromCharCode(27);
|
||||
return raw.replace(new RegExp(`${escape}\\[[0-?]*[ -/]*[@-~]`, "gu"), "");
|
||||
}
|
||||
|
||||
export function parseWorkflowRunPages(raw) {
|
||||
return JSON.parse(stripAnsi(raw)).flatMap((page) => page.workflow_runs ?? []);
|
||||
}
|
||||
|
||||
export function collectHostedGateEvidence({ sha, workflowRuns, changelogOnly = false }) {
|
||||
if (!Array.isArray(workflowRuns)) {
|
||||
throw new Error("workflowRuns must be an array.");
|
||||
}
|
||||
const workflows = [];
|
||||
const fallbackCoveredWorkflows = [];
|
||||
let ciRun;
|
||||
if (!changelogOnly) {
|
||||
ciRun = successfulRunOrThrow(workflowRuns, "CI", sha);
|
||||
workflows.push(ciRun);
|
||||
}
|
||||
for (const workflowName of SCHEDULED_HOSTED_WORKFLOWS) {
|
||||
const matchingRuns = matchingAuthoritativeRuns(workflowRuns, workflowName, sha);
|
||||
if (matchingRuns.length > 0) {
|
||||
if (
|
||||
workflowName === BUILD_ARTIFACTS_WORKFLOW &&
|
||||
canCoverQueuedBuildArtifacts(workflowRuns, sha)
|
||||
) {
|
||||
fallbackCoveredWorkflows.push({
|
||||
name: workflowName,
|
||||
coveredBy: "CI release gate",
|
||||
reason: "scheduled workflow is queued",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
workflows.push(successfulRunOrThrow(workflowRuns, workflowName, sha));
|
||||
}
|
||||
}
|
||||
const evidence = {
|
||||
headSha: sha,
|
||||
workflows: workflows.map((run) => ({
|
||||
id: run.id,
|
||||
name: run.name,
|
||||
event: run.event,
|
||||
status: run.status,
|
||||
conclusion: run.conclusion,
|
||||
createdAt: run.created_at,
|
||||
updatedAt: run.updated_at,
|
||||
url: run.html_url,
|
||||
})),
|
||||
};
|
||||
if (fallbackCoveredWorkflows.length > 0) {
|
||||
evidence.fallbackCoveredWorkflows = fallbackCoveredWorkflows;
|
||||
}
|
||||
return evidence;
|
||||
}
|
||||
|
||||
function loadWorkflowRuns(repo, sha) {
|
||||
const raw = execFileSync(
|
||||
"gh",
|
||||
["api", `repos/${repo}/actions/runs?head_sha=${sha}&per_page=100`, "--paginate", "--slurp"],
|
||||
{ encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] },
|
||||
);
|
||||
return parseWorkflowRunPages(raw);
|
||||
}
|
||||
|
||||
export function main(argv = process.argv.slice(2)) {
|
||||
const args = parseArgs(argv);
|
||||
const evidence = collectHostedGateEvidence({
|
||||
sha: args.sha,
|
||||
workflowRuns: loadWorkflowRuns(args.repo, args.sha),
|
||||
changelogOnly: args.changelogOnly,
|
||||
});
|
||||
const manifest = {
|
||||
schemaVersion: 1,
|
||||
generatedAt: new Date().toISOString(),
|
||||
repo: args.repo,
|
||||
...evidence,
|
||||
};
|
||||
mkdirSync(path.dirname(args.output), { recursive: true });
|
||||
writeFileSync(args.output, `${JSON.stringify(manifest, null, 2)}\n`);
|
||||
console.log(
|
||||
`Exact-head hosted gates passed for ${args.sha}: ${manifest.workflows
|
||||
.map((workflow) => `${workflow.name}#${workflow.id}`)
|
||||
.join(", ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (isDirectRunUrl(process.argv[1], import.meta.url)) {
|
||||
main();
|
||||
}
|
||||
86
scripts/verify-stable-main-closeout.mjs
Normal file
86
scripts/verify-stable-main-closeout.mjs
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { verifyStableMainCloseout } from "./lib/stable-release-closeout.mjs";
|
||||
|
||||
function parseArgs(argv) {
|
||||
const values = new Map();
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const key = argv[index];
|
||||
if (!key.startsWith("--")) {
|
||||
throw new Error(`unexpected argument: ${key}`);
|
||||
}
|
||||
const value = argv[index + 1];
|
||||
if (!value || value.startsWith("--")) {
|
||||
throw new Error(`${key} requires a value.`);
|
||||
}
|
||||
values.set(key.slice(2), value);
|
||||
index += 1;
|
||||
}
|
||||
|
||||
const required = [
|
||||
"tag",
|
||||
"main-dir",
|
||||
"tag-dir",
|
||||
"release-json",
|
||||
"full-release-validation-run-id",
|
||||
"release-publish-run-id",
|
||||
"rollback-drill-id",
|
||||
"rollback-drill-date",
|
||||
"output",
|
||||
];
|
||||
for (const key of required) {
|
||||
if (!values.has(key)) {
|
||||
throw new Error(`--${key} is required.`);
|
||||
}
|
||||
}
|
||||
return Object.fromEntries(values);
|
||||
}
|
||||
|
||||
function readJson(path) {
|
||||
return JSON.parse(readFileSync(path, "utf8"));
|
||||
}
|
||||
|
||||
function gitSha(dir) {
|
||||
return execFileSync("git", ["-C", dir, "rev-parse", "HEAD"], {
|
||||
encoding: "utf8",
|
||||
}).trim();
|
||||
}
|
||||
|
||||
function main() {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
const mainDir = resolve(args["main-dir"]);
|
||||
const tagDir = resolve(args["tag-dir"]);
|
||||
const result = verifyStableMainCloseout({
|
||||
tag: args.tag,
|
||||
mainPackageJson: readJson(resolve(mainDir, "package.json")),
|
||||
tagPackageJson: readJson(resolve(tagDir, "package.json")),
|
||||
mainChangelog: readFileSync(resolve(mainDir, "CHANGELOG.md"), "utf8"),
|
||||
tagChangelog: readFileSync(resolve(tagDir, "CHANGELOG.md"), "utf8"),
|
||||
mainAppcast: readFileSync(resolve(mainDir, "appcast.xml"), "utf8"),
|
||||
release: readJson(resolve(args["release-json"])),
|
||||
releaseTagSha: gitSha(tagDir),
|
||||
mainSha: gitSha(mainDir),
|
||||
fullReleaseValidationRunId: args["full-release-validation-run-id"],
|
||||
releasePublishRunId: args["release-publish-run-id"],
|
||||
rollbackDrillId: args["rollback-drill-id"],
|
||||
rollbackDrillDate: args["rollback-drill-date"],
|
||||
allowStaleRollbackDrill: args["allow-stale-rollback-drill"] === "true",
|
||||
nowMs: Date.now(),
|
||||
});
|
||||
if (result.errors.length > 0 || !result.manifest) {
|
||||
throw new Error(`stable main closeout failed:\n- ${result.errors.join("\n- ")}`);
|
||||
}
|
||||
|
||||
writeFileSync(resolve(args.output), `${JSON.stringify(result.manifest, null, 2)}\n`);
|
||||
console.log(`stable main closeout verified: ${args.tag}`);
|
||||
}
|
||||
|
||||
try {
|
||||
main();
|
||||
} catch (error) {
|
||||
console.error(error instanceof Error ? error.message : String(error));
|
||||
process.exitCode = 1;
|
||||
}
|
||||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ import {
|
|||
} from "../../scripts/package-openclaw-for-docker.mjs";
|
||||
|
||||
function isProcessAlive(pid: number): boolean {
|
||||
if (!Number.isSafeInteger(pid) || pid <= 0) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
process.kill(pid, 0);
|
||||
return true;
|
||||
|
|
@ -27,15 +30,18 @@ async function sleep(ms: number): Promise<void> {
|
|||
});
|
||||
}
|
||||
|
||||
async function waitForFile(filePath: string, timeoutMs: number): Promise<void> {
|
||||
async function readPid(filePath: string, timeoutMs: number): Promise<number> {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
if (fs.existsSync(filePath)) {
|
||||
return;
|
||||
const pid = Number(fs.readFileSync(filePath, "utf8").trim());
|
||||
if (Number.isSafeInteger(pid) && pid > 0) {
|
||||
return pid;
|
||||
}
|
||||
}
|
||||
await sleep(25);
|
||||
}
|
||||
throw new Error(`timeout waiting for ${filePath}`);
|
||||
throw new Error(`timeout waiting for a positive pid in ${filePath}`);
|
||||
}
|
||||
|
||||
async function waitForDead(pid: number, timeoutMs: number): Promise<void> {
|
||||
|
|
@ -308,8 +314,7 @@ describe("package-openclaw-for-docker", () => {
|
|||
timeoutMs: 500,
|
||||
});
|
||||
const timeoutAssertion = expect(runPromise).rejects.toThrow(/timed out after 500ms/u);
|
||||
await waitForFile(childPidPath, 2000);
|
||||
childPid = Number(fs.readFileSync(childPidPath, "utf8"));
|
||||
childPid = await readPid(childPidPath, 2000);
|
||||
await timeoutAssertion;
|
||||
await waitForDead(childPid, 2000);
|
||||
} finally {
|
||||
|
|
@ -348,8 +353,7 @@ describe("package-openclaw-for-docker", () => {
|
|||
}),
|
||||
).rejects.toThrow(/timed out after 500ms/u);
|
||||
|
||||
await waitForFile(childPidPath, 2000);
|
||||
childPid = Number(fs.readFileSync(childPidPath, "utf8"));
|
||||
childPid = await readPid(childPidPath, 2000);
|
||||
await waitForDead(childPid, 2000);
|
||||
} finally {
|
||||
if (childPid && isProcessAlive(childPid)) {
|
||||
|
|
@ -437,8 +441,7 @@ describe("package-openclaw-for-docker", () => {
|
|||
});
|
||||
runnerPid = runner.pid ?? 0;
|
||||
|
||||
await waitForFile(childPidPath, 2000);
|
||||
childPid = Number(fs.readFileSync(childPidPath, "utf8"));
|
||||
childPid = await readPid(childPidPath, 2000);
|
||||
runner.kill("SIGTERM");
|
||||
const result = await waitForExit(runner, 5000);
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
|
|
|
|||
314
test/scripts/verify-pr-hosted-gates.test.ts
Normal file
314
test/scripts/verify-pr-hosted-gates.test.ts
Normal file
|
|
@ -0,0 +1,314 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
collectHostedGateEvidence,
|
||||
parseArgs,
|
||||
parseWorkflowRunPages,
|
||||
SCHEDULED_HOSTED_WORKFLOWS,
|
||||
} from "../../scripts/verify-pr-hosted-gates.mjs";
|
||||
|
||||
const sha = "773ffd87a1e1e34451ad6e38fda37380c2569a50";
|
||||
const BUILD_ARTIFACTS_WORKFLOW = "Blacksmith Build Artifacts Testbox";
|
||||
|
||||
function successfulRun(name: string, id: number, updatedAt: string) {
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
event: "pull_request",
|
||||
status: "completed",
|
||||
conclusion: "success",
|
||||
head_sha: sha,
|
||||
path: ".github/workflows/ci.yml",
|
||||
created_at: "2026-06-17T10:46:24Z",
|
||||
updated_at: updatedAt,
|
||||
html_url: `https://github.com/openclaw/openclaw/actions/runs/${id}`,
|
||||
};
|
||||
}
|
||||
|
||||
describe("verify-pr-hosted-gates", () => {
|
||||
it("requires the latest scheduled workflow run to pass", () => {
|
||||
const evidence = collectHostedGateEvidence({
|
||||
sha,
|
||||
workflowRuns: [
|
||||
successfulRun("CI", 1, "2026-06-17T10:47:00Z"),
|
||||
{
|
||||
...successfulRun("Blacksmith Testbox", 2, "2026-06-17T10:47:30Z"),
|
||||
event: "workflow_dispatch",
|
||||
},
|
||||
successfulRun("Blacksmith Testbox", 3, "2026-06-17T10:48:00Z"),
|
||||
successfulRun("Blacksmith ARM Testbox", 4, "2026-06-17T10:49:00Z"),
|
||||
successfulRun("Blacksmith Build Artifacts Testbox", 5, "2026-06-17T10:50:00Z"),
|
||||
successfulRun("Workflow Sanity", 6, "2026-06-17T10:51:00Z"),
|
||||
],
|
||||
});
|
||||
|
||||
expect(evidence).toEqual({
|
||||
headSha: sha,
|
||||
workflows: [
|
||||
expect.objectContaining({ name: "CI", id: 1 }),
|
||||
expect.objectContaining({ name: "Blacksmith Testbox", id: 3 }),
|
||||
expect.objectContaining({ name: "Blacksmith ARM Testbox", id: 4 }),
|
||||
expect.objectContaining({ name: "Blacksmith Build Artifacts Testbox", id: 5 }),
|
||||
expect.objectContaining({ name: "Workflow Sanity", id: 6 }),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects a failed rerun of a workflow that was scheduled for the exact head", () => {
|
||||
const workflowRuns = ["CI", ...SCHEDULED_HOSTED_WORKFLOWS].map((name, index) =>
|
||||
successfulRun(name, index + 1, `2026-06-17T10:4${index}:00Z`),
|
||||
);
|
||||
workflowRuns[2] = {
|
||||
...workflowRuns[2],
|
||||
conclusion: "failure",
|
||||
updated_at: "2026-06-17T10:50:00Z",
|
||||
};
|
||||
|
||||
expect(() => collectHostedGateEvidence({ sha, workflowRuns })).toThrow(
|
||||
"Missing successful exact-head Blacksmith ARM Testbox workflow",
|
||||
);
|
||||
});
|
||||
|
||||
it("accepts a non-docs PR when CI is the only scheduled authoritative workflow", () => {
|
||||
expect(
|
||||
collectHostedGateEvidence({
|
||||
sha,
|
||||
workflowRuns: [successfulRun("CI", 1, "2026-06-17T10:47:00Z")],
|
||||
}),
|
||||
).toEqual({
|
||||
headSha: sha,
|
||||
workflows: [expect.objectContaining({ name: "CI", id: 1 })],
|
||||
});
|
||||
});
|
||||
|
||||
it("accepts the explicit exact-SHA manual CI release gate", () => {
|
||||
expect(
|
||||
collectHostedGateEvidence({
|
||||
sha,
|
||||
workflowRuns: [
|
||||
{
|
||||
...successfulRun(`CI release gate ${sha}`, 1, "2026-06-17T10:47:00Z"),
|
||||
event: "workflow_dispatch",
|
||||
path: ".github/workflows/ci.yml@refs/heads/release-controls",
|
||||
display_title: `CI release gate ${sha}`,
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toEqual({
|
||||
headSha: sha,
|
||||
workflows: [expect.objectContaining({ name: `CI release gate ${sha}`, id: 1 })],
|
||||
});
|
||||
});
|
||||
|
||||
it("prefers the exact release-gate fallback while scheduled CI remains queued", () => {
|
||||
expect(
|
||||
collectHostedGateEvidence({
|
||||
sha,
|
||||
workflowRuns: [
|
||||
{
|
||||
...successfulRun("CI", 1, "2026-06-17T10:47:00Z"),
|
||||
status: "queued",
|
||||
conclusion: null,
|
||||
updated_at: "2026-06-17T10:50:00Z",
|
||||
},
|
||||
{
|
||||
...successfulRun(`CI release gate ${sha}`, 2, "2026-06-17T10:49:00Z"),
|
||||
event: "workflow_dispatch",
|
||||
display_title: `CI release gate ${sha}`,
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toEqual({
|
||||
headSha: sha,
|
||||
workflows: [expect.objectContaining({ name: `CI release gate ${sha}`, id: 2 })],
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects a completed scheduled CI failure even when a fallback passed", () => {
|
||||
expect(() =>
|
||||
collectHostedGateEvidence({
|
||||
sha,
|
||||
workflowRuns: [
|
||||
{
|
||||
...successfulRun("CI", 1, "2026-06-17T10:50:00Z"),
|
||||
conclusion: "failure",
|
||||
},
|
||||
{
|
||||
...successfulRun(`CI release gate ${sha}`, 2, "2026-06-17T10:49:00Z"),
|
||||
event: "workflow_dispatch",
|
||||
display_title: `CI release gate ${sha}`,
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toThrow("Missing successful exact-head CI workflow");
|
||||
});
|
||||
|
||||
it("covers a queued artifact Testbox only with a completed exact CI fallback", () => {
|
||||
expect(
|
||||
collectHostedGateEvidence({
|
||||
sha,
|
||||
workflowRuns: [
|
||||
{
|
||||
...successfulRun(`CI release gate ${sha}`, 1, "2026-06-17T10:49:00Z"),
|
||||
event: "workflow_dispatch",
|
||||
display_title: `CI release gate ${sha}`,
|
||||
},
|
||||
successfulRun("CI", 3, "2026-06-17T10:51:00Z"),
|
||||
successfulRun("Blacksmith Testbox", 4, "2026-06-17T10:52:00Z"),
|
||||
successfulRun("Blacksmith ARM Testbox", 5, "2026-06-17T10:53:00Z"),
|
||||
successfulRun("Workflow Sanity", 6, "2026-06-17T10:54:00Z"),
|
||||
{
|
||||
...successfulRun(BUILD_ARTIFACTS_WORKFLOW, 2, "2026-06-17T10:50:00Z"),
|
||||
status: "queued",
|
||||
conclusion: null,
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toEqual({
|
||||
headSha: sha,
|
||||
workflows: [
|
||||
expect.objectContaining({ name: "CI", id: 3 }),
|
||||
expect.objectContaining({ name: "Blacksmith Testbox", id: 4 }),
|
||||
expect.objectContaining({ name: "Blacksmith ARM Testbox", id: 5 }),
|
||||
expect.objectContaining({ name: "Workflow Sanity", id: 6 }),
|
||||
],
|
||||
fallbackCoveredWorkflows: [
|
||||
{
|
||||
name: BUILD_ARTIFACTS_WORKFLOW,
|
||||
coveredBy: "CI release gate",
|
||||
reason: "scheduled workflow is queued",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("does not cover queued artifacts until all supporting workflow gates pass", () => {
|
||||
expect(() =>
|
||||
collectHostedGateEvidence({
|
||||
sha,
|
||||
workflowRuns: [
|
||||
{
|
||||
...successfulRun(`CI release gate ${sha}`, 1, "2026-06-17T10:49:00Z"),
|
||||
event: "workflow_dispatch",
|
||||
display_title: `CI release gate ${sha}`,
|
||||
},
|
||||
{
|
||||
...successfulRun(BUILD_ARTIFACTS_WORKFLOW, 2, "2026-06-17T10:50:00Z"),
|
||||
status: "queued",
|
||||
conclusion: null,
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toThrow("Missing successful exact-head Blacksmith Build Artifacts Testbox workflow");
|
||||
});
|
||||
|
||||
it("keeps active or terminal non-successful artifact Testboxes blocking", () => {
|
||||
const ciFallback = {
|
||||
...successfulRun(`CI release gate ${sha}`, 1, "2026-06-17T10:49:00Z"),
|
||||
event: "workflow_dispatch",
|
||||
display_title: `CI release gate ${sha}`,
|
||||
};
|
||||
|
||||
for (const artifactRun of [
|
||||
{
|
||||
...successfulRun(BUILD_ARTIFACTS_WORKFLOW, 2, "2026-06-17T10:50:00Z"),
|
||||
status: "in_progress",
|
||||
conclusion: null,
|
||||
},
|
||||
{
|
||||
...successfulRun(BUILD_ARTIFACTS_WORKFLOW, 3, "2026-06-17T10:51:00Z"),
|
||||
conclusion: "failure",
|
||||
},
|
||||
]) {
|
||||
expect(() =>
|
||||
collectHostedGateEvidence({
|
||||
sha,
|
||||
workflowRuns: [ciFallback, artifactRun],
|
||||
}),
|
||||
).toThrow("Missing successful exact-head Blacksmith Build Artifacts Testbox workflow");
|
||||
}
|
||||
|
||||
expect(() =>
|
||||
collectHostedGateEvidence({
|
||||
sha,
|
||||
workflowRuns: [
|
||||
ciFallback,
|
||||
{
|
||||
...successfulRun(BUILD_ARTIFACTS_WORKFLOW, 4, "2026-06-17T10:52:00Z"),
|
||||
conclusion: "failure",
|
||||
},
|
||||
{
|
||||
...successfulRun(BUILD_ARTIFACTS_WORKFLOW, 5, "2026-06-17T10:53:00Z"),
|
||||
status: "queued",
|
||||
conclusion: null,
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toThrow("Missing successful exact-head Blacksmith Build Artifacts Testbox workflow");
|
||||
});
|
||||
|
||||
it("rejects an unmarked manual CI run", () => {
|
||||
expect(() =>
|
||||
collectHostedGateEvidence({
|
||||
sha,
|
||||
workflowRuns: [
|
||||
{
|
||||
...successfulRun(`CI release gate ${sha}`, 1, "2026-06-17T10:47:00Z"),
|
||||
event: "workflow_dispatch",
|
||||
display_title: "CI",
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toThrow("Missing successful exact-head CI workflow");
|
||||
});
|
||||
|
||||
it("rejects a manual release-gate title from another workflow", () => {
|
||||
expect(() =>
|
||||
collectHostedGateEvidence({
|
||||
sha,
|
||||
workflowRuns: [
|
||||
{
|
||||
...successfulRun(`CI release gate ${sha}`, 1, "2026-06-17T10:47:00Z"),
|
||||
event: "workflow_dispatch",
|
||||
path: ".github/workflows/something-else.yml",
|
||||
display_title: `CI release gate ${sha}`,
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toThrow("Missing successful exact-head CI workflow");
|
||||
});
|
||||
|
||||
it("requires CI for docs unless the head changes only CHANGELOG.md", () => {
|
||||
expect(() => collectHostedGateEvidence({ sha, workflowRuns: [] })).toThrow(
|
||||
"Missing successful exact-head CI workflow",
|
||||
);
|
||||
expect(collectHostedGateEvidence({ sha, workflowRuns: [], changelogOnly: true })).toEqual({
|
||||
headSha: sha,
|
||||
workflows: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("parses required CLI arguments", () => {
|
||||
expect(
|
||||
parseArgs([
|
||||
"--repo",
|
||||
"openclaw/openclaw",
|
||||
"--sha",
|
||||
sha,
|
||||
"--output",
|
||||
".local/gates-hosted-checks.json",
|
||||
]),
|
||||
).toEqual({
|
||||
repo: "openclaw/openclaw",
|
||||
sha,
|
||||
output: ".local/gates-hosted-checks.json",
|
||||
changelogOnly: false,
|
||||
});
|
||||
expect(() => parseArgs(["--repo", "openclaw/openclaw"])).toThrow("Usage:");
|
||||
});
|
||||
|
||||
it("accepts JSON emitted through a colorizing GitHub CLI shim", () => {
|
||||
expect(
|
||||
parseWorkflowRunPages('\u001B[1;37m[{"workflow_runs":[{"id":1,"name":"CI"}]}]\u001B[0m'),
|
||||
).toEqual([{ id: 1, name: "CI" }]);
|
||||
});
|
||||
});
|
||||
211
test/stable-release-closeout.test.ts
Normal file
211
test/stable-release-closeout.test.ts
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
extractStableChangelogSection,
|
||||
parseStableReleaseTag,
|
||||
verifyStableMainCloseout,
|
||||
} from "../scripts/lib/stable-release-closeout.mjs";
|
||||
|
||||
const release = {
|
||||
tagName: "v2026.6.8",
|
||||
isDraft: false,
|
||||
isPrerelease: false,
|
||||
assets: [
|
||||
{ name: "OpenClaw-2026.6.8.zip", digest: `sha256:${"a".repeat(64)}` },
|
||||
{ name: "OpenClaw-2026.6.8.dmg", digest: `sha256:${"b".repeat(64)}` },
|
||||
{ name: "OpenClaw-2026.6.8.dSYM.zip", digest: `sha256:${"c".repeat(64)}` },
|
||||
],
|
||||
};
|
||||
const changelog =
|
||||
"# Changelog\n\n## 2026.6.8\n\n### Fixes\n\n- Shipped fix.\n\n## 2026.6.7\n\n- Old.\n";
|
||||
const validCloseoutParams = {
|
||||
tag: "v2026.6.8",
|
||||
mainPackageJson: { version: "2026.6.8" },
|
||||
tagPackageJson: { version: "2026.6.8" },
|
||||
mainChangelog: changelog,
|
||||
tagChangelog: changelog,
|
||||
mainAppcast:
|
||||
"https://github.com/openclaw/openclaw/releases/download/v2026.6.8/OpenClaw-2026.6.8.zip\n",
|
||||
release,
|
||||
releaseTagSha: "tag-sha",
|
||||
mainSha: "main-sha",
|
||||
fullReleaseValidationRunId: "11",
|
||||
releasePublishRunId: "12",
|
||||
rollbackDrillId: "rollback-drill-2026-q2",
|
||||
rollbackDrillDate: "2026-06-01",
|
||||
};
|
||||
|
||||
describe("stable release closeout", () => {
|
||||
it("parses stable and correction tags", () => {
|
||||
expect(parseStableReleaseTag("v2026.6.8")).toBe("2026.6.8");
|
||||
expect(parseStableReleaseTag("v2026.6.8-2")).toBe("2026.6.8");
|
||||
expect(() => parseStableReleaseTag("v2026.6.8-beta.1")).toThrow(
|
||||
"expected a stable release tag",
|
||||
);
|
||||
});
|
||||
|
||||
it("extracts only the requested stable changelog section", () => {
|
||||
expect(extractStableChangelogSection(changelog, "2026.6.8")).toBe(
|
||||
"## 2026.6.8\n\n### Fixes\n\n- Shipped fix.",
|
||||
);
|
||||
});
|
||||
|
||||
it("accepts an exact stable closeout with a current rollback drill", () => {
|
||||
const result = verifyStableMainCloseout({
|
||||
...validCloseoutParams,
|
||||
nowMs: Date.parse("2026-06-17T00:00:00Z"),
|
||||
});
|
||||
|
||||
expect(result.errors).toEqual([]);
|
||||
expect(result.manifest).toMatchObject({
|
||||
releaseTag: "v2026.6.8",
|
||||
releaseVersion: "2026.6.8",
|
||||
rollbackDrill: { id: "rollback-drill-2026-q2", date: "2026-06-01" },
|
||||
});
|
||||
expect(result.manifest).not.toHaveProperty("verifiedAt");
|
||||
});
|
||||
|
||||
it("writes identical closeout evidence when replayed", () => {
|
||||
const first = verifyStableMainCloseout({
|
||||
...validCloseoutParams,
|
||||
nowMs: Date.parse("2026-06-17T00:00:00Z"),
|
||||
});
|
||||
const replay = verifyStableMainCloseout({
|
||||
...validCloseoutParams,
|
||||
release: {
|
||||
...release,
|
||||
assets: [
|
||||
...release.assets,
|
||||
{
|
||||
name: "openclaw-2026.6.8-stable-main-closeout.json",
|
||||
digest: `sha256:${"d".repeat(64)}`,
|
||||
},
|
||||
{
|
||||
name: "openclaw-2026.6.8-stable-main-closeout.json.sha256",
|
||||
digest: `sha256:${"e".repeat(64)}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
nowMs: Date.parse("2026-06-18T00:00:00Z"),
|
||||
});
|
||||
|
||||
expect(replay.manifest).toEqual(first.manifest);
|
||||
});
|
||||
|
||||
it("replays an existing partial closeout using its recorded rollback drill", () => {
|
||||
const first = verifyStableMainCloseout({
|
||||
...validCloseoutParams,
|
||||
nowMs: Date.parse("2026-06-17T00:00:00Z"),
|
||||
});
|
||||
const replay = verifyStableMainCloseout({
|
||||
...validCloseoutParams,
|
||||
allowStaleRollbackDrill: true,
|
||||
nowMs: Date.parse("2026-10-01T00:00:00Z"),
|
||||
});
|
||||
|
||||
expect(replay.errors).toEqual([]);
|
||||
expect(replay.manifest).toEqual(first.manifest);
|
||||
});
|
||||
|
||||
it("requires the canonical macOS zip, dmg, and dSYM assets", () => {
|
||||
const result = verifyStableMainCloseout({
|
||||
...validCloseoutParams,
|
||||
release: {
|
||||
...release,
|
||||
assets: [{ name: "openclaw-2026.6.8-dependency-evidence.zip" }],
|
||||
},
|
||||
mainAppcast:
|
||||
"https://github.com/openclaw/openclaw/releases/download/v2026.6.8/openclaw-2026.6.8-dependency-evidence.zip\n",
|
||||
nowMs: Date.parse("2026-06-17T00:00:00Z"),
|
||||
});
|
||||
|
||||
expect(result.errors).toContain(
|
||||
"GitHub release v2026.6.8 is missing required macOS asset(s): OpenClaw-2026.6.8.zip, OpenClaw-2026.6.8.dmg, OpenClaw-2026.6.8.dSYM.zip.",
|
||||
);
|
||||
});
|
||||
|
||||
it("uses exact correction versions for correction-release state and assets", () => {
|
||||
const correctionRelease = {
|
||||
...release,
|
||||
tagName: "v2026.6.8-2",
|
||||
assets: release.assets.map((asset) => ({
|
||||
...asset,
|
||||
name: asset.name.replaceAll("2026.6.8", "2026.6.8-2"),
|
||||
})),
|
||||
};
|
||||
const result = verifyStableMainCloseout({
|
||||
...validCloseoutParams,
|
||||
tag: "v2026.6.8-2",
|
||||
mainPackageJson: { version: "2026.6.8-2" },
|
||||
tagPackageJson: { version: "2026.6.8-2" },
|
||||
mainChangelog: changelog.replaceAll("2026.6.8", "2026.6.8-2"),
|
||||
tagChangelog: changelog.replaceAll("2026.6.8", "2026.6.8-2"),
|
||||
release: correctionRelease,
|
||||
mainAppcast:
|
||||
"https://github.com/openclaw/openclaw/releases/download/v2026.6.8-2/OpenClaw-2026.6.8-2.zip\n",
|
||||
nowMs: Date.parse("2026-06-17T00:00:00Z"),
|
||||
});
|
||||
|
||||
expect(result.errors).toEqual([]);
|
||||
expect(result.manifest).toMatchObject({
|
||||
releaseVersion: "2026.6.8-2",
|
||||
mainPackageVersion: "2026.6.8-2",
|
||||
releaseTagPackageVersion: "2026.6.8-2",
|
||||
});
|
||||
});
|
||||
|
||||
it("allows a fallback correction tag for an existing base stable package", () => {
|
||||
const result = verifyStableMainCloseout({
|
||||
...validCloseoutParams,
|
||||
tag: "v2026.6.8-2",
|
||||
release: {
|
||||
...release,
|
||||
tagName: "v2026.6.8-2",
|
||||
},
|
||||
mainAppcast:
|
||||
"https://github.com/openclaw/openclaw/releases/download/v2026.6.8-2/OpenClaw-2026.6.8.zip\n",
|
||||
nowMs: Date.parse("2026-06-17T00:00:00Z"),
|
||||
});
|
||||
|
||||
expect(result.errors).toEqual([]);
|
||||
expect(result.manifest).toMatchObject({
|
||||
releaseVersion: "2026.6.8",
|
||||
mainPackageVersion: "2026.6.8",
|
||||
releaseTagPackageVersion: "2026.6.8",
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects calendar-normalized rollback drill dates", () => {
|
||||
const result = verifyStableMainCloseout({
|
||||
...validCloseoutParams,
|
||||
rollbackDrillDate: "2026-02-31",
|
||||
nowMs: Date.parse("2026-06-17T00:00:00Z"),
|
||||
});
|
||||
|
||||
expect(result.errors).toContain("rollback drill date is invalid: 2026-02-31.");
|
||||
});
|
||||
|
||||
it("rejects speculative main state, appcast drift, and stale rollback drills", () => {
|
||||
const result = verifyStableMainCloseout({
|
||||
...validCloseoutParams,
|
||||
mainPackageJson: { version: "2026.6.9" },
|
||||
mainChangelog: changelog.replace("Shipped fix.", "Different fix."),
|
||||
mainAppcast: "https://example.test/old.zip\n",
|
||||
rollbackDrillId: "rollback-drill-2026-q1",
|
||||
rollbackDrillDate: "2026-03-01",
|
||||
nowMs: Date.parse("2026-06-17T00:00:00Z"),
|
||||
});
|
||||
|
||||
expect(result.errors).toContain(
|
||||
"main package.json version is 2026.6.9, expected shipped version 2026.6.8.",
|
||||
);
|
||||
expect(result.errors).toContain(
|
||||
"main CHANGELOG.md ## 2026.6.8 does not exactly match the shipped release section.",
|
||||
);
|
||||
expect(result.errors).toContain(
|
||||
"main appcast.xml does not point at OpenClaw-2026.6.8.zip from v2026.6.8.",
|
||||
);
|
||||
expect(result.errors).toContain(
|
||||
"rollback drill is older than 90 days: 2026-03-01. Run the private rollback drill before stable closeout.",
|
||||
);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue