ci: split plugin ClawHub publishing paths

* feat: partition clawhub plugin release candidates

* fix: read clawhub trusted publisher config endpoint

* feat: split clawhub plugin bootstrap workflow

* ci: split plugin clawhub publish paths

* ci: pin clawhub package publish workflow

* ci: keep clawhub bootstrap token out of builds

* ci: fix clawhub release dry-run gating

* ci: align clawhub oidc publish refs

* ci: make clawhub bootstrap recovery idempotent

* ci: route clawhub repair candidates through bootstrap

* ci: preserve tideclaw alpha clawhub guards

* ci: simplify clawhub release ref handling

* ci: extract clawhub release routing plan

* ci: extract clawhub release runtime state

* test: guard clawhub release helper executability

* ci: pin ClawHub CLI for plugin publishing

* ci: allow historical ClawHub dry-run validation

* ci: fix ClawHub bootstrap token handoff
This commit is contained in:
Patrick Erichsen
2026-06-12 20:16:06 -07:00
committed by GitHub
parent ded3a93058
commit 6cf06e8e7e
12 changed files with 2197 additions and 211 deletions
+102 -150
View File
@@ -16,7 +16,7 @@ on:
required: false
type: string
ref:
description: Commit SHA on main, a release branch, or the matching Tideclaw alpha branch to publish from; defaults to the workflow ref
description: Dry-run target ref to validate; real OIDC publishes must dispatch the workflow with --ref set to the target release tag/ref
required: false
default: ""
type: string
@@ -24,6 +24,10 @@ on:
description: Approved OpenClaw Release Publish workflow run id
required: false
type: string
release_publish_branch:
description: Branch name of the approving OpenClaw Release Publish workflow run
required: false
type: string
dry_run:
description: Validate the full ClawHub artifact handoff without publishing.
required: false
@@ -38,9 +42,7 @@ env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
NODE_VERSION: "24.15.0"
CLAWHUB_REGISTRY: "https://clawhub.ai"
CLAWHUB_REPOSITORY: "openclaw/clawhub"
# Pinned to a reviewed ClawHub commit so release behavior stays reproducible.
CLAWHUB_REF: "c9bb13023598dcc547fdf4a93b9d42512b8c8854"
CLAWHUB_CLI_PACKAGE: "clawhub@0.21.0"
jobs:
preview_plugins_clawhub:
@@ -50,9 +52,15 @@ jobs:
outputs:
ref_revision: ${{ steps.ref.outputs.sha }}
has_candidates: ${{ steps.plan.outputs.has_candidates }}
has_bootstrap_candidates: ${{ steps.plan.outputs.has_bootstrap_candidates }}
has_missing_trusted_publisher: ${{ steps.plan.outputs.has_missing_trusted_publisher }}
candidate_count: ${{ steps.plan.outputs.candidate_count }}
bootstrap_candidate_count: ${{ steps.plan.outputs.bootstrap_candidate_count }}
missing_trusted_publisher_count: ${{ steps.plan.outputs.missing_trusted_publisher_count }}
skipped_published_count: ${{ steps.plan.outputs.skipped_published_count }}
matrix: ${{ steps.plan.outputs.matrix }}
bootstrap_matrix: ${{ steps.plan.outputs.bootstrap_matrix }}
missing_trusted_publisher_matrix: ${{ steps.plan.outputs.missing_trusted_publisher_matrix }}
steps:
- name: Checkout
uses: actions/checkout@v6
@@ -83,9 +91,27 @@ jobs:
fi
echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
- name: Validate OIDC source matches workflow ref
env:
TARGET_SHA: ${{ steps.ref.outputs.sha }}
WORKFLOW_SHA: ${{ github.sha }}
DRY_RUN: ${{ inputs.dry_run && 'true' || 'false' }}
run: |
set -euo pipefail
if [[ "${TARGET_SHA}" != "${WORKFLOW_SHA}" ]]; then
if [[ "${DRY_RUN}" == "true" ]]; then
echo "Dry-run publish target differs from workflow ref; allowing validation-only dispatch."
exit 0
fi
echo "Plugin ClawHub OIDC publishes must run from the same ref that is being published." >&2
echo "The ref input is only supported for dry_run=true." >&2
echo "For real publishes, dispatch this workflow with --ref pointing at the target release tag/ref and omit the ref input." >&2
exit 1
fi
- name: Validate ref is on a trusted publish branch
env:
WORKFLOW_REF: ${{ github.ref }}
TRUSTED_PUBLISH_BRANCH: ${{ inputs.release_publish_branch || github.ref_name }}
run: |
set -euo pipefail
if git merge-base --is-ancestor HEAD origin/main; then
@@ -96,8 +122,8 @@ jobs:
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/}"
if [[ "${TRUSTED_PUBLISH_BRANCH}" =~ ^tideclaw/alpha/[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{4}Z$ ]]; then
alpha_branch="${TRUSTED_PUBLISH_BRANCH}"
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
@@ -158,36 +184,78 @@ jobs:
cat .local/plugin-clawhub-release-plan.json
candidate_count="$(jq -r '.candidates | length' .local/plugin-clawhub-release-plan.json)"
bootstrap_candidate_count="$(jq -r '.bootstrapCandidates | length' .local/plugin-clawhub-release-plan.json)"
missing_trusted_publisher_count="$(jq -r '.missingTrustedPublisher | length' .local/plugin-clawhub-release-plan.json)"
skipped_published_count="$(jq -r '.skippedPublished | length' .local/plugin-clawhub-release-plan.json)"
has_candidates="false"
if [[ "${candidate_count}" != "0" ]]; then
has_candidates="true"
fi
has_bootstrap_candidates="false"
if [[ "${bootstrap_candidate_count}" != "0" ]]; then
has_bootstrap_candidates="true"
fi
has_missing_trusted_publisher="false"
if [[ "${missing_trusted_publisher_count}" != "0" ]]; then
has_missing_trusted_publisher="true"
fi
matrix_json="$(jq -c '.candidates' .local/plugin-clawhub-release-plan.json)"
bootstrap_matrix_json="$(jq -c '.bootstrapCandidates' .local/plugin-clawhub-release-plan.json)"
missing_trusted_publisher_matrix_json="$(jq -c '.missingTrustedPublisher' .local/plugin-clawhub-release-plan.json)"
{
echo "candidate_count=${candidate_count}"
echo "bootstrap_candidate_count=${bootstrap_candidate_count}"
echo "missing_trusted_publisher_count=${missing_trusted_publisher_count}"
echo "skipped_published_count=${skipped_published_count}"
echo "has_candidates=${has_candidates}"
echo "has_bootstrap_candidates=${has_bootstrap_candidates}"
echo "has_missing_trusted_publisher=${has_missing_trusted_publisher}"
echo "matrix=${matrix_json}"
echo "bootstrap_matrix=${bootstrap_matrix_json}"
echo "missing_trusted_publisher_matrix=${missing_trusted_publisher_matrix_json}"
} >> "$GITHUB_OUTPUT"
echo "Plugin release candidates:"
jq -r '.candidates[]? | "- \(.packageName)@\(.version) [\(.publishTag)] from \(.packageDir)"' .local/plugin-clawhub-release-plan.json
echo "Bootstrap candidates requiring token bootstrap:"
jq -r '.bootstrapCandidates[]? | "- \(.packageName)@\(.version) [\(.publishTag)] from \(.packageDir)"' .local/plugin-clawhub-release-plan.json
echo "Missing trusted publisher candidates:"
jq -r '.missingTrustedPublisher[]? | "- \(.packageName)@\(.version) [\(.publishTag)] from \(.packageDir)"' .local/plugin-clawhub-release-plan.json
echo "Already published / skipped:"
jq -r '.skippedPublished[]? | "- \(.packageName)@\(.version)"' .local/plugin-clawhub-release-plan.json
- name: Fail when trusted publisher is missing
if: steps.plan.outputs.missing_trusted_publisher_count != '0'
run: |
echo "::error::One or more ClawHub packages exist but do not have trusted publishing configured. Configure trusted publishing before running the normal OIDC publish workflow."
jq -r '.missingTrustedPublisher[]? | "::error::Missing trusted publisher: \(.packageName)@\(.version). Configure trusted publishing for openclaw/openclaw, workflow plugin-clawhub-release.yml."' .local/plugin-clawhub-release-plan.json
exit 1
- name: Fail normal publish when bootstrap is required
if: steps.plan.outputs.bootstrap_candidate_count != '0'
run: |
echo "::error::One or more ClawHub packages do not exist yet and require the token-gated Plugin ClawHub New bootstrap workflow before normal OIDC publish can run."
jq -r '.bootstrapCandidates[]? | "::error::Bootstrap required: \(.packageName)@\(.version). Dispatch plugin-clawhub-new.yml for this package, then rerun the normal release."' .local/plugin-clawhub-release-plan.json
exit 1
- name: Fail manual publish when target versions already exist
if: github.event_name == 'workflow_dispatch' && inputs.publish_scope == 'selected' && steps.plan.outputs.skipped_published_count != '0'
if: github.event_name == 'workflow_dispatch' && inputs.dry_run != true && inputs.publish_scope == 'selected' && steps.plan.outputs.skipped_published_count != '0'
run: |
echo "::error::One or more selected plugin versions already exist on ClawHub. Bump the version before running a real publish."
exit 1
- name: Validate Tideclaw alpha plugin channels
if: startsWith(github.ref, 'refs/heads/tideclaw/alpha/')
env:
TRUSTED_PUBLISH_BRANCH: ${{ inputs.release_publish_branch || github.ref_name }}
run: |
set -euo pipefail
if [[ ! "${TRUSTED_PUBLISH_BRANCH}" =~ ^tideclaw/alpha/[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{4}Z$ ]]; then
exit 0
fi
invalid="$(
jq -r '.candidates[]? | select(.publishTag != "alpha" or .channel != "alpha") | "- \(.packageName)@\(.version) [\(.publishTag)]"' .local/plugin-clawhub-release-plan.json
)"
@@ -215,7 +283,7 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
RELEASE_PUBLISH_RUN_ID: ${{ inputs.release_publish_run_id }}
EXPECTED_WORKFLOW_BRANCH: ${{ github.ref_name }}
EXPECTED_WORKFLOW_BRANCH: ${{ inputs.release_publish_branch || github.ref_name }}
run: |
set -euo pipefail
if [[ -z "${RELEASE_PUBLISH_RUN_ID// }" ]]; then
@@ -234,99 +302,8 @@ jobs:
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_clawhub
if: needs.preview_plugins_clawhub.outputs.has_candidates == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
strategy:
fail-fast: false
max-parallel: 12
matrix:
plugin: ${{ fromJson(needs.preview_plugins_clawhub.outputs.matrix) }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
persist-credentials: false
ref: ${{ github.ref }}
fetch-depth: 0
- name: Checkout target revision
env:
TARGET_SHA: ${{ needs.preview_plugins_clawhub.outputs.ref_revision }}
run: |
set -euo pipefail
git fetch --no-tags origin \
+refs/heads/main:refs/remotes/origin/main \
'+refs/heads/release/*:refs/remotes/origin/release/*'
git checkout --detach "${TARGET_SHA}"
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
install-bun: "true"
install-deps: "true"
- name: Checkout ClawHub CLI source
uses: actions/checkout@v6
with:
persist-credentials: false
repository: ${{ env.CLAWHUB_REPOSITORY }}
ref: main
path: clawhub-source
fetch-depth: 0
- name: Checkout pinned ClawHub CLI revision
working-directory: clawhub-source
env:
CLAWHUB_REF: ${{ env.CLAWHUB_REF }}
run: git checkout --detach "${CLAWHUB_REF}"
- name: Install ClawHub CLI dependencies
working-directory: clawhub-source
run: |
set -euo pipefail
for attempt in 1 2 3; do
if bun install --frozen-lockfile; then
exit 0
fi
status="$?"
if [[ "${attempt}" == "3" ]]; then
exit "${status}"
fi
echo "bun install failed while preparing ClawHub CLI; retrying (${attempt}/3)."
rm -rf node_modules "${RUNNER_TEMP}/bun-install-cache" || true
sleep $((attempt * 15))
done
- name: Bootstrap ClawHub CLI
run: |
cat > "$RUNNER_TEMP/clawhub" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
exec bun "$GITHUB_WORKSPACE/clawhub-source/packages/clawhub/src/cli.ts" "$@"
EOF
chmod +x "$RUNNER_TEMP/clawhub"
echo "$RUNNER_TEMP" >> "$GITHUB_PATH"
- name: Verify package-local runtime build
run: node scripts/check-plugin-npm-runtime-builds.mjs --package "${{ matrix.plugin.packageDir }}"
- name: Preview publish command
env:
CLAWHUB_REGISTRY: ${{ env.CLAWHUB_REGISTRY }}
SOURCE_REPO: ${{ github.repository }}
SOURCE_COMMIT: ${{ needs.preview_plugins_clawhub.outputs.ref_revision }}
SOURCE_REF: ${{ github.ref }}
PACKAGE_TAG: ${{ matrix.plugin.publishTag }}
PACKAGE_DIR: ${{ matrix.plugin.packageDir }}
run: bash scripts/plugin-clawhub-publish.sh --dry-run "${PACKAGE_DIR}"
pack_plugins_clawhub_artifacts:
needs: [preview_plugins_clawhub, preview_plugin_pack, validate_release_publish_approval]
needs: [preview_plugins_clawhub, validate_release_publish_approval]
if: github.event_name == 'workflow_dispatch' && needs.preview_plugins_clawhub.outputs.has_candidates == 'true'
runs-on: ubuntu-latest
permissions:
@@ -361,47 +338,19 @@ jobs:
install-bun: "true"
install-deps: "true"
- name: Checkout ClawHub CLI source
uses: actions/checkout@v6
with:
persist-credentials: false
repository: ${{ env.CLAWHUB_REPOSITORY }}
ref: main
path: clawhub-source
fetch-depth: 0
- name: Verify package-local runtime build
run: node scripts/check-plugin-npm-runtime-builds.mjs --package "${{ matrix.plugin.packageDir }}"
- name: Checkout pinned ClawHub CLI revision
working-directory: clawhub-source
env:
CLAWHUB_REF: ${{ env.CLAWHUB_REF }}
run: git checkout --detach "${CLAWHUB_REF}"
- name: Install ClawHub CLI dependencies
working-directory: clawhub-source
- name: Install pinned ClawHub CLI wrapper
run: |
set -euo pipefail
for attempt in 1 2 3; do
if bun install --frozen-lockfile; then
exit 0
fi
status="$?"
if [[ "${attempt}" == "3" ]]; then
exit "${status}"
fi
echo "bun install failed while preparing ClawHub CLI; retrying (${attempt}/3)."
rm -rf node_modules "${RUNNER_TEMP}/bun-install-cache" || true
sleep $((attempt * 15))
done
- name: Bootstrap ClawHub CLI
run: |
cat > "$RUNNER_TEMP/clawhub" <<'EOF'
cat > "${RUNNER_TEMP}/clawhub" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
exec bun "$GITHUB_WORKSPACE/clawhub-source/packages/clawhub/src/cli.ts" "$@"
exec npm exec --yes --package "${CLAWHUB_CLI_PACKAGE}" -- clawhub "$@"
EOF
chmod +x "$RUNNER_TEMP/clawhub"
echo "$RUNNER_TEMP" >> "$GITHUB_PATH"
chmod +x "${RUNNER_TEMP}/clawhub"
echo "${RUNNER_TEMP}" >> "${GITHUB_PATH}"
- name: Pack ClawHub package artifact
env:
@@ -422,19 +371,23 @@ jobs:
if-no-files-found: error
retention-days: 7
approve_plugin_clawhub_release:
approve_plugins_clawhub_release:
needs: [preview_plugins_clawhub, pack_plugins_clawhub_artifacts]
if: github.event_name == 'workflow_dispatch' && inputs.dry_run != true && needs.preview_plugins_clawhub.outputs.has_candidates == 'true'
if: always() && github.event_name == 'workflow_dispatch' && inputs.dry_run != true && needs.preview_plugins_clawhub.outputs.has_candidates == 'true' && needs.pack_plugins_clawhub_artifacts.result == 'success'
runs-on: ubuntu-latest
environment: clawhub-plugin-release
permissions: {}
permissions:
contents: read
steps:
- name: Approve ClawHub package publish
run: echo "ClawHub package publish approved."
- name: Approve Plugin ClawHub release publish
run: |
echo "Approved CLAW-277 03 - Split OpenClaw plugin ClawHub publishing into OIDC release and token bootstrap workflows release publish gate."
publish_plugins_clawhub:
needs: [preview_plugins_clawhub, pack_plugins_clawhub_artifacts, approve_plugin_clawhub_release]
if: always() && github.event_name == 'workflow_dispatch' && needs.preview_plugins_clawhub.outputs.has_candidates == 'true' && needs.pack_plugins_clawhub_artifacts.result == 'success' && (inputs.dry_run == true || needs.approve_plugin_clawhub_release.result == 'success')
needs:
[preview_plugins_clawhub, pack_plugins_clawhub_artifacts, approve_plugins_clawhub_release]
if: always() && github.event_name == 'workflow_dispatch' && needs.preview_plugins_clawhub.outputs.has_candidates == 'true' && needs.pack_plugins_clawhub_artifacts.result == 'success' && (inputs.dry_run == true || needs.approve_plugins_clawhub_release.result == 'success')
uses: openclaw/clawhub/.github/workflows/package-publish.yml@9d49df109d4ad3dc8a6ecf05d26b39f46d294721
permissions:
actions: read
contents: read
@@ -444,19 +397,18 @@ jobs:
max-parallel: 32
matrix:
plugin: ${{ fromJson(needs.preview_plugins_clawhub.outputs.matrix) }}
uses: openclaw/clawhub/.github/workflows/package-publish.yml@c9bb13023598dcc547fdf4a93b9d42512b8c8854
with:
dry_run: ${{ inputs.dry_run }}
json: true
package_artifact_name: ${{ matrix.plugin.artifactName }}
dry_run: ${{ inputs.dry_run }}
registry: https://clawhub.ai
site: https://clawhub.ai
tags: ${{ matrix.plugin.publishTag }}
source_repo: ${{ github.repository }}
source_commit: ${{ needs.preview_plugins_clawhub.outputs.ref_revision }}
source_ref: ${{ github.ref }}
tags: ${{ matrix.plugin.publishTag }}
secrets:
clawhub_token: ${{ secrets.CLAWHUB_TOKEN }}
source_path: ${{ matrix.plugin.packageDir }}
inspector_artifact_name: ${{ matrix.plugin.artifactName }}-inspector
publish_json_artifact_name: ${{ matrix.plugin.artifactName }}-publish-json
verify_published_clawhub_package:
needs: [preview_plugins_clawhub, publish_plugins_clawhub]