OpenClaw NPM Dist-Tag Operations #133
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: OpenClaw NPM Dist-Tag Operations | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| mode: | |
| description: Dist-tag operation to run | |
| required: true | |
| default: sync_beta_to_stable | |
| type: choice | |
| options: | |
| - sync_beta_to_stable | |
| - promote_beta_to_latest | |
| - sync_stable_dist_tags | |
| tag: | |
| description: Stable OpenClaw release tag for manual promotion and manual dist-tag sync (for example v2026.4.14 or v2026.4.14-1) | |
| required: false | |
| type: string | |
| schedule: | |
| - cron: "17 5 * * *" | |
| concurrency: | |
| group: openclaw-npm-dist-tags | |
| cancel-in-progress: false | |
| env: | |
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" | |
| NODE_VERSION: "24.x" | |
| OPENCLAW_REPOSITORY: openclaw/openclaw | |
| jobs: | |
| sync_beta_to_stable: | |
| if: ${{ github.event_name == 'schedule' || inputs.mode == 'sync_beta_to_stable' }} | |
| runs-on: ubuntu-latest | |
| 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: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| - name: Resolve npm dist-tag state | |
| id: resolve_state | |
| run: | | |
| set -euo pipefail | |
| latest_version="$(npm view openclaw dist-tags.latest)" | |
| beta_version="$(npm view openclaw dist-tags.beta)" | |
| echo "Current latest dist-tag: ${latest_version}" | |
| echo "Current beta dist-tag: ${beta_version}" | |
| if [[ -z "${latest_version}" || -z "${beta_version}" ]]; then | |
| echo "Missing npm dist-tag values." >&2 | |
| exit 1 | |
| fi | |
| LATEST_VERSION="${latest_version}" BETA_VERSION="${beta_version}" node <<'NODE' >> "$GITHUB_OUTPUT" | |
| const latest = process.env.LATEST_VERSION?.trim() ?? ""; | |
| const beta = process.env.BETA_VERSION?.trim() ?? ""; | |
| function fail(message) { | |
| console.error(message); | |
| process.exit(1); | |
| } | |
| function parseRelease(version) { | |
| const match = version.match( | |
| /^(\d{4})\.(\d+)\.(\d+)(?:-(?:(beta)\.(\d+)|(\d+)))?$/, | |
| ); | |
| if (!match) { | |
| return null; | |
| } | |
| const [, year, month, day, betaMarker, betaNumber, correctionNumber] = match; | |
| if (betaMarker) { | |
| return { | |
| year: Number(year), | |
| month: Number(month), | |
| day: Number(day), | |
| lane: "beta", | |
| ordinal: Number(betaNumber), | |
| }; | |
| } | |
| if (correctionNumber) { | |
| return { | |
| year: Number(year), | |
| month: Number(month), | |
| day: Number(day), | |
| lane: "stable", | |
| ordinal: Number(correctionNumber), | |
| }; | |
| } | |
| return { | |
| year: Number(year), | |
| month: Number(month), | |
| day: Number(day), | |
| lane: "stable", | |
| ordinal: 0, | |
| }; | |
| } | |
| function compareRelease(a, b) { | |
| for (const key of ["year", "month", "day"]) { | |
| if (a[key] !== b[key]) { | |
| return a[key] - b[key]; | |
| } | |
| } | |
| const laneRank = { beta: 0, stable: 1 }; | |
| if (laneRank[a.lane] !== laneRank[b.lane]) { | |
| return laneRank[a.lane] - laneRank[b.lane]; | |
| } | |
| return a.ordinal - b.ordinal; | |
| } | |
| const latestParsed = parseRelease(latest); | |
| const betaParsed = parseRelease(beta); | |
| if (!latestParsed) { | |
| fail(`Could not parse npm latest dist-tag version: ${latest}`); | |
| } | |
| if (latestParsed.lane !== "stable") { | |
| fail(`npm latest must point at a stable release, got ${latest}.`); | |
| } | |
| if (!betaParsed) { | |
| fail(`Could not parse npm beta dist-tag version: ${beta}`); | |
| } | |
| let action = "noop"; | |
| let reason = "beta_already_matches_latest"; | |
| if (beta !== latest) { | |
| const betaVsLatest = compareRelease(betaParsed, latestParsed); | |
| if (betaParsed.lane === "beta" && betaVsLatest > 0) { | |
| reason = "beta_points_to_newer_prerelease"; | |
| } else if (betaVsLatest > 0) { | |
| reason = "beta_points_to_newer_release"; | |
| } else { | |
| action = "sync"; | |
| reason = "sync_beta_to_latest_stable"; | |
| } | |
| } | |
| console.log(`release_version=${latest}`); | |
| console.log(`beta_version=${beta}`); | |
| console.log(`action=${action}`); | |
| console.log(`reason=${reason}`); | |
| NODE | |
| - name: Validate stable release tag exists in public repo | |
| if: ${{ steps.resolve_state.outputs.action == 'sync' }} | |
| env: | |
| RELEASE_VERSION: ${{ steps.resolve_state.outputs.release_version }} | |
| run: | | |
| set -euo pipefail | |
| RELEASE_TAG="v${RELEASE_VERSION}" | |
| if ! git ls-remote --exit-code "https://github.com/${OPENCLAW_REPOSITORY}.git" "refs/tags/${RELEASE_TAG}" "refs/tags/${RELEASE_TAG}^{}" >/dev/null; then | |
| echo "Public release tag ${RELEASE_TAG} not found in ${OPENCLAW_REPOSITORY}." >&2 | |
| exit 1 | |
| fi | |
| - name: Sync beta dist-tag to stable | |
| if: ${{ steps.resolve_state.outputs.action == 'sync' }} | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| RELEASE_VERSION: ${{ steps.resolve_state.outputs.release_version }} | |
| run: | | |
| set -euo pipefail | |
| if [[ -z "${NODE_AUTH_TOKEN:-}" ]]; then | |
| echo "Missing NPM_TOKEN secret." >&2 | |
| exit 1 | |
| fi | |
| npm_userconfig="$(mktemp)" | |
| trap 'rm -f "${npm_userconfig}"' EXIT | |
| chmod 0600 "${npm_userconfig}" | |
| printf '//registry.npmjs.org/:_authToken=%s\n' "${NODE_AUTH_TOKEN}" > "${npm_userconfig}" | |
| NPM_CONFIG_USERCONFIG="${npm_userconfig}" npm dist-tag add "openclaw@${RELEASE_VERSION}" beta | |
| synced_beta="$(npm view openclaw dist-tags.beta)" | |
| 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 beta." | |
| - name: Skip sync when npm beta should stay as-is | |
| if: ${{ steps.resolve_state.outputs.action != 'sync' }} | |
| env: | |
| RELEASE_VERSION: ${{ steps.resolve_state.outputs.release_version }} | |
| BETA_VERSION: ${{ steps.resolve_state.outputs.beta_version }} | |
| REASON: ${{ steps.resolve_state.outputs.reason }} | |
| run: | | |
| set -euo pipefail | |
| echo "No dist-tag change needed." | |
| echo "latest=${RELEASE_VERSION}" | |
| echo "beta=${BETA_VERSION}" | |
| echo "reason=${REASON}" | |
| - name: Summarize automatic dist-tag sync | |
| env: | |
| RELEASE_VERSION: ${{ steps.resolve_state.outputs.release_version }} | |
| BETA_VERSION: ${{ steps.resolve_state.outputs.beta_version }} | |
| ACTION: ${{ steps.resolve_state.outputs.action }} | |
| REASON: ${{ steps.resolve_state.outputs.reason }} | |
| run: | | |
| set -euo pipefail | |
| { | |
| echo "## npm dist-tag output" | |
| echo | |
| echo "- Mode: \`sync_beta_to_stable\`" | |
| echo "- npm \`latest\`: \`${RELEASE_VERSION}\`" | |
| echo "- npm \`beta\` before run: \`${BETA_VERSION}\`" | |
| echo "- Action: \`${ACTION}\`" | |
| echo "- Reason: \`${REASON}\`" | |
| if [[ "${ACTION}" == "sync" ]]; then | |
| echo "- Result: npm \`beta\` now points at this stable version" | |
| else | |
| echo "- Result: no change" | |
| fi | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| promote_beta_to_latest: | |
| if: ${{ github.event_name == 'workflow_dispatch' && inputs.mode == 'promote_beta_to_latest' }} | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| 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 stable tag input format | |
| env: | |
| RELEASE_TAG: ${{ inputs.tag }} | |
| run: | | |
| set -euo pipefail | |
| if [[ -z "${RELEASE_TAG}" ]]; then | |
| echo "Manual beta promotion requires a stable release tag." >&2 | |
| exit 1 | |
| fi | |
| 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: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| - name: Validate public stable tag exists | |
| env: | |
| RELEASE_TAG: ${{ inputs.tag }} | |
| run: | | |
| set -euo pipefail | |
| if ! git ls-remote --exit-code "https://github.com/${OPENCLAW_REPOSITORY}.git" "refs/tags/${RELEASE_TAG}" "refs/tags/${RELEASE_TAG}^{}" >/dev/null; then | |
| echo "Public release tag ${RELEASE_TAG} not found in ${OPENCLAW_REPOSITORY}." >&2 | |
| exit 1 | |
| fi | |
| - 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: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| RELEASE_VERSION: ${{ env.RELEASE_VERSION }} | |
| run: | | |
| set -euo pipefail | |
| npm_userconfig="$(mktemp)" | |
| trap 'rm -f "${npm_userconfig}"' EXIT | |
| chmod 0600 "${npm_userconfig}" | |
| printf '//registry.npmjs.org/:_authToken=%s\n' "${NODE_AUTH_TOKEN}" > "${npm_userconfig}" | |
| NPM_CONFIG_USERCONFIG="${npm_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." | |
| - name: Summarize beta promotion | |
| env: | |
| RELEASE_TAG: ${{ inputs.tag }} | |
| RELEASE_VERSION: ${{ env.RELEASE_VERSION }} | |
| run: | | |
| set -euo pipefail | |
| { | |
| echo "## npm dist-tag output" | |
| echo | |
| echo "- Mode: \`promote_beta_to_latest\`" | |
| echo "- Release tag: \`${RELEASE_TAG}\`" | |
| echo "- Version: \`${RELEASE_VERSION}\`" | |
| echo "- Result: npm \`latest\` now points at this stable version" | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| sync_stable_dist_tags: | |
| if: ${{ github.event_name == 'workflow_dispatch' && inputs.mode == 'sync_stable_dist_tags' }} | |
| runs-on: ubuntu-latest | |
| 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 stable tag input format | |
| env: | |
| RELEASE_TAG: ${{ inputs.tag }} | |
| run: | | |
| set -euo pipefail | |
| if [[ -z "${RELEASE_TAG}" ]]; then | |
| echo "Manual dist-tag sync requires a stable release tag." >&2 | |
| exit 1 | |
| fi | |
| 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: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| - name: Validate public stable tag exists | |
| env: | |
| RELEASE_TAG: ${{ inputs.tag }} | |
| run: | | |
| set -euo pipefail | |
| if ! git ls-remote --exit-code "https://github.com/${OPENCLAW_REPOSITORY}.git" "refs/tags/${RELEASE_TAG}" "refs/tags/${RELEASE_TAG}^{}" >/dev/null; then | |
| echo "Public release tag ${RELEASE_TAG} not found in ${OPENCLAW_REPOSITORY}." >&2 | |
| exit 1 | |
| fi | |
| - 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 | |
| npm_userconfig="$(mktemp)" | |
| trap 'rm -f "${npm_userconfig}"' EXIT | |
| chmod 0600 "${npm_userconfig}" | |
| printf '//registry.npmjs.org/:_authToken=%s\n' "${NODE_AUTH_TOKEN}" > "${npm_userconfig}" | |
| NPM_CONFIG_USERCONFIG="${npm_userconfig}" npm dist-tag add "openclaw@${RELEASE_VERSION}" latest | |
| NPM_CONFIG_USERCONFIG="${npm_userconfig}" 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." | |
| - name: Summarize manual dist-tag sync | |
| env: | |
| RELEASE_TAG: ${{ inputs.tag }} | |
| RELEASE_VERSION: ${{ env.RELEASE_VERSION }} | |
| run: | | |
| set -euo pipefail | |
| { | |
| echo "## npm dist-tag output" | |
| echo | |
| echo "- Mode: \`sync_stable_dist_tags\`" | |
| echo "- Release tag: \`${RELEASE_TAG}\`" | |
| echo "- Version: \`${RELEASE_VERSION}\`" | |
| echo "- Result: npm \`latest\` and \`beta\` now point at this stable version" | |
| } >> "$GITHUB_STEP_SUMMARY" |