mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
01ae9d40fc
Adds a protected, dry-run-by-default workflow for reserving release-enabled external plugin package names with verified npm 0.0.0 placeholders.
351 lines
14 KiB
YAML
351 lines
14 KiB
YAML
name: NPM Placeholder Bootstrap
|
|
run-name: NPM Placeholder Bootstrap ${{ inputs.ref }}
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
ref:
|
|
description: Exact main-ancestry commit SHA containing the release-enabled plugin manifests
|
|
required: true
|
|
type: string
|
|
packages:
|
|
description: Ordered comma-separated @openclaw package names to reserve at 0.0.0
|
|
required: true
|
|
type: string
|
|
dry_run:
|
|
description: Build and verify the immutable publication plan without entering npm-release
|
|
required: true
|
|
default: true
|
|
type: boolean
|
|
|
|
concurrency:
|
|
group: npm-placeholder-release
|
|
cancel-in-progress: false
|
|
|
|
env:
|
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
|
NODE_VERSION: "24.15.0"
|
|
|
|
jobs:
|
|
plan:
|
|
name: Plan npm placeholder publication
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 15
|
|
permissions:
|
|
contents: read
|
|
outputs:
|
|
artifact_digest: ${{ steps.artifact_identity.outputs.digest }}
|
|
artifact_id: ${{ steps.upload.outputs.artifact-id }}
|
|
artifact_name: ${{ steps.identity.outputs.artifact_name }}
|
|
producer_run_attempt: ${{ github.run_attempt }}
|
|
target_sha: ${{ steps.target.outputs.sha }}
|
|
steps:
|
|
- name: Checkout selected source
|
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
with:
|
|
persist-credentials: false
|
|
ref: ${{ inputs.ref }}
|
|
fetch-depth: 0
|
|
filter: blob:none
|
|
|
|
- name: Validate trusted workflow and target
|
|
id: target
|
|
env:
|
|
SOURCE_REF: ${{ inputs.ref }}
|
|
EVENT_SHA: ${{ github.sha }}
|
|
WORKFLOW_REF: ${{ github.ref }}
|
|
WORKFLOW_SHA: ${{ github.workflow_sha }}
|
|
run: |
|
|
set -euo pipefail
|
|
[[ "$WORKFLOW_REF" == "refs/heads/main" && "$WORKFLOW_SHA" =~ ^[0-9a-f]{40}$ ]] || {
|
|
echo "NPM placeholder publication must run from the trusted main workflow." >&2
|
|
exit 1
|
|
}
|
|
[[ "$SOURCE_REF" =~ ^[0-9a-f]{40}$ && "$SOURCE_REF" == "$EVENT_SHA" && "$SOURCE_REF" == "$WORKFLOW_SHA" && "$(git rev-parse HEAD)" == "$SOURCE_REF" ]] || {
|
|
echo "NPM placeholder publication requires ref to equal the exact main workflow SHA." >&2
|
|
exit 1
|
|
}
|
|
timeout --signal=TERM --kill-after=10s 120s git fetch --no-tags origin \
|
|
+refs/heads/main:refs/remotes/origin/main
|
|
git merge-base --is-ancestor "$WORKFLOW_SHA" origin/main || {
|
|
echo "NPM placeholder workflow revision is not reachable from current main." >&2
|
|
exit 1
|
|
}
|
|
git merge-base --is-ancestor "$SOURCE_REF" origin/main || {
|
|
echo "NPM placeholder target must be reachable from current main." >&2
|
|
exit 1
|
|
}
|
|
echo "sha=$SOURCE_REF" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Setup Node
|
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
with:
|
|
node-version: ${{ env.NODE_VERSION }}
|
|
|
|
- name: Create deterministic placeholder publication
|
|
id: publication
|
|
env:
|
|
PACKAGES: ${{ inputs.packages }}
|
|
TARGET_SHA: ${{ steps.target.outputs.sha }}
|
|
WORKFLOW_SHA: ${{ github.workflow_sha }}
|
|
run: |
|
|
set -euo pipefail
|
|
node scripts/npm-placeholder-publication.mjs create \
|
|
--repo-root "$GITHUB_WORKSPACE" \
|
|
--output-dir "$RUNNER_TEMP/npm-placeholder-publication" \
|
|
--packages "$PACKAGES" \
|
|
--target-sha "$TARGET_SHA" \
|
|
--workflow-sha "$WORKFLOW_SHA" \
|
|
--github-output "$GITHUB_OUTPUT"
|
|
|
|
- name: Bind immutable artifact name
|
|
id: identity
|
|
run: echo "artifact_name=npm-placeholder-publication-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Upload immutable placeholder publication
|
|
id: upload
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
|
with:
|
|
name: ${{ steps.identity.outputs.artifact_name }}
|
|
path: ${{ runner.temp }}/npm-placeholder-publication/*
|
|
if-no-files-found: error
|
|
retention-days: 30
|
|
|
|
- name: Bind immutable artifact digest
|
|
id: artifact_identity
|
|
env:
|
|
RAW_DIGEST: ${{ steps.upload.outputs.artifact-digest }}
|
|
run: |
|
|
set -euo pipefail
|
|
[[ "$RAW_DIGEST" =~ ^[0-9a-f]{64}$ ]] || {
|
|
echo "NPM placeholder artifact digest must be 64 lowercase hex characters." >&2
|
|
exit 1
|
|
}
|
|
echo "digest=sha256:${RAW_DIGEST}" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Record secretless plan
|
|
env:
|
|
NEW_PACKAGE_COUNT: ${{ steps.publication.outputs.new_package_count }}
|
|
EXISTING_WITHOUT_ZERO_COUNT: ${{ steps.publication.outputs.existing_without_zero_count }}
|
|
PACKAGE_COUNT: ${{ steps.publication.outputs.package_count }}
|
|
TARGET_SHA: ${{ steps.target.outputs.sha }}
|
|
run: |
|
|
{
|
|
echo "## NPM placeholder plan"
|
|
echo
|
|
echo "- Target: \`${TARGET_SHA}\`"
|
|
echo "- Selected packages: \`${PACKAGE_COUNT}\`"
|
|
echo "- Registry 404 packages: \`${NEW_PACKAGE_COUNT}\`"
|
|
echo "- Existing packages without \`0.0.0\`: \`${EXISTING_WITHOUT_ZERO_COUNT}\`"
|
|
echo "- Dry run: \`${{ inputs.dry_run }}\`"
|
|
echo "- Credentials: **not available to this job**"
|
|
} >> "$GITHUB_STEP_SUMMARY"
|
|
|
|
verify:
|
|
name: Verify npm placeholder publication
|
|
needs: plan
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 15
|
|
permissions:
|
|
actions: read
|
|
contents: read
|
|
steps:
|
|
- name: Checkout trusted verification tooling
|
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
with:
|
|
persist-credentials: false
|
|
ref: ${{ github.workflow_sha }}
|
|
fetch-depth: 1
|
|
|
|
- name: Checkout bound target manifests
|
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
with:
|
|
persist-credentials: false
|
|
ref: ${{ needs.plan.outputs.target_sha }}
|
|
path: target-source
|
|
fetch-depth: 1
|
|
|
|
- name: Setup trusted Node
|
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
with:
|
|
node-version: ${{ env.NODE_VERSION }}
|
|
|
|
- name: Resolve immutable artifact metadata
|
|
id: artifact
|
|
env:
|
|
ARTIFACT_DIGEST: ${{ needs.plan.outputs.artifact_digest }}
|
|
ARTIFACT_ID: ${{ needs.plan.outputs.artifact_id }}
|
|
ARTIFACT_NAME: ${{ needs.plan.outputs.artifact_name }}
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: |
|
|
set -euo pipefail
|
|
metadata="$RUNNER_TEMP/npm-placeholder-artifact.json"
|
|
gh api "repos/${GITHUB_REPOSITORY}/actions/artifacts/${ARTIFACT_ID}" > "$metadata"
|
|
jq -e \
|
|
--arg digest "$ARTIFACT_DIGEST" \
|
|
--arg name "$ARTIFACT_NAME" \
|
|
--argjson id "$ARTIFACT_ID" \
|
|
'.id == $id and .name == $name and .digest == $digest and .expired == false' \
|
|
"$metadata" >/dev/null || {
|
|
echo "NPM placeholder artifact metadata does not match the plan output." >&2
|
|
exit 1
|
|
}
|
|
echo "size_bytes=$(jq -er '.size_in_bytes' "$metadata")" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Verify immutable placeholder publication
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
TARGET_SHA: ${{ needs.plan.outputs.target_sha }}
|
|
WORKFLOW_SHA: ${{ github.workflow_sha }}
|
|
run: |
|
|
set -euo pipefail
|
|
node scripts/npm-placeholder-publication.mjs verify-artifact \
|
|
--artifact-digest "${{ needs.plan.outputs.artifact_digest }}" \
|
|
--artifact-id "${{ needs.plan.outputs.artifact_id }}" \
|
|
--artifact-name "${{ needs.plan.outputs.artifact_name }}" \
|
|
--artifact-size-bytes "${{ steps.artifact.outputs.size_bytes }}" \
|
|
--consumer-run-attempt "$GITHUB_RUN_ATTEMPT" \
|
|
--output-dir "$RUNNER_TEMP/verified-npm-placeholder-publication" \
|
|
--producer-run-attempt "${{ needs.plan.outputs.producer_run_attempt }}" \
|
|
--repository "$GITHUB_REPOSITORY" \
|
|
--run-id "$GITHUB_RUN_ID" \
|
|
--target-root "$GITHUB_WORKSPACE/target-source" \
|
|
--target-sha "$TARGET_SHA" \
|
|
--workflow-sha "$WORKFLOW_SHA"
|
|
|
|
- name: Record secretless verification
|
|
run: |
|
|
{
|
|
echo "## NPM placeholder verification"
|
|
echo
|
|
echo "- Immutable artifact: **verified**"
|
|
echo "- Credentials: **not available to this job**"
|
|
echo "- Publication: **not attempted**"
|
|
} >> "$GITHUB_STEP_SUMMARY"
|
|
|
|
publish:
|
|
name: Publish npm placeholders
|
|
needs: [plan, verify]
|
|
if: inputs.dry_run != true
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 30
|
|
environment: npm-release
|
|
permissions:
|
|
actions: read
|
|
contents: read
|
|
id-token: write
|
|
steps:
|
|
- name: Checkout trusted publication tooling
|
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
with:
|
|
persist-credentials: false
|
|
ref: ${{ github.workflow_sha }}
|
|
fetch-depth: 1
|
|
|
|
- name: Checkout bound target manifests
|
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
with:
|
|
persist-credentials: false
|
|
ref: ${{ needs.plan.outputs.target_sha }}
|
|
path: target-source
|
|
fetch-depth: 1
|
|
|
|
- name: Setup trusted Node
|
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
with:
|
|
node-version: ${{ env.NODE_VERSION }}
|
|
|
|
- name: Resolve immutable artifact metadata
|
|
id: artifact
|
|
env:
|
|
ARTIFACT_DIGEST: ${{ needs.plan.outputs.artifact_digest }}
|
|
ARTIFACT_ID: ${{ needs.plan.outputs.artifact_id }}
|
|
ARTIFACT_NAME: ${{ needs.plan.outputs.artifact_name }}
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: |
|
|
set -euo pipefail
|
|
metadata="$RUNNER_TEMP/npm-placeholder-artifact.json"
|
|
gh api "repos/${GITHUB_REPOSITORY}/actions/artifacts/${ARTIFACT_ID}" > "$metadata"
|
|
jq -e \
|
|
--arg digest "$ARTIFACT_DIGEST" \
|
|
--arg name "$ARTIFACT_NAME" \
|
|
--argjson id "$ARTIFACT_ID" \
|
|
'.id == $id and .name == $name and .digest == $digest and .expired == false' \
|
|
"$metadata" >/dev/null || {
|
|
echo "NPM placeholder artifact metadata does not match the plan output." >&2
|
|
exit 1
|
|
}
|
|
echo "size_bytes=$(jq -er '.size_in_bytes' "$metadata")" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Verify immutable placeholder publication
|
|
id: verify
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
TARGET_SHA: ${{ needs.plan.outputs.target_sha }}
|
|
WORKFLOW_SHA: ${{ github.workflow_sha }}
|
|
run: |
|
|
set -euo pipefail
|
|
node scripts/npm-placeholder-publication.mjs verify-artifact \
|
|
--artifact-digest "${{ needs.plan.outputs.artifact_digest }}" \
|
|
--artifact-id "${{ needs.plan.outputs.artifact_id }}" \
|
|
--artifact-name "${{ needs.plan.outputs.artifact_name }}" \
|
|
--artifact-size-bytes "${{ steps.artifact.outputs.size_bytes }}" \
|
|
--consumer-run-attempt "$GITHUB_RUN_ATTEMPT" \
|
|
--output-dir "$RUNNER_TEMP/verified-npm-placeholder-publication" \
|
|
--producer-run-attempt "${{ needs.plan.outputs.producer_run_attempt }}" \
|
|
--repository "$GITHUB_REPOSITORY" \
|
|
--run-id "$GITHUB_RUN_ID" \
|
|
--target-root "$GITHUB_WORKSPACE/target-source" \
|
|
--target-sha "$TARGET_SHA" \
|
|
--workflow-sha "$WORKFLOW_SHA" \
|
|
--github-output "$GITHUB_OUTPUT"
|
|
|
|
- name: Publish verified placeholders serially
|
|
env:
|
|
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
TARGET_SHA: ${{ needs.plan.outputs.target_sha }}
|
|
WORKFLOW_SHA: ${{ github.workflow_sha }}
|
|
run: |
|
|
set -euo pipefail
|
|
[[ -n "${NPM_TOKEN// }" ]] || {
|
|
echo "NPM placeholder publication requires the protected npm release token." >&2
|
|
exit 1
|
|
}
|
|
node scripts/npm-placeholder-publication.mjs publish \
|
|
--artifact-dir "$RUNNER_TEMP/verified-npm-placeholder-publication" \
|
|
--target-sha "$TARGET_SHA" \
|
|
--workflow-sha "$WORKFLOW_SHA" \
|
|
--result-path "$RUNNER_TEMP/npm-placeholder-results.json"
|
|
|
|
- name: Record publication and trusted-publisher follow-up
|
|
run: |
|
|
set -euo pipefail
|
|
results="$RUNNER_TEMP/npm-placeholder-results.json"
|
|
{
|
|
echo "## NPM placeholder publication"
|
|
echo
|
|
jq -r '.results[] | "- `\(.packageName)`: `\(.action)`"' "$results"
|
|
echo
|
|
echo "### Newly created npm packages"
|
|
created="$(jq -r '[.results[] | select(.newPackage == true) | .packageName] | .[]' "$results")"
|
|
if [[ -n "$created" ]]; then
|
|
while IFS= read -r package_name; do
|
|
echo "- \`${package_name}\`"
|
|
done <<< "$created"
|
|
echo
|
|
echo "Configure each package's trusted publisher for repository \`openclaw/openclaw\`, workflow \`plugin-npm-release.yml\`, and environment \`npm-release\`."
|
|
else
|
|
echo "- None."
|
|
fi
|
|
echo
|
|
echo "### Existing-package trust inspection"
|
|
existing="$(jq -r '[.results[] | select(.newPackage == false) | .packageName] | .[]' "$results")"
|
|
if [[ -n "$existing" ]]; then
|
|
while IFS= read -r package_name; do
|
|
echo "- Inspect \`${package_name}\` trusted-publisher configuration for \`plugin-npm-release.yml\` / \`npm-release\`."
|
|
done <<< "$existing"
|
|
else
|
|
echo "- None."
|
|
fi
|
|
} >> "$GITHUB_STEP_SUMMARY"
|