mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 12:56:01 -06:00
feat(release): add monthly npm extended-stable publication (#99352)
* feat(release): add npm stable publication
* fix(release): allow stable full validation
* feat(release): add stable guard test bypass
* fix(release): allow stable maintenance after month rollover
* docs: refresh release documentation map
* refactor(release): rename monthly channel to extended-stable
* fix(release): repair extended-stable selector forward
* docs(release): fix extended-stable link
(cherry picked from commit ba7b5db74c)
This commit is contained in:
@@ -1260,8 +1260,8 @@ jobs:
|
||||
head_sha="$(jq -r '.headSha // ""' <<< "$run_json")"
|
||||
echo "${label}: ${status}/${conclusion} attempt ${attempt} head ${head_sha}: ${url}"
|
||||
|
||||
if [[ "$CHILD_WORKFLOW_REF" == release-ci/* && -n "${TARGET_SHA// }" && "$head_sha" != "$TARGET_SHA" ]]; then
|
||||
echo "::error::${label} child run used ${head_sha}, expected ${TARGET_SHA}. Dispatch Full Release Validation from a ref pinned to the target SHA, not a moving branch."
|
||||
if [[ ( "$CHILD_WORKFLOW_REF" == release-ci/* || "$CHILD_WORKFLOW_REF" =~ ^extended-stable/[0-9]{4}\.([1-9]|1[0-2])\.33$ ) && -n "${TARGET_SHA// }" && "$head_sha" != "$TARGET_SHA" ]]; then
|
||||
echo "::error::${label} child run used ${head_sha}, expected ${TARGET_SHA}. Dispatch Full Release Validation from a release-ci or extended-stable ref pinned to the target SHA, not a moving branch."
|
||||
return 1
|
||||
fi
|
||||
|
||||
|
||||
@@ -33,6 +33,12 @@ on:
|
||||
- alpha
|
||||
- beta
|
||||
- latest
|
||||
- extended-stable
|
||||
bypass_extended_stable_guard:
|
||||
description: Testing only; relax the extended-stable patch and protected-main month policy on the canonical extended-stable branch
|
||||
required: true
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.event_name == 'workflow_dispatch' && inputs.preflight_only && format('openclaw-npm-release-{0}-{1}-preflight', inputs.tag, inputs.npm_dist_tag) || github.event_name == 'workflow_dispatch' && format('openclaw-npm-release-{0}-{1}-publish-{2}', inputs.tag, inputs.npm_dist_tag, github.run_id) || format('openclaw-npm-release-{0}', github.ref) }}
|
||||
@@ -120,6 +126,14 @@ jobs:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
install-bun: "true"
|
||||
|
||||
- name: Validate npm release request
|
||||
env:
|
||||
BYPASS_EXTENDED_STABLE_GUARD: ${{ inputs.bypass_extended_stable_guard }}
|
||||
RELEASE_NPM_DIST_TAG: ${{ inputs.npm_dist_tag }}
|
||||
RELEASE_TAG: ${{ inputs.tag }}
|
||||
NPM_WORKFLOW_REF: ${{ github.ref }}
|
||||
run: node scripts/openclaw-npm-extended-stable-release.mjs validate-request
|
||||
|
||||
- name: Ensure version is not already published
|
||||
env:
|
||||
PREFLIGHT_ONLY: ${{ inputs.preflight_only }}
|
||||
@@ -379,6 +393,14 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Validate npm release request
|
||||
env:
|
||||
BYPASS_EXTENDED_STABLE_GUARD: ${{ inputs.bypass_extended_stable_guard }}
|
||||
RELEASE_NPM_DIST_TAG: ${{ inputs.npm_dist_tag }}
|
||||
RELEASE_TAG: ${{ inputs.tag }}
|
||||
NPM_WORKFLOW_REF: ${{ github.ref }}
|
||||
run: node scripts/openclaw-npm-extended-stable-release.mjs validate-request
|
||||
|
||||
- name: Require trusted workflow ref for publish
|
||||
env:
|
||||
RELEASE_TAG: ${{ inputs.tag }}
|
||||
@@ -387,11 +409,15 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
tideclaw_alpha_publish=false
|
||||
extended_stable_publish=false
|
||||
if [[ "${RELEASE_TAG}" == *"-alpha."* && "${RELEASE_NPM_DIST_TAG}" == "alpha" && "${WORKFLOW_REF}" =~ ^refs/heads/tideclaw/alpha/[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{4}Z$ ]]; then
|
||||
tideclaw_alpha_publish=true
|
||||
fi
|
||||
if [[ "${WORKFLOW_REF}" != "refs/heads/main" ]] && [[ ! "${WORKFLOW_REF}" =~ ^refs/heads/release/[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*$ ]] && [[ "${tideclaw_alpha_publish}" != "true" ]]; then
|
||||
echo "Real publish runs must be dispatched from main, release/YYYY.M.PATCH, or a Tideclaw alpha branch for alpha prereleases. Use preflight_only=true for other branch validation."
|
||||
if [[ "${RELEASE_NPM_DIST_TAG}" == "extended-stable" && "${WORKFLOW_REF}" == refs/heads/extended-stable/* ]]; then
|
||||
extended_stable_publish=true
|
||||
fi
|
||||
if [[ "${WORKFLOW_REF}" != "refs/heads/main" ]] && [[ ! "${WORKFLOW_REF}" =~ ^refs/heads/release/[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*$ ]] && [[ "${tideclaw_alpha_publish}" != "true" ]] && [[ "${extended_stable_publish}" != "true" ]]; then
|
||||
echo "Real publish runs must be dispatched from main, release/YYYY.M.PATCH, the exact validated extended-stable branch, or a Tideclaw alpha branch for alpha prereleases. Use preflight_only=true for other branch validation."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -525,20 +551,30 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
PREFLIGHT_RUN_ID: ${{ inputs.preflight_run_id }}
|
||||
RELEASE_NPM_DIST_TAG: ${{ inputs.npm_dist_tag }}
|
||||
EXPECTED_EXTENDED_STABLE_BRANCH: ${{ github.ref_name }}
|
||||
RUN_KIND: preflight
|
||||
run: |
|
||||
set -euo pipefail
|
||||
RUN_JSON="$(gh run view "$PREFLIGHT_RUN_ID" --repo "$GITHUB_REPOSITORY" --json workflowName,headBranch,event,conclusion,url)"
|
||||
printf '%s' "$RUN_JSON" | node -e 'const fs = require("node:fs"); const run = JSON.parse(fs.readFileSync(0, "utf8")); const checks = [["workflowName", "OpenClaw NPM Release"], ["event", "workflow_dispatch"], ["conclusion", "success"]]; for (const [key, expected] of checks) { if (run[key] !== expected) { console.error(`Referenced npm preflight run ${process.env.PREFLIGHT_RUN_ID} must have ${key}=${expected}, got ${run[key] ?? "<missing>"}.`); process.exit(1); } } console.log(`Using npm preflight run ${process.env.PREFLIGHT_RUN_ID} from ${run.headBranch}: ${run.url}`);'
|
||||
EXPECTED_RELEASE_SHA="$(git rev-parse HEAD)"
|
||||
export EXPECTED_RELEASE_SHA
|
||||
RUN_JSON="$(gh run view "$PREFLIGHT_RUN_ID" --repo "$GITHUB_REPOSITORY" --json workflowName,headBranch,headSha,event,conclusion,url)"
|
||||
printf '%s' "$RUN_JSON" | node scripts/openclaw-npm-extended-stable-release.mjs verify-run
|
||||
|
||||
- name: Verify full release validation run metadata
|
||||
if: ${{ inputs.full_release_validation_run_id != '' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
FULL_RELEASE_VALIDATION_RUN_ID: ${{ inputs.full_release_validation_run_id }}
|
||||
RELEASE_NPM_DIST_TAG: ${{ inputs.npm_dist_tag }}
|
||||
EXPECTED_EXTENDED_STABLE_BRANCH: ${{ github.ref_name }}
|
||||
RUN_KIND: validation
|
||||
run: |
|
||||
set -euo pipefail
|
||||
RUN_JSON="$(gh run view "$FULL_RELEASE_VALIDATION_RUN_ID" --repo "$GITHUB_REPOSITORY" --json workflowName,headBranch,event,status,conclusion,url)"
|
||||
printf '%s' "$RUN_JSON" | node -e 'const fs = require("node:fs"); const run = JSON.parse(fs.readFileSync(0, "utf8")); const checks = [["workflowName", "Full Release Validation"], ["event", "workflow_dispatch"], ["status", "completed"], ["conclusion", "success"]]; for (const [key, expected] of checks) { if (run[key] !== expected) { console.error(`Referenced full release validation run ${process.env.FULL_RELEASE_VALIDATION_RUN_ID} must have ${key}=${expected}, got ${run[key] ?? "<missing>"}.`); process.exit(1); } } console.log(`Using full release validation run ${process.env.FULL_RELEASE_VALIDATION_RUN_ID} from ${run.headBranch}: ${run.url}`);'
|
||||
EXPECTED_RELEASE_SHA="$(git rev-parse HEAD)"
|
||||
export EXPECTED_RELEASE_SHA
|
||||
RUN_JSON="$(gh run view "$FULL_RELEASE_VALIDATION_RUN_ID" --repo "$GITHUB_REPOSITORY" --json workflowName,headBranch,headSha,event,status,conclusion,url)"
|
||||
printf '%s' "$RUN_JSON" | node scripts/openclaw-npm-extended-stable-release.mjs verify-run
|
||||
|
||||
- name: Download prepared npm tarball
|
||||
env:
|
||||
@@ -620,6 +656,7 @@ jobs:
|
||||
pnpm release:openclaw:npm:check
|
||||
|
||||
- name: Verify prepared tarball provenance
|
||||
id: preflight_provenance
|
||||
env:
|
||||
RELEASE_TAG: ${{ inputs.tag }}
|
||||
RELEASE_NPM_DIST_TAG: ${{ inputs.npm_dist_tag }}
|
||||
@@ -637,6 +674,11 @@ jobs:
|
||||
ARTIFACT_RELEASE_NPM_DIST_TAG="$(jq -r '.npmDistTag // ""' "$MANIFEST_FILE")"
|
||||
ARTIFACT_TARBALL_NAME="$(jq -r '.tarballName // ""' "$MANIFEST_FILE")"
|
||||
ARTIFACT_TARBALL_SHA256="$(jq -r '.tarballSha256 // ""' "$MANIFEST_FILE")"
|
||||
if [[ -z "$ARTIFACT_TARBALL_NAME" || "$ARTIFACT_TARBALL_NAME" != "$(basename -- "$ARTIFACT_TARBALL_NAME")" || "$ARTIFACT_TARBALL_NAME" != *.tgz ]]; then
|
||||
echo "Prepared preflight tarball name is unsafe: $ARTIFACT_TARBALL_NAME" >&2
|
||||
exit 1
|
||||
fi
|
||||
ARTIFACT_TARBALL_PATH="preflight-tarball/$ARTIFACT_TARBALL_NAME"
|
||||
if [[ "$ARTIFACT_RELEASE_TAG" != "$RELEASE_TAG" ]]; then
|
||||
echo "Prepared preflight tag mismatch: expected $RELEASE_TAG, got $ARTIFACT_RELEASE_TAG" >&2
|
||||
exit 1
|
||||
@@ -649,18 +691,25 @@ jobs:
|
||||
echo "Prepared preflight npm dist-tag mismatch: expected $RELEASE_NPM_DIST_TAG, got $ARTIFACT_RELEASE_NPM_DIST_TAG" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ -z "$ARTIFACT_TARBALL_NAME" || ! -f "preflight-tarball/$ARTIFACT_TARBALL_NAME" ]]; then
|
||||
if [[ ! -f "$ARTIFACT_TARBALL_PATH" ]]; then
|
||||
echo "Prepared preflight tarball named in manifest is missing: $ARTIFACT_TARBALL_NAME" >&2
|
||||
exit 1
|
||||
fi
|
||||
actual_tarball_sha256="$(sha256sum "preflight-tarball/$ARTIFACT_TARBALL_NAME" | awk '{print $1}')"
|
||||
actual_tarball_sha256="$(sha256sum "$ARTIFACT_TARBALL_PATH" | awk '{print $1}')"
|
||||
if [[ "$actual_tarball_sha256" != "$ARTIFACT_TARBALL_SHA256" ]]; then
|
||||
echo "Prepared preflight tarball digest mismatch." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "tarball_name=$ARTIFACT_TARBALL_NAME" >> "$GITHUB_OUTPUT"
|
||||
echo "tarball_sha256=$ARTIFACT_TARBALL_SHA256" >> "$GITHUB_OUTPUT"
|
||||
echo "tarball_path=$ARTIFACT_TARBALL_PATH" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Verify full release validation target
|
||||
if: ${{ inputs.full_release_validation_run_id != '' }}
|
||||
env:
|
||||
RELEASE_TAG: ${{ inputs.tag }}
|
||||
RELEASE_NPM_DIST_TAG: ${{ inputs.npm_dist_tag }}
|
||||
EXPECTED_WORKFLOW_REF: ${{ github.ref_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
EXPECTED_RELEASE_SHA="$(git rev-parse HEAD)"
|
||||
@@ -670,39 +719,36 @@ jobs:
|
||||
ls -la full-release-validation >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
WORKFLOW_NAME="$(jq -r '.workflowName // ""' "$MANIFEST_FILE")"
|
||||
TARGET_SHA="$(jq -r '.targetSha // ""' "$MANIFEST_FILE")"
|
||||
export EXPECTED_RELEASE_SHA MANIFEST_FILE
|
||||
node scripts/openclaw-npm-extended-stable-release.mjs verify-manifest
|
||||
RERUN_GROUP="$(jq -r '.rerunGroup // ""' "$MANIFEST_FILE")"
|
||||
if [[ "$WORKFLOW_NAME" != "Full Release Validation" ]]; then
|
||||
echo "Full release validation manifest workflow mismatch: $WORKFLOW_NAME" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$TARGET_SHA" != "$EXPECTED_RELEASE_SHA" ]]; then
|
||||
echo "Full release validation target SHA mismatch: expected $EXPECTED_RELEASE_SHA, got $TARGET_SHA" >&2
|
||||
exit 1
|
||||
fi
|
||||
RUN_RELEASE_SOAK="$(jq -r '.runReleaseSoak // ""' "$MANIFEST_FILE")"
|
||||
PERFORMANCE_BLOCKING="$(jq -r '.controls.performanceBlocking // false' "$MANIFEST_FILE")"
|
||||
if [[ "$RERUN_GROUP" != "all" ]]; then
|
||||
echo "Full release validation must run rerun_group=all before npm publish; got $RERUN_GROUP" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Resolve publish tarball
|
||||
id: publish_tarball
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TARBALL_PATH="$(find preflight-tarball -type f -name '*.tgz' -print | sort | tail -n 1)"
|
||||
if [[ -z "$TARBALL_PATH" ]]; then
|
||||
echo "Prepared preflight tarball not found." >&2
|
||||
ls -la preflight-tarball >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
echo "path=$TARBALL_PATH" >> "$GITHUB_OUTPUT"
|
||||
- name: Recheck npm release request
|
||||
env:
|
||||
BYPASS_EXTENDED_STABLE_GUARD: ${{ inputs.bypass_extended_stable_guard }}
|
||||
RELEASE_NPM_DIST_TAG: ${{ inputs.npm_dist_tag }}
|
||||
RELEASE_TAG: ${{ inputs.tag }}
|
||||
NPM_WORKFLOW_REF: ${{ github.ref }}
|
||||
run: node scripts/openclaw-npm-extended-stable-release.mjs validate-request
|
||||
|
||||
- name: Capture previous extended-stable selector
|
||||
id: previous_extended_stable
|
||||
if: ${{ inputs.npm_dist_tag == 'extended-stable' }}
|
||||
run: node scripts/openclaw-npm-extended-stable-release.mjs capture-selector
|
||||
|
||||
- name: Publish
|
||||
id: publish
|
||||
env:
|
||||
BYPASS_EXTENDED_STABLE_GUARD: ${{ inputs.bypass_extended_stable_guard }}
|
||||
OPENCLAW_PREPACK_PREPARED: "1"
|
||||
OPENCLAW_NPM_PUBLISH_TAG: ${{ inputs.npm_dist_tag }}
|
||||
PUBLISH_TARBALL_PATH: ${{ steps.publish_tarball.outputs.path }}
|
||||
PUBLISH_TARBALL_PATH: ${{ steps.preflight_provenance.outputs.tarball_path }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
publish_target="${PUBLISH_TARBALL_PATH}"
|
||||
@@ -710,3 +756,54 @@ jobs:
|
||||
publish_target="./${publish_target}"
|
||||
fi
|
||||
bash scripts/openclaw-npm-publish.sh --publish "${publish_target}"
|
||||
|
||||
- name: Verify extended-stable registry readback
|
||||
id: extended_stable_readback
|
||||
if: ${{ success() && inputs.npm_dist_tag == 'extended-stable' }}
|
||||
env:
|
||||
EXPECTED_VERSION: ${{ inputs.tag }}
|
||||
run: node scripts/openclaw-npm-extended-stable-release.mjs verify-readback
|
||||
|
||||
- name: Summarize extended-stable npm publication
|
||||
if: ${{ always() && inputs.npm_dist_tag == 'extended-stable' }}
|
||||
env:
|
||||
BYPASS_EXTENDED_STABLE_GUARD: ${{ inputs.bypass_extended_stable_guard }}
|
||||
RELEASE_TAG: ${{ inputs.tag }}
|
||||
RELEASE_SHA: ${{ github.sha }}
|
||||
EXTENDED_STABLE_BRANCH: ${{ github.ref_name }}
|
||||
PREFLIGHT_RUN_ID: ${{ inputs.preflight_run_id }}
|
||||
FULL_RELEASE_VALIDATION_RUN_ID: ${{ inputs.full_release_validation_run_id }}
|
||||
TARBALL_NAME: ${{ steps.preflight_provenance.outputs.tarball_name }}
|
||||
TARBALL_SHA256: ${{ steps.preflight_provenance.outputs.tarball_sha256 }}
|
||||
SELECTOR_CAPTURE_OUTCOME: ${{ steps.previous_extended_stable.outcome }}
|
||||
PREVIOUS_EXTENDED_STABLE: ${{ steps.previous_extended_stable.outputs.previous }}
|
||||
PUBLISH_OUTCOME: ${{ steps.publish.outcome }}
|
||||
EXACT_READBACK: ${{ steps.extended_stable_readback.outputs.exact_version }}
|
||||
SELECTOR_READBACK: ${{ steps.extended_stable_readback.outputs.extended_stable_selector }}
|
||||
READBACK_OUTCOME: ${{ steps.extended_stable_readback.outcome }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
release_version="${RELEASE_TAG#v}"
|
||||
{
|
||||
echo "## npm extended-stable publication"
|
||||
echo "- Package: openclaw@${release_version}"
|
||||
echo "- Extended-stable guard bypass: ${BYPASS_EXTENDED_STABLE_GUARD}"
|
||||
echo "- Extended-stable branch: ${EXTENDED_STABLE_BRANCH}"
|
||||
echo "- Release tag: ${RELEASE_TAG}"
|
||||
echo "- Release SHA: ${RELEASE_SHA}"
|
||||
echo "- npm preflight run: ${PREFLIGHT_RUN_ID}"
|
||||
echo "- Full Release Validation run: ${FULL_RELEASE_VALIDATION_RUN_ID}"
|
||||
echo "- Tarball: ${TARBALL_NAME:-unavailable}"
|
||||
echo "- Tarball SHA-256: ${TARBALL_SHA256:-unavailable}"
|
||||
echo "- Previous openclaw@extended-stable: ${PREVIOUS_EXTENDED_STABLE:-unavailable}"
|
||||
echo "- npm publish outcome: ${PUBLISH_OUTCOME}"
|
||||
echo "- Exact-version readback: ${EXACT_READBACK:-unavailable}"
|
||||
echo "- Extended-stable selector readback: ${SELECTOR_READBACK:-unavailable}"
|
||||
echo "- Registry readback outcome: ${READBACK_OUTCOME}"
|
||||
if [[ "$SELECTOR_CAPTURE_OUTCOME" != "success" ]]; then
|
||||
echo "- Selector capture failed; npm publish did not run and no repair is required because npm was unchanged."
|
||||
else
|
||||
repair_command="$(EXPECTED_VERSION="$RELEASE_TAG" node scripts/openclaw-npm-extended-stable-release.mjs repair-command)"
|
||||
echo "- Repair command: \`${repair_command}\`"
|
||||
fi
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
@@ -131,8 +131,8 @@ jobs:
|
||||
tideclaw_alpha_check=true
|
||||
fi
|
||||
fi
|
||||
if [[ "${WORKFLOW_REF}" != "refs/heads/main" ]] && [[ ! "${WORKFLOW_REF}" =~ ^refs/heads/release/[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*$ ]] && [[ ! "${WORKFLOW_REF}" =~ ^refs/heads/release-ci/[0-9a-f]{12}-[0-9]+$ ]] && [[ "${tideclaw_alpha_check}" != "true" ]]; then
|
||||
echo "Release checks must be dispatched from main, release/YYYY.M.PATCH, a Full Release Validation release-ci/<sha>-<timestamp> ref, or a Tideclaw alpha branch for alpha prereleases." >&2
|
||||
if [[ "${WORKFLOW_REF}" != "refs/heads/main" ]] && [[ ! "${WORKFLOW_REF}" =~ ^refs/heads/release/[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*$ ]] && [[ ! "${WORKFLOW_REF}" =~ ^refs/heads/release-ci/[0-9a-f]{12}-[0-9]+$ ]] && [[ ! "${WORKFLOW_REF}" =~ ^refs/heads/extended-stable/[0-9]{4}\.([1-9]|1[0-2])\.33$ ]] && [[ "${tideclaw_alpha_check}" != "true" ]]; then
|
||||
echo "Release checks must be dispatched from main, release/YYYY.M.PATCH, extended-stable/YYYY.M.33, a Full Release Validation release-ci/<sha>-<timestamp> ref, or a Tideclaw alpha branch for alpha prereleases." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
Reference in New Issue
Block a user