Skip to content

Commit 9d2d517

Browse files
authored
feat: publish plugins with extended-stable releases (#100448)
* feat(release): publish plugins on extended-stable * feat(plugins): align extended-stable with core * docs: explain extended-stable plugin alignment * fix(release): make plugin convergence recoverable * fix(plugins): preserve extended-stable fallback intent * fix(release): keep plugin tag mutation credential-isolated
1 parent a4e728f commit 9d2d517

31 files changed

Lines changed: 1258 additions & 81 deletions

.github/workflows/openclaw-npm-release.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ on:
2424
description: Approved OpenClaw Release Publish workflow run id
2525
required: false
2626
type: string
27+
plugin_npm_run_id:
28+
description: Successful Plugin NPM Release run id for the exact extended-stable branch and release SHA
29+
required: false
30+
type: string
2731
npm_dist_tag:
2832
description: npm dist-tag to publish to
2933
required: true
@@ -469,6 +473,7 @@ jobs:
469473
RELEASE_NPM_DIST_TAG: ${{ inputs.npm_dist_tag }}
470474
PREFLIGHT_RUN_ID: ${{ inputs.preflight_run_id }}
471475
FULL_RELEASE_VALIDATION_RUN_ID: ${{ inputs.full_release_validation_run_id }}
476+
PLUGIN_NPM_RUN_ID: ${{ inputs.plugin_npm_run_id }}
472477
RELEASE_PUBLISH_RUN_ID: ${{ inputs.release_publish_run_id }}
473478
run: |
474479
set -euo pipefail
@@ -484,6 +489,10 @@ jobs:
484489
exit 1
485490
fi
486491
fi
492+
if [[ "${RELEASE_NPM_DIST_TAG}" == "extended-stable" && -z "${PLUGIN_NPM_RUN_ID// }" ]]; then
493+
echo "Extended-stable publish requires plugin_npm_run_id from a successful Plugin NPM Release run." >&2
494+
exit 1
495+
fi
487496
if [[ -z "${RELEASE_PUBLISH_RUN_ID// }" && "${GITHUB_ACTOR}" == "github-actions[bot]" ]]; then
488497
echo "Workflow-dispatched real publish requires release_publish_run_id from the approved OpenClaw Release Publish workflow." >&2
489498
exit 1
@@ -618,6 +627,21 @@ jobs:
618627
RUN_JSON="$(gh run view "$FULL_RELEASE_VALIDATION_RUN_ID" --repo "$GITHUB_REPOSITORY" --json workflowName,headBranch,headSha,event,status,conclusion,url)"
619628
printf '%s' "$RUN_JSON" | node scripts/openclaw-npm-extended-stable-release.mjs verify-run
620629
630+
- name: Verify plugin npm release run metadata
631+
if: ${{ inputs.npm_dist_tag == 'extended-stable' }}
632+
env:
633+
GH_TOKEN: ${{ github.token }}
634+
PLUGIN_NPM_RUN_ID: ${{ inputs.plugin_npm_run_id }}
635+
RELEASE_NPM_DIST_TAG: ${{ inputs.npm_dist_tag }}
636+
EXPECTED_EXTENDED_STABLE_BRANCH: ${{ github.ref_name }}
637+
RUN_KIND: plugin
638+
run: |
639+
set -euo pipefail
640+
EXPECTED_RELEASE_SHA="$(git rev-parse HEAD)"
641+
export EXPECTED_RELEASE_SHA
642+
RUN_JSON="$(gh run view "$PLUGIN_NPM_RUN_ID" --repo "$GITHUB_REPOSITORY" --json workflowName,displayTitle,headBranch,headSha,event,status,conclusion,url)"
643+
printf '%s' "$RUN_JSON" | node scripts/openclaw-npm-extended-stable-release.mjs verify-run
644+
621645
- name: Download prepared npm tarball
622646
env:
623647
GH_TOKEN: ${{ github.token }}

.github/workflows/plugin-npm-release.yml

Lines changed: 104 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
name: 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

34
on:
45
push:
@@ -8,6 +9,7 @@ on:
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"
@@ -25,7 +27,7 @@ on:
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:
@@ -36,6 +38,14 @@ on:
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

4050
concurrency:
4151
group: plugin-npm-release-${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.ref }}
@@ -55,6 +65,7 @@ jobs:
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
@@ -75,9 +86,37 @@ jobs:
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

docs/cli/plugins.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,10 @@ remains a supported fallback and direct-install path. OpenClaw-owned
159159
on [npmjs.com/org/openclaw](https://www.npmjs.com/org/openclaw) or the
160160
[plugin inventory](/plugins/plugin-inventory). Stable installs use `latest`.
161161
Beta-channel installs and updates prefer the npm `beta` dist-tag when available,
162-
falling back to `latest`.
162+
falling back to `latest`. On the extended-stable channel, official npm plugins
163+
with bare/default or `latest` intent resolve to the exact installed core
164+
version. Exact pins and explicit non-`latest` tags, third-party packages, and
165+
non-npm sources are not rewritten.
163166
</Note>
164167

165168
<AccordionGroup>

docs/cli/update.md

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -102,14 +102,14 @@ openclaw update repair --acknowledge-clawhub-risk
102102
openclaw update repair --json
103103
```
104104

105-
| Flag | Description |
106-
| ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
107-
| `--channel <stable\|extended-stable\|beta\|dev>` | Persist the core update channel before repair. For extended-stable, plugin convergence temporarily targets the stable/latest plugin line. Extended-stable repair is rejected on Git checkouts without changing config. |
108-
| `--json` | Print machine-readable finalization JSON. |
109-
| `--timeout <seconds>` | Timeout for repair steps. Default `1800`. |
110-
| `--yes` | Skip confirmation prompts. |
111-
| `--acknowledge-clawhub-risk` | Same behavior as on `openclaw update`. |
112-
| `--no-restart` | Accepted for parity; repair never restarts the Gateway. |
105+
| Flag | Description |
106+
| ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
107+
| `--channel <stable\|extended-stable\|beta\|dev>` | Persist the core update channel before repair. For extended-stable, eligible official npm plugins that follow bare/default or `latest` intent target the exact installed core version. Extended-stable repair is rejected on Git checkouts without changing config. |
108+
| `--json` | Print machine-readable finalization JSON. |
109+
| `--timeout <seconds>` | Timeout for repair steps. Default `1800`. |
110+
| `--yes` | Skip confirmation prompts. |
111+
| `--acknowledge-clawhub-risk` | Same behavior as on `openclaw update`. |
112+
| `--no-restart` | Accepted for parity; repair never restarts the Gateway. |
113113

114114
`update repair` runs `openclaw doctor --fix`, reloads the repaired config and
115115
install records, syncs tracked plugins for the active update channel, updates
@@ -280,9 +280,11 @@ When the updated Gateway starts, plugin loading is verify-only: startup does not
280280
</Note>
281281

282282
After an extended-stable core update succeeds, post-core plugin integrity and
283-
convergence still run, but official plugins temporarily target the
284-
stable/latest line. OpenClaw does not query plugin `@extended-stable`
285-
selectors in this release.
283+
convergence target eligible official npm plugins at the exact installed core
284+
version. For default/`latest` intent, OpenClaw does not query plugin
285+
`@extended-stable` or fall back to npm `latest`; it derives the package version
286+
from the installed core. Explicit version pins, explicit non-`latest` tags,
287+
third-party packages, and non-npm sources keep their existing intent.
286288

287289
For package-manager installs, `openclaw update` resolves the target package
288290
version before invoking the package manager. npm global installs use a staged

0 commit comments

Comments
 (0)