3333 - alpha
3434 - beta
3535 - latest
36+ - extended-stable
37+ bypass_extended_stable_guard :
38+ description : Testing only; relax the extended-stable patch and protected-main month policy on the canonical extended-stable branch
39+ required : true
40+ default : false
41+ type : boolean
3642
3743concurrency :
3844 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:
120126 node-version : ${{ env.NODE_VERSION }}
121127 install-bun : " true"
122128
129+ - name : Validate npm release request
130+ env :
131+ BYPASS_EXTENDED_STABLE_GUARD : ${{ inputs.bypass_extended_stable_guard }}
132+ RELEASE_NPM_DIST_TAG : ${{ inputs.npm_dist_tag }}
133+ RELEASE_TAG : ${{ inputs.tag }}
134+ NPM_WORKFLOW_REF : ${{ github.ref }}
135+ run : node scripts/openclaw-npm-extended-stable-release.mjs validate-request
136+
123137 - name : Ensure version is not already published
124138 env :
125139 PREFLIGHT_ONLY : ${{ inputs.preflight_only }}
@@ -395,6 +409,14 @@ jobs:
395409 with :
396410 persist-credentials : false
397411
412+ - name : Validate npm release request
413+ env :
414+ BYPASS_EXTENDED_STABLE_GUARD : ${{ inputs.bypass_extended_stable_guard }}
415+ RELEASE_NPM_DIST_TAG : ${{ inputs.npm_dist_tag }}
416+ RELEASE_TAG : ${{ inputs.tag }}
417+ NPM_WORKFLOW_REF : ${{ github.ref }}
418+ run : node scripts/openclaw-npm-extended-stable-release.mjs validate-request
419+
398420 - name : Require trusted workflow ref for publish
399421 env :
400422 RELEASE_TAG : ${{ inputs.tag }}
@@ -403,11 +425,15 @@ jobs:
403425 run : |
404426 set -euo pipefail
405427 tideclaw_alpha_publish=false
428+ extended_stable_publish=false
406429 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
407430 tideclaw_alpha_publish=true
408431 fi
409- 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
410- 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."
432+ if [[ "${RELEASE_NPM_DIST_TAG}" == "extended-stable" && "${WORKFLOW_REF}" == refs/heads/extended-stable/* ]]; then
433+ extended_stable_publish=true
434+ fi
435+ 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
436+ 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."
411437 exit 1
412438 fi
413439
@@ -541,20 +567,30 @@ jobs:
541567 env :
542568 GH_TOKEN : ${{ github.token }}
543569 PREFLIGHT_RUN_ID : ${{ inputs.preflight_run_id }}
570+ RELEASE_NPM_DIST_TAG : ${{ inputs.npm_dist_tag }}
571+ EXPECTED_EXTENDED_STABLE_BRANCH : ${{ github.ref_name }}
572+ RUN_KIND : preflight
544573 run : |
545574 set -euo pipefail
546- RUN_JSON="$(gh run view "$PREFLIGHT_RUN_ID" --repo "$GITHUB_REPOSITORY" --json workflowName,headBranch,event,conclusion,url)"
547- 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}`);'
575+ EXPECTED_RELEASE_SHA="$(git rev-parse HEAD)"
576+ export EXPECTED_RELEASE_SHA
577+ RUN_JSON="$(gh run view "$PREFLIGHT_RUN_ID" --repo "$GITHUB_REPOSITORY" --json workflowName,headBranch,headSha,event,conclusion,url)"
578+ printf '%s' "$RUN_JSON" | node scripts/openclaw-npm-extended-stable-release.mjs verify-run
548579
549580 - name : Verify full release validation run metadata
550581 if : ${{ inputs.full_release_validation_run_id != '' }}
551582 env :
552583 GH_TOKEN : ${{ github.token }}
553584 FULL_RELEASE_VALIDATION_RUN_ID : ${{ inputs.full_release_validation_run_id }}
585+ RELEASE_NPM_DIST_TAG : ${{ inputs.npm_dist_tag }}
586+ EXPECTED_EXTENDED_STABLE_BRANCH : ${{ github.ref_name }}
587+ RUN_KIND : validation
554588 run : |
555589 set -euo pipefail
556- RUN_JSON="$(gh run view "$FULL_RELEASE_VALIDATION_RUN_ID" --repo "$GITHUB_REPOSITORY" --json workflowName,headBranch,event,status,conclusion,url)"
557- 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}`);'
590+ EXPECTED_RELEASE_SHA="$(git rev-parse HEAD)"
591+ export EXPECTED_RELEASE_SHA
592+ RUN_JSON="$(gh run view "$FULL_RELEASE_VALIDATION_RUN_ID" --repo "$GITHUB_REPOSITORY" --json workflowName,headBranch,headSha,event,status,conclusion,url)"
593+ printf '%s' "$RUN_JSON" | node scripts/openclaw-npm-extended-stable-release.mjs verify-run
558594
559595 - name : Download prepared npm tarball
560596 env :
@@ -636,6 +672,7 @@ jobs:
636672 pnpm release:openclaw:npm:check
637673
638674 - name : Verify prepared tarball provenance
675+ id : preflight_provenance
639676 env :
640677 RELEASE_TAG : ${{ inputs.tag }}
641678 RELEASE_NPM_DIST_TAG : ${{ inputs.npm_dist_tag }}
@@ -653,6 +690,11 @@ jobs:
653690 ARTIFACT_RELEASE_NPM_DIST_TAG="$(jq -r '.npmDistTag // ""' "$MANIFEST_FILE")"
654691 ARTIFACT_TARBALL_NAME="$(jq -r '.tarballName // ""' "$MANIFEST_FILE")"
655692 ARTIFACT_TARBALL_SHA256="$(jq -r '.tarballSha256 // ""' "$MANIFEST_FILE")"
693+ if [[ -z "$ARTIFACT_TARBALL_NAME" || "$ARTIFACT_TARBALL_NAME" != "$(basename -- "$ARTIFACT_TARBALL_NAME")" || "$ARTIFACT_TARBALL_NAME" != *.tgz ]]; then
694+ echo "Prepared preflight tarball name is unsafe: $ARTIFACT_TARBALL_NAME" >&2
695+ exit 1
696+ fi
697+ ARTIFACT_TARBALL_PATH="preflight-tarball/$ARTIFACT_TARBALL_NAME"
656698 if [[ "$ARTIFACT_RELEASE_TAG" != "$RELEASE_TAG" ]]; then
657699 echo "Prepared preflight tag mismatch: expected $RELEASE_TAG, got $ARTIFACT_RELEASE_TAG" >&2
658700 exit 1
@@ -665,20 +707,25 @@ jobs:
665707 echo "Prepared preflight npm dist-tag mismatch: expected $RELEASE_NPM_DIST_TAG, got $ARTIFACT_RELEASE_NPM_DIST_TAG" >&2
666708 exit 1
667709 fi
668- if [[ -z "$ARTIFACT_TARBALL_NAME" || ! -f "preflight-tarball/$ARTIFACT_TARBALL_NAME " ]]; then
710+ if [[ ! -f "$ARTIFACT_TARBALL_PATH " ]]; then
669711 echo "Prepared preflight tarball named in manifest is missing: $ARTIFACT_TARBALL_NAME" >&2
670712 exit 1
671713 fi
672- actual_tarball_sha256="$(sha256sum "preflight-tarball/$ARTIFACT_TARBALL_NAME " | awk '{print $1}')"
714+ actual_tarball_sha256="$(sha256sum "$ARTIFACT_TARBALL_PATH " | awk '{print $1}')"
673715 if [[ "$actual_tarball_sha256" != "$ARTIFACT_TARBALL_SHA256" ]]; then
674716 echo "Prepared preflight tarball digest mismatch." >&2
675717 exit 1
676718 fi
719+ echo "tarball_name=$ARTIFACT_TARBALL_NAME" >> "$GITHUB_OUTPUT"
720+ echo "tarball_sha256=$ARTIFACT_TARBALL_SHA256" >> "$GITHUB_OUTPUT"
721+ echo "tarball_path=$ARTIFACT_TARBALL_PATH" >> "$GITHUB_OUTPUT"
677722
678723 - name : Verify full release validation target
679724 if : ${{ inputs.full_release_validation_run_id != '' }}
680725 env :
681726 RELEASE_TAG : ${{ inputs.tag }}
727+ RELEASE_NPM_DIST_TAG : ${{ inputs.npm_dist_tag }}
728+ EXPECTED_WORKFLOW_REF : ${{ github.ref_name }}
682729 run : |
683730 set -euo pipefail
684731 EXPECTED_RELEASE_SHA="$(git rev-parse HEAD)"
@@ -688,19 +735,11 @@ jobs:
688735 ls -la full-release-validation >&2 || true
689736 exit 1
690737 fi
691- WORKFLOW_NAME="$(jq -r '.workflowName // ""' "$ MANIFEST_FILE")"
692- TARGET_SHA="$(jq -r '.targetSha // ""' "$MANIFEST_FILE")"
738+ export EXPECTED_RELEASE_SHA MANIFEST_FILE
739+ node scripts/openclaw-npm-extended-stable-release.mjs verify-manifest
693740 RERUN_GROUP="$(jq -r '.rerunGroup // ""' "$MANIFEST_FILE")"
694741 RUN_RELEASE_SOAK="$(jq -r '.runReleaseSoak // ""' "$MANIFEST_FILE")"
695742 PERFORMANCE_BLOCKING="$(jq -r '.controls.performanceBlocking // false' "$MANIFEST_FILE")"
696- if [[ "$WORKFLOW_NAME" != "Full Release Validation" ]]; then
697- echo "Full release validation manifest workflow mismatch: $WORKFLOW_NAME" >&2
698- exit 1
699- fi
700- if [[ "$TARGET_SHA" != "$EXPECTED_RELEASE_SHA" ]]; then
701- echo "Full release validation target SHA mismatch: expected $EXPECTED_RELEASE_SHA, got $TARGET_SHA" >&2
702- exit 1
703- fi
704743 if [[ "$RERUN_GROUP" != "all" ]]; then
705744 echo "Full release validation must run rerun_group=all before npm publish; got $RERUN_GROUP" >&2
706745 exit 1
@@ -714,27 +753,81 @@ jobs:
714753 exit 1
715754 fi
716755
717- - name : Resolve publish tarball
718- id : publish_tarball
719- run : |
720- set -euo pipefail
721- TARBALL_PATH="$(find preflight-tarball -type f -name '*.tgz' -print | sort | tail -n 1)"
722- if [[ -z "$TARBALL_PATH" ]]; then
723- echo "Prepared preflight tarball not found." >&2
724- ls -la preflight-tarball >&2 || true
725- exit 1
726- fi
727- echo "path=$TARBALL_PATH" >> "$GITHUB_OUTPUT"
756+ - name : Recheck npm release request
757+ env :
758+ BYPASS_EXTENDED_STABLE_GUARD : ${{ inputs.bypass_extended_stable_guard }}
759+ RELEASE_NPM_DIST_TAG : ${{ inputs.npm_dist_tag }}
760+ RELEASE_TAG : ${{ inputs.tag }}
761+ NPM_WORKFLOW_REF : ${{ github.ref }}
762+ run : node scripts/openclaw-npm-extended-stable-release.mjs validate-request
763+
764+ - name : Capture previous extended-stable selector
765+ id : previous_extended_stable
766+ if : ${{ inputs.npm_dist_tag == 'extended-stable' }}
767+ run : node scripts/openclaw-npm-extended-stable-release.mjs capture-selector
728768
729769 - name : Publish
770+ id : publish
730771 env :
772+ BYPASS_EXTENDED_STABLE_GUARD : ${{ inputs.bypass_extended_stable_guard }}
731773 OPENCLAW_PREPACK_PREPARED : " 1"
732774 OPENCLAW_NPM_PUBLISH_TAG : ${{ inputs.npm_dist_tag }}
733- PUBLISH_TARBALL_PATH : ${{ steps.publish_tarball .outputs.path }}
775+ PUBLISH_TARBALL_PATH : ${{ steps.preflight_provenance .outputs.tarball_path }}
734776 run : |
735777 set -euo pipefail
736778 publish_target="${PUBLISH_TARBALL_PATH}"
737779 if [[ -n "${publish_target}" ]]; then
738780 publish_target="./${publish_target}"
739781 fi
740782 bash scripts/openclaw-npm-publish.sh --publish "${publish_target}"
783+
784+ - name : Verify extended-stable registry readback
785+ id : extended_stable_readback
786+ if : ${{ success() && inputs.npm_dist_tag == 'extended-stable' }}
787+ env :
788+ EXPECTED_VERSION : ${{ inputs.tag }}
789+ run : node scripts/openclaw-npm-extended-stable-release.mjs verify-readback
790+
791+ - name : Summarize extended-stable npm publication
792+ if : ${{ always() && inputs.npm_dist_tag == 'extended-stable' }}
793+ env :
794+ BYPASS_EXTENDED_STABLE_GUARD : ${{ inputs.bypass_extended_stable_guard }}
795+ RELEASE_TAG : ${{ inputs.tag }}
796+ RELEASE_SHA : ${{ github.sha }}
797+ EXTENDED_STABLE_BRANCH : ${{ github.ref_name }}
798+ PREFLIGHT_RUN_ID : ${{ inputs.preflight_run_id }}
799+ FULL_RELEASE_VALIDATION_RUN_ID : ${{ inputs.full_release_validation_run_id }}
800+ TARBALL_NAME : ${{ steps.preflight_provenance.outputs.tarball_name }}
801+ TARBALL_SHA256 : ${{ steps.preflight_provenance.outputs.tarball_sha256 }}
802+ SELECTOR_CAPTURE_OUTCOME : ${{ steps.previous_extended_stable.outcome }}
803+ PREVIOUS_EXTENDED_STABLE : ${{ steps.previous_extended_stable.outputs.previous }}
804+ PUBLISH_OUTCOME : ${{ steps.publish.outcome }}
805+ EXACT_READBACK : ${{ steps.extended_stable_readback.outputs.exact_version }}
806+ SELECTOR_READBACK : ${{ steps.extended_stable_readback.outputs.extended_stable_selector }}
807+ READBACK_OUTCOME : ${{ steps.extended_stable_readback.outcome }}
808+ run : |
809+ set -euo pipefail
810+ release_version="${RELEASE_TAG#v}"
811+ {
812+ echo "## npm extended-stable publication"
813+ echo "- Package: openclaw@${release_version}"
814+ echo "- Extended-stable guard bypass: ${BYPASS_EXTENDED_STABLE_GUARD}"
815+ echo "- Extended-stable branch: ${EXTENDED_STABLE_BRANCH}"
816+ echo "- Release tag: ${RELEASE_TAG}"
817+ echo "- Release SHA: ${RELEASE_SHA}"
818+ echo "- npm preflight run: ${PREFLIGHT_RUN_ID}"
819+ echo "- Full Release Validation run: ${FULL_RELEASE_VALIDATION_RUN_ID}"
820+ echo "- Tarball: ${TARBALL_NAME:-unavailable}"
821+ echo "- Tarball SHA-256: ${TARBALL_SHA256:-unavailable}"
822+ echo "- Previous openclaw@extended-stable: ${PREVIOUS_EXTENDED_STABLE:-unavailable}"
823+ echo "- npm publish outcome: ${PUBLISH_OUTCOME}"
824+ echo "- Exact-version readback: ${EXACT_READBACK:-unavailable}"
825+ echo "- Extended-stable selector readback: ${SELECTOR_READBACK:-unavailable}"
826+ echo "- Registry readback outcome: ${READBACK_OUTCOME}"
827+ if [[ "$SELECTOR_CAPTURE_OUTCOME" != "success" ]]; then
828+ echo "- Selector capture failed; npm publish did not run and no repair is required because npm was unchanged."
829+ else
830+ repair_command="$(EXPECTED_VERSION="$RELEASE_TAG" node scripts/openclaw-npm-extended-stable-release.mjs repair-command)"
831+ echo "- Repair command: \`${repair_command}\`"
832+ fi
833+ } >> "$GITHUB_STEP_SUMMARY"
0 commit comments