mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(release): unblock Meta npm bootstrap publication (#103951)
* fix(release): isolate Meta npm bootstrap * test(release): assert OIDC npm version guard
This commit is contained in:
@@ -12,7 +12,9 @@ on:
|
||||
- "scripts/lib/npm-publish-plan.mjs"
|
||||
- "scripts/lib/plugin-npm-package-manifest.mjs"
|
||||
- "scripts/lib/plugin-npm-release.ts"
|
||||
- "scripts/lib/actions-artifact-archive.mjs"
|
||||
- "scripts/plugin-npm-publish.sh"
|
||||
- "scripts/plugin-publication-artifact.mjs"
|
||||
- "scripts/plugin-npm-release-check.ts"
|
||||
- "scripts/plugin-npm-release-plan.ts"
|
||||
- "scripts/verify-plugin-npm-published-runtime.mjs"
|
||||
@@ -342,7 +344,6 @@ jobs:
|
||||
|
||||
- name: Prepare immutable npm preflight artifact
|
||||
id: preflight_artifact
|
||||
if: ${{ github.event_name == 'workflow_dispatch' && inputs.preflight_only }}
|
||||
env:
|
||||
ARTIFACT_NAME: plugin-npm-package-source-${{ needs.preview_plugins_npm.outputs.ref_revision }}-${{ matrix.plugin.extensionId }}
|
||||
EXTENSION_ID: ${{ matrix.plugin.extensionId }}
|
||||
@@ -352,7 +353,7 @@ jobs:
|
||||
PACKAGE_VERSION: ${{ matrix.plugin.version }}
|
||||
PUBLISH_TAG: ${{ matrix.plugin.publishTag }}
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
SOURCE_REF: ${{ inputs.ref }}
|
||||
SOURCE_REF: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.sha }}
|
||||
SOURCE_SHA: ${{ needs.preview_plugins_npm.outputs.ref_revision }}
|
||||
WORKFLOW_PATH: .github/workflows/plugin-npm-release.yml
|
||||
WORKFLOW_REF: ${{ github.workflow_ref }}
|
||||
@@ -523,17 +524,16 @@ jobs:
|
||||
|
||||
- name: Upload immutable npm preflight artifact
|
||||
id: upload_preflight_artifact
|
||||
if: ${{ github.event_name == 'workflow_dispatch' && inputs.preflight_only }}
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: ${{ steps.preflight_artifact.outputs.name }}
|
||||
path: ${{ steps.preflight_artifact.outputs.dir }}
|
||||
compression-level: 0
|
||||
if-no-files-found: error
|
||||
overwrite: true
|
||||
retention-days: 30
|
||||
|
||||
- name: Record npm preflight artifact attestation
|
||||
if: ${{ github.event_name == 'workflow_dispatch' && inputs.preflight_only }}
|
||||
env:
|
||||
ARTIFACT_DIGEST: ${{ steps.upload_preflight_artifact.outputs.artifact-digest }}
|
||||
ARTIFACT_NAME: ${{ steps.preflight_artifact.outputs.name }}
|
||||
@@ -548,7 +548,7 @@ jobs:
|
||||
verify_plugin_npm_preflight:
|
||||
name: Preflight plugin npm package (${{ matrix.plugin.packageName }})
|
||||
needs: [preview_plugins_npm, preview_plugin_pack]
|
||||
if: ${{ github.event_name == 'workflow_dispatch' && inputs.preflight_only && needs.preview_plugins_npm.outputs.has_selection == 'true' }}
|
||||
if: ${{ github.event_name == 'workflow_dispatch' && ((inputs.preflight_only && needs.preview_plugins_npm.outputs.has_selection == 'true') || (!inputs.preflight_only && needs.preview_plugins_npm.outputs.has_candidates == 'true')) }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
@@ -556,7 +556,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
plugin: ${{ fromJson(needs.preview_plugins_npm.outputs.all_matrix) }}
|
||||
plugin: ${{ fromJson(inputs.preflight_only && needs.preview_plugins_npm.outputs.all_matrix || needs.preview_plugins_npm.outputs.matrix) }}
|
||||
steps:
|
||||
- name: Checkout trusted npm preflight tooling
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
@@ -593,10 +593,6 @@ jobs:
|
||||
RUN_ATTEMPT: ${{ github.run_attempt }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
[[ "${RUN_ATTEMPT}" == "1" ]] || {
|
||||
echo "Plugin npm preflight requires a fresh attempt-1 workflow run." >&2
|
||||
exit 1
|
||||
}
|
||||
git fetch --no-tags --depth=1 origin "${SOURCE_SHA}"
|
||||
source_package_json="${RUNNER_TEMP}/${EXTENSION_ID}-source-package.json"
|
||||
git show "${SOURCE_SHA}:${PACKAGE_DIR}/package.json" > "${source_package_json}"
|
||||
@@ -895,100 +891,54 @@ jobs:
|
||||
console.log(`route=${observations[0]}`);
|
||||
NODE
|
||||
|
||||
- name: Record validation-only result
|
||||
- name: Create immutable plugin npm publication evidence
|
||||
id: preflight_evidence
|
||||
env:
|
||||
ARTIFACT_NAME_PREFIX: plugin-npm-package-${{ matrix.plugin.extensionId }}-${{ matrix.plugin.version }}
|
||||
EXTENSION_ID: ${{ matrix.plugin.extensionId }}
|
||||
PACKAGE_DIR: ${{ matrix.plugin.packageDir }}
|
||||
PACKAGE_NAME: ${{ matrix.plugin.packageName }}
|
||||
PACKED_PACKAGE_JSON_SHA256: ${{ steps.publication_artifact.outputs.package_json_sha256 }}
|
||||
PACKAGE_VERSION: ${{ matrix.plugin.version }}
|
||||
PUBLICATION_ARTIFACT_DIGEST: ${{ steps.publication_artifact.outputs.artifact_digest }}
|
||||
PUBLICATION_ARTIFACT_ID: ${{ steps.publication_artifact.outputs.artifact_id }}
|
||||
PUBLICATION_ARTIFACT_NAME: ${{ steps.publication_artifact.outputs.artifact_name }}
|
||||
PUBLISH_TAG: ${{ matrix.plugin.publishTag }}
|
||||
PUBLISH_ROUTE: ${{ steps.publication_route.outputs.route }}
|
||||
SOURCE_PACKAGE_JSON_SHA256: ${{ steps.publication_artifact.outputs.source_package_json_sha256 }}
|
||||
TARBALL_NAME: ${{ steps.publication_artifact.outputs.tarball_name }}
|
||||
TARBALL_PATH: ${{ steps.publication_artifact.outputs.tarball_path }}
|
||||
TARBALL_SHA256: ${{ steps.publication_artifact.outputs.tarball_sha256 }}
|
||||
TARGET_SHA: ${{ needs.preview_plugins_npm.outputs.ref_revision }}
|
||||
WORKFLOW_SHA: ${{ github.workflow_sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
output_dir="${RUNNER_TEMP}/plugin-npm-preflight/evidence"
|
||||
rm -rf "$output_dir"
|
||||
install -d -m 0700 "$output_dir"
|
||||
install -m 0600 "$TARBALL_PATH" "$output_dir/$TARBALL_NAME"
|
||||
tarball_size="$(stat -c %s "$output_dir/$TARBALL_NAME")"
|
||||
EVIDENCE_PATH="$output_dir/plugin-npm-package-evidence.json" \
|
||||
TARBALL_SIZE="$tarball_size" \
|
||||
node --input-type=module <<'NODE'
|
||||
import { writeFileSync } from "node:fs";
|
||||
|
||||
if (
|
||||
!/^[1-9][0-9]*$/u.test(process.env.PUBLICATION_ARTIFACT_ID ?? "") ||
|
||||
!process.env.PUBLICATION_ARTIFACT_NAME ||
|
||||
!/^sha256:[0-9a-f]{64}$/u.test(process.env.PUBLICATION_ARTIFACT_DIGEST ?? "") ||
|
||||
!/^[0-9a-f]{64}$/u.test(process.env.PACKED_PACKAGE_JSON_SHA256 ?? "") ||
|
||||
!/^[0-9a-f]{64}$/u.test(process.env.SOURCE_PACKAGE_JSON_SHA256 ?? "") ||
|
||||
!/^[0-9a-f]{64}$/u.test(process.env.TARBALL_SHA256 ?? "") ||
|
||||
!/^npm-(?:oidc|token-bootstrap|mirror|tag-repair|readback)$/u.test(
|
||||
process.env.PUBLISH_ROUTE ?? "",
|
||||
)
|
||||
) {
|
||||
throw new Error("Plugin npm preflight evidence is missing its canonical artifact tuple.");
|
||||
}
|
||||
const evidence = {
|
||||
schema: "openclaw.plugin-npm-package-evidence/v2",
|
||||
schemaVersion: 2,
|
||||
workflowPath: ".github/workflows/plugin-npm-release.yml",
|
||||
workflowSha: process.env.WORKFLOW_SHA,
|
||||
runId: Number(process.env.GITHUB_RUN_ID),
|
||||
runAttempt: Number(process.env.GITHUB_RUN_ATTEMPT),
|
||||
targetSha: process.env.TARGET_SHA,
|
||||
publicationPerformed: false,
|
||||
publication: {
|
||||
route: process.env.PUBLISH_ROUTE,
|
||||
},
|
||||
publicationArtifact: {
|
||||
id: Number(process.env.PUBLICATION_ARTIFACT_ID),
|
||||
name: process.env.PUBLICATION_ARTIFACT_NAME,
|
||||
digest: process.env.PUBLICATION_ARTIFACT_DIGEST,
|
||||
packageJsonSha256: process.env.PACKED_PACKAGE_JSON_SHA256,
|
||||
sourcePackageJsonSha256: process.env.SOURCE_PACKAGE_JSON_SHA256,
|
||||
tarballSha256: process.env.TARBALL_SHA256,
|
||||
},
|
||||
package: {
|
||||
extensionId: process.env.EXTENSION_ID,
|
||||
packageDir: process.env.PACKAGE_DIR,
|
||||
name: process.env.PACKAGE_NAME,
|
||||
version: process.env.PACKAGE_VERSION,
|
||||
publishTag: process.env.PUBLISH_TAG,
|
||||
},
|
||||
tarball: {
|
||||
name: process.env.TARBALL_NAME,
|
||||
sha256: process.env.TARBALL_SHA256,
|
||||
size: Number(process.env.TARBALL_SIZE),
|
||||
},
|
||||
conclusion: "success",
|
||||
};
|
||||
writeFileSync(process.env.EVIDENCE_PATH, `${JSON.stringify(evidence, null, 2)}\n`, {
|
||||
encoding: "utf8",
|
||||
mode: 0o600,
|
||||
});
|
||||
NODE
|
||||
artifact_name="${ARTIFACT_NAME_PREFIX}-${PUBLISH_ROUTE}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
|
||||
policy_sha256="$(sha256sum .github/workflows/plugin-npm-release.yml | awk '{print $1}')"
|
||||
node scripts/plugin-publication-artifact.mjs create \
|
||||
--artifact-dir "$output_dir" \
|
||||
--artifact-name "$artifact_name" \
|
||||
--package-dir "$PACKAGE_DIR" \
|
||||
--package-name "$PACKAGE_NAME" \
|
||||
--package-version "$PACKAGE_VERSION" \
|
||||
--publication-reason "Stable npm registry preflight selected ${PUBLISH_ROUTE}." \
|
||||
--publish-tag "$PUBLISH_TAG" \
|
||||
--publisher-policy-id plugin-npm-release-workflow \
|
||||
--publisher-policy-schema openclaw.plugin-npm-publisher-policy/v1 \
|
||||
--publisher-policy-sha256 "$policy_sha256" \
|
||||
--route "$PUBLISH_ROUTE" \
|
||||
--source-package-json-sha256 "$SOURCE_PACKAGE_JSON_SHA256" \
|
||||
--target-sha "$TARGET_SHA"
|
||||
evidence_count="$(find "$output_dir" -maxdepth 1 -type f | wc -l | tr -d ' ')"
|
||||
[[ "$evidence_count" == "2" ]] || {
|
||||
echo "Plugin npm preflight evidence must contain exactly two files." >&2
|
||||
exit 1
|
||||
}
|
||||
echo "artifact_name=$artifact_name" >> "$GITHUB_OUTPUT"
|
||||
echo "artifact_path=$output_dir" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Upload immutable plugin npm preflight evidence
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: plugin-npm-package-${{ matrix.plugin.extensionId }}-${{ matrix.plugin.version }}
|
||||
name: ${{ steps.preflight_evidence.outputs.artifact_name }}
|
||||
path: ${{ steps.preflight_evidence.outputs.artifact_path }}/*
|
||||
if-no-files-found: error
|
||||
retention-days: 30
|
||||
@@ -1010,7 +960,13 @@ jobs:
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
publish_plugins_npm:
|
||||
needs: [preview_plugins_npm, preview_plugin_pack, validate_release_publish_approval]
|
||||
needs:
|
||||
[
|
||||
preview_plugins_npm,
|
||||
preview_plugin_pack,
|
||||
verify_plugin_npm_preflight,
|
||||
validate_release_publish_approval,
|
||||
]
|
||||
if: github.event_name == 'workflow_dispatch' && !inputs.preflight_only && needs.preview_plugins_npm.outputs.has_candidates == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
environment: npm-release
|
||||
@@ -1023,21 +979,206 @@ jobs:
|
||||
matrix:
|
||||
plugin: ${{ fromJson(needs.preview_plugins_npm.outputs.matrix) }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
- name: Checkout trusted publication tooling
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: ${{ github.workflow_sha }}
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Setup trusted Node
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Consume immutable npm publication evidence
|
||||
id: publication_evidence
|
||||
env:
|
||||
ARTIFACT_NAME_PREFIX: plugin-npm-package-${{ matrix.plugin.extensionId }}-${{ matrix.plugin.version }}
|
||||
EXTENSION_ID: ${{ matrix.plugin.extensionId }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
PACKAGE_DIR: ${{ matrix.plugin.packageDir }}
|
||||
PACKAGE_NAME: ${{ matrix.plugin.packageName }}
|
||||
PACKAGE_VERSION: ${{ matrix.plugin.version }}
|
||||
PUBLISH_TAG: ${{ matrix.plugin.publishTag }}
|
||||
TARGET_SHA: ${{ needs.preview_plugins_npm.outputs.ref_revision }}
|
||||
WORKFLOW_HEAD_BRANCH: ${{ github.ref_name }}
|
||||
WORKFLOW_REF: ${{ github.ref }}
|
||||
WORKFLOW_SHA: ${{ github.workflow_sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
artifacts_json="${RUNNER_TEMP}/${EXTENSION_ID}-publication-artifacts.json"
|
||||
gh api --paginate \
|
||||
"repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/artifacts?per_page=100" |
|
||||
jq -s '{artifacts: [.[].artifacts[]]}' > "$artifacts_json"
|
||||
jq \
|
||||
--arg prefix "${ARTIFACT_NAME_PREFIX}-" \
|
||||
--arg run_id "$GITHUB_RUN_ID" \
|
||||
--argjson consumer_attempt "$GITHUB_RUN_ATTEMPT" \
|
||||
'[
|
||||
.artifacts[]
|
||||
| select(.expired == false and (.name | startswith($prefix)))
|
||||
| (
|
||||
try (
|
||||
.name
|
||||
| ltrimstr($prefix)
|
||||
| capture("^(?<route>npm-(?:oidc|token-bootstrap|mirror|tag-repair|readback))-(?<run_id>[1-9][0-9]*)-(?<attempt>[1-9][0-9]*)$")
|
||||
) catch null
|
||||
) as $binding
|
||||
| select(
|
||||
$binding != null and
|
||||
$binding.run_id == $run_id and
|
||||
($binding.attempt | tonumber) <= $consumer_attempt
|
||||
)
|
||||
| . + {
|
||||
producer_attempt: ($binding.attempt | tonumber),
|
||||
publish_route: $binding.route
|
||||
}
|
||||
]
|
||||
| sort_by(.producer_attempt)
|
||||
| if length == 0 then
|
||||
[]
|
||||
else
|
||||
(last.producer_attempt) as $latest_attempt
|
||||
| map(select(.producer_attempt == $latest_attempt))
|
||||
end' \
|
||||
"$artifacts_json" > "${RUNNER_TEMP}/${EXTENSION_ID}-publication-matches.json"
|
||||
artifact_count="$(jq 'length' "${RUNNER_TEMP}/${EXTENSION_ID}-publication-matches.json")"
|
||||
[[ "$artifact_count" == "1" ]] || {
|
||||
echo "Expected one current or prior immutable publication artifact for ${PACKAGE_NAME}@${PACKAGE_VERSION}; found ${artifact_count}." >&2
|
||||
exit 1
|
||||
}
|
||||
artifact_metadata="${RUNNER_TEMP}/${EXTENSION_ID}-publication-artifact.json"
|
||||
jq '.[0] | del(.producer_attempt, .publish_route)' \
|
||||
"${RUNNER_TEMP}/${EXTENSION_ID}-publication-matches.json" > "$artifact_metadata"
|
||||
artifact_name="$(jq -er '.name' "$artifact_metadata")"
|
||||
publish_route="$(jq -er '.[0].publish_route' "${RUNNER_TEMP}/${EXTENSION_ID}-publication-matches.json")"
|
||||
producer_attempt="$(jq -er '.[0].producer_attempt' "${RUNNER_TEMP}/${EXTENSION_ID}-publication-matches.json")"
|
||||
[[ "$publish_route" =~ ^npm-(oidc|token-bootstrap|mirror|tag-repair|readback)$ ]] || {
|
||||
echo "Unsupported immutable npm publication route: ${publish_route}." >&2
|
||||
exit 1
|
||||
}
|
||||
if [[ "$publish_route" == "npm-token-bootstrap" ]]; then
|
||||
[[ "$WORKFLOW_REF" == "refs/heads/main" ]] || {
|
||||
echo "npm token bootstrap requires the trusted main plugin release workflow." >&2
|
||||
exit 1
|
||||
}
|
||||
git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
|
||||
git merge-base --is-ancestor "$WORKFLOW_SHA" origin/main || {
|
||||
echo "npm token bootstrap workflow revision is not reachable from main." >&2
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
artifact_id="$(jq -er '.id' "$artifact_metadata")"
|
||||
artifact_digest="$(jq -er '.digest' "$artifact_metadata")"
|
||||
artifact_size="$(jq -er '.size_in_bytes' "$artifact_metadata")"
|
||||
[[ "$artifact_id" =~ ^[1-9][0-9]*$ && "$artifact_digest" =~ ^sha256:[0-9a-f]{64}$ && "$artifact_size" =~ ^[1-9][0-9]*$ ]] || {
|
||||
echo "Immutable npm publication artifact metadata is invalid." >&2
|
||||
exit 1
|
||||
}
|
||||
(( artifact_size <= 268435456 )) || {
|
||||
echo "Immutable npm publication artifact exceeds 256 MiB." >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
workflow_run="${RUNNER_TEMP}/${EXTENSION_ID}-publication-run.json"
|
||||
workflow_jobs="${RUNNER_TEMP}/${EXTENSION_ID}-publication-jobs.json"
|
||||
artifact_zip="${RUNNER_TEMP}/${EXTENSION_ID}-publication.zip"
|
||||
gh api \
|
||||
"repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/attempts/${producer_attempt}" \
|
||||
> "$workflow_run"
|
||||
gh api --paginate \
|
||||
"repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/attempts/${producer_attempt}/jobs?per_page=100" |
|
||||
jq -s '{total_count: ([.[].jobs[]] | length), jobs: [.[].jobs[]]}' > "$workflow_jobs"
|
||||
curl --fail --location --silent --show-error \
|
||||
--retry 2 \
|
||||
--retry-all-errors \
|
||||
--max-time 120 \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer ${GH_TOKEN}" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
"https://api.github.com/repos/${GITHUB_REPOSITORY}/actions/artifacts/${artifact_id}/zip" \
|
||||
-o "$artifact_zip"
|
||||
|
||||
git fetch --no-tags --depth=1 origin "$TARGET_SHA"
|
||||
source_package_json="${RUNNER_TEMP}/${EXTENSION_ID}-source-package.json"
|
||||
git show "${TARGET_SHA}:${PACKAGE_DIR}/package.json" > "$source_package_json"
|
||||
source_package_json_sha256="$(sha256sum "$source_package_json" | awk '{print $1}')"
|
||||
policy_sha256="$(sha256sum .github/workflows/plugin-npm-release.yml | awk '{print $1}')"
|
||||
output_dir="${RUNNER_TEMP}/${EXTENSION_ID}-verified-publication"
|
||||
node scripts/plugin-publication-artifact.mjs verify \
|
||||
--artifact-digest "$artifact_digest" \
|
||||
--artifact-id "$artifact_id" \
|
||||
--artifact-metadata "$artifact_metadata" \
|
||||
--artifact-name "$artifact_name" \
|
||||
--artifact-size-bytes "$artifact_size" \
|
||||
--artifact-zip "$artifact_zip" \
|
||||
--consumer-run-attempt "$GITHUB_RUN_ATTEMPT" \
|
||||
--github-output "$GITHUB_OUTPUT" \
|
||||
--output-dir "$output_dir" \
|
||||
--package-dir "$PACKAGE_DIR" \
|
||||
--package-name "$PACKAGE_NAME" \
|
||||
--package-version "$PACKAGE_VERSION" \
|
||||
--producer-job-name "Preflight plugin npm package (${PACKAGE_NAME})" \
|
||||
--producer-run-attempt "$producer_attempt" \
|
||||
--producer-run-id "$GITHUB_RUN_ID" \
|
||||
--publication-reason "Stable npm registry preflight selected ${publish_route}." \
|
||||
--publish-tag "$PUBLISH_TAG" \
|
||||
--publisher-policy-id plugin-npm-release-workflow \
|
||||
--publisher-policy-schema openclaw.plugin-npm-publisher-policy/v1 \
|
||||
--publisher-policy-sha256 "$policy_sha256" \
|
||||
--repository "$GITHUB_REPOSITORY" \
|
||||
--route "$publish_route" \
|
||||
--run-state-policy same-run-producer-success \
|
||||
--source-package-json-sha256 "$source_package_json_sha256" \
|
||||
--target-sha "$TARGET_SHA" \
|
||||
--workflow-event workflow_dispatch \
|
||||
--workflow-head-branch "$WORKFLOW_HEAD_BRANCH" \
|
||||
--workflow-jobs-metadata "$workflow_jobs" \
|
||||
--workflow-path .github/workflows/plugin-npm-release.yml \
|
||||
--workflow-run-metadata "$workflow_run" \
|
||||
--workflow-sha "$WORKFLOW_SHA"
|
||||
|
||||
- name: Authorize immutable publication route
|
||||
env:
|
||||
PACKAGE_NAME: ${{ matrix.plugin.packageName }}
|
||||
PUBLISH_ROUTE: ${{ steps.publication_evidence.outputs.publish_route }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
case "$PUBLISH_ROUTE" in
|
||||
npm-oidc | npm-token-bootstrap | npm-readback)
|
||||
;;
|
||||
*)
|
||||
echo "${PACKAGE_NAME}: route ${PUBLISH_ROUTE} requires credential-isolated repair tooling, not this publish path." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
- name: Setup trusted publication dependencies
|
||||
if: steps.publication_evidence.outputs.publish_route == 'npm-token-bootstrap' || steps.publication_evidence.outputs.publish_route == 'npm-readback'
|
||||
uses: ./.github/actions/setup-node-env
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
install-bun: "false"
|
||||
|
||||
- name: Checkout OIDC publication target
|
||||
if: steps.publication_evidence.outputs.publish_route == 'npm-oidc'
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: ${{ needs.preview_plugins_npm.outputs.ref_revision }}
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Setup Node environment
|
||||
- name: Setup OIDC publication target
|
||||
if: steps.publication_evidence.outputs.publish_route == 'npm-oidc'
|
||||
uses: ./.github/actions/setup-node-env
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
install-bun: "false"
|
||||
|
||||
- name: Check npm package version
|
||||
- name: Check OIDC npm package version
|
||||
id: npm_package_version
|
||||
if: steps.publication_evidence.outputs.publish_route == 'npm-oidc'
|
||||
env:
|
||||
PACKAGE_NAME: ${{ matrix.plugin.packageName }}
|
||||
PACKAGE_VERSION: ${{ matrix.plugin.version }}
|
||||
@@ -1050,21 +1191,93 @@ jobs:
|
||||
echo "already_published=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Publish
|
||||
if: steps.npm_package_version.outputs.already_published != 'true'
|
||||
- name: Publish with trusted publisher
|
||||
if: steps.publication_evidence.outputs.publish_route == 'npm-oidc' && steps.npm_package_version.outputs.already_published != 'true'
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ inputs.npm_dist_tag != 'extended-stable' && secrets.NPM_TOKEN || '' }}
|
||||
NPM_TOKEN: ${{ inputs.npm_dist_tag != 'extended-stable' && secrets.NPM_TOKEN || '' }}
|
||||
OPENCLAW_NPM_PUBLISH_AUTH_MODE: trusted-publisher
|
||||
OPENCLAW_PLUGIN_NPM_PUBLISH_TAG: ${{ inputs.npm_dist_tag == 'extended-stable' && inputs.npm_dist_tag || '' }}
|
||||
run: bash scripts/plugin-npm-publish.sh --publish "${{ matrix.plugin.packageDir }}"
|
||||
|
||||
- name: Verify published runtime
|
||||
- name: Verify OIDC published runtime
|
||||
if: steps.publication_evidence.outputs.publish_route == 'npm-oidc'
|
||||
env:
|
||||
PACKAGE_NAME: ${{ matrix.plugin.packageName }}
|
||||
PACKAGE_VERSION: ${{ matrix.plugin.version }}
|
||||
run: node scripts/verify-plugin-npm-published-runtime.mjs "${PACKAGE_NAME}@${PACKAGE_VERSION}"
|
||||
|
||||
- name: Publish approved Meta bootstrap tarball
|
||||
if: steps.publication_evidence.outputs.publish_route == 'npm-token-bootstrap'
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
PACKAGE_DIR: ${{ matrix.plugin.packageDir }}
|
||||
PACKAGE_NAME: ${{ steps.publication_evidence.outputs.package_name }}
|
||||
PACKAGE_VERSION: ${{ steps.publication_evidence.outputs.package_version }}
|
||||
PUBLISH_TAG: ${{ steps.publication_evidence.outputs.publish_tag }}
|
||||
TARBALL_PATH: ${{ steps.publication_evidence.outputs.tarball_path }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
[[ "$PACKAGE_NAME" == "@openclaw/meta-provider" && "$PACKAGE_DIR" == "extensions/meta" ]] || {
|
||||
echo "npm token bootstrap is restricted to the approved Meta provider package." >&2
|
||||
exit 1
|
||||
}
|
||||
[[ "$PACKAGE_VERSION" == *"-beta."* && "$PUBLISH_TAG" == "beta" ]] || {
|
||||
echo "Meta npm token bootstrap requires an approved beta package and beta tag." >&2
|
||||
exit 1
|
||||
}
|
||||
[[ -n "${NPM_TOKEN// }" ]] || {
|
||||
echo "Meta npm token bootstrap requires the protected npm release token." >&2
|
||||
exit 1
|
||||
}
|
||||
publish_home="$(mktemp -d "${RUNNER_TEMP}/meta-npm-bootstrap.XXXXXX")"
|
||||
cleanup() {
|
||||
rm -rf "$publish_home"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
umask 077
|
||||
npmrc="$publish_home/npmrc"
|
||||
{
|
||||
echo "registry=https://registry.npmjs.org/"
|
||||
printf '//registry.npmjs.org/:_authToken=%s\n' "$NPM_TOKEN"
|
||||
} > "$npmrc"
|
||||
chmod 0600 "$npmrc"
|
||||
unset NODE_AUTH_TOKEN NPM_TOKEN NODE_OPTIONS
|
||||
HOME="$publish_home" \
|
||||
NPM_CONFIG_GLOBALCONFIG=/dev/null \
|
||||
NPM_CONFIG_IGNORE_SCRIPTS=true \
|
||||
NPM_CONFIG_REGISTRY=https://registry.npmjs.org/ \
|
||||
NPM_CONFIG_USERCONFIG="$npmrc" \
|
||||
npm publish "$TARBALL_PATH" \
|
||||
--access public \
|
||||
--ignore-scripts \
|
||||
--provenance \
|
||||
--tag "$PUBLISH_TAG"
|
||||
|
||||
- name: Verify Meta bootstrap published runtime
|
||||
if: steps.publication_evidence.outputs.publish_route == 'npm-token-bootstrap'
|
||||
env:
|
||||
PACKAGE_NAME: ${{ steps.publication_evidence.outputs.package_name }}
|
||||
PACKAGE_VERSION: ${{ steps.publication_evidence.outputs.package_version }}
|
||||
run: node scripts/verify-plugin-npm-published-runtime.mjs "${PACKAGE_NAME}@${PACKAGE_VERSION}"
|
||||
|
||||
- name: Verify immutable npm readback runtime
|
||||
if: steps.publication_evidence.outputs.publish_route == 'npm-readback'
|
||||
env:
|
||||
PACKAGE_NAME: ${{ steps.publication_evidence.outputs.package_name }}
|
||||
PACKAGE_VERSION: ${{ steps.publication_evidence.outputs.package_version }}
|
||||
run: node scripts/verify-plugin-npm-published-runtime.mjs "${PACKAGE_NAME}@${PACKAGE_VERSION}"
|
||||
|
||||
- name: Record Meta trusted publisher checkpoint
|
||||
if: steps.publication_evidence.outputs.publish_route == 'npm-token-bootstrap'
|
||||
env:
|
||||
PACKAGE_NAME: ${{ steps.publication_evidence.outputs.package_name }}
|
||||
run: |
|
||||
{
|
||||
echo "## Meta npm bootstrap follow-up"
|
||||
echo
|
||||
echo "- Published \`${PACKAGE_NAME}\` from the verified immutable tarball."
|
||||
echo "- Configure the GitHub trusted publisher for \`plugin-npm-release.yml\` and environment \`npm-release\` before the next OIDC publish."
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
verify_plugins_npm:
|
||||
needs: [preview_plugins_npm, publish_plugins_npm]
|
||||
if: ${{ always() && github.event_name == 'workflow_dispatch' && !inputs.preflight_only && inputs.npm_dist_tag == 'extended-stable' && needs.preview_plugins_npm.result == 'success' && (needs.publish_plugins_npm.result == 'success' || (needs.preview_plugins_npm.outputs.has_candidates == 'false' && needs.publish_plugins_npm.result == 'skipped')) }}
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
readPublicationArtifactArchive,
|
||||
sha256Digest,
|
||||
validateActionsArtifactBinding,
|
||||
validateActionsArtifactProducerJob,
|
||||
} from "./lib/actions-artifact-archive.mjs";
|
||||
import { resolveNpmPublishPlan } from "./lib/npm-publish-plan.mjs";
|
||||
|
||||
@@ -25,6 +26,7 @@ export {
|
||||
readBoundedRegularFile,
|
||||
readPublicationArtifactArchive,
|
||||
validateActionsArtifactBinding,
|
||||
validateActionsArtifactProducerJob,
|
||||
};
|
||||
|
||||
const MANIFEST_FILENAME = "plugin-publication-manifest.json";
|
||||
@@ -1018,6 +1020,8 @@ export function verifyPluginPublicationArtifact(params) {
|
||||
artifactId,
|
||||
artifactName: normalized.artifactName,
|
||||
artifactSizeBytes,
|
||||
consumerRunAttempt: params.consumerRunAttempt,
|
||||
producerJobName: params.producerJobName,
|
||||
repository: params.repository,
|
||||
runStatePolicy: params.runStatePolicy ?? "completed-success",
|
||||
runAttempt,
|
||||
@@ -1032,6 +1036,13 @@ export function verifyPluginPublicationArtifact(params) {
|
||||
expected: expectedBinding,
|
||||
workflowRun,
|
||||
});
|
||||
if (expectedBinding.runStatePolicy === "same-run-producer-success") {
|
||||
const workflowJobs = parseBoundedJsonFile(
|
||||
params.workflowJobsMetadataPath,
|
||||
"Actions workflow jobs metadata",
|
||||
);
|
||||
validateActionsArtifactProducerJob({ expected: expectedBinding, workflowJobs });
|
||||
}
|
||||
|
||||
const zipBytes = readBoundedRegularFile(params.artifactZipPath, {
|
||||
label: "Actions artifact ZIP",
|
||||
@@ -1230,12 +1241,16 @@ export function main(argv = process.argv.slice(2)) {
|
||||
: Number(values.expectedTarballSizeBytes),
|
||||
expectedTarballSha256: values.expectedTarballSha256,
|
||||
outputDir: values.outputDir,
|
||||
consumerRunAttempt:
|
||||
values.consumerRunAttempt === undefined ? undefined : Number(values.consumerRunAttempt),
|
||||
producerJobName: values.producerJobName,
|
||||
producerRunAttempt: Number(values.producerRunAttempt),
|
||||
producerRunId: Number(values.producerRunId),
|
||||
repository: values.repository,
|
||||
workflowEvent: values.workflowEvent,
|
||||
workflowHeadBranch: values.workflowHeadBranch,
|
||||
workflowPath: values.workflowPath,
|
||||
workflowJobsMetadataPath: values.workflowJobsMetadata,
|
||||
workflowRunMetadataPath: values.workflowRunMetadata,
|
||||
runStatePolicy: values.runStatePolicy,
|
||||
workflowSha: values.workflowSha,
|
||||
|
||||
@@ -29,6 +29,7 @@ const RELEASE_CHECKS_WORKFLOW = ".github/workflows/openclaw-release-checks.yml";
|
||||
const RELEASE_TELEGRAM_QA_WORKFLOW = ".github/workflows/openclaw-release-telegram-qa.yml";
|
||||
const RELEASE_PUBLISH_WORKFLOW = ".github/workflows/openclaw-release-publish.yml";
|
||||
const PLUGIN_CLAWHUB_RELEASE_WORKFLOW = ".github/workflows/plugin-clawhub-release.yml";
|
||||
const PLUGIN_NPM_RELEASE_WORKFLOW = ".github/workflows/plugin-npm-release.yml";
|
||||
const ANDROID_RELEASE_WORKFLOW = ".github/workflows/android-release.yml";
|
||||
const STABLE_MAIN_CLOSEOUT_WORKFLOW = ".github/workflows/openclaw-stable-main-closeout.yml";
|
||||
const WINDOWS_NODE_RELEASE_WORKFLOW = ".github/workflows/windows-node-release.yml";
|
||||
@@ -3142,7 +3143,7 @@ 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 pluginNpmWorkflow = readFileSync(PLUGIN_NPM_RELEASE_WORKFLOW, "utf8");
|
||||
const openclawNpmWorkflow = readFileSync(".github/workflows/openclaw-npm-release.yml", "utf8");
|
||||
const fastPretagScript = readFileSync("scripts/release-fast-pretag-check.sh", "utf8");
|
||||
const pluginPretagPackScript = readFileSync(
|
||||
@@ -3472,10 +3473,18 @@ describe("package artifact reuse", () => {
|
||||
expect(pluginNpmWorkflow).toContain("Validate release publish approval run");
|
||||
expect(clawHubWorkflow).toContain("Validate release publish approval run");
|
||||
expect(openclawNpmWorkflow).toContain("Validate release publish approval run");
|
||||
expect(pluginNpmWorkflow).toContain("Check npm package version");
|
||||
expect(pluginNpmWorkflow).toContain("already_published=true");
|
||||
expect(pluginNpmWorkflow).toContain(
|
||||
"steps.npm_package_version.outputs.already_published != 'true'",
|
||||
const pluginNpmPublishJob = workflowJob(PLUGIN_NPM_RELEASE_WORKFLOW, "publish_plugins_npm");
|
||||
const npmPackageVersionStep = workflowStep(
|
||||
pluginNpmPublishJob,
|
||||
"Check OIDC npm package version",
|
||||
);
|
||||
expect(npmPackageVersionStep).toMatchObject({
|
||||
id: "npm_package_version",
|
||||
if: "steps.publication_evidence.outputs.publish_route == 'npm-oidc'",
|
||||
});
|
||||
expect(npmPackageVersionStep.run).toContain("already_published=true");
|
||||
expect(workflowStep(pluginNpmPublishJob, "Publish with trusted publisher").if).toBe(
|
||||
"steps.publication_evidence.outputs.publish_route == 'npm-oidc' && steps.npm_package_version.outputs.already_published != 'true'",
|
||||
);
|
||||
expect(pluginNpmWorkflow).toContain("Direct Plugin NPM Release dispatch");
|
||||
expect(clawHubWorkflow).toContain("Direct Plugin ClawHub Release dispatch");
|
||||
@@ -3706,7 +3715,7 @@ wait_for_run plugin-clawhub-new.yml 123 || status=$?
|
||||
".github/workflows/macos-release.yml",
|
||||
".github/workflows/plugin-clawhub-release.yml",
|
||||
PACKAGE_ACCEPTANCE_WORKFLOW,
|
||||
".github/workflows/plugin-npm-release.yml",
|
||||
PLUGIN_NPM_RELEASE_WORKFLOW,
|
||||
];
|
||||
|
||||
for (const workflowPath of releaseWorkflowPaths) {
|
||||
|
||||
@@ -85,10 +85,18 @@ describe("plugin npm extended-stable workflow", () => {
|
||||
expect(raw.match(/--npm-dist-tag "\$\{NPM_DIST_TAG\}"/gu)).toHaveLength(2);
|
||||
const expectedOverride =
|
||||
"${{ inputs.npm_dist_tag == 'extended-stable' && inputs.npm_dist_tag || '' }}";
|
||||
for (const name of ["Preview publish command", "Preview npm pack contents", "Publish"]) {
|
||||
for (const name of [
|
||||
"Preview publish command",
|
||||
"Preview npm pack contents",
|
||||
"Publish with trusted publisher",
|
||||
]) {
|
||||
expect(
|
||||
step(
|
||||
parsed.jobs?.[name === "Publish" ? "publish_plugins_npm" : "preview_plugin_pack"],
|
||||
parsed.jobs?.[
|
||||
name === "Publish with trusted publisher"
|
||||
? "publish_plugins_npm"
|
||||
: "preview_plugin_pack"
|
||||
],
|
||||
name,
|
||||
).env,
|
||||
).toMatchObject({ OPENCLAW_PLUGIN_NPM_PUBLISH_TAG: expectedOverride });
|
||||
@@ -163,6 +171,7 @@ describe("plugin npm extended-stable workflow", () => {
|
||||
expect(prepare.env?.ARTIFACT_NAME).toBe(
|
||||
"plugin-npm-package-source-${{ needs.preview_plugins_npm.outputs.ref_revision }}-${{ matrix.plugin.extensionId }}",
|
||||
);
|
||||
expect(prepare.if).toBeUndefined();
|
||||
expect(prepare.run).toContain('bash scripts/plugin-npm-publish.sh --pack "${PACKAGE_DIR}"');
|
||||
expect(prepare.run).toContain('path.join(process.env.ARTIFACT_DIR, "preflight-manifest.json")');
|
||||
expect(prepare.run).toContain('kind: "openclaw-plugin-npm-preflight"');
|
||||
@@ -187,12 +196,14 @@ describe("plugin npm extended-stable workflow", () => {
|
||||
expect(upload.with).toMatchObject({
|
||||
"compression-level": 0,
|
||||
"if-no-files-found": "error",
|
||||
overwrite: true,
|
||||
"retention-days": 30,
|
||||
});
|
||||
|
||||
const verify = parsed.jobs?.verify_plugin_npm_preflight;
|
||||
expect(verify?.needs).toEqual(["preview_plugins_npm", "preview_plugin_pack"]);
|
||||
expect(verify?.strategy?.matrix?.plugin).toContain("all_matrix");
|
||||
expect(verify?.strategy?.matrix?.plugin).toContain("matrix");
|
||||
expect(verify?.name).toBe("Preflight plugin npm package (${{ matrix.plugin.packageName }})");
|
||||
const trustedCheckout = step(verify, "Checkout trusted npm preflight tooling");
|
||||
expect(trustedCheckout.with?.ref).toBe("${{ github.workflow_sha }}");
|
||||
@@ -240,21 +251,14 @@ describe("plugin npm extended-stable workflow", () => {
|
||||
expect(route.run).toContain('observations.push("npm-token-bootstrap")');
|
||||
expect(route.run).toContain('observations.push("npm-oidc")');
|
||||
|
||||
const evidence = step(verify, "Record validation-only result");
|
||||
const evidence = step(verify, "Create immutable plugin npm publication evidence");
|
||||
expect(evidence.env?.PUBLISH_ROUTE).toBe("${{ steps.publication_route.outputs.route }}");
|
||||
expect(evidence.run).toContain('schema: "openclaw.plugin-npm-package-evidence/v2"');
|
||||
expect(evidence.run).toContain("schemaVersion: 2");
|
||||
expect(evidence.run).toContain("packageJsonSha256: process.env.PACKED_PACKAGE_JSON_SHA256");
|
||||
expect(evidence.run).toContain(
|
||||
"sourcePackageJsonSha256: process.env.SOURCE_PACKAGE_JSON_SHA256",
|
||||
);
|
||||
expect(evidence.run).toContain("id: Number(process.env.PUBLICATION_ARTIFACT_ID)");
|
||||
expect(evidence.run).toContain("tarballSha256: process.env.TARBALL_SHA256");
|
||||
expect(evidence.run).toContain("tag-repair");
|
||||
expect(evidence.run).toContain("node scripts/plugin-publication-artifact.mjs create");
|
||||
expect(evidence.run).toContain("--publisher-policy-id plugin-npm-release-workflow");
|
||||
expect(evidence.run).toContain('--route "$PUBLISH_ROUTE"');
|
||||
expect(evidence.run).toContain('artifact_name="${ARTIFACT_NAME_PREFIX}-${PUBLISH_ROUTE}-');
|
||||
const evidenceUpload = step(verify, "Upload immutable plugin npm preflight evidence");
|
||||
expect(evidenceUpload.with?.name).toBe(
|
||||
"plugin-npm-package-${{ matrix.plugin.extensionId }}-${{ matrix.plugin.version }}",
|
||||
);
|
||||
expect(evidenceUpload.with?.name).toBe("${{ steps.preflight_evidence.outputs.artifact_name }}");
|
||||
expect(evidenceUpload.with?.path).toBe(
|
||||
"${{ steps.preflight_evidence.outputs.artifact_path }}/*",
|
||||
);
|
||||
@@ -280,7 +284,7 @@ describe("plugin npm extended-stable workflow", () => {
|
||||
expect(job?.permissions?.["id-token"], jobName).not.toBe("write");
|
||||
const serialized = JSON.stringify(job);
|
||||
expect(serialized, jobName).not.toContain("secrets.");
|
||||
expect(serialized, jobName).not.toContain("--publish");
|
||||
expect(serialized, jobName).not.toContain("plugin-npm-publish.sh --publish");
|
||||
expect(serialized, jobName).not.toMatch(/\bnpm publish\b/u);
|
||||
expect(serialized, jobName).not.toMatch(/\bnpm dist-tag\b/u);
|
||||
expect(serialized.replaceAll("clawHub: false", ""), jobName).not.toMatch(/\bclawhub\b/iu);
|
||||
@@ -308,14 +312,54 @@ describe("plugin npm extended-stable workflow", () => {
|
||||
|
||||
it("publishes extended-stable with OIDC only and verifies every package tag", () => {
|
||||
const parsed = workflow();
|
||||
const publish = step(parsed.jobs?.publish_plugins_npm, "Publish");
|
||||
const tokenExpression =
|
||||
"${{ inputs.npm_dist_tag != 'extended-stable' && secrets.NPM_TOKEN || '' }}";
|
||||
const publish = step(parsed.jobs?.publish_plugins_npm, "Publish with trusted publisher");
|
||||
expect(publish.env).toMatchObject({
|
||||
NODE_AUTH_TOKEN: tokenExpression,
|
||||
NPM_TOKEN: tokenExpression,
|
||||
OPENCLAW_NPM_PUBLISH_AUTH_MODE: "trusted-publisher",
|
||||
});
|
||||
expect(publish.env?.NODE_AUTH_TOKEN).toBeUndefined();
|
||||
expect(publish.env?.NPM_TOKEN).toBeUndefined();
|
||||
const bootstrap = step(
|
||||
parsed.jobs?.publish_plugins_npm,
|
||||
"Publish approved Meta bootstrap tarball",
|
||||
);
|
||||
expect(bootstrap.if).toContain("npm-token-bootstrap");
|
||||
expect(bootstrap.env?.NPM_TOKEN).toBe("${{ secrets.NPM_TOKEN }}");
|
||||
expect(bootstrap.run).toContain(
|
||||
'[[ "$PACKAGE_NAME" == "@openclaw/meta-provider" && "$PACKAGE_DIR" == "extensions/meta" ]]',
|
||||
);
|
||||
expect(bootstrap.run).toContain("NPM_CONFIG_USERCONFIG");
|
||||
expect(bootstrap.run).toContain("unset NODE_AUTH_TOKEN NPM_TOKEN NODE_OPTIONS");
|
||||
expect(bootstrap.run).toContain('npm publish "$TARBALL_PATH"');
|
||||
expect(bootstrap.run).toContain("--ignore-scripts");
|
||||
expect(bootstrap.run).not.toContain("bash scripts/plugin-npm-publish.sh");
|
||||
|
||||
const consume = step(
|
||||
parsed.jobs?.publish_plugins_npm,
|
||||
"Consume immutable npm publication evidence",
|
||||
);
|
||||
expect(consume.run).toContain("node scripts/plugin-publication-artifact.mjs verify");
|
||||
expect(consume.run).toContain("--run-state-policy same-run-producer-success");
|
||||
expect(consume.run).toContain("producer_attempt");
|
||||
expect(consume.run).toContain("last.producer_attempt");
|
||||
expect(consume.run).toContain(
|
||||
'--producer-job-name "Preflight plugin npm package (${PACKAGE_NAME})"',
|
||||
);
|
||||
expect(consume.run).toContain("--workflow-jobs-metadata");
|
||||
expect(consume.run).toContain("--source-package-json-sha256");
|
||||
expect(consume.run).toContain('[[ "$WORKFLOW_REF" == "refs/heads/main" ]]');
|
||||
expect(consume.run).toContain('git merge-base --is-ancestor "$WORKFLOW_SHA" origin/main');
|
||||
expect(
|
||||
step(parsed.jobs?.publish_plugins_npm, "Checkout trusted publication tooling").with?.ref,
|
||||
).toBe("${{ github.workflow_sha }}");
|
||||
expect(
|
||||
step(parsed.jobs?.publish_plugins_npm, "Setup trusted publication dependencies").if,
|
||||
).toContain("npm-token-bootstrap");
|
||||
expect(
|
||||
step(parsed.jobs?.publish_plugins_npm, "Setup trusted publication dependencies").if,
|
||||
).toContain("npm-readback");
|
||||
expect(step(parsed.jobs?.publish_plugins_npm, "Checkout OIDC publication target").if).toContain(
|
||||
"npm-oidc",
|
||||
);
|
||||
expect(parsed.jobs?.reconcile_plugins_npm).toBeUndefined();
|
||||
expect(readFileSync(workflowPath, "utf8")).not.toContain(
|
||||
'npm dist-tag add "${PACKAGE_NAME}@${PACKAGE_VERSION}" extended-stable',
|
||||
|
||||
@@ -29,6 +29,7 @@ const REPOSITORY = "openclaw/openclaw";
|
||||
const WORKFLOW_PATH = ".github/workflows/plugin-npm-release.yml";
|
||||
const ARTIFACT_NAME = "plugin-npm-package-meta-2026.7.1-beta.3";
|
||||
const PACKAGE_NAME = "@openclaw/meta-provider";
|
||||
const PRODUCER_JOB_NAME = `Preflight plugin npm package (${PACKAGE_NAME})`;
|
||||
const PACKAGE_VERSION = "2026.7.1-beta.3";
|
||||
const PACKAGE_DIR = "extensions/meta";
|
||||
const TARBALL_NAME = "openclaw-meta-provider-2026.7.1-beta.3.tgz";
|
||||
@@ -365,9 +366,26 @@ function createFixture(
|
||||
const zipPath = path.join(root, "artifact.zip");
|
||||
const metadataPath = path.join(root, "artifact.json");
|
||||
const workflowRunPath = path.join(root, "run.json");
|
||||
const workflowJobsPath = path.join(root, "jobs.json");
|
||||
writeFileSync(zipPath, zip);
|
||||
writeArtifactMetadata(metadataPath, zip);
|
||||
writeWorkflowRunMetadata(workflowRunPath);
|
||||
writeFileSync(
|
||||
workflowJobsPath,
|
||||
`${JSON.stringify({
|
||||
total_count: 1,
|
||||
jobs: [
|
||||
{
|
||||
name: PRODUCER_JOB_NAME,
|
||||
run_id: RUN_ID,
|
||||
run_attempt: RUN_ATTEMPT,
|
||||
head_sha: WORKFLOW_SHA,
|
||||
status: "completed",
|
||||
conclusion: "success",
|
||||
},
|
||||
],
|
||||
})}\n`,
|
||||
);
|
||||
return {
|
||||
artifactDir,
|
||||
created,
|
||||
@@ -378,6 +396,7 @@ function createFixture(
|
||||
root,
|
||||
tarball,
|
||||
workflowRunPath,
|
||||
workflowJobsPath,
|
||||
zip,
|
||||
zipPath,
|
||||
};
|
||||
@@ -697,6 +716,38 @@ describe("plugin publication artifact", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("accepts only the exact successful producer job for same-run publication", () => {
|
||||
const fixture = createFixture();
|
||||
const workflowRun = JSON.parse(readFileSync(fixture.workflowRunPath, "utf8"));
|
||||
workflowRun.status = "in_progress";
|
||||
workflowRun.conclusion = null;
|
||||
writeFileSync(fixture.workflowRunPath, `${JSON.stringify(workflowRun)}\n`);
|
||||
|
||||
expect(
|
||||
verifyFixture(fixture, {
|
||||
consumerRunAttempt: RUN_ATTEMPT,
|
||||
producerJobName: PRODUCER_JOB_NAME,
|
||||
runStatePolicy: "same-run-producer-success",
|
||||
workflowJobsMetadataPath: fixture.workflowJobsPath,
|
||||
}),
|
||||
).toMatchObject({ producerRunAttempt: RUN_ATTEMPT, producerRunId: RUN_ID });
|
||||
|
||||
const jobs = JSON.parse(readFileSync(fixture.workflowJobsPath, "utf8"));
|
||||
jobs.jobs[0].conclusion = "failure";
|
||||
writeFileSync(fixture.workflowJobsPath, `${JSON.stringify(jobs)}\n`);
|
||||
const failedFixture = createFixture();
|
||||
writeFileSync(failedFixture.workflowRunPath, `${JSON.stringify(workflowRun)}\n`);
|
||||
writeFileSync(failedFixture.workflowJobsPath, `${JSON.stringify(jobs)}\n`);
|
||||
expect(() =>
|
||||
verifyFixture(failedFixture, {
|
||||
consumerRunAttempt: RUN_ATTEMPT,
|
||||
producerJobName: PRODUCER_JOB_NAME,
|
||||
runStatePolicy: "same-run-producer-success",
|
||||
workflowJobsMetadataPath: failedFixture.workflowJobsPath,
|
||||
}),
|
||||
).toThrow("producer job did not complete successfully");
|
||||
});
|
||||
|
||||
it("retries bounded metadata, attempt, and archive failures against the exact run attempt", async () => {
|
||||
const zip = createZip([{ bytes: Buffer.from("proof"), name: "proof.txt" }]);
|
||||
const artifactMetadata = {
|
||||
|
||||
Reference in New Issue
Block a user