mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 11:55:47 -06:00
fix(release): bind npm publishers to live tooling
This commit is contained in:
@@ -1407,11 +1407,25 @@ jobs:
|
||||
id: publish
|
||||
env:
|
||||
BYPASS_EXTENDED_STABLE_GUARD: ${{ inputs.bypass_extended_stable_guard }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
OPENCLAW_PREPACK_PREPARED: "1"
|
||||
OPENCLAW_NPM_PUBLISH_TAG: ${{ inputs.npm_dist_tag }}
|
||||
PUBLISH_TARBALL_PATH: ${{ steps.preflight_provenance.outputs.tarball_path }}
|
||||
RELEASE_PUBLISH_RUN_ID: ${{ inputs.release_publish_run_id }}
|
||||
WORKFLOW_FULL_REF: ${{ github.ref }}
|
||||
WORKFLOW_REF: ${{ github.ref_name }}
|
||||
WORKFLOW_SHA: ${{ github.workflow_sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
verify_release_tooling_identity() {
|
||||
node trusted-workflow/scripts/release-tooling-identity.mjs verify \
|
||||
--repository "$GITHUB_REPOSITORY" \
|
||||
--workflow-ref "$WORKFLOW_REF" \
|
||||
--workflow-full-ref "$WORKFLOW_FULL_REF" \
|
||||
--workflow-sha "$WORKFLOW_SHA" \
|
||||
--release-publish-run-id "$RELEASE_PUBLISH_RUN_ID" \
|
||||
--allow-prevalidated-ref
|
||||
}
|
||||
publish_target="${PUBLISH_TARBALL_PATH}"
|
||||
if [[ -n "${publish_target}" ]]; then
|
||||
publish_target="./${publish_target}"
|
||||
@@ -1429,11 +1443,13 @@ jobs:
|
||||
echo "${package_name}@${package_version} is already published; reusing it."
|
||||
return 0
|
||||
fi
|
||||
verify_release_tooling_identity
|
||||
bash scripts/openclaw-npm-publish.sh --publish "./${tarball_path}"
|
||||
}
|
||||
while IFS=$'\t' read -r package_name tarball_name; do
|
||||
publish_if_missing "$package_name" "preflight-tarball/$tarball_name"
|
||||
done < <(jq -r '(.corePackageTarballs // [])[] | [.packageName, .tarballName] | @tsv' preflight-tarball/preflight-manifest.json)
|
||||
verify_release_tooling_identity
|
||||
bash scripts/openclaw-npm-publish.sh --publish "${publish_target}"
|
||||
|
||||
- name: Verify extended-stable registry readback
|
||||
|
||||
@@ -962,6 +962,7 @@ jobs:
|
||||
CHILD_WORKFLOW_REF: ${{ github.ref_name }}
|
||||
PARENT_WORKFLOW_SHA: ${{ github.sha }}
|
||||
PARENT_WORKFLOW_BRANCH: ${{ github.ref_name }}
|
||||
PARENT_WORKFLOW_FULL_REF: ${{ github.ref }}
|
||||
RELEASE_TAG: ${{ inputs.tag }}
|
||||
PREFLIGHT_RUN_ID: ${{ inputs.preflight_run_id }}
|
||||
FULL_RELEASE_VALIDATION_RUN_ID: ${{ inputs.full_release_validation_run_id }}
|
||||
@@ -2283,6 +2284,13 @@ jobs:
|
||||
if [[ "$(jq -r '.normal.shouldDispatch' "${clawhub_plan_path}")" == "true" ]]; then
|
||||
clawhub_dispatch_args=()
|
||||
append_clawhub_dispatch_args normal
|
||||
if [[ "${PARENT_WORKFLOW_FULL_REF}" == "refs/heads/main" ||
|
||||
"${PARENT_WORKFLOW_FULL_REF}" =~ ^refs/tags/release-publish/[a-f0-9]{12}-[1-9][0-9]*$ ]]; then
|
||||
clawhub_dispatch_args+=(
|
||||
-f release_publish_full_ref="${PARENT_WORKFLOW_FULL_REF}"
|
||||
-f release_publish_workflow_sha="${PARENT_WORKFLOW_SHA}"
|
||||
)
|
||||
fi
|
||||
plugin_clawhub_run_id="$(dispatch_workflow_at_ref \
|
||||
"$(jq -r '.normal.ref' "${clawhub_plan_path}")" \
|
||||
"${TARGET_SHA}" \
|
||||
|
||||
@@ -28,6 +28,16 @@ on:
|
||||
description: Branch name of the approving OpenClaw Release Publish workflow run
|
||||
required: false
|
||||
type: string
|
||||
release_publish_full_ref:
|
||||
description: Exact full ref of the approving OpenClaw Release Publish workflow tooling
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
release_publish_workflow_sha:
|
||||
description: Exact workflow SHA of the approving OpenClaw Release Publish tooling
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
dry_run:
|
||||
description: Validate the full ClawHub artifact handoff without publishing.
|
||||
required: false
|
||||
@@ -61,6 +71,7 @@ jobs:
|
||||
matrix: ${{ steps.plan.outputs.matrix }}
|
||||
bootstrap_matrix: ${{ steps.plan.outputs.bootstrap_matrix }}
|
||||
missing_trusted_publisher_matrix: ${{ steps.plan.outputs.missing_trusted_publisher_matrix }}
|
||||
trusted_tooling_identity_json: ${{ steps.tooling_identity.outputs.json }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
@@ -92,6 +103,55 @@ jobs:
|
||||
fi
|
||||
echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Capture trusted tooling identity
|
||||
id: tooling_identity
|
||||
env:
|
||||
CALLER_FULL_REF: ${{ github.ref }}
|
||||
CALLER_REF: ${{ github.ref_name }}
|
||||
CALLER_RUN_ATTEMPT: ${{ github.run_attempt }}
|
||||
CALLER_RUN_ID: ${{ github.run_id }}
|
||||
CALLER_SHA: ${{ github.sha }}
|
||||
TOOLING_FULL_REF: ${{ inputs.release_publish_full_ref }}
|
||||
TOOLING_REF: ${{ inputs.release_publish_branch }}
|
||||
TOOLING_SHA: ${{ inputs.release_publish_workflow_sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ -z "${TOOLING_FULL_REF}${TOOLING_SHA}" ]]; then
|
||||
echo "json=" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
[[ -n "$TOOLING_FULL_REF" && -n "$TOOLING_REF" && -n "$TOOLING_SHA" ]] || {
|
||||
echo "Trusted release tooling ref, full ref, and SHA must be provided together." >&2
|
||||
exit 1
|
||||
}
|
||||
identity="$(
|
||||
jq -cn \
|
||||
--arg repository "$GITHUB_REPOSITORY" \
|
||||
--arg workflow ".github/workflows/plugin-clawhub-release.yml" \
|
||||
--arg runId "$CALLER_RUN_ID" \
|
||||
--arg runAttempt "$CALLER_RUN_ATTEMPT" \
|
||||
--arg ref "$CALLER_REF" \
|
||||
--arg fullRef "$CALLER_FULL_REF" \
|
||||
--arg sha "$CALLER_SHA" \
|
||||
--arg toolingRef "$TOOLING_REF" \
|
||||
--arg toolingFullRef "$TOOLING_FULL_REF" \
|
||||
--arg toolingSha "$TOOLING_SHA" \
|
||||
'{
|
||||
version: 1,
|
||||
repository: $repository,
|
||||
workflow: $workflow,
|
||||
runId: $runId,
|
||||
runAttempt: $runAttempt,
|
||||
ref: $ref,
|
||||
fullRef: $fullRef,
|
||||
sha: $sha,
|
||||
toolingRef: $toolingRef,
|
||||
toolingFullRef: $toolingFullRef,
|
||||
toolingSha: $toolingSha
|
||||
}'
|
||||
)"
|
||||
echo "json=${identity}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Validate OIDC source matches workflow ref
|
||||
env:
|
||||
TARGET_SHA: ${{ steps.ref.outputs.sha }}
|
||||
@@ -388,7 +448,7 @@ jobs:
|
||||
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@d8096dfc039e86ab942ddf9ef117d04849fd84c1
|
||||
uses: openclaw/clawhub/.github/workflows/package-publish.yml@6dc1e2bd67a90b5e5c54b3a026dbdfe3691f1202
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
@@ -411,6 +471,7 @@ jobs:
|
||||
source_path: ${{ matrix.plugin.packageDir }}
|
||||
inspector_artifact_name: ${{ matrix.plugin.artifactName }}-inspector
|
||||
publish_json_artifact_name: ${{ matrix.plugin.artifactName }}-publish-json
|
||||
trusted_tooling_identity_json: ${{ needs.preview_plugins_clawhub.outputs.trusted_tooling_identity_json }}
|
||||
|
||||
verify_published_clawhub_package:
|
||||
needs: [preview_plugins_clawhub, publish_plugins_clawhub]
|
||||
|
||||
@@ -22,6 +22,8 @@ on:
|
||||
- "scripts/lib/actions-artifact-archive.mjs"
|
||||
- "scripts/plugin-npm-publish.sh"
|
||||
- "scripts/plugin-publication-artifact.mjs"
|
||||
- "scripts/release-tooling-identity.d.mts"
|
||||
- "scripts/release-tooling-identity.mjs"
|
||||
- "scripts/plugin-npm-release-check.ts"
|
||||
- "scripts/plugin-npm-release-plan.ts"
|
||||
- "scripts/verify-plugin-npm-published-runtime.mts"
|
||||
@@ -1103,6 +1105,7 @@ jobs:
|
||||
PACKAGE_NAME: ${{ matrix.plugin.packageName }}
|
||||
PACKAGE_VERSION: ${{ matrix.plugin.version }}
|
||||
PUBLISH_TAG: ${{ matrix.plugin.publishTag }}
|
||||
RELEASE_PUBLISH_RUN_ID: ${{ inputs.release_publish_run_id }}
|
||||
TARGET_SHA: ${{ needs.preview_plugins_npm.outputs.ref_revision }}
|
||||
WORKFLOW_HEAD_BRANCH: ${{ github.ref_name }}
|
||||
WORKFLOW_REF: ${{ github.ref }}
|
||||
@@ -1161,35 +1164,12 @@ jobs:
|
||||
exit 1
|
||||
}
|
||||
if [[ "$publish_route" == "npm-token-bootstrap" ]]; then
|
||||
sha_pinned_release_publish=false
|
||||
if [[ "$WORKFLOW_REF" =~ ^refs/tags/release-publish/([a-f0-9]{12})-[1-9][0-9]*$ ]]; then
|
||||
workflow_sha_prefix="${BASH_REMATCH[1]}"
|
||||
[[ "$WORKFLOW_SHA" =~ ^[a-f0-9]{40}$ && "${WORKFLOW_SHA:0:12}" == "$workflow_sha_prefix" ]] || {
|
||||
echo "npm token bootstrap release-publish tag does not match the workflow SHA." >&2
|
||||
exit 1
|
||||
}
|
||||
workflow_tag="${WORKFLOW_REF#refs/tags/}"
|
||||
remote_workflow_sha="$(
|
||||
gh api "repos/${GITHUB_REPOSITORY}/git/ref/tags/${workflow_tag}" \
|
||||
--jq '.object | select(.type == "commit") | .sha | select(test("^[a-f0-9]{40}$"))'
|
||||
)"
|
||||
[[ "$remote_workflow_sha" == "$WORKFLOW_SHA" ]] || {
|
||||
echo "npm token bootstrap release-publish tag does not resolve to the workflow SHA." >&2
|
||||
exit 1
|
||||
}
|
||||
sha_pinned_release_publish=true
|
||||
fi
|
||||
[[ "$WORKFLOW_REF" == "refs/heads/main" || "$sha_pinned_release_publish" == "true" ]] || {
|
||||
echo "npm token bootstrap requires trusted main tooling or a protected SHA-pinned release-publish tag." >&2
|
||||
exit 1
|
||||
}
|
||||
if [[ "$WORKFLOW_REF" == "refs/heads/main" ]]; then
|
||||
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 token bootstrap workflow revision is not reachable from current main." >&2
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
node scripts/release-tooling-identity.mjs verify \
|
||||
--repository "$GITHUB_REPOSITORY" \
|
||||
--workflow-ref "$WORKFLOW_HEAD_BRANCH" \
|
||||
--workflow-full-ref "$WORKFLOW_REF" \
|
||||
--workflow-sha "$WORKFLOW_SHA" \
|
||||
--release-publish-run-id "$RELEASE_PUBLISH_RUN_ID"
|
||||
fi
|
||||
artifact_id="$(jq -er '.id' "$artifact_metadata")"
|
||||
artifact_digest="$(jq -er '.digest' "$artifact_metadata")"
|
||||
@@ -1318,8 +1298,16 @@ jobs:
|
||||
- name: Publish with trusted publisher
|
||||
if: steps.publication_evidence.outputs.publish_route == 'npm-oidc' && steps.npm_package_version.outputs.already_published != 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
OPENCLAW_NPM_PUBLISH_AUTH_MODE: trusted-publisher
|
||||
OPENCLAW_PLUGIN_NPM_PUBLISH_TAG: ${{ inputs.npm_dist_tag == 'extended-stable' && inputs.npm_dist_tag || '' }}
|
||||
OPENCLAW_RELEASE_PUBLISH_RUN_ID: ${{ inputs.release_publish_run_id }}
|
||||
OPENCLAW_RELEASE_TOOLING_ALLOW_PREVALIDATED_REF: "true"
|
||||
OPENCLAW_RELEASE_TOOLING_FULL_REF: ${{ github.ref }}
|
||||
OPENCLAW_RELEASE_TOOLING_IDENTITY_REQUIRED: "true"
|
||||
OPENCLAW_RELEASE_TOOLING_REF: ${{ github.ref_name }}
|
||||
OPENCLAW_RELEASE_TOOLING_REPOSITORY: ${{ github.repository }}
|
||||
OPENCLAW_RELEASE_TOOLING_SHA: ${{ github.workflow_sha }}
|
||||
run: bash scripts/plugin-npm-publish.sh --repo-root .publication-target --publish "${{ matrix.plugin.packageDir }}"
|
||||
|
||||
- name: Verify OIDC published runtime
|
||||
@@ -1379,12 +1367,17 @@ jobs:
|
||||
- name: Publish approved bootstrap tarball
|
||||
if: steps.publication_evidence.outputs.publish_route == 'npm-token-bootstrap' && steps.bootstrap_npm_package_version.outputs.already_published != 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
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 }}
|
||||
RELEASE_PUBLISH_RUN_ID: ${{ inputs.release_publish_run_id }}
|
||||
TARBALL_PATH: ${{ steps.publication_evidence.outputs.tarball_path }}
|
||||
WORKFLOW_FULL_REF: ${{ github.ref }}
|
||||
WORKFLOW_REF: ${{ github.ref_name }}
|
||||
WORKFLOW_SHA: ${{ github.workflow_sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
[[ "$PACKAGE_VERSION" == *"-beta."* && "$PUBLISH_TAG" == "beta" ]] || {
|
||||
@@ -1410,6 +1403,12 @@ jobs:
|
||||
unset NODE_AUTH_TOKEN NPM_TOKEN NODE_OPTIONS
|
||||
# A timeout can race a committed publish. On rerun, the preceding check
|
||||
# accepts only this tarball's exact integrity and shasum before skipping.
|
||||
node scripts/release-tooling-identity.mjs verify \
|
||||
--repository "$GITHUB_REPOSITORY" \
|
||||
--workflow-ref "$WORKFLOW_REF" \
|
||||
--workflow-full-ref "$WORKFLOW_FULL_REF" \
|
||||
--workflow-sha "$WORKFLOW_SHA" \
|
||||
--release-publish-run-id "$RELEASE_PUBLISH_RUN_ID"
|
||||
HOME="$publish_home" \
|
||||
NPM_CONFIG_GLOBALCONFIG=/dev/null \
|
||||
NPM_CONFIG_IGNORE_SCRIPTS=true \
|
||||
|
||||
@@ -172,6 +172,24 @@ if [[ "${mirror_auth_requirement}" == "required" && -z "${mirror_auth_token}" ]]
|
||||
exit 1
|
||||
fi
|
||||
|
||||
verify_release_tooling_identity() {
|
||||
if [[ "${OPENCLAW_RELEASE_TOOLING_IDENTITY_REQUIRED:-}" != "true" ]]; then
|
||||
return 0
|
||||
fi
|
||||
identity_args=(
|
||||
verify
|
||||
--repository "${OPENCLAW_RELEASE_TOOLING_REPOSITORY:-}"
|
||||
--workflow-ref "${OPENCLAW_RELEASE_TOOLING_REF:-}"
|
||||
--workflow-full-ref "${OPENCLAW_RELEASE_TOOLING_FULL_REF:-}"
|
||||
--workflow-sha "${OPENCLAW_RELEASE_TOOLING_SHA:-}"
|
||||
--release-publish-run-id "${OPENCLAW_RELEASE_PUBLISH_RUN_ID:-}"
|
||||
)
|
||||
if [[ "${OPENCLAW_RELEASE_TOOLING_ALLOW_PREVALIDATED_REF:-}" == "true" ]]; then
|
||||
identity_args+=(--allow-prevalidated-ref)
|
||||
fi
|
||||
node "${tooling_root}/scripts/release-tooling-identity.mjs" "${identity_args[@]}"
|
||||
}
|
||||
|
||||
if [[ "${mode}" == "--pack" || "${mode}" == "--pack-dry-run" ]]; then
|
||||
{
|
||||
printf 'Publish command:'
|
||||
@@ -228,6 +246,9 @@ fi
|
||||
cleanup_files+=("${publish_userconfig}")
|
||||
chmod 0600 "${publish_userconfig}"
|
||||
printf '%s\n' "//registry.npmjs.org/:_authToken=${publish_auth_token}" > "${publish_userconfig}"
|
||||
fi
|
||||
verify_release_tooling_identity
|
||||
if [[ -n "${publish_auth_token}" ]]; then
|
||||
NPM_CONFIG_USERCONFIG="${publish_userconfig}" run_with_manifest_overlay "${publish_cmd[@]}"
|
||||
else
|
||||
run_with_manifest_overlay "${publish_cmd[@]}"
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
export type ReleaseToolingIdentity = {
|
||||
fullRef: string;
|
||||
ref: string;
|
||||
releasePublishRunId?: string;
|
||||
route: "main" | "prevalidated-branch" | "protected-tag";
|
||||
sha: string;
|
||||
};
|
||||
|
||||
export type ReleaseToolingIdentityInput = {
|
||||
allowPrevalidatedRef?: boolean;
|
||||
releasePublishRunId?: string;
|
||||
workflowFullRef: string;
|
||||
workflowRef: string;
|
||||
workflowSha: string;
|
||||
};
|
||||
|
||||
export function validateReleaseToolingIdentity(
|
||||
input: ReleaseToolingIdentityInput & {
|
||||
mainComparisonStatus?: unknown;
|
||||
tagRef?: unknown;
|
||||
},
|
||||
): ReleaseToolingIdentity;
|
||||
|
||||
export function verifyReleaseToolingIdentity(
|
||||
input: ReleaseToolingIdentityInput & {
|
||||
repository: string;
|
||||
runGh?: (args: string[]) => string;
|
||||
},
|
||||
): ReleaseToolingIdentity;
|
||||
@@ -0,0 +1,267 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const SHA_PATTERN = /^[a-f0-9]{40}$/u;
|
||||
const RELEASE_PUBLISH_REF_PATTERN = /^release-publish\/([a-f0-9]{12})-([1-9][0-9]*)$/u;
|
||||
const GH_COMMAND_TIMEOUT_MS = 60_000;
|
||||
|
||||
function fail(message) {
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
function isRecord(value) {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function requiredString(value, label) {
|
||||
if (typeof value !== "string" || value.trim().length === 0) {
|
||||
fail(`${label} is required.`);
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
function requiredSha(value, label) {
|
||||
const sha = requiredString(value, label);
|
||||
if (!SHA_PATTERN.test(sha)) {
|
||||
fail(`${label} must be a lowercase 40-character commit SHA.`);
|
||||
}
|
||||
return sha;
|
||||
}
|
||||
|
||||
function requireRepository(value) {
|
||||
const repository = requiredString(value, "release tooling repository");
|
||||
if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u.test(repository)) {
|
||||
fail("release tooling repository must be owner/name.");
|
||||
}
|
||||
return repository;
|
||||
}
|
||||
|
||||
function classifyIdentity({
|
||||
allowPrevalidatedRef,
|
||||
releasePublishRunId,
|
||||
workflowFullRef,
|
||||
workflowRef,
|
||||
workflowSha,
|
||||
}) {
|
||||
const ref = requiredString(workflowRef, "release tooling ref");
|
||||
const fullRef = requiredString(workflowFullRef, "release tooling full ref");
|
||||
const sha = requiredSha(workflowSha, "release tooling SHA");
|
||||
const protectedMatch = RELEASE_PUBLISH_REF_PATTERN.exec(ref);
|
||||
|
||||
if (protectedMatch) {
|
||||
if (fullRef !== `refs/tags/${ref}`) {
|
||||
fail("protected release tooling identity must use the exact tag full ref.");
|
||||
}
|
||||
if (sha.slice(0, 12) !== protectedMatch[1]) {
|
||||
fail("protected release tooling tag SHA prefix does not match the workflow SHA.");
|
||||
}
|
||||
const runId = requiredString(releasePublishRunId, "release publish run id");
|
||||
if (!/^[1-9][0-9]*$/u.test(runId) || runId !== protectedMatch[2]) {
|
||||
fail("protected release tooling tag run does not match the release publish run id.");
|
||||
}
|
||||
return { fullRef, ref, releasePublishRunId: runId, route: "protected-tag", sha };
|
||||
}
|
||||
|
||||
if (
|
||||
ref.startsWith("release-publish/") ||
|
||||
fullRef.startsWith("refs/tags/release-publish/") ||
|
||||
fullRef.startsWith("refs/heads/release-publish/")
|
||||
) {
|
||||
fail("release-publish tooling identity must be an exact protected tag.");
|
||||
}
|
||||
|
||||
if (ref === "main" || fullRef === "refs/heads/main") {
|
||||
if (ref !== "main" || fullRef !== "refs/heads/main") {
|
||||
fail("main release tooling identity must use ref main and full ref refs/heads/main.");
|
||||
}
|
||||
return { fullRef, ref, route: "main", sha };
|
||||
}
|
||||
|
||||
if (allowPrevalidatedRef !== true || fullRef !== `refs/heads/${ref}`) {
|
||||
fail(
|
||||
"release tooling identity is not trusted main, a protected tag, or a prevalidated branch.",
|
||||
);
|
||||
}
|
||||
return { fullRef, ref, route: "prevalidated-branch", sha };
|
||||
}
|
||||
|
||||
export function validateReleaseToolingIdentity({
|
||||
allowPrevalidatedRef = false,
|
||||
mainComparisonStatus,
|
||||
releasePublishRunId,
|
||||
tagRef,
|
||||
workflowFullRef,
|
||||
workflowRef,
|
||||
workflowSha,
|
||||
}) {
|
||||
const identity = classifyIdentity({
|
||||
allowPrevalidatedRef,
|
||||
releasePublishRunId,
|
||||
workflowFullRef,
|
||||
workflowRef,
|
||||
workflowSha,
|
||||
});
|
||||
|
||||
if (identity.route === "protected-tag") {
|
||||
if (
|
||||
!isRecord(tagRef) ||
|
||||
tagRef.ref !== identity.fullRef ||
|
||||
!isRecord(tagRef.object) ||
|
||||
tagRef.object.type !== "commit" ||
|
||||
tagRef.object.sha !== identity.sha
|
||||
) {
|
||||
fail(
|
||||
"protected release tooling tag is missing, moved, annotated, or bound to the wrong SHA.",
|
||||
);
|
||||
}
|
||||
} else if (
|
||||
identity.route === "main" &&
|
||||
mainComparisonStatus !== "ahead" &&
|
||||
mainComparisonStatus !== "identical"
|
||||
) {
|
||||
fail("main release tooling SHA is not reachable from current main.");
|
||||
}
|
||||
|
||||
return identity;
|
||||
}
|
||||
|
||||
function parseJson(raw, label) {
|
||||
try {
|
||||
return JSON.parse(raw);
|
||||
} catch (error) {
|
||||
throw new Error(`${label} returned invalid JSON.`, { cause: error });
|
||||
}
|
||||
}
|
||||
|
||||
function runReleaseToolingGh(args) {
|
||||
return execFileSync("gh", args, {
|
||||
encoding: "utf8",
|
||||
killSignal: "SIGKILL",
|
||||
maxBuffer: 1024 * 1024,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
timeout: GH_COMMAND_TIMEOUT_MS,
|
||||
});
|
||||
}
|
||||
|
||||
export function verifyReleaseToolingIdentity({
|
||||
allowPrevalidatedRef = false,
|
||||
releasePublishRunId,
|
||||
repository,
|
||||
runGh = runReleaseToolingGh,
|
||||
workflowFullRef,
|
||||
workflowRef,
|
||||
workflowSha,
|
||||
}) {
|
||||
const normalizedRepository = requireRepository(repository);
|
||||
const identity = classifyIdentity({
|
||||
allowPrevalidatedRef,
|
||||
releasePublishRunId,
|
||||
workflowFullRef,
|
||||
workflowRef,
|
||||
workflowSha,
|
||||
});
|
||||
|
||||
if (identity.route === "protected-tag") {
|
||||
let tagRef;
|
||||
try {
|
||||
tagRef = parseJson(
|
||||
runGh([
|
||||
"api",
|
||||
`repos/${normalizedRepository}/git/ref/tags/${identity.ref}`,
|
||||
"--method",
|
||||
"GET",
|
||||
]),
|
||||
"protected release tooling tag",
|
||||
);
|
||||
} catch (error) {
|
||||
throw new Error("protected release tooling tag is missing or unreadable.", { cause: error });
|
||||
}
|
||||
return validateReleaseToolingIdentity({
|
||||
allowPrevalidatedRef,
|
||||
releasePublishRunId,
|
||||
tagRef,
|
||||
workflowFullRef,
|
||||
workflowRef,
|
||||
workflowSha,
|
||||
});
|
||||
}
|
||||
|
||||
if (identity.route === "main") {
|
||||
let comparison;
|
||||
try {
|
||||
comparison = parseJson(
|
||||
runGh([
|
||||
"api",
|
||||
`repos/${normalizedRepository}/compare/${identity.sha}...main`,
|
||||
"--method",
|
||||
"GET",
|
||||
]),
|
||||
"main release tooling comparison",
|
||||
);
|
||||
} catch (error) {
|
||||
throw new Error("main release tooling ancestry could not be verified.", { cause: error });
|
||||
}
|
||||
return validateReleaseToolingIdentity({
|
||||
allowPrevalidatedRef,
|
||||
mainComparisonStatus: isRecord(comparison) ? comparison.status : undefined,
|
||||
releasePublishRunId,
|
||||
workflowFullRef,
|
||||
workflowRef,
|
||||
workflowSha,
|
||||
});
|
||||
}
|
||||
|
||||
return identity;
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const options = {
|
||||
allowPrevalidatedRef: false,
|
||||
releasePublishRunId: "",
|
||||
repository: "",
|
||||
workflowFullRef: "",
|
||||
workflowRef: "",
|
||||
workflowSha: "",
|
||||
};
|
||||
if (argv.shift() !== "verify") {
|
||||
fail("usage: release-tooling-identity.mjs verify [options]");
|
||||
}
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const arg = argv[index];
|
||||
if (arg === "--allow-prevalidated-ref") {
|
||||
options.allowPrevalidatedRef = true;
|
||||
continue;
|
||||
}
|
||||
const value = argv[(index += 1)] ?? "";
|
||||
if (arg === "--release-publish-run-id") {
|
||||
options.releasePublishRunId = value;
|
||||
} else if (arg === "--repository") {
|
||||
options.repository = value;
|
||||
} else if (arg === "--workflow-full-ref") {
|
||||
options.workflowFullRef = value;
|
||||
} else if (arg === "--workflow-ref") {
|
||||
options.workflowRef = value;
|
||||
} else if (arg === "--workflow-sha") {
|
||||
options.workflowSha = value;
|
||||
} else {
|
||||
fail(`unknown release tooling identity argument: ${arg}`);
|
||||
}
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
function main(argv = process.argv.slice(2)) {
|
||||
const identity = verifyReleaseToolingIdentity(parseArgs([...argv]));
|
||||
process.stdout.write(`${JSON.stringify(identity)}\n`);
|
||||
}
|
||||
|
||||
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
||||
try {
|
||||
main();
|
||||
} catch (error) {
|
||||
console.error(error instanceof Error ? error.message : String(error));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -1627,20 +1627,69 @@ describe("package acceptance workflow", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("allows protected SHA-pinned tooling tags to consume token-bootstrap evidence", () => {
|
||||
it("uses the canonical tooling identity verifier for token-bootstrap evidence", () => {
|
||||
const publishJob = workflowJob(PLUGIN_NPM_RELEASE_WORKFLOW, "publish_plugins_npm");
|
||||
const evidenceStep = workflowStep(publishJob, "Consume immutable npm publication evidence");
|
||||
|
||||
expect(evidenceStep.run).toContain("^refs/tags/release-publish/([a-f0-9]{12})-[1-9][0-9]*$");
|
||||
expect(evidenceStep.run).toContain(
|
||||
'[[ "$WORKFLOW_REF" == "refs/heads/main" || "$sha_pinned_release_publish" == "true" ]]',
|
||||
expect(evidenceStep.env?.RELEASE_PUBLISH_RUN_ID).toBe("${{ inputs.release_publish_run_id }}");
|
||||
expect(evidenceStep.run).toContain("node scripts/release-tooling-identity.mjs verify");
|
||||
expect(evidenceStep.run).toContain('--workflow-ref "$WORKFLOW_HEAD_BRANCH"');
|
||||
expect(evidenceStep.run).toContain('--workflow-full-ref "$WORKFLOW_REF"');
|
||||
expect(evidenceStep.run).toContain('--workflow-sha "$WORKFLOW_SHA"');
|
||||
expect(evidenceStep.run).toContain('--release-publish-run-id "$RELEASE_PUBLISH_RUN_ID"');
|
||||
expect(evidenceStep.run).not.toContain("--allow-prevalidated-ref");
|
||||
});
|
||||
|
||||
it("revalidates protected tooling immediately before every core and plugin npm publish", () => {
|
||||
const corePublish = workflowStep(
|
||||
workflowJob(OPENCLAW_NPM_RELEASE_WORKFLOW, "publish_openclaw_npm"),
|
||||
"Publish",
|
||||
);
|
||||
expect(evidenceStep.run).toContain(
|
||||
'gh api "repos/${GITHUB_REPOSITORY}/git/ref/tags/${workflow_tag}"',
|
||||
expect(corePublish.env).toMatchObject({
|
||||
GH_TOKEN: "${{ github.token }}",
|
||||
RELEASE_PUBLISH_RUN_ID: "${{ inputs.release_publish_run_id }}",
|
||||
WORKFLOW_FULL_REF: "${{ github.ref }}",
|
||||
WORKFLOW_REF: "${{ github.ref_name }}",
|
||||
WORKFLOW_SHA: "${{ github.workflow_sha }}",
|
||||
});
|
||||
expect(corePublish.run).toContain(
|
||||
"node trusted-workflow/scripts/release-tooling-identity.mjs verify",
|
||||
);
|
||||
expect(evidenceStep.run).toContain('[[ "$remote_workflow_sha" == "$WORKFLOW_SHA" ]]');
|
||||
expect(evidenceStep.run).toContain('if [[ "$WORKFLOW_REF" == "refs/heads/main" ]]; then');
|
||||
expect(evidenceStep.run).toContain('git merge-base --is-ancestor "$WORKFLOW_SHA" origin/main');
|
||||
expect(corePublish.run).toContain("--allow-prevalidated-ref");
|
||||
expect(corePublish.run).toMatch(
|
||||
/verify_release_tooling_identity\s+bash scripts\/openclaw-npm-publish\.sh --publish "\.\/\$\{tarball_path\}"/u,
|
||||
);
|
||||
expect(corePublish.run).toMatch(
|
||||
/verify_release_tooling_identity\s+bash scripts\/openclaw-npm-publish\.sh --publish "\$\{publish_target\}"/u,
|
||||
);
|
||||
|
||||
const pluginPublishJob = workflowJob(PLUGIN_NPM_RELEASE_WORKFLOW, "publish_plugins_npm");
|
||||
const oidcPublish = workflowStep(pluginPublishJob, "Publish with trusted publisher");
|
||||
expect(oidcPublish.env).toMatchObject({
|
||||
GH_TOKEN: "${{ github.token }}",
|
||||
OPENCLAW_RELEASE_PUBLISH_RUN_ID: "${{ inputs.release_publish_run_id }}",
|
||||
OPENCLAW_RELEASE_TOOLING_ALLOW_PREVALIDATED_REF: "true",
|
||||
OPENCLAW_RELEASE_TOOLING_FULL_REF: "${{ github.ref }}",
|
||||
OPENCLAW_RELEASE_TOOLING_IDENTITY_REQUIRED: "true",
|
||||
OPENCLAW_RELEASE_TOOLING_REF: "${{ github.ref_name }}",
|
||||
OPENCLAW_RELEASE_TOOLING_REPOSITORY: "${{ github.repository }}",
|
||||
OPENCLAW_RELEASE_TOOLING_SHA: "${{ github.workflow_sha }}",
|
||||
});
|
||||
|
||||
const bootstrapPublish = workflowStep(pluginPublishJob, "Publish approved bootstrap tarball");
|
||||
expect(bootstrapPublish.env).toMatchObject({
|
||||
GH_TOKEN: "${{ github.token }}",
|
||||
RELEASE_PUBLISH_RUN_ID: "${{ inputs.release_publish_run_id }}",
|
||||
WORKFLOW_FULL_REF: "${{ github.ref }}",
|
||||
WORKFLOW_REF: "${{ github.ref_name }}",
|
||||
WORKFLOW_SHA: "${{ github.workflow_sha }}",
|
||||
});
|
||||
const identityIndex =
|
||||
bootstrapPublish.run?.indexOf("node scripts/release-tooling-identity.mjs verify") ?? -1;
|
||||
const publishIndex = bootstrapPublish.run?.indexOf('npm publish "$TARBALL_PATH"') ?? -1;
|
||||
expect(identityIndex).toBeGreaterThan(-1);
|
||||
expect(publishIndex).toBeGreaterThan(identityIndex);
|
||||
expect(bootstrapPublish.run?.slice(identityIndex, publishIndex)).not.toContain("npm view");
|
||||
});
|
||||
|
||||
it("binds release evidence validation to the exact trusted workflow ref", () => {
|
||||
@@ -6893,6 +6942,7 @@ describe("package artifact reuse", () => {
|
||||
".github/workflows/plugin-clawhub-new.yml",
|
||||
"publish_bootstrap_plugins",
|
||||
);
|
||||
const publishOrchestration = workflowStep(releasePublishJob, "Dispatch publish workflows");
|
||||
const postpublishEvidence = workflowStep(releasePublishJob, "Upload postpublish evidence");
|
||||
|
||||
expect(packageJson.scripts).toMatchObject({
|
||||
@@ -6919,13 +6969,53 @@ describe("package artifact reuse", () => {
|
||||
"approve_plugins_clawhub_release",
|
||||
]);
|
||||
expect(clawHubPublish.uses).toBe(
|
||||
"openclaw/clawhub/.github/workflows/package-publish.yml@d8096dfc039e86ab942ddf9ef117d04849fd84c1",
|
||||
"openclaw/clawhub/.github/workflows/package-publish.yml@6dc1e2bd67a90b5e5c54b3a026dbdfe3691f1202",
|
||||
);
|
||||
expect(clawHubPublish.permissions).toMatchObject({
|
||||
actions: "read",
|
||||
contents: "read",
|
||||
"id-token": "write",
|
||||
});
|
||||
expect(clawHubPublish.with?.trusted_tooling_identity_json).toBe(
|
||||
"${{ needs.preview_plugins_clawhub.outputs.trusted_tooling_identity_json }}",
|
||||
);
|
||||
const clawHubPreview = workflowJob(PLUGIN_CLAWHUB_RELEASE_WORKFLOW, "preview_plugins_clawhub");
|
||||
expect(clawHubPreview.outputs?.trusted_tooling_identity_json).toBe(
|
||||
"${{ steps.tooling_identity.outputs.json }}",
|
||||
);
|
||||
const toolingIdentity = workflowStep(clawHubPreview, "Capture trusted tooling identity");
|
||||
expect(toolingIdentity.env).toMatchObject({
|
||||
CALLER_FULL_REF: "${{ github.ref }}",
|
||||
CALLER_REF: "${{ github.ref_name }}",
|
||||
CALLER_RUN_ATTEMPT: "${{ github.run_attempt }}",
|
||||
CALLER_RUN_ID: "${{ github.run_id }}",
|
||||
CALLER_SHA: "${{ github.sha }}",
|
||||
TOOLING_FULL_REF: "${{ inputs.release_publish_full_ref }}",
|
||||
TOOLING_REF: "${{ inputs.release_publish_branch }}",
|
||||
TOOLING_SHA: "${{ inputs.release_publish_workflow_sha }}",
|
||||
});
|
||||
for (const field of [
|
||||
"version: 1",
|
||||
"repository: $repository",
|
||||
"workflow: $workflow",
|
||||
"runId: $runId",
|
||||
"runAttempt: $runAttempt",
|
||||
"ref: $ref",
|
||||
"fullRef: $fullRef",
|
||||
"sha: $sha",
|
||||
"toolingRef: $toolingRef",
|
||||
"toolingFullRef: $toolingFullRef",
|
||||
"toolingSha: $toolingSha",
|
||||
]) {
|
||||
expect(toolingIdentity.run).toContain(field);
|
||||
}
|
||||
expect(publishOrchestration.env?.PARENT_WORKFLOW_FULL_REF).toBe("${{ github.ref }}");
|
||||
expect(publishOrchestration.run).toContain(
|
||||
'-f release_publish_full_ref="${PARENT_WORKFLOW_FULL_REF}"',
|
||||
);
|
||||
expect(publishOrchestration.run).toContain(
|
||||
'-f release_publish_workflow_sha="${PARENT_WORKFLOW_SHA}"',
|
||||
);
|
||||
expect(clawHubBootstrapValidation.environment).toBe("clawhub-plugin-bootstrap");
|
||||
expect(clawHubBootstrapPublish.environment).toBe("clawhub-plugin-bootstrap");
|
||||
|
||||
|
||||
@@ -413,7 +413,7 @@ describe("plugin npm extended-stable workflow", () => {
|
||||
.split("\n")
|
||||
.filter((line) => line.includes('npm publish "$TARBALL_PATH"'));
|
||||
|
||||
expect(gitFetchLines).toHaveLength(6);
|
||||
expect(gitFetchLines).toHaveLength(5);
|
||||
expect(
|
||||
gitFetchLines.every((line) => line.includes("timeout --signal=TERM --kill-after=10s 120s")),
|
||||
).toBe(true);
|
||||
@@ -468,18 +468,11 @@ describe("plugin npm extended-stable workflow", () => {
|
||||
expect(consume.run).toContain("--connect-timeout 10");
|
||||
expect(consume.run).toContain("--max-time 120");
|
||||
expect(consume.run).toContain("actions/artifacts/${artifact_id}/zip");
|
||||
expect(consume.run).toContain("sha_pinned_release_publish=false");
|
||||
expect(consume.run).toContain(
|
||||
'[[ "$WORKFLOW_REF" =~ ^refs/tags/release-publish/([a-f0-9]{12})-[1-9][0-9]*$ ]]',
|
||||
);
|
||||
expect(consume.run).toContain(
|
||||
'[[ "$WORKFLOW_SHA" =~ ^[a-f0-9]{40}$ && "${WORKFLOW_SHA:0:12}" == "$workflow_sha_prefix" ]]',
|
||||
);
|
||||
expect(consume.run).toContain("sha_pinned_release_publish=true");
|
||||
expect(consume.run).toContain(
|
||||
'[[ "$WORKFLOW_REF" == "refs/heads/main" || "$sha_pinned_release_publish" == "true" ]]',
|
||||
);
|
||||
expect(consume.run).toContain('git merge-base --is-ancestor "$WORKFLOW_SHA" origin/main');
|
||||
expect(consume.run).toContain("node scripts/release-tooling-identity.mjs verify");
|
||||
expect(consume.run).toContain('--workflow-ref "$WORKFLOW_HEAD_BRANCH"');
|
||||
expect(consume.run).toContain('--workflow-full-ref "$WORKFLOW_REF"');
|
||||
expect(consume.run).toContain('--workflow-sha "$WORKFLOW_SHA"');
|
||||
expect(consume.run).toContain('--release-publish-run-id "$RELEASE_PUBLISH_RUN_ID"');
|
||||
expect(
|
||||
step(parsed.jobs?.publish_plugins_npm, "Checkout trusted publication tooling").with?.ref,
|
||||
).toBe("${{ github.workflow_sha }}");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Plugin NPM Publish tests cover publish wrapper argument safety.
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { chmodSync, mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { chmodSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { delimiter, join } from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
@@ -41,6 +41,21 @@ function makePackage(version: string): { packageDir: string; path: string; root:
|
||||
}
|
||||
|
||||
describe("plugin npm publish wrapper", () => {
|
||||
it("revalidates release tooling after preparation and immediately before npm publish", () => {
|
||||
const source = readFileSync(scriptPath, "utf8");
|
||||
const buildIndex = source.indexOf("build_package_runtime");
|
||||
const identityIndex = source.lastIndexOf("verify_release_tooling_identity");
|
||||
const publishIndex = source.indexOf(
|
||||
'run_with_manifest_overlay "${publish_cmd[@]}"',
|
||||
identityIndex,
|
||||
);
|
||||
|
||||
expect(buildIndex).toBeGreaterThan(-1);
|
||||
expect(identityIndex).toBeGreaterThan(buildIndex);
|
||||
expect(publishIndex).toBeGreaterThan(identityIndex);
|
||||
expect(source.slice(identityIndex, publishIndex)).not.toContain("npm view");
|
||||
});
|
||||
|
||||
it("prints help before package or npm checks", () => {
|
||||
const result = runPluginPublishWrapper(["--help"]);
|
||||
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
validateReleaseToolingIdentity,
|
||||
verifyReleaseToolingIdentity,
|
||||
} from "../../scripts/release-tooling-identity.mjs";
|
||||
|
||||
const SHA = "a".repeat(40);
|
||||
const OTHER_SHA = "b".repeat(40);
|
||||
const RUN_ID = "12345";
|
||||
const REF = `release-publish/${SHA.slice(0, 12)}-${RUN_ID}`;
|
||||
const FULL_REF = `refs/tags/${REF}`;
|
||||
|
||||
function protectedIdentity(
|
||||
overrides: Partial<Parameters<typeof verifyReleaseToolingIdentity>[0]> = {},
|
||||
) {
|
||||
return {
|
||||
releasePublishRunId: RUN_ID,
|
||||
repository: "openclaw/openclaw",
|
||||
workflowFullRef: FULL_REF,
|
||||
workflowRef: REF,
|
||||
workflowSha: SHA,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("release tooling identity", () => {
|
||||
it("accepts only the live exact lightweight protected tag", () => {
|
||||
const runGh = vi.fn(() =>
|
||||
JSON.stringify({
|
||||
ref: FULL_REF,
|
||||
object: { sha: SHA, type: "commit" },
|
||||
}),
|
||||
);
|
||||
|
||||
expect(verifyReleaseToolingIdentity({ ...protectedIdentity(), runGh })).toEqual({
|
||||
fullRef: FULL_REF,
|
||||
ref: REF,
|
||||
releasePublishRunId: RUN_ID,
|
||||
route: "protected-tag",
|
||||
sha: SHA,
|
||||
});
|
||||
expect(runGh).toHaveBeenCalledWith([
|
||||
"api",
|
||||
`repos/openclaw/openclaw/git/ref/tags/${REF}`,
|
||||
"--method",
|
||||
"GET",
|
||||
]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[
|
||||
"moved tag",
|
||||
{
|
||||
runGh: () =>
|
||||
JSON.stringify({
|
||||
ref: FULL_REF,
|
||||
object: { sha: OTHER_SHA, type: "commit" },
|
||||
}),
|
||||
},
|
||||
"missing, moved, annotated, or bound to the wrong SHA",
|
||||
],
|
||||
[
|
||||
"deleted tag",
|
||||
{
|
||||
runGh: () => {
|
||||
throw new Error("HTTP 404");
|
||||
},
|
||||
},
|
||||
"missing or unreadable",
|
||||
],
|
||||
[
|
||||
"annotated tag",
|
||||
{
|
||||
runGh: () =>
|
||||
JSON.stringify({
|
||||
ref: FULL_REF,
|
||||
object: { sha: OTHER_SHA, type: "tag" },
|
||||
}),
|
||||
},
|
||||
"missing, moved, annotated, or bound to the wrong SHA",
|
||||
],
|
||||
[
|
||||
"wrong SHA prefix",
|
||||
{
|
||||
workflowRef: `release-publish/${OTHER_SHA.slice(0, 12)}-${RUN_ID}`,
|
||||
workflowFullRef: `refs/tags/release-publish/${OTHER_SHA.slice(0, 12)}-${RUN_ID}`,
|
||||
},
|
||||
"SHA prefix does not match",
|
||||
],
|
||||
["wrong release run", { releasePublishRunId: "54321" }, "run does not match"],
|
||||
["same-name branch", { workflowFullRef: `refs/heads/${REF}` }, "exact tag full ref"],
|
||||
])("rejects $0", (_label, overrides, expectedError) => {
|
||||
expect(() =>
|
||||
verifyReleaseToolingIdentity({
|
||||
...protectedIdentity(),
|
||||
...overrides,
|
||||
}),
|
||||
).toThrow(expectedError);
|
||||
});
|
||||
|
||||
it.each(["ahead", "identical"])(
|
||||
"accepts main tooling reachable from current main: %s",
|
||||
(status) => {
|
||||
const runGh = vi.fn(() => JSON.stringify({ status }));
|
||||
expect(
|
||||
verifyReleaseToolingIdentity({
|
||||
repository: "openclaw/openclaw",
|
||||
runGh,
|
||||
workflowFullRef: "refs/heads/main",
|
||||
workflowRef: "main",
|
||||
workflowSha: SHA,
|
||||
}),
|
||||
).toMatchObject({ route: "main", sha: SHA });
|
||||
},
|
||||
);
|
||||
|
||||
it("rejects main tooling outside current main ancestry", () => {
|
||||
expect(() =>
|
||||
validateReleaseToolingIdentity({
|
||||
mainComparisonStatus: "diverged",
|
||||
workflowFullRef: "refs/heads/main",
|
||||
workflowRef: "main",
|
||||
workflowSha: SHA,
|
||||
}),
|
||||
).toThrow("not reachable from current main");
|
||||
});
|
||||
|
||||
it("preserves explicitly prevalidated non-main branch routes", () => {
|
||||
expect(
|
||||
verifyReleaseToolingIdentity({
|
||||
allowPrevalidatedRef: true,
|
||||
repository: "openclaw/openclaw",
|
||||
runGh: vi.fn(() => {
|
||||
throw new Error("prevalidated branches do not require a remote identity query");
|
||||
}),
|
||||
workflowFullRef: "refs/heads/release/2026.8.1",
|
||||
workflowRef: "release/2026.8.1",
|
||||
workflowSha: SHA,
|
||||
}),
|
||||
).toMatchObject({ route: "prevalidated-branch" });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user