mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-22 12:58:09 +00:00
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:
214
.github/workflows/openclaw-release-publish.yml
vendored
214
.github/workflows/openclaw-release-publish.yml
vendored
@@ -387,7 +387,9 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
dispatch_workflow() {
|
||||
dispatch_workflow_at_ref() {
|
||||
local workflow_ref="$1"
|
||||
shift
|
||||
local workflow="$1"
|
||||
shift
|
||||
|
||||
@@ -397,7 +399,7 @@ jobs:
|
||||
-F per_page=100 \
|
||||
--jq '[.workflow_runs[].id]')"
|
||||
|
||||
dispatch_output="$(gh workflow run --repo "$GITHUB_REPOSITORY" "$workflow" --ref "$CHILD_WORKFLOW_REF" "$@" 2>&1)"
|
||||
dispatch_output="$(gh workflow run --repo "$GITHUB_REPOSITORY" "$workflow" --ref "$workflow_ref" "$@" 2>&1)"
|
||||
printf '%s\n' "$dispatch_output" >&2
|
||||
run_id="$(
|
||||
printf '%s\n' "$dispatch_output" |
|
||||
@@ -432,6 +434,10 @@ jobs:
|
||||
printf '%s\n' "${run_id}"
|
||||
}
|
||||
|
||||
dispatch_workflow() {
|
||||
dispatch_workflow_at_ref "$CHILD_WORKFLOW_REF" "$@"
|
||||
}
|
||||
|
||||
print_pending_deployments() {
|
||||
local workflow="$1"
|
||||
local run_id="$2"
|
||||
@@ -710,6 +716,71 @@ jobs:
|
||||
exit 1
|
||||
}
|
||||
|
||||
resolve_clawhub_release_plan() {
|
||||
local -a plan_args
|
||||
|
||||
clawhub_plan_path="${RUNNER_TEMP}/openclaw-release-clawhub-plan.json"
|
||||
plan_args=(
|
||||
--release-tag "${RELEASE_TAG}"
|
||||
--release-publish-branch "${CHILD_WORKFLOW_REF}"
|
||||
--release-publish-run-id "${GITHUB_RUN_ID}"
|
||||
--plugin-publish-scope "${PLUGIN_PUBLISH_SCOPE}"
|
||||
)
|
||||
if [[ -n "${PLUGINS// }" ]]; then
|
||||
plan_args+=(--plugins "${PLUGINS}")
|
||||
fi
|
||||
|
||||
CLAWHUB_REGISTRY="${CLAWHUB_REGISTRY:-https://clawhub.ai}" \
|
||||
node --import tsx scripts/openclaw-release-clawhub-plan.ts "${plan_args[@]}" > "${clawhub_plan_path}"
|
||||
|
||||
echo "Resolved OpenClaw release ClawHub dispatch plan:"
|
||||
cat "${clawhub_plan_path}"
|
||||
|
||||
clawhub_workflow_ref="$(jq -r '.clawHubWorkflowRef' "${clawhub_plan_path}")"
|
||||
normal_plugins="$(jq -r '.summary.normalPlugins' "${clawhub_plan_path}")"
|
||||
bootstrap_plugins="$(jq -r '.summary.bootstrapPlugins' "${clawhub_plan_path}")"
|
||||
missing_trusted_plugins="$(jq -r '.summary.missingTrustedPlugins' "${clawhub_plan_path}")"
|
||||
normal_plugin_count="$(jq -r '.summary.normalCount' "${clawhub_plan_path}")"
|
||||
bootstrap_plugin_count="$(jq -r '.summary.bootstrapCount' "${clawhub_plan_path}")"
|
||||
missing_trusted_plugin_count="$(jq -r '.summary.missingTrustedPublisherCount' "${clawhub_plan_path}")"
|
||||
|
||||
{
|
||||
echo "### ClawHub release plan"
|
||||
echo
|
||||
echo "- Normal OIDC candidates: \`${normal_plugin_count}\`"
|
||||
echo "- Bootstrap/repair candidates: \`${bootstrap_plugin_count}\`"
|
||||
echo "- Existing-package trusted-publisher repairs: \`${missing_trusted_plugin_count}\`"
|
||||
if [[ -n "${normal_plugins}" ]]; then
|
||||
echo "- Normal plugins: \`${normal_plugins}\`"
|
||||
fi
|
||||
if [[ -n "${bootstrap_plugins}" ]]; then
|
||||
echo "- Bootstrap/repair plugins: \`${bootstrap_plugins}\`"
|
||||
fi
|
||||
if [[ -n "${missing_trusted_plugins}" ]]; then
|
||||
echo "- Trusted-publisher repair plugins: \`${missing_trusted_plugins}\`"
|
||||
fi
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
}
|
||||
|
||||
append_clawhub_dispatch_args() {
|
||||
local target="$1"
|
||||
while IFS=$'\t' read -r key value; do
|
||||
clawhub_dispatch_args+=(-f "${key}=${value}")
|
||||
done < <(jq -r --arg target "${target}" '.[$target].inputs | to_entries[] | [.key, .value] | @tsv' "${clawhub_plan_path}")
|
||||
}
|
||||
|
||||
write_clawhub_runtime_state() {
|
||||
local force_skip_clawhub="$1"
|
||||
local output_path="$2"
|
||||
node --import tsx scripts/openclaw-release-clawhub-runtime-state.ts \
|
||||
--repository "${GITHUB_REPOSITORY}" \
|
||||
--wait-for-clawhub "${WAIT_FOR_CLAWHUB}" \
|
||||
--force-skip-clawhub "${force_skip_clawhub}" \
|
||||
--normal-run-id "${plugin_clawhub_run_id:-}" \
|
||||
--bootstrap-run-id "${plugin_clawhub_bootstrap_run_id:-}" \
|
||||
--bootstrap-completed "${plugin_clawhub_bootstrap_completed:-false}" > "${output_path}"
|
||||
}
|
||||
|
||||
create_or_update_github_release() {
|
||||
local release_version notes_version title notes_file changelog_file latest_arg prerelease_args
|
||||
release_version="${RELEASE_TAG#v}"
|
||||
@@ -798,7 +869,7 @@ jobs:
|
||||
}
|
||||
|
||||
verify_published_release() {
|
||||
local release_version evidence_path skip_clawhub
|
||||
local release_version evidence_path skip_clawhub clawhub_runtime_state_path
|
||||
local -a verify_args
|
||||
|
||||
skip_clawhub="${1:-false}"
|
||||
@@ -815,17 +886,18 @@ jobs:
|
||||
--dist-tag "${RELEASE_NPM_DIST_TAG}"
|
||||
--repo "${GITHUB_REPOSITORY}"
|
||||
--workflow-ref "${CHILD_WORKFLOW_REF}"
|
||||
--clawhub-workflow-ref "${clawhub_workflow_ref}"
|
||||
--full-release-validation-run "${FULL_RELEASE_VALIDATION_RUN_ID}"
|
||||
--plugin-npm-run "${plugin_npm_run_id}"
|
||||
--openclaw-npm-run "${openclaw_npm_run_id}"
|
||||
--evidence-out "${evidence_path}"
|
||||
--skip-github-release
|
||||
)
|
||||
if [[ "${skip_clawhub}" == "true" || "${WAIT_FOR_CLAWHUB}" != "true" ]]; then
|
||||
verify_args+=(--skip-clawhub)
|
||||
else
|
||||
verify_args+=(--plugin-clawhub-run "${plugin_clawhub_run_id}")
|
||||
fi
|
||||
clawhub_runtime_state_path="${RUNNER_TEMP}/openclaw-release-clawhub-runtime-state-verify.json"
|
||||
write_clawhub_runtime_state "${skip_clawhub}" "${clawhub_runtime_state_path}"
|
||||
while IFS= read -r arg; do
|
||||
verify_args+=("${arg}")
|
||||
done < <(jq -r '.verifierArgs[]' "${clawhub_runtime_state_path}")
|
||||
if [[ -n "${PLUGINS// }" ]]; then
|
||||
verify_args+=(--plugins "${PLUGINS}")
|
||||
fi
|
||||
@@ -841,7 +913,7 @@ jobs:
|
||||
}
|
||||
|
||||
append_release_proof_to_github_release() {
|
||||
local release_version body_file notes_file tarball integrity telegram_line clawhub_line
|
||||
local release_version body_file notes_file tarball integrity telegram_line clawhub_line clawhub_bootstrap_line clawhub_runtime_state_path
|
||||
|
||||
release_version="${RELEASE_TAG#v}"
|
||||
body_file="${RUNNER_TEMP}/release-body.md"
|
||||
@@ -855,11 +927,10 @@ jobs:
|
||||
else
|
||||
telegram_line="- npm Telegram beta E2E: not supplied"
|
||||
fi
|
||||
if [[ "${WAIT_FOR_CLAWHUB}" == "true" ]]; then
|
||||
clawhub_line="- plugin ClawHub publish: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${plugin_clawhub_run_id}"
|
||||
else
|
||||
clawhub_line="- plugin ClawHub publish: dispatched separately, not awaited by this proof: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${plugin_clawhub_run_id}"
|
||||
fi
|
||||
clawhub_runtime_state_path="${RUNNER_TEMP}/openclaw-release-clawhub-runtime-state-proof.json"
|
||||
write_clawhub_runtime_state false "${clawhub_runtime_state_path}"
|
||||
clawhub_line="$(jq -r '.proofLines.normal' "${clawhub_runtime_state_path}")"
|
||||
clawhub_bootstrap_line="$(jq -r '.proofLines.bootstrap' "${clawhub_runtime_state_path}")"
|
||||
|
||||
RELEASE_BODY_FILE="${body_file}" \
|
||||
RELEASE_NOTES_FILE="${notes_file}" \
|
||||
@@ -875,6 +946,7 @@ jobs:
|
||||
PLUGIN_NPM_RUN_ID="${plugin_npm_run_id}" \
|
||||
OPENCLAW_NPM_RUN_ID="${openclaw_npm_run_id}" \
|
||||
CLAWHUB_LINE="${clawhub_line}" \
|
||||
CLAWHUB_BOOTSTRAP_LINE="${clawhub_bootstrap_line}" \
|
||||
TELEGRAM_LINE="${telegram_line}" \
|
||||
node --input-type=module <<'NODE'
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
@@ -899,6 +971,7 @@ jobs:
|
||||
`- full release validation: https://github.com/${process.env.RELEASE_REPO}/actions/runs/${process.env.FULL_RELEASE_VALIDATION_RUN_ID}`,
|
||||
`- plugin npm publish: https://github.com/${process.env.RELEASE_REPO}/actions/runs/${process.env.PLUGIN_NPM_RUN_ID}`,
|
||||
process.env.CLAWHUB_LINE,
|
||||
process.env.CLAWHUB_BOOTSTRAP_LINE,
|
||||
`- OpenClaw npm publish: https://github.com/${process.env.RELEASE_REPO}/actions/runs/${process.env.OPENCLAW_NPM_RUN_ID}`,
|
||||
process.env.TELEGRAM_LINE,
|
||||
].join("\n");
|
||||
@@ -915,6 +988,7 @@ jobs:
|
||||
echo "### Publish sequence"
|
||||
echo
|
||||
echo "- Workflow ref: \`${CHILD_WORKFLOW_REF}\`"
|
||||
echo "- ClawHub workflow ref: release tag \`${RELEASE_TAG}\`"
|
||||
echo "- Release tag: \`${RELEASE_TAG}\`"
|
||||
echo "- Release SHA: \`${TARGET_SHA}\`"
|
||||
echo "- Release approval: this workflow job"
|
||||
@@ -933,27 +1007,66 @@ jobs:
|
||||
|
||||
guard_existing_public_release
|
||||
guard_openclaw_npm_not_already_published
|
||||
resolve_clawhub_release_plan
|
||||
|
||||
npm_args=(-f publish_scope="${PLUGIN_PUBLISH_SCOPE}" -f ref="${TARGET_SHA}" -f release_publish_run_id="${GITHUB_RUN_ID}")
|
||||
clawhub_args=(-f publish_scope="${PLUGIN_PUBLISH_SCOPE}" -f ref="${TARGET_SHA}" -f release_publish_run_id="${GITHUB_RUN_ID}")
|
||||
if [[ -n "${PLUGINS}" ]]; then
|
||||
npm_args+=(-f plugins="${PLUGINS}")
|
||||
clawhub_args+=(-f plugins="${PLUGINS}")
|
||||
fi
|
||||
|
||||
plugin_npm_run_id="$(dispatch_workflow plugin-npm-release.yml "${npm_args[@]}")"
|
||||
plugin_clawhub_run_id="$(dispatch_workflow plugin-clawhub-release.yml "${clawhub_args[@]}")"
|
||||
plugin_clawhub_run_id=""
|
||||
if [[ "$(jq -r '.normal.shouldDispatch' "${clawhub_plan_path}")" == "true" ]]; then
|
||||
clawhub_dispatch_args=()
|
||||
append_clawhub_dispatch_args normal
|
||||
plugin_clawhub_run_id="$(dispatch_workflow_at_ref \
|
||||
"$(jq -r '.normal.ref' "${clawhub_plan_path}")" \
|
||||
"$(jq -r '.normal.workflow' "${clawhub_plan_path}")" \
|
||||
"${clawhub_dispatch_args[@]}")"
|
||||
else
|
||||
echo "- plugin-clawhub-release.yml: no normal OIDC candidates" >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
plugin_clawhub_bootstrap_run_id=""
|
||||
plugin_clawhub_bootstrap_completed="false"
|
||||
if [[ "$(jq -r '.bootstrap.shouldDispatch' "${clawhub_plan_path}")" == "true" ]]; then
|
||||
clawhub_dispatch_args=()
|
||||
append_clawhub_dispatch_args bootstrap
|
||||
plugin_clawhub_bootstrap_run_id="$(dispatch_workflow_at_ref \
|
||||
"$(jq -r '.bootstrap.ref' "${clawhub_plan_path}")" \
|
||||
"$(jq -r '.bootstrap.workflow' "${clawhub_plan_path}")" \
|
||||
"${clawhub_dispatch_args[@]}")"
|
||||
else
|
||||
echo "- plugin-clawhub-new.yml: no bootstrap candidates" >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
{
|
||||
echo "- Plugin npm run ID: \`${plugin_npm_run_id}\`"
|
||||
echo "- Plugin ClawHub run ID: \`${plugin_clawhub_run_id}\`"
|
||||
echo "- Plugin ClawHub run ID: \`${plugin_clawhub_run_id:-none}\`"
|
||||
echo "- Plugin ClawHub bootstrap run ID: \`${plugin_clawhub_bootstrap_run_id:-none}\`"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
if ! wait_for_run plugin-npm-release.yml "${plugin_npm_run_id}"; then
|
||||
echo "Plugin npm publish failed; cancelling ClawHub publish child ${plugin_clawhub_run_id}." >&2
|
||||
gh run cancel --repo "$GITHUB_REPOSITORY" "${plugin_clawhub_run_id}" >/dev/null 2>&1 || true
|
||||
echo "Plugin npm publish failed; cancelling dispatched ClawHub child workflows." >&2
|
||||
if [[ -n "${plugin_clawhub_run_id}" ]]; then
|
||||
gh run cancel --repo "$GITHUB_REPOSITORY" "${plugin_clawhub_run_id}" >/dev/null 2>&1 || true
|
||||
fi
|
||||
if [[ -n "${plugin_clawhub_bootstrap_run_id}" ]]; then
|
||||
gh run cancel --repo "$GITHUB_REPOSITORY" "${plugin_clawhub_bootstrap_run_id}" >/dev/null 2>&1 || true
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -n "${plugin_clawhub_bootstrap_run_id}" && "${WAIT_FOR_CLAWHUB}" == "true" ]]; then
|
||||
echo "Waiting for plugin-clawhub-new.yml bootstrap to finish before continuing release publish."
|
||||
if wait_for_run plugin-clawhub-new.yml "${plugin_clawhub_bootstrap_run_id}"; then
|
||||
plugin_clawhub_bootstrap_completed="true"
|
||||
else
|
||||
if [[ -n "${plugin_clawhub_run_id}" ]]; then
|
||||
gh run cancel --repo "$GITHUB_REPOSITORY" "${plugin_clawhub_run_id}" >/dev/null 2>&1 || true
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
openclaw_npm_run_id=""
|
||||
if [[ "${PUBLISH_OPENCLAW_NPM}" == "true" ]]; then
|
||||
openclaw_npm_run_id="$(dispatch_workflow openclaw-npm-release.yml \
|
||||
@@ -970,19 +1083,52 @@ jobs:
|
||||
|
||||
clawhub_result=""
|
||||
clawhub_pid=""
|
||||
clawhub_bootstrap_result=""
|
||||
clawhub_bootstrap_pid=""
|
||||
if [[ "${WAIT_FOR_CLAWHUB}" == "true" ]]; then
|
||||
clawhub_result="$RUNNER_TEMP/clawhub-result.txt"
|
||||
wait_run_pid=""
|
||||
wait_for_run_background plugin-clawhub-release.yml "${plugin_clawhub_run_id}" "${clawhub_result}"
|
||||
clawhub_pid="${wait_run_pid}"
|
||||
else
|
||||
wait_for_job_success plugin-clawhub-release.yml "${plugin_clawhub_run_id}" "Validate release publish approval"
|
||||
if approve_child_publish_environment plugin-clawhub-release.yml "${plugin_clawhub_run_id}"; then
|
||||
:
|
||||
else
|
||||
echo "- plugin-clawhub-release.yml: child environment gate not ready; publish was left dispatched (${plugin_clawhub_run_id})" >> "$GITHUB_STEP_SUMMARY"
|
||||
if [[ -n "${plugin_clawhub_run_id}" ]]; then
|
||||
clawhub_result="$RUNNER_TEMP/clawhub-result.txt"
|
||||
wait_run_pid=""
|
||||
wait_for_run_background plugin-clawhub-release.yml "${plugin_clawhub_run_id}" "${clawhub_result}"
|
||||
clawhub_pid="${wait_run_pid}"
|
||||
fi
|
||||
if [[ -n "${plugin_clawhub_bootstrap_run_id}" ]]; then
|
||||
if [[ "${plugin_clawhub_bootstrap_completed}" == "true" ]]; then
|
||||
echo "- plugin-clawhub-new.yml: bootstrap already completed before continuing" >> "$GITHUB_STEP_SUMMARY"
|
||||
else
|
||||
clawhub_bootstrap_result="$RUNNER_TEMP/clawhub-bootstrap-result.txt"
|
||||
wait_run_pid=""
|
||||
wait_for_run_background plugin-clawhub-new.yml "${plugin_clawhub_bootstrap_run_id}" "${clawhub_bootstrap_result}"
|
||||
clawhub_bootstrap_pid="${wait_run_pid}"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
if [[ -n "${plugin_clawhub_run_id}" ]]; then
|
||||
wait_for_job_success plugin-clawhub-release.yml "${plugin_clawhub_run_id}" "Validate release publish approval"
|
||||
if approve_child_publish_environment plugin-clawhub-release.yml "${plugin_clawhub_run_id}"; then
|
||||
:
|
||||
else
|
||||
echo "- plugin-clawhub-release.yml: child environment gate not ready; publish was left dispatched (${plugin_clawhub_run_id})" >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
echo "- plugin-clawhub-release.yml: publish not awaited (${plugin_clawhub_run_id})" >> "$GITHUB_STEP_SUMMARY"
|
||||
else
|
||||
echo "- plugin-clawhub-release.yml: no normal OIDC publish to await" >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
if [[ -n "${plugin_clawhub_bootstrap_run_id}" ]]; then
|
||||
if [[ "${plugin_clawhub_bootstrap_completed}" == "true" ]]; then
|
||||
echo "- plugin-clawhub-new.yml: bootstrap already completed before continuing" >> "$GITHUB_STEP_SUMMARY"
|
||||
else
|
||||
wait_for_job_success plugin-clawhub-new.yml "${plugin_clawhub_bootstrap_run_id}" "Validate release publish approval"
|
||||
if approve_child_publish_environment plugin-clawhub-new.yml "${plugin_clawhub_bootstrap_run_id}"; then
|
||||
:
|
||||
else
|
||||
echo "- plugin-clawhub-new.yml: child environment gate not ready; bootstrap was left dispatched (${plugin_clawhub_bootstrap_run_id})" >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
echo "- plugin-clawhub-new.yml: bootstrap not awaited (${plugin_clawhub_bootstrap_run_id})" >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
else
|
||||
echo "- plugin-clawhub-new.yml: no bootstrap publish to await" >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
echo "- plugin-clawhub-release.yml: publish not awaited (${plugin_clawhub_run_id})" >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
|
||||
openclaw_result=""
|
||||
@@ -1011,6 +1157,12 @@ jobs:
|
||||
if [[ -f "${clawhub_result}" && "$(cat "${clawhub_result}")" != "success" ]]; then
|
||||
failed=1
|
||||
fi
|
||||
if [[ -n "${clawhub_bootstrap_pid}" ]] && ! wait "${clawhub_bootstrap_pid}"; then
|
||||
failed=1
|
||||
fi
|
||||
if [[ -f "${clawhub_bootstrap_result}" && "$(cat "${clawhub_bootstrap_result}")" != "success" ]]; then
|
||||
failed=1
|
||||
fi
|
||||
|
||||
if [[ -n "${openclaw_npm_run_id}" && "${openclaw_failed}" == "0" ]]; then
|
||||
if [[ "${failed}" == "0" ]]; then
|
||||
|
||||
504
.github/workflows/plugin-clawhub-new.yml
vendored
Normal file
504
.github/workflows/plugin-clawhub-new.yml
vendored
Normal file
@@ -0,0 +1,504 @@
|
||||
name: Plugin ClawHub New
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
plugins:
|
||||
description: Comma-separated plugin package names to bootstrap on ClawHub
|
||||
required: true
|
||||
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
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
release_publish_run_id:
|
||||
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 token-gated ClawHub bootstrap handoff without publishing.
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
concurrency:
|
||||
group: plugin-clawhub-new-${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.sha }}
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
NODE_VERSION: "24.15.0"
|
||||
CLAWHUB_REGISTRY: "https://clawhub.ai"
|
||||
CLAWHUB_CLI_PACKAGE: "clawhub@0.21.0"
|
||||
|
||||
jobs:
|
||||
resolve_bootstrap_plan:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
outputs:
|
||||
ref_revision: ${{ steps.ref.outputs.sha }}
|
||||
has_bootstrap_candidates: ${{ steps.plan.outputs.has_bootstrap_candidates }}
|
||||
bootstrap_candidate_count: ${{ steps.plan.outputs.bootstrap_candidate_count }}
|
||||
matrix: ${{ steps.plan.outputs.matrix }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: ${{ github.ref }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Resolve checked-out ref
|
||||
id: ref
|
||||
env:
|
||||
TARGET_REF: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || '' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git fetch --no-tags origin \
|
||||
+refs/heads/main:refs/remotes/origin/main \
|
||||
'+refs/heads/release/*:refs/remotes/origin/release/*'
|
||||
if [[ -n "${TARGET_REF}" ]]; then
|
||||
if git rev-parse --verify --quiet "${TARGET_REF}^{commit}" >/dev/null; then
|
||||
target_sha="$(git rev-parse "${TARGET_REF}^{commit}")"
|
||||
elif git rev-parse --verify --quiet "origin/${TARGET_REF}^{commit}" >/dev/null; then
|
||||
target_sha="$(git rev-parse "origin/${TARGET_REF}^{commit}")"
|
||||
else
|
||||
echo "Unable to resolve requested publish ref: ${TARGET_REF}" >&2
|
||||
exit 1
|
||||
fi
|
||||
git checkout --detach "${target_sha}"
|
||||
fi
|
||||
echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Validate ref is on a trusted publish branch
|
||||
env:
|
||||
TRUSTED_PUBLISH_BRANCH: ${{ inputs.release_publish_branch || github.ref_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
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 [[ "${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
|
||||
fi
|
||||
fi
|
||||
echo "Plugin ClawHub bootstraps must target a commit reachable from main, release/*, or the matching Tideclaw alpha branch." >&2
|
||||
exit 1
|
||||
|
||||
- name: Setup Node environment
|
||||
uses: ./.github/actions/setup-node-env
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
install-bun: "false"
|
||||
|
||||
- name: Validate publishable plugin metadata
|
||||
env:
|
||||
RELEASE_PLUGINS: ${{ inputs.plugins }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ -z "${RELEASE_PLUGINS// }" ]]; then
|
||||
echo "Plugin ClawHub bootstrap requires at least one package name in plugins." >&2
|
||||
exit 1
|
||||
fi
|
||||
pnpm release:plugins:clawhub:check -- --selection-mode selected --plugins "${RELEASE_PLUGINS}"
|
||||
|
||||
- name: Resolve plugin bootstrap plan
|
||||
id: plan
|
||||
env:
|
||||
RELEASE_PLUGINS: ${{ inputs.plugins }}
|
||||
CLAWHUB_REGISTRY: ${{ env.CLAWHUB_REGISTRY }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p .local
|
||||
node --import tsx scripts/plugin-clawhub-release-plan.ts \
|
||||
--selection-mode selected \
|
||||
--plugins "${RELEASE_PLUGINS}" > .local/plugin-clawhub-release-plan.json
|
||||
|
||||
cat .local/plugin-clawhub-release-plan.json
|
||||
|
||||
bootstrap_candidate_count="$(jq -r '(.bootstrapCandidates | length) + (.missingTrustedPublisher | length)' .local/plugin-clawhub-release-plan.json)"
|
||||
selected_count="$(jq -r '.all | length' .local/plugin-clawhub-release-plan.json)"
|
||||
matrix_json="$(
|
||||
jq -c '
|
||||
[
|
||||
.bootstrapCandidates[]? + {
|
||||
bootstrapMode: "publish",
|
||||
requiresManualOverride: false
|
||||
},
|
||||
.missingTrustedPublisher[]? + {
|
||||
bootstrapMode: (if .alreadyPublished then "configure-only" else "publish" end),
|
||||
requiresManualOverride: true
|
||||
}
|
||||
]
|
||||
' .local/plugin-clawhub-release-plan.json
|
||||
)"
|
||||
has_bootstrap_candidates="false"
|
||||
if [[ "${bootstrap_candidate_count}" != "0" ]]; then
|
||||
has_bootstrap_candidates="true"
|
||||
fi
|
||||
|
||||
invalid_scope="$(
|
||||
jq -r '
|
||||
(.bootstrapCandidates[]?, .missingTrustedPublisher[]?)
|
||||
| select(.packageName | startswith("@openclaw/") | not)
|
||||
| "- \(.packageName)@\(.version)"
|
||||
' .local/plugin-clawhub-release-plan.json
|
||||
)"
|
||||
if [[ -n "${invalid_scope}" ]]; then
|
||||
echo "Plugin ClawHub bootstrap only supports @openclaw/* packages." >&2
|
||||
printf '%s\n' "${invalid_scope}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
not_bootstrap="$(
|
||||
jq -r '
|
||||
(.bootstrapCandidates | map(.packageName)) as $bootstrapNames
|
||||
| (.missingTrustedPublisher | map(.packageName)) as $repairNames
|
||||
| .all[]?
|
||||
| select(.packageName as $name | ($bootstrapNames + $repairNames | index($name) | not))
|
||||
| "- \(.packageName)@\(.version)"
|
||||
' .local/plugin-clawhub-release-plan.json
|
||||
)"
|
||||
if [[ -n "${not_bootstrap}" ]]; then
|
||||
echo "Selected packages must all be first-publish bootstrap candidates or trusted-publisher repair candidates." >&2
|
||||
printf '%s\n' "${not_bootstrap}" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "${selected_count}" == "0" || "${bootstrap_candidate_count}" == "0" ]]; then
|
||||
echo "No selected packages require ClawHub bootstrap." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
{
|
||||
echo "bootstrap_candidate_count=${bootstrap_candidate_count}"
|
||||
echo "has_bootstrap_candidates=${has_bootstrap_candidates}"
|
||||
echo "matrix=${matrix_json}"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
echo "ClawHub bootstrap candidates:"
|
||||
jq -r '
|
||||
.bootstrapCandidates[]? | "- \(.packageName)@\(.version) [\(.publishTag)] from \(.packageDir)"
|
||||
' .local/plugin-clawhub-release-plan.json
|
||||
echo "ClawHub trusted-publisher repair candidates:"
|
||||
jq -r '
|
||||
.missingTrustedPublisher[]? | "- \(.packageName)@\(.version) [\(.publishTag)] from \(.packageDir), alreadyPublished=\(.alreadyPublished)"
|
||||
' .local/plugin-clawhub-release-plan.json
|
||||
|
||||
- name: Validate Tideclaw alpha plugin channels
|
||||
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 '
|
||||
(.bootstrapCandidates[]?, .missingTrustedPublisher[]?)
|
||||
| select(.publishTag != "alpha" or .channel != "alpha")
|
||||
| "- \(.packageName)@\(.version) [\(.publishTag)]"
|
||||
' .local/plugin-clawhub-release-plan.json
|
||||
)"
|
||||
if [[ -n "${invalid}" ]]; then
|
||||
echo "Tideclaw alpha ClawHub bootstraps 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: resolve_bootstrap_plan
|
||||
if: github.event_name == 'workflow_dispatch' && needs.resolve_bootstrap_plan.outputs.has_bootstrap_candidates == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@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: ${{ inputs.release_publish_branch || github.ref_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ -z "${RELEASE_PUBLISH_RUN_ID// }" ]]; then
|
||||
if [[ "${GITHUB_ACTOR}" == "github-actions[bot]" ]]; then
|
||||
echo "Plugin ClawHub bootstrap dispatched by another workflow must include release_publish_run_id." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Direct Plugin ClawHub New dispatch; relying on this workflow's clawhub-plugin-bootstrap environment approval."
|
||||
exit 0
|
||||
fi
|
||||
direct_recovery=false
|
||||
if [[ "${GITHUB_ACTOR}" != "github-actions[bot]" ]]; then
|
||||
direct_recovery=true
|
||||
echo "Direct Plugin ClawHub New recovery with release_publish_run_id; relying on this workflow's clawhub-plugin-bootstrap 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
|
||||
|
||||
validate_bootstrap_trusted_publisher_cli:
|
||||
needs: [resolve_bootstrap_plan, validate_release_publish_approval]
|
||||
if: always() && github.event_name == 'workflow_dispatch' && inputs.dry_run != true && needs.resolve_bootstrap_plan.outputs.has_bootstrap_candidates == 'true' && needs.validate_release_publish_approval.result == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Validate pinned ClawHub trusted publisher CLI support
|
||||
env:
|
||||
CLAWHUB_CLI_PACKAGE: ${{ env.CLAWHUB_CLI_PACKAGE }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
help_output="$(
|
||||
npm exec --yes --package "${CLAWHUB_CLI_PACKAGE}" -- \
|
||||
clawhub package trusted-publisher set --help 2>&1 || true
|
||||
)"
|
||||
printf '%s\n' "${help_output}"
|
||||
if ! grep -Fq "Usage: clawhub package trusted-publisher set" <<<"${help_output}"; then
|
||||
echo "::error::CLAW-277 03 - Split OpenClaw plugin ClawHub publishing into OIDC release and token bootstrap workflows requires ${CLAWHUB_CLI_PACKAGE} to expose 'package trusted-publisher set' before token bootstrap publish can run. The pinned CLI returned parent help or no set command, so this workflow is stopping before creating a ClawHub package row."
|
||||
exit 1
|
||||
fi
|
||||
for required_flag in --repository --workflow-filename; do
|
||||
if ! grep -Fq -- "${required_flag}" <<<"${help_output}"; then
|
||||
echo "::error::CLAW-277 03 - Split OpenClaw plugin ClawHub publishing into OIDC release and token bootstrap workflows requires ${CLAWHUB_CLI_PACKAGE} trusted-publisher set help to include ${required_flag}."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
publish_bootstrap_plugins:
|
||||
needs:
|
||||
[
|
||||
resolve_bootstrap_plan,
|
||||
validate_release_publish_approval,
|
||||
validate_bootstrap_trusted_publisher_cli,
|
||||
]
|
||||
if: always() && github.event_name == 'workflow_dispatch' && needs.resolve_bootstrap_plan.outputs.has_bootstrap_candidates == 'true' && needs.validate_release_publish_approval.result == 'success' && (inputs.dry_run == true || needs.validate_bootstrap_trusted_publisher_cli.result == 'success')
|
||||
runs-on: ubuntu-latest
|
||||
environment: clawhub-plugin-bootstrap
|
||||
permissions:
|
||||
contents: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 8
|
||||
matrix:
|
||||
plugin: ${{ fromJson(needs.resolve_bootstrap_plan.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.resolve_bootstrap_plan.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: Verify package-local runtime build
|
||||
run: node scripts/check-plugin-npm-runtime-builds.mjs --package "${{ matrix.plugin.packageDir }}"
|
||||
|
||||
- name: Install pinned ClawHub CLI wrapper
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cat > "${RUNNER_TEMP}/clawhub" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
exec npm exec --yes --package "${CLAWHUB_CLI_PACKAGE}" -- clawhub "$@"
|
||||
EOF
|
||||
chmod +x "${RUNNER_TEMP}/clawhub"
|
||||
echo "${RUNNER_TEMP}" >> "${GITHUB_PATH}"
|
||||
|
||||
- name: Write ClawHub token config
|
||||
if: inputs.dry_run != true
|
||||
env:
|
||||
CLAWHUB_REGISTRY: ${{ env.CLAWHUB_REGISTRY }}
|
||||
CLAWHUB_TOKEN: ${{ secrets.CLAWHUB_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
config_path="${RUNNER_TEMP}/clawhub-config.json"
|
||||
CONFIG_PATH="${config_path}" node --input-type=module <<'NODE'
|
||||
import { writeFileSync } from "node:fs";
|
||||
|
||||
const registry = process.env.CLAWHUB_REGISTRY?.trim();
|
||||
const token = process.env.CLAWHUB_TOKEN?.trim();
|
||||
const configPath = process.env.CONFIG_PATH;
|
||||
if (!registry) {
|
||||
throw new Error("CLAWHUB_REGISTRY is required for token-gated ClawHub bootstrap.");
|
||||
}
|
||||
if (!token) {
|
||||
throw new Error("CLAWHUB_TOKEN is required for token-gated ClawHub bootstrap.");
|
||||
}
|
||||
if (!configPath) {
|
||||
throw new Error("CONFIG_PATH is required.");
|
||||
}
|
||||
|
||||
writeFileSync(configPath, `${JSON.stringify({ registry, token }, null, 2)}\n`, {
|
||||
encoding: "utf8",
|
||||
mode: 0o600,
|
||||
});
|
||||
NODE
|
||||
echo "CLAWHUB_CONFIG_PATH=${config_path}" >> "${GITHUB_ENV}"
|
||||
|
||||
- name: Publish ClawHub bootstrap package
|
||||
env:
|
||||
CLAWHUB_REGISTRY: ${{ env.CLAWHUB_REGISTRY }}
|
||||
SOURCE_REPO: ${{ github.repository }}
|
||||
SOURCE_COMMIT: ${{ needs.resolve_bootstrap_plan.outputs.ref_revision }}
|
||||
SOURCE_REF: ${{ github.ref }}
|
||||
PACKAGE_TAG: ${{ matrix.plugin.publishTag }}
|
||||
PACKAGE_DIR: ${{ matrix.plugin.packageDir }}
|
||||
BOOTSTRAP_MODE: ${{ matrix.plugin.bootstrapMode }}
|
||||
REQUIRES_MANUAL_OVERRIDE: ${{ matrix.plugin.requiresManualOverride && 'true' || 'false' }}
|
||||
DRY_RUN: ${{ inputs.dry_run && 'true' || 'false' }}
|
||||
OPENCLAW_PLUGIN_NPM_RUNTIME_BUILD: "0"
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ "${BOOTSTRAP_MODE}" == "configure-only" ]]; then
|
||||
echo "Skipping bootstrap publish because ${PACKAGE_DIR} version is already present on ClawHub; configuring trusted publisher only."
|
||||
elif [[ "${DRY_RUN}" == "true" ]]; then
|
||||
bash scripts/plugin-clawhub-publish.sh --dry-run "${PACKAGE_DIR}"
|
||||
else
|
||||
if [[ "${REQUIRES_MANUAL_OVERRIDE}" == "true" ]]; then
|
||||
export OPENCLAW_CLAWHUB_MANUAL_OVERRIDE_REASON="GitHub Actions trusted publisher repair before OIDC migration"
|
||||
fi
|
||||
bash scripts/plugin-clawhub-publish.sh --publish "${PACKAGE_DIR}"
|
||||
fi
|
||||
|
||||
- name: Configure trusted publisher for normal OIDC releases
|
||||
if: inputs.dry_run != true
|
||||
env:
|
||||
CLAWHUB_CLI_PACKAGE: ${{ env.CLAWHUB_CLI_PACKAGE }}
|
||||
PACKAGE_NAME: ${{ matrix.plugin.packageName }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
npm exec --yes --package "${CLAWHUB_CLI_PACKAGE}" -- \
|
||||
clawhub package trusted-publisher set "${PACKAGE_NAME}" \
|
||||
--repository openclaw/openclaw \
|
||||
--workflow-filename plugin-clawhub-release.yml
|
||||
|
||||
verify_bootstrap_clawhub_package:
|
||||
needs: [resolve_bootstrap_plan, publish_bootstrap_plugins]
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.dry_run != true && needs.resolve_bootstrap_plan.outputs.has_bootstrap_candidates == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 8
|
||||
matrix:
|
||||
plugin: ${{ fromJson(needs.resolve_bootstrap_plan.outputs.matrix) }}
|
||||
steps:
|
||||
- name: Verify bootstrap ClawHub package and trusted publisher
|
||||
env:
|
||||
CLAWHUB_REGISTRY: ${{ env.CLAWHUB_REGISTRY }}
|
||||
PACKAGE_NAME: ${{ matrix.plugin.packageName }}
|
||||
PACKAGE_VERSION: ${{ matrix.plugin.version }}
|
||||
PACKAGE_TAG: ${{ matrix.plugin.publishTag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
node --input-type=module <<'EOF'
|
||||
const registry = (process.env.CLAWHUB_REGISTRY ?? "https://clawhub.ai").replace(/\/+$/, "");
|
||||
const packageName = process.env.PACKAGE_NAME;
|
||||
const packageVersion = process.env.PACKAGE_VERSION;
|
||||
const packageTag = process.env.PACKAGE_TAG;
|
||||
if (!packageName || !packageVersion || !packageTag) {
|
||||
throw new Error("Missing ClawHub bootstrap verification env.");
|
||||
}
|
||||
const encodedName = encodeURIComponent(packageName);
|
||||
const encodedVersion = encodeURIComponent(packageVersion);
|
||||
const detailUrl = `${registry}/api/v1/packages/${encodedName}`;
|
||||
const trustedPublisherUrl = `${detailUrl}/trusted-publisher`;
|
||||
const versionUrl = `${detailUrl}/versions/${encodedVersion}`;
|
||||
const artifactUrl = `${versionUrl}/artifact/download`;
|
||||
|
||||
async function fetchWithRetry(url, options = {}) {
|
||||
let lastStatus = "unknown";
|
||||
for (let attempt = 1; attempt <= 12; attempt += 1) {
|
||||
try {
|
||||
const response = await fetch(url, { redirect: "manual", ...options });
|
||||
lastStatus = response.status;
|
||||
if (response.status !== 429 && response.status < 500) {
|
||||
return response;
|
||||
}
|
||||
} catch (error) {
|
||||
lastStatus = error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, attempt * 5000));
|
||||
}
|
||||
throw new Error(`${url} did not stabilize; last status ${lastStatus}.`);
|
||||
}
|
||||
|
||||
const detailResponse = await fetchWithRetry(detailUrl, {
|
||||
headers: { accept: "application/json" },
|
||||
});
|
||||
if (!detailResponse.ok) {
|
||||
throw new Error(`${detailUrl} returned HTTP ${detailResponse.status}.`);
|
||||
}
|
||||
const detail = await detailResponse.json();
|
||||
const tags = detail?.package?.tags ?? {};
|
||||
if (tags[packageTag] !== packageVersion) {
|
||||
throw new Error(
|
||||
`${packageName}: ClawHub tag ${packageTag} points to ${tags[packageTag] ?? "<missing>"}, expected ${packageVersion}.`,
|
||||
);
|
||||
}
|
||||
|
||||
const trustedPublisherResponse = await fetchWithRetry(trustedPublisherUrl, {
|
||||
headers: { accept: "application/json" },
|
||||
});
|
||||
if (!trustedPublisherResponse.ok) {
|
||||
throw new Error(`${trustedPublisherUrl} returned HTTP ${trustedPublisherResponse.status}.`);
|
||||
}
|
||||
const trustedPublisherDetail = await trustedPublisherResponse.json();
|
||||
const trustedPublisher = trustedPublisherDetail?.trustedPublisher;
|
||||
if (
|
||||
trustedPublisher?.repository !== "openclaw/openclaw" ||
|
||||
trustedPublisher?.workflowFilename !== "plugin-clawhub-release.yml" ||
|
||||
trustedPublisher?.environment != null
|
||||
) {
|
||||
throw new Error(
|
||||
`${packageName}: trusted publisher config did not match openclaw/openclaw plugin-clawhub-release.yml without an environment pin.`,
|
||||
);
|
||||
}
|
||||
|
||||
const versionResponse = await fetchWithRetry(versionUrl);
|
||||
if (!versionResponse.ok) {
|
||||
throw new Error(`${versionUrl} returned HTTP ${versionResponse.status}.`);
|
||||
}
|
||||
const artifactResponse = await fetchWithRetry(artifactUrl, { method: "HEAD" });
|
||||
if (artifactResponse.status < 200 || artifactResponse.status >= 400) {
|
||||
throw new Error(`${artifactUrl} returned HTTP ${artifactResponse.status}.`);
|
||||
}
|
||||
console.log(`${packageName}@${packageVersion} bootstrap verified on ClawHub.`);
|
||||
EOF
|
||||
252
.github/workflows/plugin-clawhub-release.yml
vendored
252
.github/workflows/plugin-clawhub-release.yml
vendored
@@ -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]
|
||||
|
||||
314
scripts/lib/openclaw-release-clawhub-plan.ts
Normal file
314
scripts/lib/openclaw-release-clawhub-plan.ts
Normal file
@@ -0,0 +1,314 @@
|
||||
// OpenClaw release ClawHub plan script supports release workflow routing.
|
||||
import { resolve } from "node:path";
|
||||
import {
|
||||
collectPluginClawHubReleasePlan,
|
||||
type PublishablePluginPackage,
|
||||
} from "./plugin-clawhub-release.ts";
|
||||
import {
|
||||
parsePluginReleaseSelection,
|
||||
parsePluginReleaseSelectionMode,
|
||||
type PluginReleaseSelectionMode,
|
||||
} from "./plugin-npm-release.ts";
|
||||
|
||||
type ClawHubPlanPackage = Pick<PublishablePluginPackage, "packageName">;
|
||||
|
||||
type ClawHubDispatchInputs = Record<string, string>;
|
||||
|
||||
type ClawHubDispatchTarget = {
|
||||
workflow: "plugin-clawhub-release.yml" | "plugin-clawhub-new.yml";
|
||||
ref: string;
|
||||
shouldDispatch: boolean;
|
||||
packages: string[];
|
||||
inputs: ClawHubDispatchInputs;
|
||||
};
|
||||
|
||||
export type OpenClawReleaseClawHubPlanArgs = {
|
||||
releaseTag: string;
|
||||
releasePublishBranch: string;
|
||||
releasePublishRunId: string;
|
||||
pluginPublishScope: PluginReleaseSelectionMode;
|
||||
plugins: string[];
|
||||
};
|
||||
|
||||
export type OpenClawReleaseClawHubPlan = {
|
||||
clawHubWorkflowRef: string;
|
||||
releasePublishBranch: string;
|
||||
normal: ClawHubDispatchTarget;
|
||||
bootstrap: ClawHubDispatchTarget;
|
||||
summary: {
|
||||
normalCount: number;
|
||||
bootstrapCount: number;
|
||||
missingTrustedPublisherCount: number;
|
||||
normalPlugins: string;
|
||||
bootstrapPlugins: string;
|
||||
missingTrustedPlugins: string;
|
||||
};
|
||||
verifier: {
|
||||
clawHubWorkflowRef: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type OpenClawReleaseClawHubRuntimeStateArgs = {
|
||||
repository: string;
|
||||
waitForClawHub: boolean;
|
||||
forceSkipClawHub: boolean;
|
||||
normalRunId?: string;
|
||||
bootstrapRunId?: string;
|
||||
bootstrapCompleted: boolean;
|
||||
};
|
||||
|
||||
export type OpenClawReleaseClawHubRuntimeState = {
|
||||
verifierArgs: string[];
|
||||
proofLines: {
|
||||
normal: string;
|
||||
bootstrap: string;
|
||||
};
|
||||
};
|
||||
|
||||
function requireArg(value: string | undefined, label: string): string {
|
||||
const trimmed = value?.trim();
|
||||
if (!trimmed) {
|
||||
throw new Error(`${label} is required.`);
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
function packageNames(packages: readonly ClawHubPlanPackage[]): string[] {
|
||||
return packages.map((plugin) => plugin.packageName);
|
||||
}
|
||||
|
||||
function joinPackageNames(packages: readonly string[]): string {
|
||||
return packages.join(",");
|
||||
}
|
||||
|
||||
function optionalArg(value: string | undefined): string | undefined {
|
||||
const trimmed = value?.trim();
|
||||
return trimmed ? trimmed : undefined;
|
||||
}
|
||||
|
||||
function runUrl(repository: string, runId: string): string {
|
||||
return `https://github.com/${repository}/actions/runs/${runId}`;
|
||||
}
|
||||
|
||||
function assertNoPackageOverlap(
|
||||
normalPackages: readonly string[],
|
||||
bootstrapPackages: readonly string[],
|
||||
) {
|
||||
const normalPackageSet = new Set(normalPackages);
|
||||
const overlap = bootstrapPackages.filter((packageName) => normalPackageSet.has(packageName));
|
||||
if (overlap.length > 0) {
|
||||
throw new Error(
|
||||
`ClawHub release plan routed package(s) to both normal and bootstrap workflows: ${overlap.join(", ")}.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function createDispatchTarget(params: {
|
||||
workflow: ClawHubDispatchTarget["workflow"];
|
||||
ref: string;
|
||||
packages: readonly string[];
|
||||
releasePublishRunId: string;
|
||||
releasePublishBranch: string;
|
||||
includePublishScope: boolean;
|
||||
}): ClawHubDispatchTarget {
|
||||
if (params.packages.length === 0) {
|
||||
return {
|
||||
workflow: params.workflow,
|
||||
ref: params.ref,
|
||||
shouldDispatch: false,
|
||||
packages: [],
|
||||
inputs: {},
|
||||
};
|
||||
}
|
||||
|
||||
const plugins = joinPackageNames(params.packages);
|
||||
return {
|
||||
workflow: params.workflow,
|
||||
ref: params.ref,
|
||||
shouldDispatch: true,
|
||||
packages: [...params.packages],
|
||||
inputs: {
|
||||
...(params.includePublishScope ? { publish_scope: "selected" } : {}),
|
||||
plugins,
|
||||
release_publish_run_id: params.releasePublishRunId,
|
||||
release_publish_branch: params.releasePublishBranch,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function buildOpenClawReleaseClawHubRuntimeState(
|
||||
args: OpenClawReleaseClawHubRuntimeStateArgs,
|
||||
): OpenClawReleaseClawHubRuntimeState {
|
||||
const repository = requireArg(args.repository, "repository");
|
||||
const normalRunId = optionalArg(args.normalRunId);
|
||||
const bootstrapRunId = optionalArg(args.bootstrapRunId);
|
||||
|
||||
const shouldIncludeNormalRun =
|
||||
!args.forceSkipClawHub && normalRunId !== undefined && args.waitForClawHub;
|
||||
const shouldIncludeBootstrapRun =
|
||||
!args.forceSkipClawHub && bootstrapRunId !== undefined && args.bootstrapCompleted;
|
||||
const shouldVerifyClawHubPackages =
|
||||
bootstrapRunId !== undefined &&
|
||||
args.bootstrapCompleted &&
|
||||
(normalRunId === undefined || args.waitForClawHub);
|
||||
const shouldSkipClawHubPackages =
|
||||
args.forceSkipClawHub || !(shouldIncludeNormalRun || shouldVerifyClawHubPackages);
|
||||
|
||||
const verifierArgs = shouldSkipClawHubPackages ? ["--skip-clawhub"] : [];
|
||||
if (shouldIncludeNormalRun) {
|
||||
verifierArgs.push("--plugin-clawhub-run", normalRunId);
|
||||
}
|
||||
if (shouldIncludeBootstrapRun) {
|
||||
verifierArgs.push("--plugin-clawhub-bootstrap-run", bootstrapRunId);
|
||||
}
|
||||
|
||||
let normalProofLine = "- plugin ClawHub publish: no normal OIDC candidates";
|
||||
if (normalRunId !== undefined && args.waitForClawHub) {
|
||||
normalProofLine = `- plugin ClawHub publish: ${runUrl(repository, normalRunId)}`;
|
||||
} else if (normalRunId !== undefined) {
|
||||
normalProofLine = `- plugin ClawHub publish: dispatched separately, not awaited by this proof: ${runUrl(repository, normalRunId)}`;
|
||||
}
|
||||
|
||||
let bootstrapProofLine = "- plugin ClawHub bootstrap: not needed";
|
||||
if (bootstrapRunId !== undefined && (args.bootstrapCompleted || args.waitForClawHub)) {
|
||||
bootstrapProofLine = `- plugin ClawHub bootstrap: ${runUrl(repository, bootstrapRunId)}`;
|
||||
} else if (bootstrapRunId !== undefined) {
|
||||
bootstrapProofLine = `- plugin ClawHub bootstrap: dispatched separately, not awaited by this proof: ${runUrl(repository, bootstrapRunId)}`;
|
||||
}
|
||||
|
||||
return {
|
||||
verifierArgs,
|
||||
proofLines: {
|
||||
normal: normalProofLine,
|
||||
bootstrap: bootstrapProofLine,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function parseOpenClawReleaseClawHubPlanArgs(
|
||||
argv: string[],
|
||||
): OpenClawReleaseClawHubPlanArgs {
|
||||
const values = [...argv];
|
||||
if (values[0] === "--") {
|
||||
values.shift();
|
||||
}
|
||||
|
||||
let releaseTag: string | undefined;
|
||||
let releasePublishBranch: string | undefined;
|
||||
let releasePublishRunId: string | undefined;
|
||||
let pluginPublishScope: PluginReleaseSelectionMode | undefined;
|
||||
let plugins: string[] = [];
|
||||
let pluginsFlagProvided = false;
|
||||
|
||||
for (let index = 0; index < values.length; index += 1) {
|
||||
const arg = values[index];
|
||||
const next = () => {
|
||||
const value = values[index + 1];
|
||||
if (value === undefined || value.startsWith("-")) {
|
||||
throw new Error(`${arg} requires a value.`);
|
||||
}
|
||||
index += 1;
|
||||
return value;
|
||||
};
|
||||
|
||||
switch (arg) {
|
||||
case "--release-tag":
|
||||
releaseTag = next();
|
||||
break;
|
||||
case "--release-publish-branch":
|
||||
releasePublishBranch = next();
|
||||
break;
|
||||
case "--release-publish-run-id":
|
||||
releasePublishRunId = next();
|
||||
break;
|
||||
case "--plugin-publish-scope":
|
||||
pluginPublishScope = parsePluginReleaseSelectionMode(next());
|
||||
break;
|
||||
case "--plugins":
|
||||
plugins = parsePluginReleaseSelection(next());
|
||||
pluginsFlagProvided = true;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown argument: ${arg}`);
|
||||
}
|
||||
}
|
||||
|
||||
const resolvedPluginPublishScope = pluginPublishScope ?? "all-publishable";
|
||||
if (pluginsFlagProvided && plugins.length === 0) {
|
||||
throw new Error("--plugins must include at least one package name.");
|
||||
}
|
||||
if (resolvedPluginPublishScope === "selected" && !pluginsFlagProvided) {
|
||||
throw new Error("plugin-publish-scope=selected requires --plugins.");
|
||||
}
|
||||
if (resolvedPluginPublishScope === "all-publishable" && pluginsFlagProvided) {
|
||||
throw new Error("plugin-publish-scope=all-publishable must not be combined with --plugins.");
|
||||
}
|
||||
|
||||
return {
|
||||
releaseTag: requireArg(releaseTag, "--release-tag"),
|
||||
releasePublishBranch: requireArg(releasePublishBranch, "--release-publish-branch"),
|
||||
releasePublishRunId: requireArg(releasePublishRunId, "--release-publish-run-id"),
|
||||
pluginPublishScope: resolvedPluginPublishScope,
|
||||
plugins,
|
||||
};
|
||||
}
|
||||
|
||||
export async function buildOpenClawReleaseClawHubPlan(
|
||||
args: OpenClawReleaseClawHubPlanArgs,
|
||||
options: {
|
||||
rootDir?: string;
|
||||
fetchImpl?: typeof fetch;
|
||||
registryBaseUrl?: string;
|
||||
} = {},
|
||||
): Promise<OpenClawReleaseClawHubPlan> {
|
||||
const releaseTag = requireArg(args.releaseTag, "releaseTag");
|
||||
const releasePublishBranch = requireArg(args.releasePublishBranch, "releasePublishBranch");
|
||||
const releasePublishRunId = requireArg(args.releasePublishRunId, "releasePublishRunId");
|
||||
const plan = await collectPluginClawHubReleasePlan({
|
||||
rootDir: options.rootDir ?? resolve("."),
|
||||
selection: args.plugins,
|
||||
selectionMode: args.pluginPublishScope,
|
||||
fetchImpl: options.fetchImpl,
|
||||
registryBaseUrl: options.registryBaseUrl,
|
||||
});
|
||||
|
||||
const normalPackages = packageNames(plan.candidates);
|
||||
const bootstrapPackages = [
|
||||
...packageNames(plan.bootstrapCandidates),
|
||||
...packageNames(plan.missingTrustedPublisher),
|
||||
];
|
||||
const missingTrustedPlugins = packageNames(plan.missingTrustedPublisher);
|
||||
assertNoPackageOverlap(normalPackages, bootstrapPackages);
|
||||
|
||||
return {
|
||||
clawHubWorkflowRef: releaseTag,
|
||||
releasePublishBranch,
|
||||
normal: createDispatchTarget({
|
||||
workflow: "plugin-clawhub-release.yml",
|
||||
ref: releaseTag,
|
||||
packages: normalPackages,
|
||||
releasePublishRunId,
|
||||
releasePublishBranch,
|
||||
includePublishScope: true,
|
||||
}),
|
||||
bootstrap: createDispatchTarget({
|
||||
workflow: "plugin-clawhub-new.yml",
|
||||
ref: releaseTag,
|
||||
packages: bootstrapPackages,
|
||||
releasePublishRunId,
|
||||
releasePublishBranch,
|
||||
includePublishScope: false,
|
||||
}),
|
||||
summary: {
|
||||
normalCount: normalPackages.length,
|
||||
bootstrapCount: bootstrapPackages.length,
|
||||
missingTrustedPublisherCount: missingTrustedPlugins.length,
|
||||
normalPlugins: joinPackageNames(normalPackages),
|
||||
bootstrapPlugins: joinPackageNames(bootstrapPackages),
|
||||
missingTrustedPlugins: joinPackageNames(missingTrustedPlugins),
|
||||
},
|
||||
verifier: {
|
||||
clawHubWorkflowRef: releaseTag,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -60,15 +60,34 @@ type PluginReleasePlanItem = PublishablePluginPackage & {
|
||||
type PluginReleasePlan = {
|
||||
all: PluginReleasePlanItem[];
|
||||
candidates: PluginReleasePlanItem[];
|
||||
bootstrapCandidates: PluginReleasePlanItem[];
|
||||
missingTrustedPublisher: PluginReleasePlanItem[];
|
||||
skippedPublished: PluginReleasePlanItem[];
|
||||
};
|
||||
|
||||
type ClawHubTrustedPublisherDetail = {
|
||||
trustedPublisher?: unknown;
|
||||
};
|
||||
|
||||
type ClawHubTrustedPublisherConfig = {
|
||||
repository?: unknown;
|
||||
workflowFilename?: unknown;
|
||||
environment?: unknown;
|
||||
};
|
||||
|
||||
type PluginReleasePlanItemWithPackageState = PluginReleasePlanItem & {
|
||||
packageExists: boolean;
|
||||
hasTrustedPublisher: boolean;
|
||||
};
|
||||
|
||||
type ClawHubPublishablePluginPackageFilters = {
|
||||
extensionIds?: readonly string[];
|
||||
packageNames?: readonly string[];
|
||||
};
|
||||
|
||||
const CLAWHUB_DEFAULT_REGISTRY = "https://clawhub.ai";
|
||||
const OPENCLAW_PLUGIN_CLAWHUB_REPOSITORY = "openclaw/openclaw";
|
||||
const OPENCLAW_PLUGIN_CLAWHUB_WORKFLOW_FILENAME = "plugin-clawhub-release.yml";
|
||||
const SAFE_EXTENSION_ID_RE = /^[a-z0-9][a-z0-9._-]*$/;
|
||||
const CLAWHUB_SHARED_RELEASE_INPUT_PATHS = [
|
||||
".github/workflows/plugin-clawhub-release.yml",
|
||||
@@ -357,6 +376,97 @@ async function isPluginVersionPublishedOnClawHub(
|
||||
);
|
||||
}
|
||||
|
||||
async function doesClawHubPackageExist(
|
||||
packageName: string,
|
||||
options: {
|
||||
fetchImpl?: typeof fetch;
|
||||
registryBaseUrl?: string;
|
||||
} = {},
|
||||
): Promise<boolean> {
|
||||
const fetchImpl = options.fetchImpl ?? fetch;
|
||||
const url = new URL(
|
||||
`/api/v1/packages/${encodeURIComponent(packageName)}`,
|
||||
getRegistryBaseUrl(options.registryBaseUrl),
|
||||
);
|
||||
const response = await fetchImpl(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (response.status === 404) {
|
||||
return false;
|
||||
}
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to query ClawHub package ${packageName}: ${response.status} ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function hasClawHubTrustedPublisher(
|
||||
packageName: string,
|
||||
options: {
|
||||
fetchImpl?: typeof fetch;
|
||||
registryBaseUrl?: string;
|
||||
} = {},
|
||||
): Promise<boolean> {
|
||||
const fetchImpl = options.fetchImpl ?? fetch;
|
||||
const url = new URL(
|
||||
`/api/v1/packages/${encodeURIComponent(packageName)}/trusted-publisher`,
|
||||
getRegistryBaseUrl(options.registryBaseUrl),
|
||||
);
|
||||
const response = await fetchImpl(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to query ClawHub trusted publisher for ${packageName}: ${response.status} ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
let trustedPublisherDetail: ClawHubTrustedPublisherDetail;
|
||||
try {
|
||||
trustedPublisherDetail = (await response.json()) as ClawHubTrustedPublisherDetail;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to parse ClawHub trusted publisher ${packageName} response.`, {
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
|
||||
return isOpenClawPluginTrustedPublisher(trustedPublisherDetail.trustedPublisher);
|
||||
}
|
||||
|
||||
function isOpenClawPluginTrustedPublisher(value: unknown): boolean {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
return false;
|
||||
}
|
||||
const trustedPublisher = value as ClawHubTrustedPublisherConfig;
|
||||
return (
|
||||
trustedPublisher.repository === OPENCLAW_PLUGIN_CLAWHUB_REPOSITORY &&
|
||||
trustedPublisher.workflowFilename === OPENCLAW_PLUGIN_CLAWHUB_WORKFLOW_FILENAME &&
|
||||
trustedPublisher.environment == null
|
||||
);
|
||||
}
|
||||
|
||||
function stripPackageReleaseState(
|
||||
item: PluginReleasePlanItemWithPackageState,
|
||||
): PluginReleasePlanItem {
|
||||
const {
|
||||
packageExists: _packageExists,
|
||||
hasTrustedPublisher: _hasTrustedPublisher,
|
||||
...planItem
|
||||
} = item;
|
||||
return planItem;
|
||||
}
|
||||
|
||||
export async function collectPluginClawHubReleasePlan(params?: {
|
||||
rootDir?: string;
|
||||
selection?: string[];
|
||||
@@ -395,22 +505,56 @@ export async function collectPluginClawHubReleasePlan(params?: {
|
||||
assertPluginReleaseVersionFloors(selectedPublishable, "Plugin ClawHub release plan");
|
||||
}
|
||||
|
||||
const all = await Promise.all(
|
||||
selectedPublishable.map(async (plugin) =>
|
||||
Object.assign({}, plugin, {
|
||||
alreadyPublished: await isPluginVersionPublishedOnClawHub(
|
||||
plugin.packageName,
|
||||
plugin.version,
|
||||
{ registryBaseUrl: params?.registryBaseUrl, fetchImpl: params?.fetchImpl },
|
||||
),
|
||||
const planned = await Promise.all(
|
||||
selectedPublishable.map(async (plugin): Promise<PluginReleasePlanItemWithPackageState> => {
|
||||
const packageExists = await doesClawHubPackageExist(plugin.packageName, {
|
||||
registryBaseUrl: params?.registryBaseUrl,
|
||||
fetchImpl: params?.fetchImpl,
|
||||
});
|
||||
const hasTrustedPublisher = packageExists
|
||||
? await hasClawHubTrustedPublisher(plugin.packageName, {
|
||||
registryBaseUrl: params?.registryBaseUrl,
|
||||
fetchImpl: params?.fetchImpl,
|
||||
})
|
||||
: false;
|
||||
const alreadyPublished = packageExists
|
||||
? await isPluginVersionPublishedOnClawHub(plugin.packageName, plugin.version, {
|
||||
registryBaseUrl: params?.registryBaseUrl,
|
||||
fetchImpl: params?.fetchImpl,
|
||||
})
|
||||
: false;
|
||||
|
||||
return {
|
||||
extensionId: plugin.extensionId,
|
||||
packageDir: plugin.packageDir,
|
||||
packageName: plugin.packageName,
|
||||
version: plugin.version,
|
||||
channel: plugin.channel,
|
||||
publishTag: plugin.publishTag,
|
||||
packageExists,
|
||||
hasTrustedPublisher,
|
||||
alreadyPublished,
|
||||
artifactName: formatClawHubPackageArtifactName(plugin),
|
||||
}),
|
||||
),
|
||||
};
|
||||
}),
|
||||
);
|
||||
const all = planned.map(stripPackageReleaseState);
|
||||
|
||||
return {
|
||||
all,
|
||||
candidates: all.filter((plugin) => !plugin.alreadyPublished),
|
||||
skippedPublished: all.filter((plugin) => plugin.alreadyPublished),
|
||||
candidates: planned
|
||||
.filter(
|
||||
(plugin) => plugin.packageExists && plugin.hasTrustedPublisher && !plugin.alreadyPublished,
|
||||
)
|
||||
.map(stripPackageReleaseState),
|
||||
bootstrapCandidates: planned
|
||||
.filter((plugin) => !plugin.packageExists)
|
||||
.map(stripPackageReleaseState),
|
||||
missingTrustedPublisher: planned
|
||||
.filter((plugin) => plugin.packageExists && !plugin.hasTrustedPublisher)
|
||||
.map(stripPackageReleaseState),
|
||||
skippedPublished: planned
|
||||
.filter((plugin) => plugin.alreadyPublished)
|
||||
.map(stripPackageReleaseState),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ export type ReleaseVerifyBetaArgs = {
|
||||
repo: string;
|
||||
registry: string;
|
||||
workflowRef?: string;
|
||||
clawHubWorkflowRef?: string;
|
||||
pluginSelection: string[];
|
||||
evidenceOut?: string;
|
||||
skipPostpublish: boolean;
|
||||
@@ -29,6 +30,7 @@ export type ReleaseVerifyBetaArgs = {
|
||||
openclawNpm?: string;
|
||||
pluginNpm?: string;
|
||||
pluginClawHub?: string;
|
||||
pluginClawHubBootstrap?: string;
|
||||
npmTelegram?: string;
|
||||
};
|
||||
};
|
||||
@@ -119,7 +121,7 @@ export function parseReleaseVerifyBetaArgs(argv: string[]): ReleaseVerifyBetaArg
|
||||
const version = values.shift();
|
||||
if (!version || version.startsWith("-")) {
|
||||
throw new Error(
|
||||
"Usage: pnpm release:verify-beta -- <version> [--workflow-ref REF] [--full-release-validation-run ID] [--openclaw-npm-run ID] [--plugin-npm-run ID] [--plugin-clawhub-run ID] [--npm-telegram-run ID] [--skip-github-release] [--skip-clawhub]",
|
||||
"Usage: pnpm release:verify-beta -- <version> [--workflow-ref REF] [--clawhub-workflow-ref REF] [--full-release-validation-run ID] [--openclaw-npm-run ID] [--plugin-npm-run ID] [--plugin-clawhub-run ID] [--plugin-clawhub-bootstrap-run ID] [--npm-telegram-run ID] [--skip-github-release] [--skip-clawhub]",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -130,6 +132,7 @@ export function parseReleaseVerifyBetaArgs(argv: string[]): ReleaseVerifyBetaArg
|
||||
repo: DEFAULT_REPO,
|
||||
registry: DEFAULT_CLAWHUB_REGISTRY,
|
||||
workflowRef: undefined,
|
||||
clawHubWorkflowRef: undefined,
|
||||
pluginSelection: [],
|
||||
evidenceOut: undefined,
|
||||
skipPostpublish: false,
|
||||
@@ -166,6 +169,9 @@ export function parseReleaseVerifyBetaArgs(argv: string[]): ReleaseVerifyBetaArg
|
||||
case "--workflow-ref":
|
||||
parsed.workflowRef = next();
|
||||
break;
|
||||
case "--clawhub-workflow-ref":
|
||||
parsed.clawHubWorkflowRef = next();
|
||||
break;
|
||||
case "--plugins":
|
||||
parsed.pluginSelection = parsePluginReleaseSelection(next());
|
||||
if (parsed.pluginSelection.length === 0) {
|
||||
@@ -187,6 +193,9 @@ export function parseReleaseVerifyBetaArgs(argv: string[]): ReleaseVerifyBetaArg
|
||||
case "--plugin-clawhub-run":
|
||||
parsed.workflowRuns.pluginClawHub = next();
|
||||
break;
|
||||
case "--plugin-clawhub-bootstrap-run":
|
||||
parsed.workflowRuns.pluginClawHubBootstrap = next();
|
||||
break;
|
||||
case "--npm-telegram-run":
|
||||
parsed.workflowRuns.npmTelegram = next();
|
||||
break;
|
||||
@@ -567,17 +576,31 @@ export async function verifyBetaRelease(
|
||||
);
|
||||
}
|
||||
if (args.workflowRuns.pluginClawHub !== undefined) {
|
||||
const clawHubWorkflowRef = args.clawHubWorkflowRef ?? args.workflowRef;
|
||||
workflowRuns.push(
|
||||
verifyWorkflowRun({
|
||||
id: args.workflowRuns.pluginClawHub,
|
||||
label: "Plugin ClawHub Release",
|
||||
repo: args.repo,
|
||||
expectedWorkflowName: "Plugin ClawHub Release",
|
||||
expectedHeadBranch: args.workflowRef,
|
||||
expectedHeadBranch: clawHubWorkflowRef,
|
||||
rerunFailed: args.rerunFailedClawHub,
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (args.workflowRuns.pluginClawHubBootstrap !== undefined) {
|
||||
const clawHubWorkflowRef = args.clawHubWorkflowRef ?? args.workflowRef;
|
||||
workflowRuns.push(
|
||||
verifyWorkflowRun({
|
||||
id: args.workflowRuns.pluginClawHubBootstrap,
|
||||
label: "Plugin ClawHub New",
|
||||
repo: args.repo,
|
||||
expectedWorkflowName: "Plugin ClawHub New",
|
||||
expectedHeadBranch: clawHubWorkflowRef,
|
||||
rerunFailed: false,
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (args.workflowRuns.openclawNpm !== undefined) {
|
||||
workflowRuns.push(
|
||||
verifyWorkflowRun({
|
||||
|
||||
14
scripts/openclaw-release-clawhub-plan.ts
Executable file
14
scripts/openclaw-release-clawhub-plan.ts
Executable file
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env -S node --import tsx
|
||||
// OpenClaw release ClawHub plan CLI emits release workflow routing as JSON.
|
||||
|
||||
import { pathToFileURL } from "node:url";
|
||||
import {
|
||||
buildOpenClawReleaseClawHubPlan,
|
||||
parseOpenClawReleaseClawHubPlanArgs,
|
||||
} from "./lib/openclaw-release-clawhub-plan.ts";
|
||||
|
||||
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
||||
const args = parseOpenClawReleaseClawHubPlanArgs(process.argv.slice(2));
|
||||
const plan = await buildOpenClawReleaseClawHubPlan(args);
|
||||
console.log(JSON.stringify(plan, null, 2));
|
||||
}
|
||||
92
scripts/openclaw-release-clawhub-runtime-state.ts
Executable file
92
scripts/openclaw-release-clawhub-runtime-state.ts
Executable file
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env -S node --import tsx
|
||||
import { buildOpenClawReleaseClawHubRuntimeState } from "./lib/openclaw-release-clawhub-plan.ts";
|
||||
|
||||
function parseBoolean(value: string, label: string): boolean {
|
||||
if (value === "true") {
|
||||
return true;
|
||||
}
|
||||
if (value === "false") {
|
||||
return false;
|
||||
}
|
||||
throw new Error(`${label} must be true or false.`);
|
||||
}
|
||||
|
||||
function parseArgs(argv: string[]) {
|
||||
const values = [...argv];
|
||||
if (values[0] === "--") {
|
||||
values.shift();
|
||||
}
|
||||
|
||||
let repository: string | undefined;
|
||||
let waitForClawHub: boolean | undefined;
|
||||
let forceSkipClawHub: boolean | undefined;
|
||||
let normalRunId: string | undefined;
|
||||
let bootstrapRunId: string | undefined;
|
||||
let bootstrapCompleted: boolean | undefined;
|
||||
|
||||
for (let index = 0; index < values.length; index += 1) {
|
||||
const arg = values[index];
|
||||
const next = () => {
|
||||
const value = values[index + 1];
|
||||
if (value === undefined || value.startsWith("-")) {
|
||||
throw new Error(`${arg} requires a value.`);
|
||||
}
|
||||
index += 1;
|
||||
return value;
|
||||
};
|
||||
|
||||
switch (arg) {
|
||||
case "--repository":
|
||||
repository = next();
|
||||
break;
|
||||
case "--wait-for-clawhub":
|
||||
waitForClawHub = parseBoolean(next(), "--wait-for-clawhub");
|
||||
break;
|
||||
case "--force-skip-clawhub":
|
||||
forceSkipClawHub = parseBoolean(next(), "--force-skip-clawhub");
|
||||
break;
|
||||
case "--normal-run-id":
|
||||
normalRunId = next();
|
||||
break;
|
||||
case "--bootstrap-run-id":
|
||||
bootstrapRunId = next();
|
||||
break;
|
||||
case "--bootstrap-completed":
|
||||
bootstrapCompleted = parseBoolean(next(), "--bootstrap-completed");
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown argument: ${arg}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!repository?.trim()) {
|
||||
throw new Error("--repository is required.");
|
||||
}
|
||||
if (waitForClawHub === undefined) {
|
||||
throw new Error("--wait-for-clawhub is required.");
|
||||
}
|
||||
if (forceSkipClawHub === undefined) {
|
||||
throw new Error("--force-skip-clawhub is required.");
|
||||
}
|
||||
if (bootstrapCompleted === undefined) {
|
||||
throw new Error("--bootstrap-completed is required.");
|
||||
}
|
||||
|
||||
return {
|
||||
repository,
|
||||
waitForClawHub,
|
||||
forceSkipClawHub,
|
||||
normalRunId,
|
||||
bootstrapRunId,
|
||||
bootstrapCompleted,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const state = buildOpenClawReleaseClawHubRuntimeState(parseArgs(process.argv.slice(2)));
|
||||
process.stdout.write(`${JSON.stringify(state, null, 2)}\n`);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
console.error(message);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -42,6 +42,7 @@ source_repo="${SOURCE_REPO:-${GITHUB_REPOSITORY:-openclaw/openclaw}}"
|
||||
source_commit="${SOURCE_COMMIT:-$(git -C "${invocation_root}" rev-parse HEAD)}"
|
||||
source_ref="${SOURCE_REF:-$(git -C "${invocation_root}" symbolic-ref -q HEAD || true)}"
|
||||
clawhub_workdir="${CLAWDHUB_WORKDIR:-${CLAWHUB_WORKDIR:-${invocation_root}}}"
|
||||
manual_override_reason="${OPENCLAW_CLAWHUB_MANUAL_OVERRIDE_REASON:-}"
|
||||
|
||||
pack_dir="$(mktemp -d "${RUNNER_TEMP:-/tmp}/openclaw-clawhub-pack.XXXXXX")"
|
||||
cleanup() {
|
||||
@@ -158,6 +159,13 @@ if [[ -n "${source_ref}" ]]; then
|
||||
)
|
||||
fi
|
||||
|
||||
if [[ -n "${manual_override_reason}" ]]; then
|
||||
publish_cmd+=(
|
||||
--manual-override-reason
|
||||
"${manual_override_reason}"
|
||||
)
|
||||
fi
|
||||
|
||||
printf 'Publish command: CLAWHUB_WORKDIR=%q' "${clawhub_workdir}"
|
||||
printf ' %q' "${publish_cmd[@]}"
|
||||
printf '\n'
|
||||
|
||||
@@ -10,6 +10,11 @@ import {
|
||||
} from "node:fs";
|
||||
import { delimiter, join } from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
buildOpenClawReleaseClawHubPlan,
|
||||
buildOpenClawReleaseClawHubRuntimeState,
|
||||
parseOpenClawReleaseClawHubPlanArgs,
|
||||
} from "../scripts/lib/openclaw-release-clawhub-plan.ts";
|
||||
import {
|
||||
collectClawHubPublishablePluginPackages,
|
||||
collectClawHubVersionGateErrors,
|
||||
@@ -320,17 +325,212 @@ describe("resolveSelectedClawHubPublishablePluginPackages", () => {
|
||||
});
|
||||
|
||||
describe("collectPluginClawHubReleasePlan", () => {
|
||||
it("skips versions that already exist on ClawHub", async () => {
|
||||
it("keeps existing trusted packages with missing versions as normal candidates", async () => {
|
||||
const repoDir = createTempPluginRepo();
|
||||
const { fetchImpl, requests } = createClawHubPlanFetch({
|
||||
packages: {
|
||||
"@openclaw/demo-plugin": {
|
||||
status: 200,
|
||||
body: {
|
||||
package: {},
|
||||
owner: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
trustedPublishers: {
|
||||
"@openclaw/demo-plugin": {
|
||||
status: 200,
|
||||
body: {
|
||||
trustedPublisher: {
|
||||
repository: "openclaw/openclaw",
|
||||
workflowFilename: "plugin-clawhub-release.yml",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
versions: {
|
||||
"@openclaw/demo-plugin@2026.4.1": 404,
|
||||
},
|
||||
});
|
||||
|
||||
const plan = await collectPluginClawHubReleasePlan({
|
||||
rootDir: repoDir,
|
||||
selection: ["@openclaw/demo-plugin"],
|
||||
fetchImpl: async () => new Response("{}", { status: 200 }),
|
||||
fetchImpl,
|
||||
registryBaseUrl: "https://clawhub.ai",
|
||||
});
|
||||
|
||||
expect(plan.candidates.map((plugin) => plugin.packageName)).toEqual(["@openclaw/demo-plugin"]);
|
||||
expect(plan.bootstrapCandidates).toStrictEqual([]);
|
||||
expect(plan.missingTrustedPublisher).toStrictEqual([]);
|
||||
expect(requests).toEqual([
|
||||
"/api/v1/packages/%40openclaw%2Fdemo-plugin",
|
||||
"/api/v1/packages/%40openclaw%2Fdemo-plugin/trusted-publisher",
|
||||
"/api/v1/packages/%40openclaw%2Fdemo-plugin/versions/2026.4.1",
|
||||
]);
|
||||
});
|
||||
|
||||
it("routes missing package rows to bootstrap candidates instead of normal candidates", async () => {
|
||||
const repoDir = createTempPluginRepo();
|
||||
const { fetchImpl } = createClawHubPlanFetch({
|
||||
packages: {
|
||||
"@openclaw/demo-plugin": {
|
||||
status: 404,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const plan = await collectPluginClawHubReleasePlan({
|
||||
rootDir: repoDir,
|
||||
selection: ["@openclaw/demo-plugin"],
|
||||
fetchImpl,
|
||||
registryBaseUrl: "https://clawhub.ai",
|
||||
});
|
||||
|
||||
expect(plan.candidates).toStrictEqual([]);
|
||||
expect(plan.bootstrapCandidates.map((plugin) => plugin.packageName)).toEqual([
|
||||
"@openclaw/demo-plugin",
|
||||
]);
|
||||
expect(plan.bootstrapCandidates[0]).toMatchObject({
|
||||
alreadyPublished: false,
|
||||
artifactName: "clawhub-package-openclaw-demo-plugin-2026.4.1",
|
||||
packageName: "@openclaw/demo-plugin",
|
||||
version: "2026.4.1",
|
||||
});
|
||||
expect(plan.missingTrustedPublisher).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it("routes existing packages without trusted publisher config out of normal candidates", async () => {
|
||||
const repoDir = createTempPluginRepo();
|
||||
const { fetchImpl } = createClawHubPlanFetch({
|
||||
packages: {
|
||||
"@openclaw/demo-plugin": {
|
||||
status: 200,
|
||||
body: {
|
||||
package: {},
|
||||
owner: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
trustedPublishers: {
|
||||
"@openclaw/demo-plugin": {
|
||||
status: 200,
|
||||
body: {
|
||||
trustedPublisher: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
versions: {
|
||||
"@openclaw/demo-plugin@2026.4.1": 404,
|
||||
},
|
||||
});
|
||||
|
||||
const plan = await collectPluginClawHubReleasePlan({
|
||||
rootDir: repoDir,
|
||||
selection: ["@openclaw/demo-plugin"],
|
||||
fetchImpl,
|
||||
registryBaseUrl: "https://clawhub.ai",
|
||||
});
|
||||
|
||||
expect(plan.candidates).toStrictEqual([]);
|
||||
expect(plan.bootstrapCandidates).toStrictEqual([]);
|
||||
expect(plan.missingTrustedPublisher.map((plugin) => plugin.packageName)).toEqual([
|
||||
"@openclaw/demo-plugin",
|
||||
]);
|
||||
expect(plan.missingTrustedPublisher[0]).toMatchObject({
|
||||
alreadyPublished: false,
|
||||
artifactName: "clawhub-package-openclaw-demo-plugin-2026.4.1",
|
||||
packageName: "@openclaw/demo-plugin",
|
||||
version: "2026.4.1",
|
||||
});
|
||||
});
|
||||
|
||||
it("routes environment-pinned trusted publisher config out of normal candidates", async () => {
|
||||
const repoDir = createTempPluginRepo();
|
||||
const { fetchImpl } = createClawHubPlanFetch({
|
||||
packages: {
|
||||
"@openclaw/demo-plugin": {
|
||||
status: 200,
|
||||
body: {
|
||||
package: {},
|
||||
owner: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
trustedPublishers: {
|
||||
"@openclaw/demo-plugin": {
|
||||
status: 200,
|
||||
body: {
|
||||
trustedPublisher: {
|
||||
repository: "openclaw/openclaw",
|
||||
workflowFilename: "plugin-clawhub-release.yml",
|
||||
environment: "clawhub-plugin-release",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
versions: {
|
||||
"@openclaw/demo-plugin@2026.4.1": 404,
|
||||
},
|
||||
});
|
||||
|
||||
const plan = await collectPluginClawHubReleasePlan({
|
||||
rootDir: repoDir,
|
||||
selection: ["@openclaw/demo-plugin"],
|
||||
fetchImpl,
|
||||
registryBaseUrl: "https://clawhub.ai",
|
||||
});
|
||||
|
||||
expect(plan.candidates).toStrictEqual([]);
|
||||
expect(plan.bootstrapCandidates).toStrictEqual([]);
|
||||
expect(plan.missingTrustedPublisher.map((plugin) => plugin.packageName)).toEqual([
|
||||
"@openclaw/demo-plugin",
|
||||
]);
|
||||
});
|
||||
|
||||
it("skips versions that already exist on ClawHub", async () => {
|
||||
const repoDir = createTempPluginRepo();
|
||||
const { fetchImpl } = createClawHubPlanFetch({
|
||||
packages: {
|
||||
"@openclaw/demo-plugin": {
|
||||
status: 200,
|
||||
body: {
|
||||
package: {},
|
||||
owner: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
trustedPublishers: {
|
||||
"@openclaw/demo-plugin": {
|
||||
status: 200,
|
||||
body: {
|
||||
trustedPublisher: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
versions: {
|
||||
"@openclaw/demo-plugin@2026.4.1": 200,
|
||||
},
|
||||
});
|
||||
|
||||
const plan = await collectPluginClawHubReleasePlan({
|
||||
rootDir: repoDir,
|
||||
selection: ["@openclaw/demo-plugin"],
|
||||
fetchImpl,
|
||||
registryBaseUrl: "https://clawhub.ai",
|
||||
});
|
||||
|
||||
expect(plan.candidates).toStrictEqual([]);
|
||||
expect(plan.bootstrapCandidates).toStrictEqual([]);
|
||||
expect(plan.missingTrustedPublisher.map((plugin) => plugin.packageName)).toEqual([
|
||||
"@openclaw/demo-plugin",
|
||||
]);
|
||||
expect(plan.missingTrustedPublisher[0]).toMatchObject({
|
||||
alreadyPublished: true,
|
||||
artifactName: "clawhub-package-openclaw-demo-plugin-2026.4.1",
|
||||
packageName: "@openclaw/demo-plugin",
|
||||
version: "2026.4.1",
|
||||
});
|
||||
expect(plan.skippedPublished).toHaveLength(1);
|
||||
expect(plan.skippedPublished[0]).toEqual({
|
||||
alreadyPublished: true,
|
||||
@@ -369,7 +569,31 @@ describe("collectPluginClawHubReleasePlan", () => {
|
||||
const plan = await collectPluginClawHubReleasePlan({
|
||||
rootDir: repoDir,
|
||||
selection: ["@openclaw/demo-plugin"],
|
||||
fetchImpl: async () => new Response("{}", { status: 404 }),
|
||||
fetchImpl: createClawHubPlanFetch({
|
||||
packages: {
|
||||
"@openclaw/demo-plugin": {
|
||||
status: 200,
|
||||
body: {
|
||||
package: {},
|
||||
owner: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
trustedPublishers: {
|
||||
"@openclaw/demo-plugin": {
|
||||
status: 200,
|
||||
body: {
|
||||
trustedPublisher: {
|
||||
repository: "openclaw/openclaw",
|
||||
workflowFilename: "plugin-clawhub-release.yml",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
versions: {
|
||||
"@openclaw/demo-plugin@2026.4.1": 404,
|
||||
},
|
||||
}).fetchImpl,
|
||||
registryBaseUrl: "https://clawhub.ai",
|
||||
});
|
||||
|
||||
@@ -380,6 +604,280 @@ describe("collectPluginClawHubReleasePlan", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildOpenClawReleaseClawHubPlan", () => {
|
||||
it("emits a dispatch plan that keeps ClawHub children on the release tag", async () => {
|
||||
const repoDir = createTempPluginRepo({
|
||||
extraExtensionIds: ["demo-two", "demo-three"],
|
||||
});
|
||||
const { fetchImpl } = createClawHubPlanFetch({
|
||||
packages: {
|
||||
"@openclaw/demo-plugin": {
|
||||
status: 200,
|
||||
body: {
|
||||
package: {},
|
||||
owner: {},
|
||||
},
|
||||
},
|
||||
"@openclaw/demo-two": {
|
||||
status: 404,
|
||||
},
|
||||
"@openclaw/demo-three": {
|
||||
status: 200,
|
||||
body: {
|
||||
package: {},
|
||||
owner: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
trustedPublishers: {
|
||||
"@openclaw/demo-plugin": {
|
||||
status: 200,
|
||||
body: {
|
||||
trustedPublisher: {
|
||||
repository: "openclaw/openclaw",
|
||||
workflowFilename: "plugin-clawhub-release.yml",
|
||||
},
|
||||
},
|
||||
},
|
||||
"@openclaw/demo-three": {
|
||||
status: 200,
|
||||
body: {
|
||||
trustedPublisher: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
versions: {
|
||||
"@openclaw/demo-plugin@2026.4.1": 404,
|
||||
"@openclaw/demo-three@2026.4.1": 404,
|
||||
},
|
||||
});
|
||||
|
||||
const plan = await buildOpenClawReleaseClawHubPlan(
|
||||
{
|
||||
releaseTag: "v2026.4.1-beta.1",
|
||||
releasePublishBranch: "main",
|
||||
releasePublishRunId: "12345",
|
||||
pluginPublishScope: "all-publishable",
|
||||
plugins: [],
|
||||
},
|
||||
{
|
||||
rootDir: repoDir,
|
||||
fetchImpl,
|
||||
registryBaseUrl: "https://clawhub.ai",
|
||||
},
|
||||
);
|
||||
|
||||
expect(plan.clawHubWorkflowRef).toBe("v2026.4.1-beta.1");
|
||||
expect(plan.releasePublishBranch).toBe("main");
|
||||
expect(plan.normal).toEqual({
|
||||
workflow: "plugin-clawhub-release.yml",
|
||||
ref: "v2026.4.1-beta.1",
|
||||
shouldDispatch: true,
|
||||
packages: ["@openclaw/demo-plugin"],
|
||||
inputs: {
|
||||
publish_scope: "selected",
|
||||
plugins: "@openclaw/demo-plugin",
|
||||
release_publish_run_id: "12345",
|
||||
release_publish_branch: "main",
|
||||
},
|
||||
});
|
||||
expect(plan.bootstrap).toEqual({
|
||||
workflow: "plugin-clawhub-new.yml",
|
||||
ref: "v2026.4.1-beta.1",
|
||||
shouldDispatch: true,
|
||||
packages: ["@openclaw/demo-two", "@openclaw/demo-three"],
|
||||
inputs: {
|
||||
plugins: "@openclaw/demo-two,@openclaw/demo-three",
|
||||
release_publish_run_id: "12345",
|
||||
release_publish_branch: "main",
|
||||
},
|
||||
});
|
||||
expect(new Set([...plan.normal.packages, ...plan.bootstrap.packages]).size).toBe(3);
|
||||
expect(plan.summary).toEqual({
|
||||
normalCount: 1,
|
||||
bootstrapCount: 2,
|
||||
missingTrustedPublisherCount: 1,
|
||||
normalPlugins: "@openclaw/demo-plugin",
|
||||
bootstrapPlugins: "@openclaw/demo-two,@openclaw/demo-three",
|
||||
missingTrustedPlugins: "@openclaw/demo-three",
|
||||
});
|
||||
expect(plan.verifier).toEqual({
|
||||
clawHubWorkflowRef: "v2026.4.1-beta.1",
|
||||
});
|
||||
});
|
||||
|
||||
it("routes already-published packages missing trusted publisher config to bootstrap repair", async () => {
|
||||
const repoDir = createTempPluginRepo();
|
||||
const { fetchImpl } = createClawHubPlanFetch({
|
||||
packages: {
|
||||
"@openclaw/demo-plugin": {
|
||||
status: 200,
|
||||
body: {
|
||||
package: {},
|
||||
owner: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
trustedPublishers: {
|
||||
"@openclaw/demo-plugin": {
|
||||
status: 200,
|
||||
body: {
|
||||
trustedPublisher: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
versions: {
|
||||
"@openclaw/demo-plugin@2026.4.1": 200,
|
||||
},
|
||||
});
|
||||
|
||||
const plan = await buildOpenClawReleaseClawHubPlan(
|
||||
{
|
||||
releaseTag: "v2026.4.1-beta.1",
|
||||
releasePublishBranch: "release/2026.4.1",
|
||||
releasePublishRunId: "12345",
|
||||
pluginPublishScope: "selected",
|
||||
plugins: ["@openclaw/demo-plugin"],
|
||||
},
|
||||
{
|
||||
rootDir: repoDir,
|
||||
fetchImpl,
|
||||
registryBaseUrl: "https://clawhub.ai",
|
||||
},
|
||||
);
|
||||
|
||||
expect(plan.normal.shouldDispatch).toBe(false);
|
||||
expect(plan.bootstrap).toMatchObject({
|
||||
workflow: "plugin-clawhub-new.yml",
|
||||
ref: "v2026.4.1-beta.1",
|
||||
shouldDispatch: true,
|
||||
packages: ["@openclaw/demo-plugin"],
|
||||
inputs: {
|
||||
plugins: "@openclaw/demo-plugin",
|
||||
release_publish_run_id: "12345",
|
||||
release_publish_branch: "release/2026.4.1",
|
||||
},
|
||||
});
|
||||
expect(plan.summary).toMatchObject({
|
||||
normalCount: 0,
|
||||
bootstrapCount: 1,
|
||||
missingTrustedPublisherCount: 1,
|
||||
bootstrapPlugins: "@openclaw/demo-plugin",
|
||||
missingTrustedPlugins: "@openclaw/demo-plugin",
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects incompatible all-publishable plugin selection args", () => {
|
||||
expect(() =>
|
||||
parseOpenClawReleaseClawHubPlanArgs([
|
||||
"--release-tag",
|
||||
"v2026.4.1-beta.1",
|
||||
"--release-publish-branch",
|
||||
"main",
|
||||
"--release-publish-run-id",
|
||||
"12345",
|
||||
"--plugin-publish-scope",
|
||||
"all-publishable",
|
||||
"--plugins",
|
||||
"@openclaw/demo-plugin",
|
||||
]),
|
||||
).toThrow("plugin-publish-scope=all-publishable must not be combined with --plugins.");
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildOpenClawReleaseClawHubRuntimeState", () => {
|
||||
it("includes the normal ClawHub run in verifier args when the release waits for it", () => {
|
||||
const state = buildOpenClawReleaseClawHubRuntimeState({
|
||||
repository: "openclaw/openclaw",
|
||||
waitForClawHub: true,
|
||||
forceSkipClawHub: false,
|
||||
normalRunId: "111",
|
||||
bootstrapRunId: "",
|
||||
bootstrapCompleted: false,
|
||||
});
|
||||
|
||||
expect(state.verifierArgs).toEqual(["--plugin-clawhub-run", "111"]);
|
||||
expect(state.proofLines.normal).toBe(
|
||||
"- plugin ClawHub publish: https://github.com/openclaw/openclaw/actions/runs/111",
|
||||
);
|
||||
expect(state.proofLines.bootstrap).toBe("- plugin ClawHub bootstrap: not needed");
|
||||
});
|
||||
|
||||
it("includes a completed bootstrap run even when there is no normal ClawHub run", () => {
|
||||
const state = buildOpenClawReleaseClawHubRuntimeState({
|
||||
repository: "openclaw/openclaw",
|
||||
waitForClawHub: false,
|
||||
forceSkipClawHub: false,
|
||||
normalRunId: "",
|
||||
bootstrapRunId: "222",
|
||||
bootstrapCompleted: true,
|
||||
});
|
||||
|
||||
expect(state.verifierArgs).toEqual(["--plugin-clawhub-bootstrap-run", "222"]);
|
||||
expect(state.proofLines.normal).toBe("- plugin ClawHub publish: no normal OIDC candidates");
|
||||
expect(state.proofLines.bootstrap).toBe(
|
||||
"- plugin ClawHub bootstrap: https://github.com/openclaw/openclaw/actions/runs/222",
|
||||
);
|
||||
});
|
||||
|
||||
it("skips ClawHub verification for non-awaited incomplete runs while keeping proof links", () => {
|
||||
const state = buildOpenClawReleaseClawHubRuntimeState({
|
||||
repository: "openclaw/openclaw",
|
||||
waitForClawHub: false,
|
||||
forceSkipClawHub: false,
|
||||
normalRunId: "111",
|
||||
bootstrapRunId: "222",
|
||||
bootstrapCompleted: false,
|
||||
});
|
||||
|
||||
expect(state.verifierArgs).toEqual(["--skip-clawhub"]);
|
||||
expect(state.proofLines.normal).toBe(
|
||||
"- plugin ClawHub publish: dispatched separately, not awaited by this proof: https://github.com/openclaw/openclaw/actions/runs/111",
|
||||
);
|
||||
expect(state.proofLines.bootstrap).toBe(
|
||||
"- plugin ClawHub bootstrap: dispatched separately, not awaited by this proof: https://github.com/openclaw/openclaw/actions/runs/222",
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps completed bootstrap run evidence when the normal ClawHub run is not awaited", () => {
|
||||
const state = buildOpenClawReleaseClawHubRuntimeState({
|
||||
repository: "openclaw/openclaw",
|
||||
waitForClawHub: false,
|
||||
forceSkipClawHub: false,
|
||||
normalRunId: "111",
|
||||
bootstrapRunId: "222",
|
||||
bootstrapCompleted: true,
|
||||
});
|
||||
|
||||
expect(state.verifierArgs).toEqual(["--skip-clawhub", "--plugin-clawhub-bootstrap-run", "222"]);
|
||||
expect(state.proofLines.normal).toBe(
|
||||
"- plugin ClawHub publish: dispatched separately, not awaited by this proof: https://github.com/openclaw/openclaw/actions/runs/111",
|
||||
);
|
||||
expect(state.proofLines.bootstrap).toBe(
|
||||
"- plugin ClawHub bootstrap: https://github.com/openclaw/openclaw/actions/runs/222",
|
||||
);
|
||||
});
|
||||
|
||||
it("forces skip-clawhub after a failed child run even if ClawHub runs completed", () => {
|
||||
const state = buildOpenClawReleaseClawHubRuntimeState({
|
||||
repository: "openclaw/openclaw",
|
||||
waitForClawHub: true,
|
||||
forceSkipClawHub: true,
|
||||
normalRunId: "111",
|
||||
bootstrapRunId: "222",
|
||||
bootstrapCompleted: true,
|
||||
});
|
||||
|
||||
expect(state.verifierArgs).toEqual(["--skip-clawhub"]);
|
||||
expect(state.proofLines.normal).toBe(
|
||||
"- plugin ClawHub publish: https://github.com/openclaw/openclaw/actions/runs/111",
|
||||
);
|
||||
expect(state.proofLines.bootstrap).toBe(
|
||||
"- plugin ClawHub bootstrap: https://github.com/openclaw/openclaw/actions/runs/222",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("plugin-clawhub-publish.sh", () => {
|
||||
it("previews the publish command through the ClawHub CLI dry-run preflight", () => {
|
||||
const repoDir = createTempPluginRepo();
|
||||
@@ -449,6 +947,70 @@ exit 0
|
||||
expect(invocations).toContain("--dry-run");
|
||||
});
|
||||
|
||||
it("passes a manual override reason when trusted publisher repair requires one", () => {
|
||||
const repoDir = createTempPluginRepo();
|
||||
const binDir = join(repoDir, "bin");
|
||||
const markerPath = join(repoDir, "clawhub-invoked");
|
||||
mkdirSync(binDir, { recursive: true });
|
||||
const clawhubPath = join(binDir, "clawhub");
|
||||
writeFileSync(
|
||||
clawhubPath,
|
||||
`#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
printf '%s\\n' "$*" >> ${JSON.stringify(markerPath)}
|
||||
if [[ "\${1:-}" == "--workdir" ]]; then
|
||||
shift 2
|
||||
fi
|
||||
if [[ "\${1:-}" == "package" && "\${2:-}" == "pack" ]]; then
|
||||
pack_destination=""
|
||||
while [[ "$#" -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--pack-destination)
|
||||
pack_destination="\${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
mkdir -p "$pack_destination"
|
||||
pack_path="$pack_destination/openclaw-demo-plugin-2026.4.1.tgz"
|
||||
printf 'fake tgz\\n' > "$pack_path"
|
||||
printf '{"path":"%s","name":"@openclaw/demo-plugin","version":"2026.4.1"}\\n' "$pack_path"
|
||||
fi
|
||||
exit 0
|
||||
`,
|
||||
);
|
||||
chmodSync(clawhubPath, 0o755);
|
||||
|
||||
execFileSync(
|
||||
"bash",
|
||||
[
|
||||
join(process.cwd(), "scripts/plugin-clawhub-publish.sh"),
|
||||
"--publish",
|
||||
"extensions/demo-plugin",
|
||||
],
|
||||
{
|
||||
cwd: repoDir,
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
...process.env,
|
||||
OPENCLAW_CLAWHUB_MANUAL_OVERRIDE_REASON:
|
||||
"GitHub Actions trusted publisher repair before OIDC migration",
|
||||
OPENCLAW_PLUGIN_NPM_RUNTIME_BUILD: "0",
|
||||
PATH: `${binDir}${delimiter}${process.env.PATH ?? ""}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const invocations = readFileSync(markerPath, "utf8");
|
||||
expect(invocations).toContain("package publish ");
|
||||
expect(invocations).toContain(
|
||||
"--manual-override-reason GitHub Actions trusted publisher repair before OIDC migration",
|
||||
);
|
||||
});
|
||||
|
||||
it("packs a reusable workflow artifact without publishing", () => {
|
||||
const repoDir = createTempPluginRepo();
|
||||
const binDir = join(repoDir, "bin");
|
||||
@@ -625,6 +1187,73 @@ function commitSharedReleaseToolingChange(repoDir: string) {
|
||||
return { baseRef, headRef };
|
||||
}
|
||||
|
||||
function createClawHubPlanFetch(config: {
|
||||
packages: Record<
|
||||
string,
|
||||
{
|
||||
status: number;
|
||||
body?: unknown;
|
||||
}
|
||||
>;
|
||||
trustedPublishers?: Record<
|
||||
string,
|
||||
{
|
||||
status: number;
|
||||
body?: unknown;
|
||||
}
|
||||
>;
|
||||
versions?: Record<string, number>;
|
||||
}) {
|
||||
const requests: string[] = [];
|
||||
const fetchImpl: typeof fetch = async (input) => {
|
||||
const requestUrl =
|
||||
typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
||||
const url = new URL(requestUrl);
|
||||
requests.push(url.pathname);
|
||||
|
||||
const packageMatch = url.pathname.match(/^\/api\/v1\/packages\/([^/]+)$/u);
|
||||
if (packageMatch) {
|
||||
const packageName = decodeURIComponent(packageMatch[1]);
|
||||
const packageResponse = config.packages[packageName];
|
||||
if (!packageResponse) {
|
||||
throw new Error(`Unexpected package detail request for ${packageName}`);
|
||||
}
|
||||
return new Response(JSON.stringify(packageResponse.body ?? {}), {
|
||||
status: packageResponse.status,
|
||||
});
|
||||
}
|
||||
|
||||
const trustedPublisherMatch = url.pathname.match(
|
||||
/^\/api\/v1\/packages\/([^/]+)\/trusted-publisher$/u,
|
||||
);
|
||||
if (trustedPublisherMatch) {
|
||||
const packageName = decodeURIComponent(trustedPublisherMatch[1]);
|
||||
const trustedPublisherResponse = config.trustedPublishers?.[packageName];
|
||||
if (!trustedPublisherResponse) {
|
||||
throw new Error(`Unexpected trusted-publisher request for ${packageName}`);
|
||||
}
|
||||
return new Response(JSON.stringify(trustedPublisherResponse.body ?? {}), {
|
||||
status: trustedPublisherResponse.status,
|
||||
});
|
||||
}
|
||||
|
||||
const versionMatch = url.pathname.match(/^\/api\/v1\/packages\/([^/]+)\/versions\/([^/]+)$/u);
|
||||
if (versionMatch) {
|
||||
const packageName = decodeURIComponent(versionMatch[1]);
|
||||
const version = decodeURIComponent(versionMatch[2]);
|
||||
const status = config.versions?.[`${packageName}@${version}`];
|
||||
if (!status) {
|
||||
throw new Error(`Unexpected version detail request for ${packageName}@${version}`);
|
||||
}
|
||||
return new Response("{}", { status });
|
||||
}
|
||||
|
||||
throw new Error(`Unexpected ClawHub request to ${url.pathname}`);
|
||||
};
|
||||
|
||||
return { fetchImpl, requests };
|
||||
}
|
||||
|
||||
function git(cwd: string, args: string[]) {
|
||||
return execFileSync("git", ["-C", cwd, ...args], {
|
||||
encoding: "utf8",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Package Acceptance Workflow tests cover package acceptance workflow script behavior.
|
||||
import { readdirSync, readFileSync } from "node:fs";
|
||||
import { readdirSync, readFileSync, statSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { parse } from "yaml";
|
||||
|
||||
@@ -56,6 +56,10 @@ function readWorkflow(path: string): Workflow {
|
||||
return parse(readFileSync(path, "utf8")) as Workflow;
|
||||
}
|
||||
|
||||
function isExecutable(path: string): boolean {
|
||||
return (statSync(path).mode & 0o111) !== 0;
|
||||
}
|
||||
|
||||
function workflowPaths(): string[] {
|
||||
return readdirSync(".github/workflows")
|
||||
.filter((name) => name.endsWith(".yml"))
|
||||
@@ -1378,7 +1382,6 @@ describe("package artifact reuse", () => {
|
||||
|
||||
it("keeps release QA and repo E2E lanes off scarce 32-core runners", () => {
|
||||
const releaseChecksWorkflow = readFileSync(RELEASE_CHECKS_WORKFLOW, "utf8");
|
||||
const qaWorkflow = readFileSync(QA_LIVE_TRANSPORTS_WORKFLOW, "utf8");
|
||||
const liveE2eWorkflow = readFileSync(LIVE_E2E_WORKFLOW, "utf8");
|
||||
|
||||
for (const jobName of [
|
||||
@@ -1547,9 +1550,14 @@ describe("package artifact reuse", () => {
|
||||
};
|
||||
const releaseWorkflow = readFileSync(RELEASE_PUBLISH_WORKFLOW, "utf8");
|
||||
const clawHubWorkflow = readFileSync(".github/workflows/plugin-clawhub-release.yml", "utf8");
|
||||
const clawHubNewWorkflow = readFileSync(".github/workflows/plugin-clawhub-new.yml", "utf8");
|
||||
const pluginNpmWorkflow = readFileSync(".github/workflows/plugin-npm-release.yml", "utf8");
|
||||
const openclawNpmWorkflow = readFileSync(".github/workflows/openclaw-npm-release.yml", "utf8");
|
||||
const approvalScript = readFileSync("scripts/validate-release-publish-approval.mjs", "utf8");
|
||||
const clawHubReleasePlanScript = readFileSync(
|
||||
"scripts/lib/openclaw-release-clawhub-plan.ts",
|
||||
"utf8",
|
||||
);
|
||||
const clawHubResolveRefIndex = clawHubWorkflow.indexOf("- name: Resolve checked-out ref");
|
||||
const clawHubValidateRefIndex = clawHubWorkflow.indexOf(
|
||||
"- name: Validate ref is on a trusted publish branch",
|
||||
@@ -1571,30 +1579,80 @@ describe("package artifact reuse", () => {
|
||||
expect(packageJson.scripts?.["release:fast-pretag-check"]).toBe(
|
||||
"bash scripts/release-fast-pretag-check.sh",
|
||||
);
|
||||
expect(clawHubWorkflow).toContain('CLAWHUB_REF: "c9bb13023598dcc547fdf4a93b9d42512b8c8854"');
|
||||
expect(clawHubWorkflow).toContain('CLAWHUB_CLI_PACKAGE: "clawhub@0.21.0"');
|
||||
expect(clawHubWorkflow).not.toContain("CLAWHUB_REPOSITORY:");
|
||||
expect(clawHubWorkflow).not.toContain("CLAWHUB_REF:");
|
||||
expect(clawHubWorkflow).toContain("pack_plugins_clawhub_artifacts:");
|
||||
expect(clawHubWorkflow).toContain("Verify package-local runtime build");
|
||||
expect(clawHubWorkflow).toContain("Install pinned ClawHub CLI wrapper");
|
||||
expect(clawHubWorkflow).toContain("Pack ClawHub package artifact");
|
||||
expect(clawHubWorkflow).toContain("Upload ClawHub package artifact");
|
||||
expect(clawHubWorkflow).toContain("Validate OIDC source matches workflow ref");
|
||||
expect(clawHubWorkflow).toContain(
|
||||
"Dry-run target ref to validate; real OIDC publishes must dispatch the workflow with --ref set to the target release tag/ref",
|
||||
);
|
||||
expect(clawHubWorkflow).toContain(
|
||||
"Plugin ClawHub OIDC publishes must run from the same ref that is being published.",
|
||||
);
|
||||
expect(clawHubWorkflow).toContain("The ref input is only supported for dry_run=true.");
|
||||
expect(clawHubWorkflow).toContain(
|
||||
"Dry-run publish target differs from workflow ref; allowing validation-only dispatch.",
|
||||
);
|
||||
expect(clawHubWorkflow).toContain(
|
||||
"github.event_name == 'workflow_dispatch' && inputs.dry_run != true && inputs.publish_scope == 'selected' && steps.plan.outputs.skipped_published_count != '0'",
|
||||
);
|
||||
expect(clawHubWorkflow).toContain(
|
||||
"uses: openclaw/clawhub/.github/workflows/package-publish.yml@9d49df109d4ad3dc8a6ecf05d26b39f46d294721",
|
||||
);
|
||||
expect(clawHubWorkflow).toContain("dry_run:");
|
||||
expect(clawHubWorkflow).toContain("default: false");
|
||||
expect(clawHubWorkflow).toContain("approve_plugin_clawhub_release:");
|
||||
expect(clawHubWorkflow).not.toContain("approve_plugin_clawhub_release:");
|
||||
expect(clawHubWorkflow).toContain("approve_plugins_clawhub_release:");
|
||||
expect(clawHubWorkflow).toContain("environment: clawhub-plugin-release");
|
||||
expect(clawHubWorkflow).toContain("inputs.dry_run != true");
|
||||
expect(clawHubWorkflow).toContain("Approve ClawHub package publish");
|
||||
expect(clawHubWorkflow).toContain("release_publish_branch:");
|
||||
expect(clawHubWorkflow).toContain(
|
||||
"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')",
|
||||
"TRUSTED_PUBLISH_BRANCH: ${{ inputs.release_publish_branch || github.ref_name }}",
|
||||
);
|
||||
expect(clawHubWorkflow).toContain(
|
||||
"uses: openclaw/clawhub/.github/workflows/package-publish.yml@c9bb13023598dcc547fdf4a93b9d42512b8c8854",
|
||||
"EXPECTED_WORKFLOW_BRANCH: ${{ inputs.release_publish_branch || github.ref_name }}",
|
||||
);
|
||||
expect(clawHubWorkflow).toContain(
|
||||
"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')",
|
||||
);
|
||||
expect(clawHubWorkflow).toContain("dry_run: ${{ inputs.dry_run }}");
|
||||
expect(clawHubWorkflow).toContain("package_artifact_name: ${{ matrix.plugin.artifactName }}");
|
||||
expect(clawHubWorkflow).toContain("clawhub_token: ${{ secrets.CLAWHUB_TOKEN }}");
|
||||
expect(clawHubWorkflow).toContain("source_repo: ${{ github.repository }}");
|
||||
expect(clawHubWorkflow).toContain(
|
||||
"source_commit: ${{ needs.preview_plugins_clawhub.outputs.ref_revision }}",
|
||||
);
|
||||
expect(clawHubWorkflow).toContain("source_ref: ${{ github.ref }}");
|
||||
expect(clawHubWorkflow).toContain("source_path: ${{ matrix.plugin.packageDir }}");
|
||||
expect(clawHubWorkflow).toContain(
|
||||
"inspector_artifact_name: ${{ matrix.plugin.artifactName }}-inspector",
|
||||
);
|
||||
expect(clawHubWorkflow).toContain(
|
||||
"publish_json_artifact_name: ${{ matrix.plugin.artifactName }}-publish-json",
|
||||
);
|
||||
expect(clawHubWorkflow).toContain("tags: ${{ matrix.plugin.publishTag }}");
|
||||
expect(clawHubWorkflow).toContain("dry_run: ${{ inputs.dry_run }}");
|
||||
expect(clawHubWorkflow).not.toContain("secrets.CLAWHUB_TOKEN");
|
||||
expect(clawHubWorkflow).not.toContain("clawhub_token:");
|
||||
expect(clawHubWorkflow).toContain("bootstrapCandidates");
|
||||
expect(clawHubWorkflow).toContain("missingTrustedPublisher");
|
||||
expect(clawHubWorkflow).toContain("bootstrap_candidate_count");
|
||||
expect(clawHubWorkflow).toContain("missing_trusted_publisher_count");
|
||||
expect(clawHubWorkflow).toContain("Bootstrap candidates requiring token bootstrap:");
|
||||
expect(clawHubWorkflow).toContain("Missing trusted publisher candidates:");
|
||||
expect(clawHubWorkflow).toContain("verify_published_clawhub_package:");
|
||||
expect(clawHubWorkflow).toContain("inputs.dry_run != true");
|
||||
expect(clawHubWorkflow).toContain("Verify published ClawHub package");
|
||||
expect(clawHubWorkflow).not.toContain("bash scripts/plugin-clawhub-publish.sh --publish");
|
||||
expect(clawHubWorkflow).not.toContain("Write ClawHub token config");
|
||||
expect(clawHubWorkflow).toContain("bun install failed while preparing ClawHub CLI; retrying");
|
||||
expect(clawHubWorkflow).not.toContain("Checkout ClawHub CLI source");
|
||||
expect(clawHubWorkflow).not.toContain("packages/clawhub/src/cli.ts");
|
||||
expect(clawHubWorkflow).not.toContain(
|
||||
"bun install failed while preparing ClawHub CLI; retrying",
|
||||
);
|
||||
expect(clawHubWorkflow).toContain("max-parallel: 32");
|
||||
expect(clawHubResolveRefIndex).toBeGreaterThanOrEqual(0);
|
||||
expect(clawHubValidateRefIndex).toBeGreaterThan(clawHubResolveRefIndex);
|
||||
@@ -1602,6 +1660,29 @@ describe("package artifact reuse", () => {
|
||||
expect(clawHubMetadataIndex).toBeGreaterThan(clawHubSetupIndex);
|
||||
expect(releaseWorkflow).toContain("Plugin npm run ID");
|
||||
expect(releaseWorkflow).toContain("Plugin ClawHub run ID");
|
||||
expect(releaseWorkflow).toContain("plugin-clawhub-new.yml");
|
||||
expect(releaseWorkflow).toContain("Plugin ClawHub bootstrap run ID");
|
||||
expect(releaseWorkflow).toContain("scripts/openclaw-release-clawhub-plan.ts");
|
||||
expect(releaseWorkflow).toContain("scripts/openclaw-release-clawhub-runtime-state.ts");
|
||||
expect(isExecutable("scripts/openclaw-release-clawhub-plan.ts")).toBe(true);
|
||||
expect(isExecutable("scripts/openclaw-release-clawhub-runtime-state.ts")).toBe(true);
|
||||
expect(releaseWorkflow).toContain("openclaw-release-clawhub-plan.json");
|
||||
expect(releaseWorkflow).toContain("openclaw-release-clawhub-runtime-state");
|
||||
expect(releaseWorkflow).toContain("bootstrap_plugins");
|
||||
expect(releaseWorkflow).toContain("missing_trusted_plugins");
|
||||
expect(releaseWorkflow).toContain(".summary.bootstrapPlugins");
|
||||
expect(releaseWorkflow).toContain(".summary.missingTrustedPlugins");
|
||||
expect(releaseWorkflow).toContain("append_clawhub_dispatch_args");
|
||||
expect(releaseWorkflow).toContain("write_clawhub_runtime_state");
|
||||
expect(releaseWorkflow).toContain(".[$target].inputs | to_entries[]");
|
||||
expect(releaseWorkflow).toContain(".verifierArgs[]");
|
||||
expect(releaseWorkflow).toContain(".proofLines.normal");
|
||||
expect(releaseWorkflow).toContain(".proofLines.bootstrap");
|
||||
expect(releaseWorkflow).toContain("Bootstrap/repair candidates:");
|
||||
expect(releaseWorkflow).toContain("Trusted-publisher repair plugins:");
|
||||
expect(releaseWorkflow).toContain(
|
||||
"Waiting for plugin-clawhub-new.yml bootstrap to finish before continuing release publish.",
|
||||
);
|
||||
expect(releaseWorkflow).toContain("OpenClaw npm run ID");
|
||||
expect(releaseWorkflow).toContain("npm_telegram_run_id");
|
||||
expect(releaseWorkflow).toContain('release_publish_run_id="${GITHUB_RUN_ID}"');
|
||||
@@ -1612,7 +1693,7 @@ describe("package artifact reuse", () => {
|
||||
);
|
||||
expect(releaseWorkflow).toContain("registry tarball");
|
||||
expect(releaseWorkflow).toContain("release SHA");
|
||||
expect(releaseWorkflow).toContain("not awaited by this proof");
|
||||
expect(clawHubReleasePlanScript).toContain("not awaited by this proof");
|
||||
expect(releaseWorkflow).toContain("wait_for_job_success");
|
||||
expect(releaseWorkflow).toContain("Validate release publish approval");
|
||||
expect(releaseWorkflow).toContain('conclusion" == "skipped"');
|
||||
@@ -1621,13 +1702,17 @@ describe("package artifact reuse", () => {
|
||||
expect(releaseWorkflow).toContain("release:verify-beta");
|
||||
expect(releaseWorkflow).toContain('--workflow-ref "${CHILD_WORKFLOW_REF}"');
|
||||
expect(releaseWorkflow).toContain("--skip-github-release");
|
||||
expect(clawHubReleasePlanScript).toContain("--plugin-clawhub-bootstrap-run");
|
||||
expect(releaseWorkflow).toContain('verify_args+=(--plugins "${PLUGINS}")');
|
||||
expect(releaseWorkflow).toContain("openclaw-release-postpublish-evidence");
|
||||
expect(releaseWorkflow).toContain("Failed child job summary");
|
||||
expect(releaseWorkflow).toContain("Workflow completion waits for ClawHub");
|
||||
expect(releaseWorkflow).toContain("Workflow completion does not wait for ClawHub");
|
||||
expect(releaseWorkflow).toContain('[[ "${WAIT_FOR_CLAWHUB}" == "true" ]]');
|
||||
expect(releaseWorkflow).toContain("--skip-clawhub");
|
||||
expect(releaseWorkflow).toContain(
|
||||
'[[ -n "${plugin_clawhub_bootstrap_run_id}" && "${WAIT_FOR_CLAWHUB}" == "true" ]]',
|
||||
);
|
||||
expect(clawHubReleasePlanScript).toContain("--skip-clawhub");
|
||||
expect(pluginNpmWorkflow).toContain("Validate release publish approval run");
|
||||
expect(clawHubWorkflow).toContain("Validate release publish approval run");
|
||||
expect(openclawNpmWorkflow).toContain("Validate release publish approval run");
|
||||
@@ -1651,9 +1736,71 @@ describe("package artifact reuse", () => {
|
||||
expect(approvalScript).toContain("must still be in_progress");
|
||||
expect(approvalScript).toContain("completed with success/failure");
|
||||
expect(pluginNpmWorkflow).toContain("environment: npm-release");
|
||||
expect(clawHubWorkflow).toContain("environment: clawhub-plugin-release");
|
||||
expect(clawHubWorkflow.match(/environment: clawhub-plugin-release/g)?.length).toBe(1);
|
||||
expect(clawHubNewWorkflow).toContain("name: Plugin ClawHub New");
|
||||
expect(clawHubNewWorkflow).toContain('CLAWHUB_CLI_PACKAGE: "clawhub@0.21.0"');
|
||||
expect(clawHubNewWorkflow).not.toContain("CLAWHUB_REPOSITORY:");
|
||||
expect(clawHubNewWorkflow).not.toContain("CLAWHUB_REF:");
|
||||
expect(clawHubNewWorkflow).toContain("environment: clawhub-plugin-bootstrap");
|
||||
expect(clawHubNewWorkflow).toContain("secrets.CLAWHUB_TOKEN");
|
||||
expect(clawHubNewWorkflow).not.toContain(
|
||||
"uses: openclaw/clawhub/.github/workflows/package-publish.yml",
|
||||
);
|
||||
expect(clawHubNewWorkflow).not.toContain("clawhub_token:");
|
||||
expect(clawHubNewWorkflow).toContain("Validate pinned ClawHub trusted publisher CLI support");
|
||||
expect(clawHubNewWorkflow).toContain('npm exec --yes --package "${CLAWHUB_CLI_PACKAGE}"');
|
||||
expect(clawHubNewWorkflow).toContain(
|
||||
"CLAW-277 03 - Split OpenClaw plugin ClawHub publishing into OIDC release and token bootstrap workflows",
|
||||
);
|
||||
expect(clawHubNewWorkflow).toContain("Usage: clawhub package trusted-publisher set");
|
||||
expect(clawHubNewWorkflow).toContain("Write ClawHub token config");
|
||||
expect(clawHubNewWorkflow).toContain("CLAWHUB_CONFIG_PATH=${config_path}");
|
||||
expect(clawHubNewWorkflow).toContain(
|
||||
"CLAWHUB_REGISTRY is required for token-gated ClawHub bootstrap.",
|
||||
);
|
||||
expect(clawHubNewWorkflow).toContain(
|
||||
"CLAWHUB_TOKEN is required for token-gated ClawHub bootstrap.",
|
||||
);
|
||||
expect(clawHubNewWorkflow).toContain("JSON.stringify({ registry, token }, null, 2)");
|
||||
expect(clawHubNewWorkflow).toContain("Publish ClawHub bootstrap package");
|
||||
expect(clawHubNewWorkflow).toContain("bash scripts/plugin-clawhub-publish.sh --publish");
|
||||
expect(clawHubNewWorkflow).toContain("bootstrapMode");
|
||||
expect(clawHubNewWorkflow).toContain("BOOTSTRAP_MODE: ${{ matrix.plugin.bootstrapMode }}");
|
||||
expect(clawHubNewWorkflow).toContain("requiresManualOverride");
|
||||
expect(clawHubNewWorkflow).toContain(
|
||||
'OPENCLAW_CLAWHUB_MANUAL_OVERRIDE_REASON="GitHub Actions trusted publisher repair before OIDC migration"',
|
||||
);
|
||||
expect(clawHubNewWorkflow).toContain("configure-only");
|
||||
expect(clawHubNewWorkflow).toContain(
|
||||
"version is already present on ClawHub; configuring trusted publisher only",
|
||||
);
|
||||
expect(clawHubNewWorkflow).toContain(
|
||||
"EXPECTED_WORKFLOW_BRANCH: ${{ inputs.release_publish_branch || github.ref_name }}",
|
||||
);
|
||||
expect(clawHubNewWorkflow).toContain(
|
||||
"TRUSTED_PUBLISH_BRANCH: ${{ inputs.release_publish_branch || github.ref_name }}",
|
||||
);
|
||||
expect(clawHubNewWorkflow).toContain('OPENCLAW_PLUGIN_NPM_RUNTIME_BUILD: "0"');
|
||||
expect(clawHubNewWorkflow).toContain("trusted-publisher set");
|
||||
expect(clawHubNewWorkflow).toContain("--workflow-filename plugin-clawhub-release.yml");
|
||||
expect(clawHubNewWorkflow).not.toContain("--environment clawhub-plugin-release");
|
||||
expect(clawHubNewWorkflow).toContain("trustedPublisher?.environment != null");
|
||||
expect(clawHubNewWorkflow).toContain("without an environment pin");
|
||||
expect(clawHubNewWorkflow).not.toContain("Checkout ClawHub CLI source");
|
||||
expect(clawHubNewWorkflow).not.toContain("packages/clawhub/src/cli.ts");
|
||||
expect(clawHubNewWorkflow).toContain("verify_bootstrap_clawhub_package:");
|
||||
expect(clawHubNewWorkflow).toContain("Verify bootstrap ClawHub package and trusted publisher");
|
||||
expect(clawHubNewWorkflow).toContain("/trusted-publisher");
|
||||
expect(clawHubNewWorkflow).toContain('trustedPublisher?.repository !== "openclaw/openclaw"');
|
||||
expect(openclawNpmWorkflow).toContain("environment: npm-release");
|
||||
expect(releaseWorkflow).toContain("default: from-validation");
|
||||
expect(releaseWorkflow).toContain('--release-publish-branch "${CHILD_WORKFLOW_REF}"');
|
||||
expect(releaseWorkflow).toContain('--release-publish-run-id "${GITHUB_RUN_ID}"');
|
||||
expect(releaseWorkflow).toContain("jq -r '.normal.ref' \"${clawhub_plan_path}\"");
|
||||
expect(releaseWorkflow).toContain("jq -r '.normal.workflow' \"${clawhub_plan_path}\"");
|
||||
expect(releaseWorkflow).toContain("jq -r '.bootstrap.ref' \"${clawhub_plan_path}\"");
|
||||
expect(releaseWorkflow).toContain("jq -r '.bootstrap.workflow' \"${clawhub_plan_path}\"");
|
||||
expect(releaseWorkflow).toContain('--clawhub-workflow-ref "${clawhub_workflow_ref}"');
|
||||
expect(releaseWorkflow).toContain(
|
||||
'if [[ "$EXPECTED_RELEASE_PROFILE" != "from-validation" && "$release_profile" != "$EXPECTED_RELEASE_PROFILE" ]]; then',
|
||||
);
|
||||
|
||||
@@ -15,6 +15,7 @@ describe("parseReleaseVerifyBetaArgs", () => {
|
||||
repo: "openclaw/openclaw",
|
||||
registry: "https://clawhub.ai",
|
||||
workflowRef: undefined,
|
||||
clawHubWorkflowRef: undefined,
|
||||
pluginSelection: [],
|
||||
evidenceOut: undefined,
|
||||
skipPostpublish: false,
|
||||
@@ -32,6 +33,8 @@ describe("parseReleaseVerifyBetaArgs", () => {
|
||||
"2026.5.10-beta.3",
|
||||
"--workflow-ref",
|
||||
"release/2026.5.10",
|
||||
"--clawhub-workflow-ref",
|
||||
"v2026.5.10-beta.3",
|
||||
"--plugins",
|
||||
"@openclaw/plugin-a,@openclaw/plugin-b",
|
||||
"--full-release-validation-run",
|
||||
@@ -42,6 +45,8 @@ describe("parseReleaseVerifyBetaArgs", () => {
|
||||
"22",
|
||||
"--plugin-clawhub-run",
|
||||
"33",
|
||||
"--plugin-clawhub-bootstrap-run",
|
||||
"34",
|
||||
"--npm-telegram-run",
|
||||
"44",
|
||||
"--evidence-out",
|
||||
@@ -58,6 +63,7 @@ describe("parseReleaseVerifyBetaArgs", () => {
|
||||
repo: "openclaw/openclaw",
|
||||
registry: "https://clawhub.ai",
|
||||
workflowRef: "release/2026.5.10",
|
||||
clawHubWorkflowRef: "v2026.5.10-beta.3",
|
||||
pluginSelection: ["@openclaw/plugin-a", "@openclaw/plugin-b"],
|
||||
evidenceOut: ".artifacts/release-evidence.json",
|
||||
skipPostpublish: true,
|
||||
@@ -69,6 +75,7 @@ describe("parseReleaseVerifyBetaArgs", () => {
|
||||
openclawNpm: "11",
|
||||
pluginNpm: "22",
|
||||
pluginClawHub: "33",
|
||||
pluginClawHubBootstrap: "34",
|
||||
npmTelegram: "44",
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user