diff --git a/.agents/skills/openclaw-release-maintainer/SKILL.md b/.agents/skills/openclaw-release-maintainer/SKILL.md index fbb125aeec9..38953426191 100644 --- a/.agents/skills/openclaw-release-maintainer/SKILL.md +++ b/.agents/skills/openclaw-release-maintainer/SKILL.md @@ -120,6 +120,10 @@ node --import tsx scripts/openclaw-npm-postpublish-verify.ts `.github/workflows/openclaw-npm-release.yml`, but it still needs a valid `NPM_TOKEN` because `npm dist-tag` management is separate from trusted publishing. +- Direct stable publishes can also run the same workflow with + `sync_stable_dist_tags=true` to point both `latest` and `beta` at the + already-published stable version. This also needs the `npm-release` + environment approval and `NPM_TOKEN`. - The publish run must be started manually with `workflow_dispatch`. - The npm workflow and the private mac publish workflow accept `preflight_only=true` to run validation/build/package steps without uploading @@ -248,19 +252,25 @@ node --import tsx scripts/openclaw-npm-postpublish-verify.ts passes with the same stable tag, `promote_beta_to_latest=true`, `preflight_only=false`, empty `preflight_run_id`, and `npm_dist_tag=beta`, then verify `latest` now points at that version. -17. Start +17. If the stable release was published directly to `latest` and `beta` should + follow it, start `.github/workflows/openclaw-npm-release.yml` again with + the same stable tag, `sync_stable_dist_tags=true`, + `promote_beta_to_latest=false`, `preflight_only=false`, empty + `preflight_run_id`, and `npm_dist_tag=latest`, then verify both `latest` + and `beta` point at that version. +18. Start `openclaw/releases-private/.github/workflows/openclaw-macos-publish.yml` for the real publish with the successful private mac `preflight_run_id` and wait for success. -18. Verify the successful real private mac run uploaded the `.zip`, `.dmg`, +19. Verify the successful real private mac run uploaded the `.zip`, `.dmg`, and `.dSYM.zip` artifacts to the existing GitHub release in `openclaw/openclaw`. -19. For stable releases, download `macos-appcast-` from the successful +20. For stable releases, download `macos-appcast-` from the successful private mac run, update `appcast.xml` on `main`, and verify the feed. -20. For beta releases, publish the mac assets but expect no shared production +21. For beta releases, publish the mac assets but expect no shared production `appcast.xml` artifact and do not update the shared production feed unless a separate beta feed exists. -21. After publish, verify npm and the attached release artifacts. +22. After publish, verify npm and the attached release artifacts. ## GHSA advisory work diff --git a/.github/workflows/openclaw-npm-release.yml b/.github/workflows/openclaw-npm-release.yml index 89e92c12271..13f583c13a9 100644 --- a/.github/workflows/openclaw-npm-release.yml +++ b/.github/workflows/openclaw-npm-release.yml @@ -29,9 +29,14 @@ on: required: true default: false type: boolean + sync_stable_dist_tags: + description: Skip publish and point both latest and beta at an already-published stable version + required: true + default: false + type: boolean concurrency: - group: openclaw-npm-release-${{ github.event_name == 'workflow_dispatch' && format('{0}-{1}-{2}', inputs.tag, inputs.npm_dist_tag, inputs.promote_beta_to_latest) || github.ref }} + group: openclaw-npm-release-${{ github.event_name == 'workflow_dispatch' && format('{0}-{1}-{2}-{3}', inputs.tag, inputs.npm_dist_tag, inputs.promote_beta_to_latest, inputs.sync_stable_dist_tags) || github.ref }} cancel-in-progress: false env: @@ -44,7 +49,7 @@ jobs: # KEEP THIS WORKFLOW SHORT AND DETERMINISTIC OR IT CAN GET STUCK AND JEOPARDIZE THE RELEASE. # RELEASE-TIME LIVE OR END-TO-END VALIDATION BELONGS IN openclaw-release-checks.yml. preflight_openclaw_npm: - if: ${{ inputs.preflight_only && !inputs.promote_beta_to_latest }} + if: ${{ inputs.preflight_only && !inputs.promote_beta_to_latest && !inputs.sync_stable_dist_tags }} runs-on: blacksmith-32vcpu-ubuntu-2404 permissions: contents: read @@ -241,7 +246,7 @@ jobs: if-no-files-found: error validate_publish_request: - if: ${{ !inputs.preflight_only && !inputs.promote_beta_to_latest }} + if: ${{ !inputs.preflight_only && !inputs.promote_beta_to_latest && !inputs.sync_stable_dist_tags }} runs-on: blacksmith-32vcpu-ubuntu-2404 permissions: contents: read @@ -270,7 +275,7 @@ jobs: # KEEP THE REAL RELEASE/PUBLISH PATH ON A GITHUB-HOSTED RUNNER. # npm trusted publishing + provenance requires this to stay on ubuntu-latest. needs: [validate_publish_request] - if: ${{ !inputs.preflight_only && !inputs.promote_beta_to_latest }} + if: ${{ !inputs.preflight_only && !inputs.promote_beta_to_latest && !inputs.sync_stable_dist_tags }} runs-on: ubuntu-latest environment: npm-release permissions: @@ -411,7 +416,7 @@ jobs: # KEEP THE MUTATING RELEASE PATH ON A GITHUB-HOSTED RUNNER TOO. # This job changes the public npm dist-tags, so we keep it aligned with the # real release path instead of moving it onto the larger Blacksmith runners. - if: ${{ inputs.promote_beta_to_latest }} + if: ${{ inputs.promote_beta_to_latest && !inputs.sync_stable_dist_tags }} runs-on: ubuntu-latest environment: npm-release permissions: @@ -506,3 +511,101 @@ jobs: exit 1 fi echo "Promoted openclaw@${RELEASE_VERSION} from beta to latest." + + sync_stable_dist_tags: + # This mode is for direct stable publishes where latest is correct but beta + # should also point at the same stable build after release. + if: ${{ inputs.sync_stable_dist_tags && !inputs.promote_beta_to_latest }} + runs-on: ubuntu-latest + environment: npm-release + permissions: + contents: read + steps: + - name: Require main workflow ref for dist-tag sync + env: + WORKFLOW_REF: ${{ github.ref }} + run: | + set -euo pipefail + if [[ "${WORKFLOW_REF}" != "refs/heads/main" ]]; then + echo "Dist-tag sync runs must be dispatched from main." + exit 1 + fi + + - name: Validate sync inputs + env: + PREFLIGHT_ONLY: ${{ inputs.preflight_only }} + PREFLIGHT_RUN_ID: ${{ inputs.preflight_run_id }} + RELEASE_NPM_DIST_TAG: ${{ inputs.npm_dist_tag }} + run: | + set -euo pipefail + if [[ "${PREFLIGHT_ONLY}" == "true" ]]; then + echo "Dist-tag sync mode cannot run with preflight_only=true." + exit 1 + fi + if [[ -n "${PREFLIGHT_RUN_ID}" ]]; then + echo "Dist-tag sync mode does not use preflight_run_id." + exit 1 + fi + if [[ "${RELEASE_NPM_DIST_TAG}" != "latest" ]]; then + echo "Dist-tag sync mode expects npm_dist_tag=latest because it points latest and beta at the stable version." + exit 1 + fi + + - name: Validate stable tag input format + env: + RELEASE_TAG: ${{ inputs.tag }} + run: | + set -euo pipefail + if [[ ! "${RELEASE_TAG}" =~ ^v[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*(-[1-9][0-9]*)?$ ]]; then + echo "Invalid stable release tag format: ${RELEASE_TAG}" >&2 + exit 1 + fi + echo "RELEASE_VERSION=${RELEASE_TAG#v}" >> "$GITHUB_ENV" + + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup Node environment + uses: ./.github/actions/setup-node-env + with: + node-version: ${{ env.NODE_VERSION }} + pnpm-version: ${{ env.PNPM_VERSION }} + install-bun: "false" + use-sticky-disk: "false" + install-deps: "false" + + - name: Validate published stable version + env: + RELEASE_VERSION: ${{ env.RELEASE_VERSION }} + run: | + set -euo pipefail + if ! npm view "openclaw@${RELEASE_VERSION}" version >/dev/null 2>&1; then + echo "openclaw@${RELEASE_VERSION} is not published on npm." >&2 + exit 1 + fi + + echo "Current latest dist-tag: $(npm view openclaw dist-tags.latest)" + echo "Current beta dist-tag: $(npm view openclaw dist-tags.beta)" + + - name: Sync stable dist-tags + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + RELEASE_VERSION: ${{ env.RELEASE_VERSION }} + run: | + set -euo pipefail + printf '//registry.npmjs.org/:_authToken=%s\n' "${NODE_AUTH_TOKEN}" > "${HOME}/.npmrc" + npm whoami >/dev/null + npm dist-tag add "openclaw@${RELEASE_VERSION}" latest + npm dist-tag add "openclaw@${RELEASE_VERSION}" beta + + synced_latest="$(npm view openclaw dist-tags.latest)" + synced_beta="$(npm view openclaw dist-tags.beta)" + if [[ "${synced_latest}" != "${RELEASE_VERSION}" ]]; then + echo "npm latest points at ${synced_latest}, expected ${RELEASE_VERSION} after sync." >&2 + exit 1 + fi + if [[ "${synced_beta}" != "${RELEASE_VERSION}" ]]; then + echo "npm beta points at ${synced_beta}, expected ${RELEASE_VERSION} after sync." >&2 + exit 1 + fi + echo "Synced openclaw@${RELEASE_VERSION} to npm latest and beta." diff --git a/docs/reference/RELEASING.md b/docs/reference/RELEASING.md index 1ab6fb5dfe1..97432a7fa22 100644 --- a/docs/reference/RELEASING.md +++ b/docs/reference/RELEASING.md @@ -75,7 +75,9 @@ OpenClaw has three public release lanes: - stable npm releases default to `beta` - stable npm publish can target `latest` explicitly via workflow input - stable npm promotion from `beta` to `latest` is still available as an explicit manual mode on the trusted `OpenClaw NPM Release` workflow - - that promotion mode still needs a valid `NPM_TOKEN` in the `npm-release` environment because npm `dist-tag` management is separate from trusted publishing + - direct stable publishes can also run an explicit dist-tag sync mode that + points both `latest` and `beta` at the already-published stable version + - those dist-tag modes still need a valid `NPM_TOKEN` in the `npm-release` environment because npm `dist-tag` management is separate from trusted publishing - public `macOS Release` is validation-only - real private mac publish must pass successful private mac `preflight_run_id` and `validate_run_id` @@ -113,6 +115,8 @@ OpenClaw has three public release lanes: - `npm_dist_tag`: npm target tag for the publish path; defaults to `beta` - `promote_beta_to_latest`: `true` to skip publish and move an already-published stable `beta` build onto `latest` +- `sync_stable_dist_tags`: `true` to skip publish and point both `latest` and + `beta` at an already-published stable version `OpenClaw Release Checks` accepts these operator-controlled inputs: @@ -129,8 +133,12 @@ Rules: the workflow verifies that metadata before publish continues - Promotion mode must use a stable or correction tag, `preflight_only=false`, an empty `preflight_run_id`, and `npm_dist_tag=beta` -- Promotion mode also requires a valid `NPM_TOKEN` in the `npm-release` - environment because `npm dist-tag add` still needs regular npm auth +- Dist-tag sync mode must use a stable or correction tag, + `preflight_only=false`, an empty `preflight_run_id`, `npm_dist_tag=latest`, + and `promote_beta_to_latest=false` +- Promotion and dist-tag sync modes also require a valid `NPM_TOKEN` in the + `npm-release` environment because `npm dist-tag add` still needs regular npm + auth ## Stable npm release sequence @@ -152,9 +160,13 @@ When cutting a stable npm release: same stable `tag`, `promote_beta_to_latest=true`, `preflight_only=false`, `preflight_run_id` empty, and `npm_dist_tag=beta` when you want to move that published build to `latest` +7. If the release intentionally published directly to `latest` and `beta` + should follow the same stable build, run `OpenClaw NPM Release` with the same + stable `tag`, `sync_stable_dist_tags=true`, `promote_beta_to_latest=false`, + `preflight_only=false`, `preflight_run_id` empty, and `npm_dist_tag=latest` -The promotion mode still requires the `npm-release` environment approval and a -valid `NPM_TOKEN` in that environment. +The promotion and dist-tag sync modes still require the `npm-release` +environment approval and a valid `NPM_TOKEN` in that environment. That keeps the direct publish path and the beta-first promotion path both documented and operator-visible.