chore(release): add protected npm placeholder publishing (#119875)

Adds a protected, dry-run-by-default workflow for reserving release-enabled external plugin package names with verified npm 0.0.0 placeholders.
This commit is contained in:
Vincent Koc
2026-08-06 17:37:09 +08:00
committed by GitHub
parent 5a8c27ca41
commit 01ae9d40fc
4 changed files with 1832 additions and 0 deletions
@@ -0,0 +1,350 @@
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"
+80
View File
@@ -0,0 +1,80 @@
export type RegistryResult = {
status: number;
packument: Record<string, unknown> | null;
};
export type PlaceholderEntry = {
packageDir: string;
packageName: string;
sourcePackageJsonSha256: string;
action: "publish" | "skip" | "tag";
newPackage: boolean;
preExistingDistTags: Record<string, string>;
tarball: {
name: string;
integrity: string;
sha256: string;
shasum: string;
sizeBytes: number;
};
};
export function createPlaceholderTarball(packageName: string): Buffer;
export function parseSelectedPackages(input: string): string[];
export function resolveSelectedPackageSources(
repoRoot: string,
packageNames: string[],
): Array<{
packageDir: string;
packageName: string;
sourcePackageJsonSha256: string;
}>;
export function classifyRegistryState(params: {
expectedIntegrity: string;
expectedShasum: string;
packageName: string;
registry: RegistryResult;
}): {
action: "publish" | "skip" | "tag";
newPackage: boolean;
nonPlaceholderTags: Record<string, string>;
};
export function createPlaceholderPublication(params: {
repoRoot: string;
outputDir: string;
packages: string;
targetSha: string;
workflowSha: string;
fetchImpl?: typeof fetch;
}): Promise<{
schema: string;
targetSha: string;
workflowPath: string;
workflowSha: string;
version: string;
publishTag: string;
packages: PlaceholderEntry[];
}>;
export function verifyPlaceholderArtifact(params: Record<string, unknown>): Promise<{
artifactSha256: string;
manifest: Record<string, unknown>;
}>;
export function assertFinalRegistryState(entry: PlaceholderEntry, registry: RegistryResult): void;
export function publishPlaceholders(params: {
artifactDir: string;
npmToken: string;
targetSha: string;
workflowSha: string;
fetchImpl?: typeof fetch;
npmRunner?: (args: string[], options: { cwd: string; env: NodeJS.ProcessEnv }) => void;
registryAttempts?: number;
sleep?: (delayMs: number) => Promise<void>;
tempRoot?: string;
}): Promise<{
results: Array<{
action: "publish" | "skip" | "tag";
newPackage: boolean;
packageName: string;
}>;
}>;
export function main(argv?: string[]): Promise<void>;
+800
View File
@@ -0,0 +1,800 @@
#!/usr/bin/env node
import { spawnSync } from "node:child_process";
import { createHash } from "node:crypto";
import {
lstatSync,
mkdtempSync,
mkdirSync,
readFileSync,
readdirSync,
rmSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { basename, join, resolve } from "node:path";
import { pathToFileURL } from "node:url";
import { gzipSync } from "node:zlib";
import { readPublicationArtifactArchive, sha256Digest } from "./lib/actions-artifact-archive.mjs";
import { fetchNpmRegistryPackumentWithRetry } from "./lib/npm-publish-plan.mjs";
const MANIFEST_FILENAME = "npm-placeholder-manifest.json";
const MANIFEST_SCHEMA = "openclaw.npm-placeholder-publication/v1";
const PACKAGE_VERSION = "0.0.0";
const PUBLISH_TAG = "placeholder";
const WORKFLOW_PATH = ".github/workflows/npm-placeholder-bootstrap.yml";
const PACKAGE_NAME_RE = /^@openclaw\/[a-z0-9][a-z0-9._-]*$/u;
const SHA_RE = /^[0-9a-f]{40}$/u;
const SHA256_RE = /^[0-9a-f]{64}$/u;
const MAX_ARTIFACT_BYTES = 32 * 1024 * 1024;
const MAX_FILE_BYTES = 2 * 1024 * 1024;
function compareCodeUnits(left, right) {
return left < right ? -1 : left > right ? 1 : 0;
}
function assertTrimmedString(value, label) {
if (typeof value !== "string" || value.length === 0 || value.trim() !== value) {
throw new Error(`${label} must be a non-empty trimmed string.`);
}
return value;
}
function assertCommitSha(value, label) {
const sha = assertTrimmedString(value, label);
if (!SHA_RE.test(sha)) {
throw new Error(`${label} must be a full lowercase commit SHA.`);
}
return sha;
}
function sha256(bytes) {
return createHash("sha256").update(bytes).digest("hex");
}
function npmIntegrity(bytes) {
return `sha512-${createHash("sha512").update(bytes).digest("base64")}`;
}
function npmShasum(bytes) {
return createHash("sha1").update(bytes).digest("hex");
}
function canonicalJson(value) {
return `${JSON.stringify(value, null, 2)}\n`;
}
function placeholderPackageJson(packageName) {
return {
name: packageName,
version: PACKAGE_VERSION,
description: "Reserved package name for an official OpenClaw plugin.",
license: "MIT",
repository: {
type: "git",
url: "git+https://github.com/openclaw/openclaw.git",
},
publishConfig: {
access: "public",
tag: PUBLISH_TAG,
},
};
}
function placeholderReadme(packageName) {
return `# ${packageName}\n\nReserved placeholder for the official OpenClaw plugin package. Use a published release version instead.\n`;
}
function writeTarString(header, offset, length, value) {
const bytes = Buffer.from(value, "utf8");
if (bytes.length > length) {
throw new Error(`Tar header value exceeds ${length} bytes.`);
}
bytes.copy(header, offset);
}
function writeTarOctal(header, offset, length, value) {
const encoded = value.toString(8).padStart(length - 1, "0");
writeTarString(header, offset, length, `${encoded}\0`);
}
function tarEntry(path, content) {
const header = Buffer.alloc(512);
writeTarString(header, 0, 100, path);
writeTarOctal(header, 100, 8, 0o644);
writeTarOctal(header, 108, 8, 0);
writeTarOctal(header, 116, 8, 0);
writeTarOctal(header, 124, 12, content.length);
writeTarOctal(header, 136, 12, 0);
header.fill(0x20, 148, 156);
header[156] = "0".charCodeAt(0);
writeTarString(header, 257, 6, "ustar\0");
writeTarString(header, 263, 2, "00");
const checksum = header.reduce((sum, byte) => sum + byte, 0);
writeTarString(header, 148, 8, `${checksum.toString(8).padStart(6, "0")}\0 `);
const padding = Buffer.alloc((512 - (content.length % 512)) % 512);
return Buffer.concat([header, content, padding]);
}
export function createPlaceholderTarball(packageName) {
if (!PACKAGE_NAME_RE.test(packageName)) {
throw new Error(`Invalid OpenClaw package name: ${packageName}`);
}
const packageJson = Buffer.from(canonicalJson(placeholderPackageJson(packageName)), "utf8");
const readme = Buffer.from(placeholderReadme(packageName), "utf8");
const tar = Buffer.concat([
tarEntry("package/package.json", packageJson),
tarEntry("package/README.md", readme),
Buffer.alloc(1024),
]);
return gzipSync(tar, { level: 9, mtime: 0 });
}
export function parseSelectedPackages(input) {
const raw = assertTrimmedString(input, "package selection");
const packages = raw.split(",").map((value) => value.trim());
if (packages.some((value) => value.length === 0)) {
throw new Error("Package selection must not contain empty entries.");
}
for (const packageName of packages) {
if (!PACKAGE_NAME_RE.test(packageName)) {
throw new Error(`Invalid OpenClaw package name: ${packageName}`);
}
}
if (new Set(packages).size !== packages.length) {
throw new Error("Package selection must not contain duplicates.");
}
return packages;
}
function readJsonRegularFile(path, label) {
const info = lstatSync(path);
if (!info.isFile() || info.isSymbolicLink() || info.size === 0 || info.size > MAX_FILE_BYTES) {
throw new Error(`${label} must be a bounded regular file.`);
}
let value;
try {
value = JSON.parse(readFileSync(path, "utf8"));
} catch (error) {
const detail = error instanceof Error ? error.message : String(error);
throw new Error(`${label} is invalid JSON: ${detail}`, { cause: error });
}
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new Error(`${label} must contain an object.`);
}
return value;
}
export function resolveSelectedPackageSources(repoRoot, packageNames) {
const extensionsDir = resolve(repoRoot, "extensions");
const officialPackageNames = new Set();
for (const catalogName of ["plugin", "provider", "channel"]) {
const catalogPath = resolve(
repoRoot,
"scripts",
"lib",
`official-external-${catalogName}-catalog.json`,
);
const catalog = readJsonRegularFile(catalogPath, `Official external ${catalogName} catalog`);
if (!Array.isArray(catalog.entries)) {
throw new Error(`Official external ${catalogName} catalog entries must be an array.`);
}
for (const entry of catalog.entries) {
if (
entry?.source === "official" &&
typeof entry.name === "string" &&
entry.openclaw?.install?.npmSpec === entry.name
) {
officialPackageNames.add(entry.name);
}
}
}
const candidates = [];
for (const entry of readdirSync(extensionsDir, { withFileTypes: true })) {
if (!entry.isDirectory() || entry.isSymbolicLink()) {
continue;
}
const packageDir = `extensions/${entry.name}`;
const packageJsonPath = resolve(repoRoot, packageDir, "package.json");
let info;
try {
info = lstatSync(packageJsonPath);
} catch (error) {
if (error && typeof error === "object" && error.code === "ENOENT") {
continue;
}
throw error;
}
if (!info.isFile() || info.isSymbolicLink()) {
throw new Error(`${packageDir}/package.json must be a regular file.`);
}
const packageJsonBytes = readFileSync(packageJsonPath);
const packageJson = readJsonRegularFile(packageJsonPath, `${packageDir}/package.json`);
candidates.push({ packageDir, packageJson, packageJsonBytes });
}
return packageNames.map((packageName) => {
const matches = candidates.filter((candidate) => candidate.packageJson.name === packageName);
if (matches.length !== 1) {
throw new Error(`${packageName} must map uniquely to extensions/*/package.json.`);
}
const [match] = matches;
if (
match.packageJson.private === true ||
["private", "restricted"].includes(match.packageJson.publishConfig?.access) ||
match.packageJson.openclaw?.install?.npmSpec !== packageName ||
match.packageJson.openclaw?.build?.bundledDist !== false ||
match.packageJson.openclaw?.release?.publishToNpm !== true
) {
throw new Error(`${packageName} is not a public release-enabled npm plugin.`);
}
if (!officialPackageNames.has(packageName)) {
throw new Error(`${packageName} is not present in an official external package catalog.`);
}
return {
packageDir: match.packageDir,
packageName,
sourcePackageJsonSha256: sha256(match.packageJsonBytes),
};
});
}
function normalizeDistTags(value, packageName) {
if (value === undefined) {
return {};
}
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new Error(`${packageName}: npm dist-tags must be an object.`);
}
const entries = [];
for (const [tag, version] of Object.entries(value)) {
if (
typeof tag !== "string" ||
tag.length === 0 ||
typeof version !== "string" ||
version.length === 0
) {
throw new Error(`${packageName}: npm dist-tags contain an invalid entry.`);
}
entries.push([tag, version]);
}
return Object.fromEntries(entries.toSorted(([left], [right]) => compareCodeUnits(left, right)));
}
export function classifyRegistryState(params) {
const { expectedIntegrity, expectedShasum, packageName, registry } = params;
if (registry.status === 404) {
return { action: "publish", newPackage: true, nonPlaceholderTags: {} };
}
if (registry.status !== 200 || !registry.packument || typeof registry.packument !== "object") {
throw new Error(`${packageName}: npm registry returned HTTP ${registry.status}.`);
}
const distTags = normalizeDistTags(registry.packument["dist-tags"], packageName);
if (distTags[PUBLISH_TAG] !== undefined && distTags[PUBLISH_TAG] !== PACKAGE_VERSION) {
throw new Error(
`${packageName}: placeholder dist-tag points to ${distTags[PUBLISH_TAG]}, expected ${PACKAGE_VERSION}.`,
);
}
const nonPlaceholderTags = Object.fromEntries(
Object.entries(distTags).filter(([tag]) => tag !== PUBLISH_TAG),
);
const publishedDist = registry.packument.versions?.[PACKAGE_VERSION]?.dist;
if (publishedDist !== undefined) {
if (
publishedDist?.integrity !== expectedIntegrity ||
publishedDist?.shasum !== expectedShasum
) {
throw new Error(`${packageName}@${PACKAGE_VERSION}: npm registry tarball bytes differ.`);
}
return {
action: distTags[PUBLISH_TAG] === PACKAGE_VERSION ? "skip" : "tag",
newPackage: false,
nonPlaceholderTags,
};
}
return { action: "publish", newPackage: false, nonPlaceholderTags };
}
async function readRegistry(packageName, fetchImpl = fetch) {
const result = await fetchNpmRegistryPackumentWithRetry({
packageName,
packageUrl: `https://registry.npmjs.org/${encodeURIComponent(packageName)}`,
fetchImpl,
});
return { packument: result.packument, status: result.status };
}
async function readStableRegistry(packageName, fetchImpl = fetch) {
const observations = [];
for (let attempt = 0; attempt < 3; attempt += 1) {
observations.push(await readRegistry(packageName, fetchImpl));
}
const serialized = observations.map((observation) => JSON.stringify(observation));
if (new Set(serialized).size !== 1) {
throw new Error(`${packageName}: npm registry state changed during placeholder planning.`);
}
return observations[0];
}
function assertFreshDirectory(path) {
try {
lstatSync(path);
throw new Error(`Output directory already exists: ${path}`);
} catch (error) {
if (!(error && typeof error === "object" && error.code === "ENOENT")) {
throw error;
}
}
mkdirSync(path, { recursive: true, mode: 0o700 });
}
export async function createPlaceholderPublication(params) {
const repoRoot = resolve(params.repoRoot);
const outputDir = resolve(params.outputDir);
const targetSha = assertCommitSha(params.targetSha, "target SHA");
const workflowSha = assertCommitSha(params.workflowSha, "workflow SHA");
const packageNames = parseSelectedPackages(params.packages);
const sources = resolveSelectedPackageSources(repoRoot, packageNames);
assertFreshDirectory(outputDir);
const packages = [];
for (const source of sources) {
const tarball = createPlaceholderTarball(source.packageName);
const tarballName = `${source.packageName.slice(1).replaceAll("/", "-")}-${PACKAGE_VERSION}.tgz`;
const identity = {
integrity: npmIntegrity(tarball),
sha256: sha256(tarball),
shasum: npmShasum(tarball),
sizeBytes: tarball.length,
};
const registry = await readStableRegistry(source.packageName, params.fetchImpl);
const state = classifyRegistryState({
expectedIntegrity: identity.integrity,
expectedShasum: identity.shasum,
packageName: source.packageName,
registry,
});
writeFileSync(join(outputDir, tarballName), tarball, { flag: "wx", mode: 0o600 });
packages.push({
...source,
action: state.action,
newPackage: state.newPackage,
preExistingDistTags: state.nonPlaceholderTags,
tarball: { name: tarballName, ...identity },
});
}
const manifest = {
schema: MANIFEST_SCHEMA,
targetSha,
workflowPath: WORKFLOW_PATH,
workflowSha,
version: PACKAGE_VERSION,
publishTag: PUBLISH_TAG,
packages,
};
writeFileSync(join(outputDir, MANIFEST_FILENAME), canonicalJson(manifest), {
flag: "wx",
mode: 0o600,
});
return manifest;
}
function validateManifest(value, params) {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new Error("Placeholder manifest must be an object.");
}
const topLevelKeys = Object.keys(value).toSorted();
if (
JSON.stringify(topLevelKeys) !==
JSON.stringify([
"packages",
"publishTag",
"schema",
"targetSha",
"version",
"workflowPath",
"workflowSha",
]) ||
value.schema !== MANIFEST_SCHEMA ||
value.targetSha !== params.targetSha ||
value.workflowSha !== params.workflowSha ||
value.workflowPath !== WORKFLOW_PATH ||
value.version !== PACKAGE_VERSION ||
value.publishTag !== PUBLISH_TAG ||
!Array.isArray(value.packages) ||
value.packages.length === 0
) {
throw new Error("Placeholder manifest identity does not match the approved publication.");
}
const names = value.packages.map((entry) => entry?.packageName);
if (new Set(names).size !== names.length || names.some((name) => !PACKAGE_NAME_RE.test(name))) {
throw new Error("Placeholder manifest package inventory is invalid.");
}
for (const entry of value.packages) {
const entryKeys = Object.keys(entry).toSorted();
if (
JSON.stringify(entryKeys) !==
JSON.stringify([
"action",
"newPackage",
"packageDir",
"packageName",
"preExistingDistTags",
"sourcePackageJsonSha256",
"tarball",
])
) {
throw new Error(`${entry.packageName}: placeholder manifest entry shape is invalid.`);
}
const expectedTarballName = `${entry.packageName.slice(1).replaceAll("/", "-")}-${PACKAGE_VERSION}.tgz`;
const tarballKeys =
entry.tarball && typeof entry.tarball === "object"
? Object.keys(entry.tarball).toSorted()
: [];
if (
!["publish", "skip", "tag"].includes(entry.action) ||
typeof entry.newPackage !== "boolean" ||
!/^extensions\/[a-z0-9][a-z0-9._-]*$/u.test(entry.packageDir) ||
!SHA256_RE.test(entry.sourcePackageJsonSha256) ||
JSON.stringify(tarballKeys) !==
JSON.stringify(["integrity", "name", "sha256", "shasum", "sizeBytes"]) ||
entry.tarball.name !== expectedTarballName ||
!/^sha512-[A-Za-z0-9+/]+={0,2}$/u.test(entry.tarball.integrity) ||
!SHA256_RE.test(entry.tarball.sha256) ||
!/^[0-9a-f]{40}$/u.test(entry.tarball.shasum) ||
!Number.isSafeInteger(entry.tarball.sizeBytes) ||
entry.tarball.sizeBytes <= 0 ||
entry.tarball.sizeBytes > MAX_FILE_BYTES
) {
throw new Error(`${entry.packageName}: placeholder manifest package binding is invalid.`);
}
const normalizedTags = normalizeDistTags(entry.preExistingDistTags, entry.packageName);
if (
Object.hasOwn(entry.preExistingDistTags, PUBLISH_TAG) ||
JSON.stringify(normalizedTags) !== JSON.stringify(entry.preExistingDistTags) ||
(entry.newPackage &&
(entry.action !== "publish" || Object.keys(entry.preExistingDistTags).length !== 0)) ||
((entry.action === "tag" || entry.action === "skip") && entry.newPackage)
) {
throw new Error(
`${entry.packageName}: placeholder manifest dist-tag snapshot is not canonical.`,
);
}
}
return value;
}
export async function verifyPlaceholderArtifact(params) {
const targetSha = assertCommitSha(params.targetSha, "target SHA");
const workflowSha = assertCommitSha(params.workflowSha, "workflow SHA");
const artifactName = assertTrimmedString(params.artifactName, "artifact name");
const runId = Number(params.runId);
const producerRunAttempt = Number(params.producerRunAttempt);
if (artifactName !== `npm-placeholder-publication-${runId}-${producerRunAttempt}`) {
throw new Error("Placeholder artifact name does not match its workflow run and attempt.");
}
const expected = {
artifactDigest: assertTrimmedString(params.artifactDigest, "artifact digest"),
artifactId: Number(params.artifactId),
artifactName,
artifactSizeBytes: Number(params.artifactSizeBytes),
consumerRunAttempt: Number(params.consumerRunAttempt),
producerJobName: "Plan npm placeholder publication",
repository: assertTrimmedString(params.repository, "repository"),
runAttempt: producerRunAttempt,
runId,
runStatePolicy: "same-run-producer-success",
workflowEvent: "workflow_dispatch",
workflowHeadBranch: "main",
workflowPath: WORKFLOW_PATH,
workflowSha,
};
const archive = await readPublicationArtifactArchive({
archivePolicy: {
minEntries: 2,
maxEntries: 101,
maxArchiveBytes: MAX_ARTIFACT_BYTES,
maxExpandedBytes: MAX_ARTIFACT_BYTES,
allowPath: (name) =>
basename(name) === name &&
(name === MANIFEST_FILENAME || /^[A-Za-z0-9._-]+\.tgz$/u.test(name)),
maxEntryBytes: (name) => (name === MANIFEST_FILENAME ? MAX_FILE_BYTES : MAX_FILE_BYTES),
},
expected,
maxArchiveBytes: MAX_ARTIFACT_BYTES,
token: assertTrimmedString(params.token, "GitHub token"),
});
const manifestBytes = archive.files.get(MANIFEST_FILENAME);
if (!manifestBytes) {
throw new Error("Placeholder artifact is missing its manifest.");
}
const manifest = validateManifest(JSON.parse(manifestBytes.toString("utf8")), {
targetSha,
workflowSha,
});
if (archive.files.size !== manifest.packages.length + 1) {
throw new Error("Placeholder artifact file count does not match its manifest.");
}
const sources = resolveSelectedPackageSources(
params.targetRoot,
manifest.packages.map((entry) => entry.packageName),
);
const sourceByName = new Map(sources.map((source) => [source.packageName, source]));
const outputDir = resolve(params.outputDir);
assertFreshDirectory(outputDir);
for (const entry of manifest.packages) {
const source = sourceByName.get(entry.packageName);
if (
source?.packageDir !== entry.packageDir ||
source?.sourcePackageJsonSha256 !== entry.sourcePackageJsonSha256 ||
!SHA256_RE.test(entry.tarball?.sha256)
) {
throw new Error(`${entry.packageName}: source package binding changed.`);
}
const tarball = archive.files.get(entry.tarball.name);
const expectedTarball = createPlaceholderTarball(entry.packageName);
if (
!tarball ||
!tarball.equals(expectedTarball) ||
entry.tarball.sizeBytes !== tarball.length ||
entry.tarball.sha256 !== sha256(tarball) ||
entry.tarball.integrity !== npmIntegrity(tarball) ||
entry.tarball.shasum !== npmShasum(tarball)
) {
throw new Error(`${entry.packageName}: placeholder tarball is not canonical.`);
}
writeFileSync(join(outputDir, entry.tarball.name), tarball, { flag: "wx", mode: 0o600 });
}
writeFileSync(join(outputDir, MANIFEST_FILENAME), canonicalJson(manifest), {
flag: "wx",
mode: 0o600,
});
return { artifactSha256: sha256Digest(archive.archiveBytes), manifest };
}
function sameTags(left, right) {
return JSON.stringify(left) === JSON.stringify(right);
}
export function assertFinalRegistryState(entry, registry) {
const state = classifyRegistryState({
expectedIntegrity: entry.tarball.integrity,
expectedShasum: entry.tarball.shasum,
packageName: entry.packageName,
registry,
});
if (state.action !== "skip") {
throw new Error(`${entry.packageName}: placeholder publication did not reach its final state.`);
}
if (!sameTags(state.nonPlaceholderTags, entry.preExistingDistTags)) {
throw new Error(`${entry.packageName}: a non-placeholder dist-tag changed during publication.`);
}
}
function defaultNpmRunner(args, options) {
const result = spawnSync("npm", args, {
cwd: options.cwd,
encoding: "utf8",
env: options.env,
killSignal: "SIGTERM",
stdio: ["ignore", "pipe", "pipe"],
timeout: 300_000,
});
if (result.status !== 0) {
throw new Error(
`npm ${args[0]} failed (${result.status ?? "signal"}): ${(result.stderr || result.stdout).trim()}`,
);
}
}
async function readFinalRegistry(entry, params) {
const attempts = params.registryAttempts ?? 8;
const sleep =
params.sleep ??
((delayMs) =>
new Promise((done) => {
setTimeout(done, delayMs);
}));
let lastError;
for (let attempt = 1; attempt <= attempts; attempt += 1) {
try {
const registry = await readRegistry(entry.packageName, params.fetchImpl);
assertFinalRegistryState(entry, registry);
return;
} catch (error) {
lastError = error;
if (attempt < attempts) {
await sleep(attempt * 1000);
}
}
}
throw lastError;
}
export async function publishPlaceholders(params) {
const artifactDir = resolve(params.artifactDir);
const manifest = validateManifest(
readJsonRegularFile(join(artifactDir, MANIFEST_FILENAME), "Placeholder manifest"),
{
targetSha: params.targetSha,
workflowSha: params.workflowSha,
},
);
const npmToken = assertTrimmedString(params.npmToken, "NPM token");
const publishHome = mkdtempSync(
join(resolve(params.tempRoot ?? tmpdir()), "openclaw-npm-placeholder-"),
);
try {
const npmrc = join(publishHome, "npmrc");
writeFileSync(
npmrc,
`registry=https://registry.npmjs.org/\n//registry.npmjs.org/:_authToken=${npmToken}\n`,
{
mode: 0o600,
},
);
const childEnv = { ...process.env };
delete childEnv.NPM_TOKEN;
delete childEnv.NODE_AUTH_TOKEN;
delete childEnv.NODE_OPTIONS;
Object.assign(childEnv, {
HOME: publishHome,
NPM_CONFIG_GLOBALCONFIG: "/dev/null",
NPM_CONFIG_IGNORE_SCRIPTS: "true",
NPM_CONFIG_REGISTRY: "https://registry.npmjs.org/",
NPM_CONFIG_USERCONFIG: npmrc,
});
const npmRunner = params.npmRunner ?? defaultNpmRunner;
const results = [];
for (const entry of manifest.packages) {
const current = await readRegistry(entry.packageName, params.fetchImpl);
const state = classifyRegistryState({
expectedIntegrity: entry.tarball.integrity,
expectedShasum: entry.tarball.shasum,
packageName: entry.packageName,
registry: current,
});
if (!sameTags(state.nonPlaceholderTags, entry.preExistingDistTags)) {
throw new Error(`${entry.packageName}: npm dist-tags changed after the immutable plan.`);
}
let mutationError;
try {
if (state.action === "publish") {
npmRunner(
[
"publish",
join(artifactDir, entry.tarball.name),
"--access",
"public",
"--ignore-scripts",
"--provenance",
"--tag",
PUBLISH_TAG,
],
{ cwd: artifactDir, env: childEnv },
);
} else if (state.action === "tag") {
npmRunner(["dist-tag", "add", `${entry.packageName}@${PACKAGE_VERSION}`, PUBLISH_TAG], {
cwd: artifactDir,
env: childEnv,
});
}
} catch (error) {
mutationError = error;
}
try {
await readFinalRegistry(entry, params);
} catch (readbackError) {
if (mutationError) {
throw new Error(
`${entry.packageName}: npm mutation failed and exact registry readback did not converge: ${mutationError.message}`,
{ cause: readbackError },
);
}
throw readbackError;
}
results.push({
action: state.action,
newPackage: entry.newPackage,
packageName: entry.packageName,
});
}
return { results };
} finally {
rmSync(publishHome, { force: true, recursive: true });
}
}
function parseCliArgs(argv) {
const [command, ...rest] = argv;
if (!["create", "verify-artifact", "publish"].includes(command)) {
throw new Error("Usage: npm-placeholder-publication.mjs <create|verify-artifact|publish> ...");
}
const values = {};
for (let index = 0; index < rest.length; index += 2) {
const key = rest[index];
const value = rest[index + 1];
if (!key?.startsWith("--") || value === undefined || value.startsWith("--")) {
throw new Error(`Invalid ${command} argument near ${key ?? "<missing>"}.`);
}
const name = key.slice(2).replace(/-([a-z])/gu, (_, letter) => letter.toUpperCase());
if (values[name] !== undefined) {
throw new Error(`Duplicate ${command} option: ${key}`);
}
values[name] = value;
}
return { command, values };
}
function appendGithubOutput(path, values) {
if (!path) {
return;
}
writeFileSync(
path,
`${Object.entries(values)
.map(([key, value]) => `${key}=${value}`)
.join("\n")}\n`,
{ flag: "a" },
);
}
export async function main(argv = process.argv.slice(2)) {
const { command, values } = parseCliArgs(argv);
if (command === "create") {
const manifest = await createPlaceholderPublication({
outputDir: values.outputDir,
packages: values.packages,
repoRoot: values.repoRoot,
targetSha: values.targetSha,
workflowSha: values.workflowSha,
});
appendGithubOutput(values.githubOutput, {
existing_without_zero_count: manifest.packages.filter(
(entry) => !entry.newPackage && entry.action === "publish",
).length,
new_package_count: manifest.packages.filter((entry) => entry.newPackage).length,
package_count: manifest.packages.length,
});
return;
}
if (command === "verify-artifact") {
const result = await verifyPlaceholderArtifact({
artifactDigest: values.artifactDigest,
artifactId: values.artifactId,
artifactName: values.artifactName,
artifactSizeBytes: values.artifactSizeBytes,
consumerRunAttempt: values.consumerRunAttempt,
outputDir: values.outputDir,
producerRunAttempt: values.producerRunAttempt,
repository: values.repository,
runId: values.runId,
targetRoot: values.targetRoot,
targetSha: values.targetSha,
token: process.env.GH_TOKEN,
workflowSha: values.workflowSha,
});
appendGithubOutput(values.githubOutput, { artifact_sha256: result.artifactSha256 });
return;
}
const result = await publishPlaceholders({
artifactDir: values.artifactDir,
npmToken: process.env.NPM_TOKEN,
targetSha: values.targetSha,
workflowSha: values.workflowSha,
});
const resultPath = resolve(values.resultPath);
writeFileSync(resultPath, canonicalJson(result), { flag: "wx", mode: 0o600 });
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
main().catch(
/** @param {unknown} error */ (error) => {
console.error(error instanceof Error ? error.message : String(error));
process.exitCode = 1;
},
);
}
@@ -0,0 +1,602 @@
import { createHash } from "node:crypto";
import { mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { parse } from "yaml";
import {
assertFinalRegistryState,
classifyRegistryState,
createPlaceholderPublication,
createPlaceholderTarball,
parseSelectedPackages,
publishPlaceholders,
resolveSelectedPackageSources,
verifyPlaceholderArtifact,
} from "../../scripts/npm-placeholder-publication.mjs";
import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js";
const SHA = "a".repeat(40);
const WORKFLOW_SHA = "b".repeat(40);
const WORKFLOW = ".github/workflows/npm-placeholder-bootstrap.yml";
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
function packageJson(name: string) {
return {
name,
openclaw: {
install: {
npmSpec: name,
},
build: {
bundledDist: false,
},
release: {
publishToClawHub: true,
publishToNpm: true,
},
},
};
}
function createRepo(packages: Array<{ dir: string; manifest: Record<string, unknown> }>) {
const root = tempDirs.make("npm-placeholder-repo-");
for (const entry of packages) {
const dir = join(root, "extensions", entry.dir);
mkdirSync(dir, { recursive: true });
writeFileSync(join(dir, "package.json"), `${JSON.stringify(entry.manifest)}\n`);
}
const catalogDir = join(root, "scripts", "lib");
mkdirSync(catalogDir, { recursive: true });
writeFileSync(
join(catalogDir, "official-external-plugin-catalog.json"),
`${JSON.stringify({
entries: packages.map((entry) => ({
name: entry.manifest.name,
source: "official",
openclaw: { install: { npmSpec: entry.manifest.name } },
})),
})}\n`,
);
for (const name of ["provider", "channel"]) {
writeFileSync(join(catalogDir, `official-external-${name}-catalog.json`), '{"entries":[]}\n');
}
return root;
}
function registryResponse(packument?: Record<string, unknown>) {
if (packument === undefined) {
return new Response("", { status: 404 });
}
return new Response(JSON.stringify(packument), {
status: 200,
headers: { "content-type": "application/json" },
});
}
function identity(packageName: string) {
const tarball = createPlaceholderTarball(packageName);
return {
integrity: `sha512-${createHash("sha512").update(tarball).digest("base64")}`,
shasum: createHash("sha1").update(tarball).digest("hex"),
};
}
describe("npm placeholder publication", () => {
it("preserves selected multi-package order and binds unique release-enabled manifests", async () => {
const names = ["@openclaw/zoom-meetings", "@openclaw/comfy-provider"] as const;
const root = createRepo([
{ dir: "comfy", manifest: packageJson(names[1]) },
{ dir: "zoom-meetings", manifest: packageJson(names[0]) },
]);
const outputDir = join(tempDirs.make("npm-placeholder-output-parent-"), "publication");
const manifest = await createPlaceholderPublication({
repoRoot: root,
outputDir,
packages: names.join(","),
targetSha: SHA,
workflowSha: WORKFLOW_SHA,
fetchImpl: async () => registryResponse(),
});
expect(manifest.packages.map((entry) => entry.packageName)).toEqual(names);
expect(manifest.packages.map((entry) => entry.packageDir)).toEqual([
"extensions/zoom-meetings",
"extensions/comfy",
]);
expect(manifest.packages.every((entry) => entry.action === "publish")).toBe(true);
expect(manifest.packages.every((entry) => entry.newPackage)).toBe(true);
expect(readFileSync(join(outputDir, "npm-placeholder-manifest.json"), "utf8")).toContain(
`"targetSha": "${SHA}"`,
);
});
it("creates deterministic canonical two-file placeholder tarballs", () => {
const first = createPlaceholderTarball("@openclaw/comfy-provider");
const second = createPlaceholderTarball("@openclaw/comfy-provider");
expect(first).toEqual(second);
expect(first.subarray(0, 2)).toEqual(Buffer.from([0x1f, 0x8b]));
});
it("requires three stable registry observations during planning", async () => {
const packageName = "@openclaw/comfy-provider";
const root = createRepo([{ dir: "comfy", manifest: packageJson(packageName) }]);
const responses = [
registryResponse(),
registryResponse({ "dist-tags": {}, versions: {} }),
registryResponse(),
];
await expect(
createPlaceholderPublication({
repoRoot: root,
outputDir: join(tempDirs.make("npm-placeholder-unstable-parent-"), "publication"),
packages: packageName,
targetSha: SHA,
workflowSha: WORKFLOW_SHA,
fetchImpl: async () => responses.shift() ?? registryResponse(),
}),
).rejects.toThrow("npm registry state changed during placeholder planning");
});
it("classifies E404, existing-version backfill, and exact idempotent reruns", () => {
const packageName = "@openclaw/meta-provider";
const expected = identity(packageName);
expect(
classifyRegistryState({
expectedIntegrity: expected.integrity,
expectedShasum: expected.shasum,
packageName,
registry: { status: 404, packument: null },
}),
).toEqual({ action: "publish", newPackage: true, nonPlaceholderTags: {} });
expect(
classifyRegistryState({
expectedIntegrity: expected.integrity,
expectedShasum: expected.shasum,
packageName,
registry: {
status: 200,
packument: {
"dist-tags": { latest: "2026.7.2", beta: "2026.7.2-beta.7" },
versions: { "2026.7.2": {} },
},
},
}),
).toEqual({
action: "publish",
newPackage: false,
nonPlaceholderTags: { beta: "2026.7.2-beta.7", latest: "2026.7.2" },
});
expect(
classifyRegistryState({
expectedIntegrity: expected.integrity,
expectedShasum: expected.shasum,
packageName,
registry: {
status: 200,
packument: {
"dist-tags": { placeholder: "0.0.0", latest: "2026.7.2" },
versions: { "0.0.0": { dist: expected } },
},
},
}),
).toEqual({
action: "skip",
newPackage: false,
nonPlaceholderTags: { latest: "2026.7.2" },
});
expect(
classifyRegistryState({
expectedIntegrity: expected.integrity,
expectedShasum: expected.shasum,
packageName,
registry: {
status: 200,
packument: {
"dist-tags": { latest: "2026.7.2" },
versions: { "0.0.0": { dist: expected } },
},
},
}),
).toEqual({
action: "tag",
newPackage: false,
nonPlaceholderTags: { latest: "2026.7.2" },
});
});
it("rejects mismatched 0.0.0 bytes and conflicting placeholder tags", () => {
const packageName = "@openclaw/duckduckgo-plugin";
const expected = identity(packageName);
expect(() =>
classifyRegistryState({
expectedIntegrity: expected.integrity,
expectedShasum: expected.shasum,
packageName,
registry: {
status: 200,
packument: {
"dist-tags": { placeholder: "0.0.0" },
versions: {
"0.0.0": { dist: { integrity: "sha512-different", shasum: "different" } },
},
},
},
}),
).toThrow("npm registry tarball bytes differ");
expect(() =>
classifyRegistryState({
expectedIntegrity: expected.integrity,
expectedShasum: expected.shasum,
packageName,
registry: {
status: 200,
packument: {
"dist-tags": { placeholder: "1.2.3" },
versions: {},
},
},
}),
).toThrow("placeholder dist-tag points to 1.2.3");
});
it("publishes serially and preserves every non-placeholder dist-tag", async () => {
const names = ["@openclaw/byteplus-provider", "@openclaw/meta-provider"] as const;
const root = createRepo([
{ dir: "byteplus", manifest: packageJson(names[0]) },
{ dir: "meta", manifest: packageJson(names[1]) },
]);
const artifactDir = join(tempDirs.make("npm-placeholder-artifact-parent-"), "artifact");
const existingMeta = registryResponse({
"dist-tags": { latest: "2026.7.2", beta: "2026.7.2-beta.7" },
versions: { "2026.7.2": {} },
});
const manifest = await createPlaceholderPublication({
repoRoot: root,
outputDir: artifactDir,
packages: names.join(","),
targetSha: SHA,
workflowSha: WORKFLOW_SHA,
fetchImpl: async (input) =>
String(input).includes(encodeURIComponent(names[0]))
? registryResponse()
: existingMeta.clone(),
});
expect(manifest.packages).toHaveLength(2);
const [firstPackage, secondPackage] = manifest.packages;
if (!firstPackage || !secondPackage) {
throw new Error("Expected two placeholder manifest packages.");
}
const firstIdentity = firstPackage.tarball;
const secondIdentity = secondPackage.tarball;
const publishResponses = [
registryResponse(),
registryResponse({
"dist-tags": { placeholder: "0.0.0" },
versions: { "0.0.0": { dist: firstIdentity } },
}),
registryResponse({
"dist-tags": { latest: "2026.7.2", beta: "2026.7.2-beta.7" },
versions: { "2026.7.2": {} },
}),
registryResponse({
"dist-tags": {
latest: "2026.7.2",
beta: "2026.7.2-beta.7",
placeholder: "0.0.0",
},
versions: {
"0.0.0": { dist: secondIdentity },
"2026.7.2": {},
},
}),
];
const calls: string[][] = [];
const tempRoot = tempDirs.make("npm-placeholder-token-success-");
const result = await publishPlaceholders({
artifactDir,
npmToken: "test-token",
targetSha: SHA,
workflowSha: WORKFLOW_SHA,
fetchImpl: async () => publishResponses.shift() ?? registryResponse(),
npmRunner: (args) => calls.push(args),
registryAttempts: 1,
sleep: async () => undefined,
tempRoot,
});
expect(calls.map((args) => args[0])).toEqual(["publish", "publish"]);
expect(
calls.map((args) => {
const tarballPath = args[1];
if (!tarballPath) {
throw new Error("Expected npm publish tarball argument.");
}
return basenameForTarball(tarballPath);
}),
).toEqual(manifest.packages.map((entry) => entry.tarball.name));
expect(result.results.map((entry) => entry.packageName)).toEqual(names);
expect(readdirSync(tempRoot)).toEqual([]);
});
it("rejects malicious package input, duplicate identity, and unsafe manifest paths", () => {
expect(() => parseSelectedPackages("@openclaw/good,../../evil")).toThrow(
"Invalid OpenClaw package name",
);
expect(() => parseSelectedPackages("@openclaw/good,@openclaw/good")).toThrow("duplicates");
const duplicateRoot = createRepo([
{ dir: "one", manifest: packageJson("@openclaw/good") },
{ dir: "two", manifest: packageJson("@openclaw/good") },
]);
expect(() => resolveSelectedPackageSources(duplicateRoot, ["@openclaw/good"])).toThrow(
"must map uniquely",
);
const unsafeRoot = createRepo([{ dir: "good", manifest: packageJson("@openclaw/good") }]);
writeFileSync(join(unsafeRoot, "extensions", "good", "package.json"), "{}\n");
expect(() => resolveSelectedPackageSources(unsafeRoot, ["@openclaw/good"])).toThrow(
"must map uniquely",
);
const privateRoot = createRepo([
{
dir: "private",
manifest: {
...packageJson("@openclaw/private"),
publishConfig: { access: "private" },
},
},
]);
expect(() => resolveSelectedPackageSources(privateRoot, ["@openclaw/private"])).toThrow(
"not a public release-enabled npm plugin",
);
});
it("fails final verification when any pre-existing dist-tag changes", () => {
const packageName = "@openclaw/meta-provider";
const expected = identity(packageName);
const entry = {
packageDir: "extensions/meta",
packageName,
sourcePackageJsonSha256: "c".repeat(64),
action: "publish" as const,
newPackage: false,
preExistingDistTags: { latest: "2026.7.2" },
tarball: {
name: "openclaw-meta-provider-0.0.0.tgz",
sha256: "d".repeat(64),
sizeBytes: 1,
...expected,
},
};
expect(() =>
assertFinalRegistryState(entry, {
status: 200,
packument: {
"dist-tags": { latest: "2026.7.1", placeholder: "0.0.0" },
versions: { "0.0.0": { dist: expected } },
},
}),
).toThrow("non-placeholder dist-tag changed");
});
it("rejects manifest state combinations that could lie about registry creation", async () => {
const packageName = "@openclaw/comfy-provider";
const root = createRepo([{ dir: "comfy", manifest: packageJson(packageName) }]);
const artifactDir = join(tempDirs.make("npm-placeholder-invalid-parent-"), "artifact");
await createPlaceholderPublication({
repoRoot: root,
outputDir: artifactDir,
packages: packageName,
targetSha: SHA,
workflowSha: WORKFLOW_SHA,
fetchImpl: async () => registryResponse(),
});
const manifestPath = join(artifactDir, "npm-placeholder-manifest.json");
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
manifest.packages[0].action = "skip";
writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
await expect(
publishPlaceholders({
artifactDir,
npmToken: "test-token",
targetSha: SHA,
workflowSha: WORKFLOW_SHA,
fetchImpl: async () => registryResponse(),
registryAttempts: 1,
sleep: async () => undefined,
}),
).rejects.toThrow("dist-tag snapshot is not canonical");
});
it("removes token-bearing npm config after mutation and readback failure", async () => {
const packageName = "@openclaw/comfy-provider";
const root = createRepo([{ dir: "comfy", manifest: packageJson(packageName) }]);
const artifactDir = join(tempDirs.make("npm-placeholder-failure-parent-"), "artifact");
await createPlaceholderPublication({
repoRoot: root,
outputDir: artifactDir,
packages: packageName,
targetSha: SHA,
workflowSha: WORKFLOW_SHA,
fetchImpl: async () => registryResponse(),
});
const tempRoot = tempDirs.make("npm-placeholder-token-failure-");
await expect(
publishPlaceholders({
artifactDir,
npmToken: "test-token",
targetSha: SHA,
workflowSha: WORKFLOW_SHA,
fetchImpl: async () => registryResponse(),
npmRunner: () => {
throw new Error("ambiguous npm failure");
},
registryAttempts: 1,
sleep: async () => undefined,
tempRoot,
}),
).rejects.toThrow("mutation failed and exact registry readback did not converge");
expect(readdirSync(tempRoot)).toEqual([]);
});
it("repairs a missing placeholder tag and accepts exact readback after an ambiguous error", async () => {
const packageName = "@openclaw/meta-provider";
const root = createRepo([{ dir: "meta", manifest: packageJson(packageName) }]);
const expected = identity(packageName);
const before = {
"dist-tags": { latest: "2026.7.2" },
versions: { "0.0.0": { dist: expected } },
};
const artifactDir = join(tempDirs.make("npm-placeholder-tag-parent-"), "artifact");
await createPlaceholderPublication({
repoRoot: root,
outputDir: artifactDir,
packages: packageName,
targetSha: SHA,
workflowSha: WORKFLOW_SHA,
fetchImpl: async () => registryResponse(before),
});
const responses = [
registryResponse(before),
registryResponse({
"dist-tags": { latest: "2026.7.2", placeholder: "0.0.0" },
versions: { "0.0.0": { dist: expected } },
}),
];
const calls: string[][] = [];
const tempRoot = tempDirs.make("npm-placeholder-tag-token-");
const result = await publishPlaceholders({
artifactDir,
npmToken: "test-token",
targetSha: SHA,
workflowSha: WORKFLOW_SHA,
fetchImpl: async () => responses.shift() ?? registryResponse(),
npmRunner: (args) => {
calls.push(args);
throw new Error("ambiguous npm failure");
},
registryAttempts: 1,
sleep: async () => undefined,
tempRoot,
});
expect(calls).toEqual([["dist-tag", "add", `${packageName}@0.0.0`, "placeholder"]]);
expect(result.results).toEqual([{ action: "tag", newPackage: false, packageName }]);
expect(readdirSync(tempRoot)).toEqual([]);
});
it("preserves prototype-looking npm dist-tags as ordinary tag entries", () => {
const packageName = "@openclaw/meta-provider";
const expected = identity(packageName);
const distTags = JSON.parse(
'{"__proto__":"2026.7.1","latest":"2026.7.2","placeholder":"0.0.0"}',
);
expect(
classifyRegistryState({
expectedIntegrity: expected.integrity,
expectedShasum: expected.shasum,
packageName,
registry: {
status: 200,
packument: {
"dist-tags": distTags,
versions: { "0.0.0": { dist: expected } },
},
},
}),
).toEqual({
action: "skip",
newPackage: false,
nonPlaceholderTags: Object.fromEntries([
["__proto__", "2026.7.1"],
["latest", "2026.7.2"],
]),
});
});
it("keeps npm credentials isolated to the protected serial publish step", () => {
const workflow = parse(readFileSync(WORKFLOW, "utf8")) as {
concurrency?: { group?: string; "cancel-in-progress"?: boolean };
jobs?: Record<
string,
{
environment?: string;
permissions?: Record<string, string>;
steps?: Array<{ name?: string; env?: Record<string, string>; run?: string }>;
}
>;
};
const plan = workflow.jobs?.plan;
const verify = workflow.jobs?.verify;
const publish = workflow.jobs?.publish;
expect(workflow.concurrency).toEqual({
group: "npm-placeholder-release",
"cancel-in-progress": false,
});
expect(plan?.environment).toBeUndefined();
expect(JSON.stringify(plan)).not.toContain("NPM_TOKEN");
expect(verify?.environment).toBeUndefined();
expect(JSON.stringify(verify)).not.toContain("NPM_TOKEN");
expect(
verify?.steps?.find((step) => step.name === "Verify immutable placeholder publication"),
).toBeDefined();
expect(publish?.environment).toBe("npm-release");
expect(publish?.permissions).toMatchObject({
actions: "read",
contents: "read",
"id-token": "write",
});
const publishStep = publish?.steps?.find(
(step) => step.name === "Publish verified placeholders serially",
);
expect(publishStep?.env?.NPM_TOKEN).toBe("${{ secrets.NPM_TOKEN }}");
expect(readFileSync("scripts/npm-placeholder-publication.mjs", "utf8")).toContain(
'"--provenance"',
);
expect(publishStep?.run).not.toContain("npm trust");
expect(readFileSync(WORKFLOW, "utf8")).toContain("plugin-npm-release.yml");
const planSteps = plan?.steps ?? [];
const digestStep = planSteps.find((step) => step.name === "Bind immutable artifact digest");
expect(digestStep?.env?.RAW_DIGEST).toBe("${{ steps.upload.outputs.artifact-digest }}");
expect(digestStep?.run).toContain("digest=sha256:${RAW_DIGEST}");
for (const job of [verify, publish]) {
const metadataStep = job?.steps?.find(
(step) => step.name === "Resolve immutable artifact metadata",
);
expect(metadataStep?.run).toContain(".digest == $digest");
}
});
it("binds the artifact name to the exact workflow run and producer attempt", async () => {
await expect(
verifyPlaceholderArtifact({
artifactDigest: `sha256:${"a".repeat(64)}`,
artifactId: 1,
artifactName: "npm-placeholder-publication-123-2",
artifactSizeBytes: 1,
consumerRunAttempt: 1,
outputDir: "/tmp/unused",
producerRunAttempt: 1,
repository: "openclaw/openclaw",
runId: 123,
targetRoot: "/tmp/unused",
targetSha: SHA,
token: "test-token",
workflowSha: WORKFLOW_SHA,
}),
).rejects.toThrow("artifact name does not match its workflow run and attempt");
});
});
function basenameForTarball(path: string) {
return path.split("/").at(-1);
}