mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-06 17:35:20 +08:00
* 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
400 lines
18 KiB
YAML
400 lines
18 KiB
YAML
name: Plugin NPM Release
|
|
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) }}
|
|
|
|
on:
|
|
push:
|
|
branches:
|
|
- main
|
|
paths:
|
|
- ".github/workflows/plugin-npm-release.yml"
|
|
- "extensions/**"
|
|
- "package.json"
|
|
- "scripts/lib/npm-publish-plan.mjs"
|
|
- "scripts/lib/plugin-npm-package-manifest.mjs"
|
|
- "scripts/lib/plugin-npm-release.ts"
|
|
- "scripts/plugin-npm-publish.sh"
|
|
- "scripts/plugin-npm-release-check.ts"
|
|
- "scripts/plugin-npm-release-plan.ts"
|
|
- "scripts/verify-plugin-npm-published-runtime.mjs"
|
|
workflow_dispatch:
|
|
inputs:
|
|
publish_scope:
|
|
description: Publish the selected plugins or all publishable plugins from the ref
|
|
required: true
|
|
default: selected
|
|
type: choice
|
|
options:
|
|
- selected
|
|
- all-publishable
|
|
ref:
|
|
description: Commit SHA on main, a release branch, the canonical extended-stable branch, or the matching Tideclaw alpha branch to publish from
|
|
required: true
|
|
type: string
|
|
plugins:
|
|
description: Comma-separated plugin package names to publish when publish_scope=selected
|
|
required: false
|
|
type: string
|
|
release_publish_run_id:
|
|
description: Approved OpenClaw Release Publish workflow run id
|
|
required: false
|
|
type: string
|
|
npm_dist_tag:
|
|
description: Optional npm dist-tag override
|
|
required: true
|
|
default: default
|
|
type: choice
|
|
options:
|
|
- default
|
|
- extended-stable
|
|
|
|
concurrency:
|
|
group: plugin-npm-release-${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.ref }}
|
|
cancel-in-progress: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
|
|
|
|
env:
|
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
|
NODE_VERSION: "24.15.0"
|
|
|
|
jobs:
|
|
preview_plugins_npm:
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: read
|
|
outputs:
|
|
ref_revision: ${{ steps.ref.outputs.sha }}
|
|
has_candidates: ${{ steps.plan.outputs.has_candidates }}
|
|
candidate_count: ${{ steps.plan.outputs.candidate_count }}
|
|
matrix: ${{ steps.plan.outputs.matrix }}
|
|
all_matrix: ${{ steps.plan.outputs.all_matrix }}
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
|
with:
|
|
persist-credentials: false
|
|
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.sha }}
|
|
fetch-depth: 0
|
|
|
|
- name: Setup Node environment
|
|
uses: ./.github/actions/setup-node-env
|
|
with:
|
|
node-version: ${{ env.NODE_VERSION }}
|
|
install-bun: "false"
|
|
|
|
- name: Resolve checked-out ref
|
|
id: ref
|
|
run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Validate ref is on a trusted publish branch
|
|
env:
|
|
NPM_DIST_TAG: ${{ github.event_name == 'workflow_dispatch' && inputs.npm_dist_tag || 'default' }}
|
|
PUBLISH_SCOPE: ${{ github.event_name == 'workflow_dispatch' && inputs.publish_scope || '' }}
|
|
RELEASE_PLUGINS: ${{ github.event_name == 'workflow_dispatch' && inputs.plugins || '' }}
|
|
SOURCE_REF: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.sha }}
|
|
WORKFLOW_REF: ${{ github.ref }}
|
|
run: |
|
|
set -euo pipefail
|
|
if [[ "${NPM_DIST_TAG}" == "extended-stable" ]]; then
|
|
if [[ "${PUBLISH_SCOPE}" != "all-publishable" || -n "${RELEASE_PLUGINS// }" ]]; then
|
|
echo "Extended-stable plugin publication requires publish_scope=all-publishable without an explicit plugin list." >&2
|
|
exit 1
|
|
fi
|
|
if [[ ! "${SOURCE_REF}" =~ ^[0-9a-f]{40}$ ]] || [[ "$(git rev-parse HEAD)" != "$(git rev-parse "${SOURCE_REF}^{commit}")" ]]; then
|
|
echo "Extended-stable plugin publication requires ref to be the exact 40-character source SHA." >&2
|
|
exit 1
|
|
fi
|
|
package_version="$(node -p "require('./package.json').version")"
|
|
if [[ ! "${package_version}" =~ ^([0-9]{4})\.([1-9]|1[0-2])\.([1-9][0-9]*)$ ]] || (( 10#${BASH_REMATCH[3]:-0} < 33 )); then
|
|
echo "Extended-stable plugin publication requires a final YYYY.M.PATCH version with PATCH >= 33." >&2
|
|
exit 1
|
|
fi
|
|
release_year="${BASH_REMATCH[1]}"
|
|
release_month="${BASH_REMATCH[2]}"
|
|
extended_stable_branch="extended-stable/${release_year}.${release_month}.33"
|
|
git fetch --no-tags origin "+refs/heads/${extended_stable_branch}:refs/remotes/origin/${extended_stable_branch}"
|
|
if [[ "${WORKFLOW_REF}" == "refs/heads/${extended_stable_branch}" ]] && [[ "$(git rev-parse HEAD)" == "$(git rev-parse "refs/remotes/origin/${extended_stable_branch}")" ]]; then
|
|
exit 0
|
|
fi
|
|
echo "Extended-stable plugin npm publishes must run from ${extended_stable_branch} at its exact branch tip." >&2
|
|
exit 1
|
|
fi
|
|
git fetch --no-tags origin \
|
|
+refs/heads/main:refs/remotes/origin/main \
|
|
'+refs/heads/release/*:refs/remotes/origin/release/*'
|
|
if git merge-base --is-ancestor HEAD origin/main; then
|
|
exit 0
|
|
fi
|
|
while IFS= read -r release_ref; do
|
|
if git merge-base --is-ancestor HEAD "${release_ref}"; then
|
|
exit 0
|
|
fi
|
|
done < <(git for-each-ref --format='%(refname)' refs/remotes/origin/release)
|
|
if [[ "${WORKFLOW_REF}" =~ ^refs/heads/tideclaw/alpha/[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{4}Z$ ]]; then
|
|
alpha_branch="${WORKFLOW_REF#refs/heads/}"
|
|
git fetch --no-tags origin "+refs/heads/${alpha_branch}:refs/remotes/origin/${alpha_branch}"
|
|
if git merge-base --is-ancestor HEAD "refs/remotes/origin/${alpha_branch}"; then
|
|
exit 0
|
|
fi
|
|
fi
|
|
echo "Plugin npm publishes must target a commit reachable from main, release/*, or the matching Tideclaw alpha branch." >&2
|
|
exit 1
|
|
|
|
- name: Validate publishable plugin metadata
|
|
env:
|
|
PUBLISH_SCOPE: ${{ github.event_name == 'workflow_dispatch' && inputs.publish_scope || '' }}
|
|
RELEASE_PLUGINS: ${{ github.event_name == 'workflow_dispatch' && inputs.plugins || '' }}
|
|
BASE_REF: ${{ github.event_name != 'workflow_dispatch' && github.event.before || '' }}
|
|
HEAD_REF: ${{ steps.ref.outputs.sha }}
|
|
NPM_DIST_TAG: ${{ github.event_name == 'workflow_dispatch' && inputs.npm_dist_tag || 'default' }}
|
|
run: |
|
|
set -euo pipefail
|
|
if [[ -n "${PUBLISH_SCOPE}" ]]; then
|
|
release_args=(--selection-mode "${PUBLISH_SCOPE}")
|
|
if [[ -n "${RELEASE_PLUGINS}" ]]; then
|
|
release_args+=(--plugins "${RELEASE_PLUGINS}")
|
|
fi
|
|
if [[ "${NPM_DIST_TAG}" == "extended-stable" ]]; then
|
|
release_args+=(--npm-dist-tag "${NPM_DIST_TAG}")
|
|
fi
|
|
pnpm release:plugins:npm:check -- "${release_args[@]}"
|
|
elif [[ -n "${BASE_REF}" ]]; then
|
|
pnpm release:plugins:npm:check -- --base-ref "${BASE_REF}" --head-ref "${HEAD_REF}"
|
|
else
|
|
pnpm release:plugins:npm:check
|
|
fi
|
|
|
|
- name: Resolve plugin release plan
|
|
id: plan
|
|
env:
|
|
PUBLISH_SCOPE: ${{ github.event_name == 'workflow_dispatch' && inputs.publish_scope || '' }}
|
|
RELEASE_PLUGINS: ${{ github.event_name == 'workflow_dispatch' && inputs.plugins || '' }}
|
|
BASE_REF: ${{ github.event_name != 'workflow_dispatch' && github.event.before || '' }}
|
|
HEAD_REF: ${{ steps.ref.outputs.sha }}
|
|
NPM_DIST_TAG: ${{ github.event_name == 'workflow_dispatch' && inputs.npm_dist_tag || 'default' }}
|
|
run: |
|
|
set -euo pipefail
|
|
mkdir -p .local
|
|
if [[ -n "${PUBLISH_SCOPE}" ]]; then
|
|
plan_args=(--selection-mode "${PUBLISH_SCOPE}")
|
|
if [[ -n "${RELEASE_PLUGINS}" ]]; then
|
|
plan_args+=(--plugins "${RELEASE_PLUGINS}")
|
|
fi
|
|
if [[ "${NPM_DIST_TAG}" == "extended-stable" ]]; then
|
|
plan_args+=(--npm-dist-tag "${NPM_DIST_TAG}")
|
|
fi
|
|
node --import tsx scripts/plugin-npm-release-plan.ts "${plan_args[@]}" > .local/plugin-npm-release-plan.json
|
|
elif [[ -n "${BASE_REF}" ]]; then
|
|
node --import tsx scripts/plugin-npm-release-plan.ts --base-ref "${BASE_REF}" --head-ref "${HEAD_REF}" > .local/plugin-npm-release-plan.json
|
|
else
|
|
node --import tsx scripts/plugin-npm-release-plan.ts > .local/plugin-npm-release-plan.json
|
|
fi
|
|
|
|
cat .local/plugin-npm-release-plan.json
|
|
|
|
candidate_count="$(jq -r '.candidates | length' .local/plugin-npm-release-plan.json)"
|
|
has_candidates="false"
|
|
if [[ "${candidate_count}" != "0" ]]; then
|
|
has_candidates="true"
|
|
fi
|
|
matrix_json="$(jq -c '.candidates' .local/plugin-npm-release-plan.json)"
|
|
all_matrix_json="$(jq -c '.all' .local/plugin-npm-release-plan.json)"
|
|
|
|
{
|
|
echo "candidate_count=${candidate_count}"
|
|
echo "has_candidates=${has_candidates}"
|
|
echo "matrix=${matrix_json}"
|
|
echo "all_matrix=${all_matrix_json}"
|
|
} >> "$GITHUB_OUTPUT"
|
|
|
|
echo "Plugin release candidates:"
|
|
jq -r '.candidates[]? | "- \(.packageName)@\(.version) [\(.publishTag)] from \(.packageDir)"' .local/plugin-npm-release-plan.json
|
|
|
|
echo "Already published / skipped:"
|
|
jq -r '.skippedPublished[]? | "- \(.packageName)@\(.version)"' .local/plugin-npm-release-plan.json
|
|
|
|
- name: Validate Tideclaw alpha plugin channels
|
|
if: startsWith(github.ref, 'refs/heads/tideclaw/alpha/')
|
|
run: |
|
|
set -euo pipefail
|
|
invalid="$(
|
|
jq -r '.candidates[]? | select(.publishTag != "alpha" or .channel != "alpha") | "- \(.packageName)@\(.version) [\(.publishTag)]"' .local/plugin-npm-release-plan.json
|
|
)"
|
|
if [[ -n "${invalid}" ]]; then
|
|
echo "Tideclaw alpha plugin npm publishes may only publish alpha plugin versions." >&2
|
|
printf '%s\n' "${invalid}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
validate_release_publish_approval:
|
|
name: Validate release publish approval
|
|
needs: preview_plugins_npm
|
|
if: github.event_name == 'workflow_dispatch' && needs.preview_plugins_npm.outputs.has_candidates == 'true'
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
actions: read
|
|
contents: read
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Validate release publish approval run
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
RELEASE_PUBLISH_RUN_ID: ${{ inputs.release_publish_run_id }}
|
|
EXPECTED_WORKFLOW_BRANCH: ${{ github.ref_name }}
|
|
run: |
|
|
set -euo pipefail
|
|
if [[ -z "${RELEASE_PUBLISH_RUN_ID// }" ]]; then
|
|
if [[ "${GITHUB_ACTOR}" == "github-actions[bot]" ]]; then
|
|
echo "Plugin npm publish dispatched by another workflow must include release_publish_run_id." >&2
|
|
exit 1
|
|
fi
|
|
echo "Direct Plugin NPM Release dispatch; relying on this workflow's npm-release environment approval."
|
|
exit 0
|
|
fi
|
|
direct_recovery=false
|
|
if [[ "${GITHUB_ACTOR}" != "github-actions[bot]" ]]; then
|
|
direct_recovery=true
|
|
echo "Direct Plugin NPM Release recovery with release_publish_run_id; relying on this workflow's npm-release environment approval."
|
|
fi
|
|
RUN_JSON="$(gh run view "$RELEASE_PUBLISH_RUN_ID" --repo "$GITHUB_REPOSITORY" --json workflowName,headBranch,event,status,conclusion,url)"
|
|
printf '%s' "$RUN_JSON" | DIRECT_RELEASE_RECOVERY="${direct_recovery}" node scripts/validate-release-publish-approval.mjs
|
|
|
|
preview_plugin_pack:
|
|
needs: preview_plugins_npm
|
|
if: needs.preview_plugins_npm.outputs.has_candidates == 'true'
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: read
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
plugin: ${{ fromJson(needs.preview_plugins_npm.outputs.matrix) }}
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
|
with:
|
|
persist-credentials: false
|
|
ref: ${{ needs.preview_plugins_npm.outputs.ref_revision }}
|
|
fetch-depth: 1
|
|
|
|
- name: Setup Node environment
|
|
uses: ./.github/actions/setup-node-env
|
|
with:
|
|
node-version: ${{ env.NODE_VERSION }}
|
|
install-bun: "false"
|
|
|
|
- name: Preview publish command
|
|
env:
|
|
OPENCLAW_PLUGIN_NPM_PUBLISH_TAG: ${{ inputs.npm_dist_tag == 'extended-stable' && inputs.npm_dist_tag || '' }}
|
|
run: bash scripts/plugin-npm-publish.sh --dry-run "${{ matrix.plugin.packageDir }}"
|
|
|
|
- name: Preview npm pack contents
|
|
env:
|
|
OPENCLAW_PLUGIN_NPM_PUBLISH_TAG: ${{ inputs.npm_dist_tag == 'extended-stable' && inputs.npm_dist_tag || '' }}
|
|
run: bash scripts/plugin-npm-publish.sh --pack-dry-run "${{ matrix.plugin.packageDir }}"
|
|
|
|
publish_plugins_npm:
|
|
needs: [preview_plugins_npm, preview_plugin_pack, validate_release_publish_approval]
|
|
if: github.event_name == 'workflow_dispatch' && needs.preview_plugins_npm.outputs.has_candidates == 'true'
|
|
runs-on: ubuntu-latest
|
|
environment: npm-release
|
|
permissions:
|
|
actions: read
|
|
contents: read
|
|
id-token: write
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
plugin: ${{ fromJson(needs.preview_plugins_npm.outputs.matrix) }}
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
|
with:
|
|
persist-credentials: false
|
|
ref: ${{ needs.preview_plugins_npm.outputs.ref_revision }}
|
|
fetch-depth: 1
|
|
|
|
- name: Setup Node environment
|
|
uses: ./.github/actions/setup-node-env
|
|
with:
|
|
node-version: ${{ env.NODE_VERSION }}
|
|
install-bun: "false"
|
|
|
|
- name: Check npm package version
|
|
id: npm_package_version
|
|
env:
|
|
PACKAGE_NAME: ${{ matrix.plugin.packageName }}
|
|
PACKAGE_VERSION: ${{ matrix.plugin.version }}
|
|
run: |
|
|
set -euo pipefail
|
|
if npm view "${PACKAGE_NAME}@${PACKAGE_VERSION}" version >/dev/null 2>&1; then
|
|
echo "${PACKAGE_NAME}@${PACKAGE_VERSION} is already published on npm."
|
|
echo "already_published=true" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "already_published=false" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
|
|
- name: Publish
|
|
if: steps.npm_package_version.outputs.already_published != 'true'
|
|
env:
|
|
NODE_AUTH_TOKEN: ${{ inputs.npm_dist_tag != 'extended-stable' && secrets.NPM_TOKEN || '' }}
|
|
NPM_TOKEN: ${{ inputs.npm_dist_tag != 'extended-stable' && secrets.NPM_TOKEN || '' }}
|
|
OPENCLAW_NPM_PUBLISH_AUTH_MODE: trusted-publisher
|
|
OPENCLAW_PLUGIN_NPM_PUBLISH_TAG: ${{ inputs.npm_dist_tag == 'extended-stable' && inputs.npm_dist_tag || '' }}
|
|
run: bash scripts/plugin-npm-publish.sh --publish "${{ matrix.plugin.packageDir }}"
|
|
|
|
- name: Verify published runtime
|
|
env:
|
|
PACKAGE_NAME: ${{ matrix.plugin.packageName }}
|
|
PACKAGE_VERSION: ${{ matrix.plugin.version }}
|
|
run: node scripts/verify-plugin-npm-published-runtime.mjs "${PACKAGE_NAME}@${PACKAGE_VERSION}"
|
|
|
|
verify_plugins_npm:
|
|
needs: [preview_plugins_npm, publish_plugins_npm]
|
|
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')) }}
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: read
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
plugin: ${{ fromJson(needs.preview_plugins_npm.outputs.all_matrix) }}
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
|
with:
|
|
persist-credentials: false
|
|
ref: ${{ needs.preview_plugins_npm.outputs.ref_revision }}
|
|
fetch-depth: 1
|
|
|
|
- name: Setup Node environment
|
|
uses: ./.github/actions/setup-node-env
|
|
with:
|
|
node-version: ${{ env.NODE_VERSION }}
|
|
install-bun: "false"
|
|
|
|
- name: Verify complete plugin registry readback
|
|
env:
|
|
PACKAGE_NAME: ${{ matrix.plugin.packageName }}
|
|
PACKAGE_VERSION: ${{ matrix.plugin.version }}
|
|
run: |
|
|
set -euo pipefail
|
|
exact_version=missing
|
|
tagged_version=missing
|
|
for attempt in {1..12}; do
|
|
exact_version="$(npm view "${PACKAGE_NAME}@${PACKAGE_VERSION}" version 2>/dev/null || true)"
|
|
tagged_version="$(npm view "${PACKAGE_NAME}@extended-stable" version 2>/dev/null || true)"
|
|
if [[ "${exact_version}" == "${PACKAGE_VERSION}" && "${tagged_version}" == "${PACKAGE_VERSION}" ]]; then
|
|
echo "Verified ${PACKAGE_NAME}@${PACKAGE_VERSION} and @extended-stable."
|
|
exit 0
|
|
fi
|
|
if [[ "${attempt}" != "12" ]]; then
|
|
sleep 10
|
|
fi
|
|
done
|
|
echo "npm registry did not converge for ${PACKAGE_NAME}@${PACKAGE_VERSION} (exact=${exact_version:-missing}, extended-stable=${tagged_version:-missing})." >&2
|
|
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
|
|
exit 1
|