11name : Plugin NPM Release
2+ run-name : ${{ github.event_name == 'workflow_dispatch' && format('Plugin NPM Release [{0}] {1}', inputs.npm_dist_tag, inputs.ref) || format('Plugin NPM Release [default] {0}', github.sha) }}
23
34on :
45 push :
89 - " .github/workflows/plugin-npm-release.yml"
910 - " extensions/**"
1011 - " package.json"
12+ - " scripts/lib/npm-publish-plan.mjs"
1113 - " scripts/lib/plugin-npm-package-manifest.mjs"
1214 - " scripts/lib/plugin-npm-release.ts"
1315 - " scripts/plugin-npm-publish.sh"
2527 - selected
2628 - all-publishable
2729 ref :
28- description : Commit SHA on main, a release branch, or the matching Tideclaw alpha branch to publish from
30+ description : Commit SHA on main, a release branch, the canonical extended-stable branch, or the matching Tideclaw alpha branch to publish from
2931 required : true
3032 type : string
3133 plugins :
3638 description : Approved OpenClaw Release Publish workflow run id
3739 required : false
3840 type : string
41+ npm_dist_tag :
42+ description : Optional npm dist-tag override
43+ required : true
44+ default : default
45+ type : choice
46+ options :
47+ - default
48+ - extended-stable
3949
4050concurrency :
4151 group : plugin-npm-release-${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.ref }}
5565 has_candidates : ${{ steps.plan.outputs.has_candidates }}
5666 candidate_count : ${{ steps.plan.outputs.candidate_count }}
5767 matrix : ${{ steps.plan.outputs.matrix }}
68+ all_matrix : ${{ steps.plan.outputs.all_matrix }}
5869 steps :
5970 - name : Checkout
6071 uses : actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
7586
7687 - name : Validate ref is on a trusted publish branch
7788 env :
89+ NPM_DIST_TAG : ${{ github.event_name == 'workflow_dispatch' && inputs.npm_dist_tag || 'default' }}
90+ PUBLISH_SCOPE : ${{ github.event_name == 'workflow_dispatch' && inputs.publish_scope || '' }}
91+ RELEASE_PLUGINS : ${{ github.event_name == 'workflow_dispatch' && inputs.plugins || '' }}
92+ SOURCE_REF : ${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.sha }}
7893 WORKFLOW_REF : ${{ github.ref }}
7994 run : |
8095 set -euo pipefail
96+ if [[ "${NPM_DIST_TAG}" == "extended-stable" ]]; then
97+ if [[ "${PUBLISH_SCOPE}" != "all-publishable" || -n "${RELEASE_PLUGINS// }" ]]; then
98+ echo "Extended-stable plugin publication requires publish_scope=all-publishable without an explicit plugin list." >&2
99+ exit 1
100+ fi
101+ if [[ ! "${SOURCE_REF}" =~ ^[0-9a-f]{40}$ ]] || [[ "$(git rev-parse HEAD)" != "$(git rev-parse "${SOURCE_REF}^{commit}")" ]]; then
102+ echo "Extended-stable plugin publication requires ref to be the exact 40-character source SHA." >&2
103+ exit 1
104+ fi
105+ package_version="$(node -p "require('./package.json').version")"
106+ if [[ ! "${package_version}" =~ ^([0-9]{4})\.([1-9]|1[0-2])\.([1-9][0-9]*)$ ]] || (( 10#${BASH_REMATCH[3]:-0} < 33 )); then
107+ echo "Extended-stable plugin publication requires a final YYYY.M.PATCH version with PATCH >= 33." >&2
108+ exit 1
109+ fi
110+ release_year="${BASH_REMATCH[1]}"
111+ release_month="${BASH_REMATCH[2]}"
112+ extended_stable_branch="extended-stable/${release_year}.${release_month}.33"
113+ git fetch --no-tags origin "+refs/heads/${extended_stable_branch}:refs/remotes/origin/${extended_stable_branch}"
114+ if [[ "${WORKFLOW_REF}" == "refs/heads/${extended_stable_branch}" ]] && [[ "$(git rev-parse HEAD)" == "$(git rev-parse "refs/remotes/origin/${extended_stable_branch}")" ]]; then
115+ exit 0
116+ fi
117+ echo "Extended-stable plugin npm publishes must run from ${extended_stable_branch} at its exact branch tip." >&2
118+ exit 1
119+ fi
81120 git fetch --no-tags origin \
82121 +refs/heads/main:refs/remotes/origin/main \
83122 '+refs/heads/release/*:refs/remotes/origin/release/*'
@@ -105,13 +144,17 @@ jobs:
105144 RELEASE_PLUGINS : ${{ github.event_name == 'workflow_dispatch' && inputs.plugins || '' }}
106145 BASE_REF : ${{ github.event_name != 'workflow_dispatch' && github.event.before || '' }}
107146 HEAD_REF : ${{ steps.ref.outputs.sha }}
147+ NPM_DIST_TAG : ${{ github.event_name == 'workflow_dispatch' && inputs.npm_dist_tag || 'default' }}
108148 run : |
109149 set -euo pipefail
110150 if [[ -n "${PUBLISH_SCOPE}" ]]; then
111151 release_args=(--selection-mode "${PUBLISH_SCOPE}")
112152 if [[ -n "${RELEASE_PLUGINS}" ]]; then
113153 release_args+=(--plugins "${RELEASE_PLUGINS}")
114154 fi
155+ if [[ "${NPM_DIST_TAG}" == "extended-stable" ]]; then
156+ release_args+=(--npm-dist-tag "${NPM_DIST_TAG}")
157+ fi
115158 pnpm release:plugins:npm:check -- "${release_args[@]}"
116159 elif [[ -n "${BASE_REF}" ]]; then
117160 pnpm release:plugins:npm:check -- --base-ref "${BASE_REF}" --head-ref "${HEAD_REF}"
@@ -126,6 +169,7 @@ jobs:
126169 RELEASE_PLUGINS : ${{ github.event_name == 'workflow_dispatch' && inputs.plugins || '' }}
127170 BASE_REF : ${{ github.event_name != 'workflow_dispatch' && github.event.before || '' }}
128171 HEAD_REF : ${{ steps.ref.outputs.sha }}
172+ NPM_DIST_TAG : ${{ github.event_name == 'workflow_dispatch' && inputs.npm_dist_tag || 'default' }}
129173 run : |
130174 set -euo pipefail
131175 mkdir -p .local
@@ -134,6 +178,9 @@ jobs:
134178 if [[ -n "${RELEASE_PLUGINS}" ]]; then
135179 plan_args+=(--plugins "${RELEASE_PLUGINS}")
136180 fi
181+ if [[ "${NPM_DIST_TAG}" == "extended-stable" ]]; then
182+ plan_args+=(--npm-dist-tag "${NPM_DIST_TAG}")
183+ fi
137184 node --import tsx scripts/plugin-npm-release-plan.ts "${plan_args[@]}" > .local/plugin-npm-release-plan.json
138185 elif [[ -n "${BASE_REF}" ]]; then
139186 node --import tsx scripts/plugin-npm-release-plan.ts --base-ref "${BASE_REF}" --head-ref "${HEAD_REF}" > .local/plugin-npm-release-plan.json
@@ -149,11 +196,13 @@ jobs:
149196 has_candidates="true"
150197 fi
151198 matrix_json="$(jq -c '.candidates' .local/plugin-npm-release-plan.json)"
199+ all_matrix_json="$(jq -c '.all' .local/plugin-npm-release-plan.json)"
152200
153201 {
154202 echo "candidate_count=${candidate_count}"
155203 echo "has_candidates=${has_candidates}"
156204 echo "matrix=${matrix_json}"
205+ echo "all_matrix=${all_matrix_json}"
157206 } >> "$GITHUB_OUTPUT"
158207
159208 echo "Plugin release candidates:"
@@ -237,9 +286,13 @@ jobs:
237286 install-bun : " false"
238287
239288 - name : Preview publish command
289+ env :
290+ OPENCLAW_PLUGIN_NPM_PUBLISH_TAG : ${{ inputs.npm_dist_tag == 'extended-stable' && inputs.npm_dist_tag || '' }}
240291 run : bash scripts/plugin-npm-publish.sh --dry-run "${{ matrix.plugin.packageDir }}"
241292
242293 - name : Preview npm pack contents
294+ env :
295+ OPENCLAW_PLUGIN_NPM_PUBLISH_TAG : ${{ inputs.npm_dist_tag == 'extended-stable' && inputs.npm_dist_tag || '' }}
243296 run : bash scripts/plugin-npm-publish.sh --pack-dry-run "${{ matrix.plugin.packageDir }}"
244297
245298 publish_plugins_npm :
@@ -286,13 +339,61 @@ jobs:
286339 - name : Publish
287340 if : steps.npm_package_version.outputs.already_published != 'true'
288341 env :
289- NODE_AUTH_TOKEN : ${{ secrets.NPM_TOKEN }}
290- NPM_TOKEN : ${{ secrets.NPM_TOKEN }}
342+ NODE_AUTH_TOKEN : ${{ inputs.npm_dist_tag != 'extended-stable' && secrets.NPM_TOKEN || '' }}
343+ NPM_TOKEN : ${{ inputs.npm_dist_tag != 'extended-stable' && secrets.NPM_TOKEN || '' }}
291344 OPENCLAW_NPM_PUBLISH_AUTH_MODE : trusted-publisher
345+ OPENCLAW_PLUGIN_NPM_PUBLISH_TAG : ${{ inputs.npm_dist_tag == 'extended-stable' && inputs.npm_dist_tag || '' }}
292346 run : bash scripts/plugin-npm-publish.sh --publish "${{ matrix.plugin.packageDir }}"
293347
294348 - name : Verify published runtime
295349 env :
296350 PACKAGE_NAME : ${{ matrix.plugin.packageName }}
297351 PACKAGE_VERSION : ${{ matrix.plugin.version }}
298352 run : node scripts/verify-plugin-npm-published-runtime.mjs "${PACKAGE_NAME}@${PACKAGE_VERSION}"
353+
354+ verify_plugins_npm :
355+ needs : [preview_plugins_npm, publish_plugins_npm]
356+ if : ${{ always() && github.event_name == 'workflow_dispatch' && inputs.npm_dist_tag == 'extended-stable' && needs.preview_plugins_npm.result == 'success' && (needs.publish_plugins_npm.result == 'success' || (needs.preview_plugins_npm.outputs.has_candidates == 'false' && needs.publish_plugins_npm.result == 'skipped')) }}
357+ runs-on : ubuntu-latest
358+ permissions :
359+ contents : read
360+ strategy :
361+ fail-fast : false
362+ matrix :
363+ plugin : ${{ fromJson(needs.preview_plugins_npm.outputs.all_matrix) }}
364+ steps :
365+ - name : Checkout
366+ uses : actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
367+ with :
368+ persist-credentials : false
369+ ref : ${{ needs.preview_plugins_npm.outputs.ref_revision }}
370+ fetch-depth : 1
371+
372+ - name : Setup Node environment
373+ uses : ./.github/actions/setup-node-env
374+ with :
375+ node-version : ${{ env.NODE_VERSION }}
376+ install-bun : " false"
377+
378+ - name : Verify complete plugin registry readback
379+ env :
380+ PACKAGE_NAME : ${{ matrix.plugin.packageName }}
381+ PACKAGE_VERSION : ${{ matrix.plugin.version }}
382+ run : |
383+ set -euo pipefail
384+ exact_version=missing
385+ tagged_version=missing
386+ for attempt in {1..12}; do
387+ exact_version="$(npm view "${PACKAGE_NAME}@${PACKAGE_VERSION}" version 2>/dev/null || true)"
388+ tagged_version="$(npm view "${PACKAGE_NAME}@extended-stable" version 2>/dev/null || true)"
389+ if [[ "${exact_version}" == "${PACKAGE_VERSION}" && "${tagged_version}" == "${PACKAGE_VERSION}" ]]; then
390+ echo "Verified ${PACKAGE_NAME}@${PACKAGE_VERSION} and @extended-stable."
391+ exit 0
392+ fi
393+ if [[ "${attempt}" != "12" ]]; then
394+ sleep 10
395+ fi
396+ done
397+ echo "npm registry did not converge for ${PACKAGE_NAME}@${PACKAGE_VERSION} (exact=${exact_version:-missing}, extended-stable=${tagged_version:-missing})." >&2
398+ echo "This OIDC-only source workflow does not mutate tags on an already-published package; use credential-isolated release tooling for manual tag repair." >&2
399+ exit 1
0 commit comments