From 8dff2898f68a0bc92fa633d9b181c3b06ba0cdab Mon Sep 17 00:00:00 2001 From: Dallin Romney Date: Thu, 13 Aug 2026 12:58:58 +0800 Subject: [PATCH] ci: simplify extended-stable release finalization --- .agents/skills/openclaw-testing/SKILL.md | 5 +- .../release-openclaw-maintainer/SKILL.md | 14 +-- .../workflows/openclaw-release-publish.yml | 104 +++++++----------- docs/ci.md | 6 +- docs/reference/RELEASING.md | 12 +- test/scripts/release-no-push-workflow.test.ts | 91 ++++++++------- 6 files changed, 100 insertions(+), 132 deletions(-) diff --git a/.agents/skills/openclaw-testing/SKILL.md b/.agents/skills/openclaw-testing/SKILL.md index e5877d51dcaa..cec7c586c720 100644 --- a/.agents/skills/openclaw-testing/SKILL.md +++ b/.agents/skills/openclaw-testing/SKILL.md @@ -319,8 +319,9 @@ node scripts/full-release-validation-at-sha.mjs \ Extended-stable may use the helper's trusted main-pinned `release-ci/*` harness or a direct run from `extended-stable/YYYY.M.33` with -`release_profile=stable`. In either case, require a complete v3 manifest that -binds the exact canonical branch tip, workflow SHA, and run attempt. Use +`release_profile=stable`. In either case, require a complete Full Release +Validation evidence manifest using schema version 3. It must bind the exact +canonical branch tip, workflow SHA, and run attempt. Use `$release-openclaw-ci` for failure classification and identity rules. The helper verifies and pins the recorded Tooling SHA on trusted `main`, passes diff --git a/.agents/skills/release-openclaw-maintainer/SKILL.md b/.agents/skills/release-openclaw-maintainer/SKILL.md index 67af6260283d..80c377fec54b 100644 --- a/.agents/skills/release-openclaw-maintainer/SKILL.md +++ b/.agents/skills/release-openclaw-maintainer/SKILL.md @@ -304,13 +304,13 @@ on pinned current `main` as the exact command and validation contract. approval-gated `docker-channel-promote.yml` from current `main` with the exact tag; never rebuild or move the release tag. 10. Dispatch the extended-stable `OpenClaw Release Publish` closeout. It must - reverify the exact npm selector and tarball, render the canonical release - notes, publish Docker, and only then create the public non-prerelease GitHub - Release with `latest=false`. Re-run failed jobs to retry a failed finalizer - without repeating successful Docker work; a fresh dispatch safely repeats - same-version Docker verification and promotion. Do not publish macOS, - Windows, mobile, website, ClawHub, npm `latest`, or private dist-tag - artifacts from this path. + reverify the exact npm selector and tarball, then publish Docker. Only after + Docker succeeds does the finalizer render the canonical release notes and + create the public non-prerelease GitHub Release with `latest=false`. Re-run + failed jobs to retry a failed finalizer without repeating successful Docker + work; a fresh dispatch safely repeats same-version Docker verification and + promotion. Do not publish macOS, Windows, mobile, website, ClawHub, npm + `latest`, or private dist-tag artifacts from this path. ## Keep release channel naming aligned diff --git a/.github/workflows/openclaw-release-publish.yml b/.github/workflows/openclaw-release-publish.yml index 1442d85a3c64..9c57206257da 100644 --- a/.github/workflows/openclaw-release-publish.yml +++ b/.github/workflows/openclaw-release-publish.yml @@ -2492,8 +2492,8 @@ jobs: path: ${{ runner.temp }}/openclaw-release-postpublish-evidence if-no-files-found: error - prepare_extended_stable_release: - name: Verify npm and prepare extended-stable release notes + verify_core_npm_registry: + name: Verify already-published core npm package needs: [resolve_release_target] if: ${{ inputs.publish_docker_only }} runs-on: ubuntu-latest @@ -2524,60 +2524,10 @@ jobs: exit 1 fi - - name: Checkout trusted release tooling - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - ref: ${{ github.sha }} - fetch-depth: 1 - persist-credentials: false - - - name: Setup trusted release tooling - uses: ./.github/actions/setup-node-env - with: - install-bun: "false" - - - name: Render canonical extended-stable release notes - env: - RELEASE_TAG: ${{ inputs.tag }} - TARGET_SHA: ${{ needs.resolve_release_target.outputs.sha }} - run: | - set -euo pipefail - release_version="${RELEASE_TAG#v}" - notes_dir="${RUNNER_TEMP}/extended-stable-release-notes" - changelog_file="${notes_dir}/CHANGELOG.md" - notes_file="${notes_dir}/release-notes.md" - verification_file="${notes_dir}/verification.md" - mkdir -p "${notes_dir}" - - git fetch --no-tags --depth=1 origin "${TARGET_SHA}" - git show "${TARGET_SHA}:CHANGELOG.md" > "${changelog_file}" - cat > "${verification_file}" <&2 - exit 1 - fi + git fetch --no-tags --depth=1 origin "${TARGET_SHA}" + git show "${TARGET_SHA}:CHANGELOG.md" > "${changelog_file}" + cat > "${verification_file}" < { expect(dockerCall.needs).toEqual([ "resolve_release_target", "publish", - "prepare_extended_stable_release", + "verify_core_npm_registry", ]); expect(dockerCall.if).toContain("needs.publish.result == 'success'"); - expect(dockerCall.if).toContain("needs.prepare_extended_stable_release.result == 'success'"); + expect(dockerCall.if).toContain("needs.verify_core_npm_registry.result == 'success'"); expect(dockerCall.with).toEqual({ tag: "${{ inputs.tag }}", release_sha: "${{ needs.resolve_release_target.outputs.sha }}", @@ -1104,66 +1104,59 @@ describe("release validation no-push transport", () => { "publish_docker", ]); - const prepareRelease = job(releasePublish, "prepare_extended_stable_release"); - const prepareSteps = prepareRelease.steps ?? []; - const prepareStepNames = prepareSteps.map((workflowStep) => workflowStep.name); + const verifyRegistry = job(releasePublish, "verify_core_npm_registry"); const verifyNpm = step( - prepareRelease, + verifyRegistry, "Verify exact npm and selector readback matches preflight bytes", ); - const renderNotes = step(prepareRelease, "Render canonical extended-stable release notes"); - const checkoutIndex = prepareStepNames.indexOf("Checkout trusted release tooling"); - const setupIndex = prepareStepNames.indexOf("Setup trusted release tooling"); - const renderIndex = prepareStepNames.indexOf("Render canonical extended-stable release notes"); - const uploadIndex = prepareStepNames.indexOf("Upload canonical extended-stable release notes"); - expect(prepareRelease.needs).toEqual(["resolve_release_target"]); - expect(prepareRelease.if).toBe("${{ inputs.publish_docker_only }}"); - expect(prepareRelease.environment).toBeUndefined(); - expect(prepareRelease.permissions).toEqual({ contents: "read" }); - expect(setupIndex).toBeGreaterThan(checkoutIndex); - expect(renderIndex).toBeGreaterThan(setupIndex); - expect(uploadIndex).toBeGreaterThan(renderIndex); + expect(verifyRegistry.needs).toEqual(["resolve_release_target"]); + expect(verifyRegistry.if).toBe("${{ inputs.publish_docker_only }}"); + expect(verifyRegistry.environment).toBeUndefined(); + expect(verifyRegistry.permissions).toEqual({ contents: "read" }); + expect(verifyRegistry.steps).toHaveLength(1); expect(verifyNpm.run).toContain('npm view "openclaw@${version}" version'); expect(verifyNpm.run).toContain("Published npm tarball does not match"); - expect(step(prepareRelease, "Checkout trusted release tooling").with).toMatchObject({ - ref: "${{ github.sha }}", - "persist-credentials": false, - }); - expect(step(prepareRelease, "Setup trusted release tooling")).toMatchObject({ - uses: "./.github/actions/setup-node-env", - with: { "install-bun": "false" }, - }); - expect(renderNotes.run).toContain('git fetch --no-tags --depth=1 origin "${TARGET_SHA}"'); - expect(renderNotes.run).toContain('git show "${TARGET_SHA}:CHANGELOG.md"'); - expect(renderNotes.run).toContain("node --import tsx scripts/render-github-release-notes.mts"); - expect(renderNotes.run).not.toContain("render-github-release-notes.mjs"); - const uploadNotes = step(prepareRelease, "Upload canonical extended-stable release notes"); - expect(uploadNotes.with).toMatchObject({ - name: "extended-stable-release-notes-${{ inputs.tag }}", - "if-no-files-found": "error", - }); const finalizeRelease = job(releasePublish, "finalize_extended_stable_github_release"); - const downloadNotes = step(finalizeRelease, "Download canonical extended-stable release notes"); - const publishRelease = step(finalizeRelease, "Publish canonical extended-stable release"); + const finalizeSteps = finalizeRelease.steps ?? []; + const finalizeStepNames = finalizeSteps.map((workflowStep) => workflowStep.name); + const checkoutIndex = finalizeStepNames.indexOf("Checkout trusted release tooling"); + const setupIndex = finalizeStepNames.indexOf("Setup trusted release tooling"); + const publishIndex = finalizeStepNames.indexOf( + "Render and publish canonical extended-stable release", + ); + const publishRelease = step( + finalizeRelease, + "Render and publish canonical extended-stable release", + ); const publishReleaseRun = publishRelease.run ?? ""; - expect(finalizeRelease.needs).toEqual([ - "resolve_release_target", - "prepare_extended_stable_release", - "publish_docker", - ]); + expect(finalizeRelease.needs).toEqual(["resolve_release_target", "publish_docker"]); expect(finalizeRelease.if).toContain("inputs.publish_docker_only"); - expect(finalizeRelease.if).toContain( - "needs.prepare_extended_stable_release.result == 'success'", - ); expect(finalizeRelease.if).toContain("needs.publish_docker.result == 'success'"); expect(finalizeRelease.environment).toBe("npm-release"); expect(finalizeRelease.permissions).toEqual({ contents: "write" }); - expect(downloadNotes.with).toMatchObject({ - name: "extended-stable-release-notes-${{ inputs.tag }}", + expect(checkoutIndex).toBeGreaterThan(-1); + expect(setupIndex).toBeGreaterThan(checkoutIndex); + expect(publishIndex).toBeGreaterThan(setupIndex); + expect(step(finalizeRelease, "Checkout trusted release tooling").with).toMatchObject({ + ref: "${{ github.sha }}", + "persist-credentials": false, }); + expect(step(finalizeRelease, "Setup trusted release tooling")).toMatchObject({ + uses: "./.github/actions/setup-node-env", + with: { "install-bun": "false" }, + }); + expect(publishReleaseRun).toContain('git fetch --no-tags --depth=1 origin "${TARGET_SHA}"'); + expect(publishReleaseRun).toContain('git show "${TARGET_SHA}:CHANGELOG.md"'); + expect(publishReleaseRun).toContain( + "node --import tsx scripts/render-github-release-notes.mts", + ); + expect(publishReleaseRun).not.toContain("render-github-release-notes.mjs"); + expect(publishReleaseRun.indexOf("render-github-release-notes.mts")).toBeLessThan( + publishReleaseRun.indexOf('gh release create "${RELEASE_TAG}"'), + ); expect(publishReleaseRun).toContain("verify_release_tag_target"); expect(publishReleaseRun).toContain('gh release create "${RELEASE_TAG}"'); expect(publishReleaseRun).toContain("--verify-tag"); @@ -1181,6 +1174,8 @@ describe("release validation no-push transport", () => { expect(publishReleaseRun).not.toContain("/statuses"); const releasePublishText = readFileSync(releasePublishPath, "utf8"); + expect(releasePublishText).not.toContain("prepare_extended_stable_release"); + expect(releasePublishText).not.toContain("extended-stable-release-notes-${{ inputs.tag }}"); expect(releasePublishText).not.toContain("verify_extended_stable_docker_completion"); expect(releasePublishText).not.toContain("Docker completion status"); expect(JSON.stringify(dockerRelease)).not.toContain("statuses"); @@ -1199,7 +1194,7 @@ describe("release validation no-push transport", () => { const releasePublish = readWorkflow(".github/workflows/openclaw-release-publish.yml"); const publishDraft = step( job(releasePublish, "finalize_extended_stable_github_release"), - "Publish canonical extended-stable release", + "Render and publish canonical extended-stable release", ); const verifyTag = shellFunctionSource(publishDraft.run ?? "", "verify_release_tag_target"); const targetSha = "a".repeat(40);