diff --git a/.agents/skills/openclaw-release-maintainer/SKILL.md b/.agents/skills/openclaw-release-maintainer/SKILL.md index eae99eddee4..f382d7587e6 100644 --- a/.agents/skills/openclaw-release-maintainer/SKILL.md +++ b/.agents/skills/openclaw-release-maintainer/SKILL.md @@ -119,6 +119,9 @@ node --import tsx scripts/openclaw-npm-postpublish-verify.ts - Stable npm promotion from `beta` to `latest` also uses GitHub Actions OIDC by exchanging the workflow token for a short-lived npm registry token; it should not depend on a stored `NPM_TOKEN`. +- Keep that promotion inside `.github/workflows/openclaw-npm-release.yml`. + npm trusted publishing is bound to the trusted workflow file identity, so a + separate promotion workflow needs its own npm trust configuration. - 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 @@ -243,9 +246,10 @@ node --import tsx scripts/openclaw-npm-postpublish-verify.ts preflight run, and pass the successful npm `preflight_run_id`. 15. Wait for `npm-release` approval from `@openclaw/openclaw-release-managers`. 16. If the stable release was published to `beta`, start - `.github/workflows/openclaw-npm-promote-beta.yml` with the exact stable - version after beta validation passes, then verify `latest` now points at - that version. + `.github/workflows/openclaw-npm-release.yml` again after beta validation + 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 `openclaw/releases-private/.github/workflows/openclaw-macos-publish.yml` for the real publish with the successful private mac `preflight_run_id` and diff --git a/.github/workflows/openclaw-npm-promote-beta.yml b/.github/workflows/openclaw-npm-promote-beta.yml deleted file mode 100644 index bea8ec1f48a..00000000000 --- a/.github/workflows/openclaw-npm-promote-beta.yml +++ /dev/null @@ -1,89 +0,0 @@ -name: OpenClaw NPM Promote Beta - -on: - workflow_dispatch: - inputs: - version: - description: Stable version currently on npm beta to promote to latest (for example 2026.4.2 or 2026.4.2-1) - required: true - type: string - -concurrency: - group: openclaw-npm-promote-beta-${{ inputs.version }} - cancel-in-progress: false - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" - NODE_VERSION: "24.x" - PNPM_VERSION: "10.23.0" - -jobs: - promote_beta_to_latest: - runs-on: ubuntu-latest - environment: npm-release - permissions: - contents: read - id-token: write - steps: - - name: Validate version input format - env: - RELEASE_VERSION: ${{ inputs.version }} - run: | - set -euo pipefail - if [[ ! "${RELEASE_VERSION}" =~ ^[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*(-[1-9][0-9]*)?$ ]]; then - echo "Invalid stable release version format: ${RELEASE_VERSION}" >&2 - exit 1 - fi - - - 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 npm dist-tags - env: - RELEASE_VERSION: ${{ inputs.version }} - run: | - set -euo pipefail - beta_version="$(npm view openclaw dist-tags.beta)" - latest_version="$(npm view openclaw dist-tags.latest)" - - echo "Current beta dist-tag: ${beta_version}" - echo "Current latest dist-tag: ${latest_version}" - - if [[ "${beta_version}" != "${RELEASE_VERSION}" ]]; then - echo "npm beta points at ${beta_version}, expected ${RELEASE_VERSION}." >&2 - exit 1 - fi - - 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 - - - name: Promote beta to latest - env: - RELEASE_VERSION: ${{ inputs.version }} - run: | - set -euo pipefail - registry_token="$(node scripts/npm-oidc-exchange-token.mjs openclaw)" - userconfig="$(mktemp)" - trap 'rm -f "${userconfig}"' EXIT - chmod 0600 "${userconfig}" - printf '%s\n' "//registry.npmjs.org/:_authToken=${registry_token}" > "${userconfig}" - - NPM_CONFIG_USERCONFIG="${userconfig}" \ - npm dist-tag add "openclaw@${RELEASE_VERSION}" latest - promoted_latest="$(npm view openclaw dist-tags.latest)" - if [[ "${promoted_latest}" != "${RELEASE_VERSION}" ]]; then - echo "npm latest points at ${promoted_latest}, expected ${RELEASE_VERSION} after promotion." >&2 - exit 1 - fi - echo "Promoted openclaw@${RELEASE_VERSION} from beta to latest." diff --git a/.github/workflows/openclaw-npm-release.yml b/.github/workflows/openclaw-npm-release.yml index 90675cd17f4..a1db6c324c5 100644 --- a/.github/workflows/openclaw-npm-release.yml +++ b/.github/workflows/openclaw-npm-release.yml @@ -24,9 +24,14 @@ on: options: - beta - latest + promote_beta_to_latest: + description: Skip publish and promote the stable version already on npm beta to latest + required: true + default: false + type: boolean concurrency: - group: openclaw-npm-release-${{ github.event_name == 'workflow_dispatch' && format('{0}-{1}', inputs.tag, inputs.npm_dist_tag) || github.ref }} + 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 }} cancel-in-progress: false env: @@ -36,7 +41,7 @@ env: jobs: preflight_openclaw_npm: - if: ${{ inputs.preflight_only }} + if: ${{ inputs.preflight_only && !inputs.promote_beta_to_latest }} runs-on: ubuntu-latest permissions: contents: read @@ -157,7 +162,7 @@ jobs: if-no-files-found: error validate_publish_request: - if: ${{ !inputs.preflight_only }} + if: ${{ !inputs.preflight_only && !inputs.promote_beta_to_latest }} runs-on: ubuntu-latest permissions: contents: read @@ -185,7 +190,7 @@ jobs: publish_openclaw_npm: # npm trusted publishing + provenance requires a GitHub-hosted runner. needs: [validate_publish_request] - if: ${{ !inputs.preflight_only }} + if: ${{ !inputs.preflight_only && !inputs.promote_beta_to_latest }} runs-on: ubuntu-latest environment: npm-release permissions: @@ -321,3 +326,105 @@ jobs: publish_target="./${publish_target}" fi bash scripts/openclaw-npm-publish.sh --publish "${publish_target}" + + promote_beta_to_latest: + if: ${{ inputs.promote_beta_to_latest }} + runs-on: ubuntu-latest + environment: npm-release + permissions: + contents: read + id-token: write + steps: + - name: Require main workflow ref for promotion + env: + WORKFLOW_REF: ${{ github.ref }} + run: | + set -euo pipefail + if [[ "${WORKFLOW_REF}" != "refs/heads/main" ]]; then + echo "Promotion runs must be dispatched from main." + exit 1 + fi + + - name: Validate promotion 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 "Promotion mode cannot run with preflight_only=true." + exit 1 + fi + if [[ -n "${PREFLIGHT_RUN_ID}" ]]; then + echo "Promotion mode does not use preflight_run_id." + exit 1 + fi + if [[ "${RELEASE_NPM_DIST_TAG}" != "beta" ]]; then + echo "Promotion mode expects npm_dist_tag=beta because it moves beta to latest without publishing." + 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 npm dist-tags + env: + RELEASE_VERSION: ${{ env.RELEASE_VERSION }} + run: | + set -euo pipefail + beta_version="$(npm view openclaw dist-tags.beta)" + latest_version="$(npm view openclaw dist-tags.latest)" + + echo "Current beta dist-tag: ${beta_version}" + echo "Current latest dist-tag: ${latest_version}" + + if [[ "${beta_version}" != "${RELEASE_VERSION}" ]]; then + echo "npm beta points at ${beta_version}, expected ${RELEASE_VERSION}." >&2 + exit 1 + fi + + 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 + + - name: Promote beta to latest + env: + RELEASE_VERSION: ${{ env.RELEASE_VERSION }} + run: | + set -euo pipefail + registry_token="$(node scripts/npm-oidc-exchange-token.mjs openclaw)" + userconfig="$(mktemp)" + trap 'rm -f "${userconfig}"' EXIT + chmod 0600 "${userconfig}" + printf '%s\n' "//registry.npmjs.org/:_authToken=${registry_token}" > "${userconfig}" + + NPM_CONFIG_USERCONFIG="${userconfig}" \ + npm dist-tag add "openclaw@${RELEASE_VERSION}" latest + promoted_latest="$(npm view openclaw dist-tags.latest)" + if [[ "${promoted_latest}" != "${RELEASE_VERSION}" ]]; then + echo "npm latest points at ${promoted_latest}, expected ${RELEASE_VERSION} after promotion." >&2 + exit 1 + fi + echo "Promoted openclaw@${RELEASE_VERSION} from beta to latest." diff --git a/docs/reference/RELEASING.md b/docs/reference/RELEASING.md index 3e147ba9a72..9bd75406359 100644 --- a/docs/reference/RELEASING.md +++ b/docs/reference/RELEASING.md @@ -51,8 +51,8 @@ OpenClaw has three public release lanes: - real npm publish must pass a successful npm `preflight_run_id` - 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 a separate manual workflow step - - that promotion workflow exchanges the GitHub Actions OIDC token for a short-lived npm registry token instead of depending on a stored `NPM_TOKEN` + - 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 exchanges the GitHub Actions OIDC token for a short-lived npm registry token instead of depending on a stored `NPM_TOKEN` - public `macOS Release` is validation-only - real private mac publish must pass successful private mac `preflight_run_id` and `validate_run_id` @@ -87,6 +87,8 @@ OpenClaw has three public release lanes: - `preflight_run_id`: required on the real publish path so the workflow reuses the prepared tarball from the successful preflight run - `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` Rules: @@ -94,6 +96,10 @@ Rules: - Beta prerelease tags may publish only to `beta` - The real publish path must use the same `npm_dist_tag` used during preflight; 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 stays inside the trusted `OpenClaw NPM Release` workflow file + because npm trusted publishing is bound to that workflow identity ## Stable npm release sequence @@ -105,11 +111,12 @@ When cutting a stable npm release: 3. Save the successful `preflight_run_id` 4. Run `OpenClaw NPM Release` again with `preflight_only=false`, the same `tag`, the same `npm_dist_tag`, and the saved `preflight_run_id` -5. If the release landed on `beta`, run `OpenClaw NPM Promote Beta` later with - the exact stable version when you want to move that published build to - `latest` +5. If the release landed on `beta`, run `OpenClaw NPM Release` later with the + 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` -The promotion workflow still requires the `npm-release` environment approval, +The promotion mode still requires the `npm-release` environment approval, but it no longer depends on a long-lived npm publish token. That keeps the direct publish path and the beta-first promotion path both @@ -118,7 +125,7 @@ documented and operator-visible. ## Public references - [`.github/workflows/openclaw-npm-release.yml`](https://github.com/openclaw/openclaw/blob/main/.github/workflows/openclaw-npm-release.yml) -- [`.github/workflows/openclaw-npm-promote-beta.yml`](https://github.com/openclaw/openclaw/blob/main/.github/workflows/openclaw-npm-promote-beta.yml) +- [`scripts/npm-oidc-exchange-token.mjs`](https://github.com/openclaw/openclaw/blob/main/scripts/npm-oidc-exchange-token.mjs) - [`scripts/openclaw-npm-release-check.ts`](https://github.com/openclaw/openclaw/blob/main/scripts/openclaw-npm-release-check.ts) - [`scripts/package-mac-dist.sh`](https://github.com/openclaw/openclaw/blob/main/scripts/package-mac-dist.sh) - [`scripts/make_appcast.sh`](https://github.com/openclaw/openclaw/blob/main/scripts/make_appcast.sh)