Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
281 changes: 281 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ on:
release:
types:
- published
workflow_dispatch:
inputs:
source_run_id:
description: Failed v0.1.0-alpha.1 Publish run containing the verified artifact
required: true
type: string

permissions:
contents: read
Expand All @@ -15,6 +21,7 @@ concurrency:
jobs:
verify:
name: Verify the immutable release artifact
if: github.event_name == 'release'
runs-on: ubuntu-latest
timeout-minutes: 30
outputs:
Expand Down Expand Up @@ -343,3 +350,277 @@ jobs:
process.exitCode = 1;
});
EOF

recover-verify:
name: Verify the failed alpha.1 publication source
if: github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
timeout-minutes: 10
outputs:
dist-tag: next
release-commit: ${{ steps.trust.outputs.release-commit }}
source-run-id: ${{ steps.trust.outputs.source-run-id }}
version: 0.1.0-alpha.1
permissions:
actions: read
contents: read
steps:
- name: Check out the recovery implementation
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 0
persist-credentials: false
- name: Reject an untrusted recovery source
id: trust
env:
EXPECTED_REPOSITORY: cometapi-dev/cometapi-node
EXPECTED_TAG: v0.1.0-alpha.1
GH_TOKEN: ${{ github.token }}
SOURCE_RUN_ID: ${{ inputs.source_run_id }}
shell: bash
run: |
set -euo pipefail
if [[ "$GITHUB_REPOSITORY" != "$EXPECTED_REPOSITORY" || \
"$GITHUB_REF" != "refs/heads/main" ]]; then
echo "Recovery is restricted to the canonical repository's main branch." >&2
exit 1
fi
if [[ ! "$SOURCE_RUN_ID" =~ ^[1-9][0-9]*$ ]]; then
echo "source_run_id must be a positive integer." >&2
exit 1
fi

git fetch --no-tags origin \
"+refs/tags/${EXPECTED_TAG}:refs/tags/${EXPECTED_TAG}" \
"+refs/heads/main:refs/remotes/origin/main"
release_commit="$(git rev-parse --verify "refs/tags/${EXPECTED_TAG}^{commit}")"
if ! git merge-base --is-ancestor "$release_commit" refs/remotes/origin/main; then
echo "The immutable release tag is not reachable from origin/main." >&2
exit 1
fi

release_json="$(gh api \
"repos/${GITHUB_REPOSITORY}/releases/tags/${EXPECTED_TAG}")"
run_json="$(gh api \
"repos/${GITHUB_REPOSITORY}/actions/runs/${SOURCE_RUN_ID}")"
jobs_json="$(gh api \
"repos/${GITHUB_REPOSITORY}/actions/runs/${SOURCE_RUN_ID}/jobs?per_page=100")"
artifacts_json="$(gh api \
"repos/${GITHUB_REPOSITORY}/actions/runs/${SOURCE_RUN_ID}/artifacts?per_page=100")"

RELEASE_JSON="$release_json" RUN_JSON="$run_json" \
JOBS_JSON="$jobs_json" ARTIFACTS_JSON="$artifacts_json" \
RELEASE_COMMIT="$release_commit" EXPECTED_TAG="$EXPECTED_TAG" node <<'EOF'
const release = JSON.parse(process.env.RELEASE_JSON);
const run = JSON.parse(process.env.RUN_JSON);
const jobs = JSON.parse(process.env.JOBS_JSON).jobs;
const artifacts = JSON.parse(process.env.ARTIFACTS_JSON).artifacts;
const expectedCommit = process.env.RELEASE_COMMIT;
const expectedTag = process.env.EXPECTED_TAG;

const reject = (message) => {
throw new Error(message);
};
if (
release.tag_name !== expectedTag ||
release.draft !== false ||
release.prerelease !== true ||
release.immutable !== true
) {
reject("Recovery requires the published immutable alpha.1 prerelease.");
}
if (
run.event !== "release" ||
run.path !== ".github/workflows/publish.yml" ||
run.head_branch !== expectedTag ||
run.head_sha !== expectedCommit ||
run.status !== "completed" ||
run.conclusion !== "failure"
) {
reject("The source run does not match the failed alpha.1 release workflow.");
}
const conclusions = new Map(jobs.map((job) => [job.name, job.conclusion]));
if (
conclusions.get("Verify the immutable release artifact") !== "success" ||
conclusions.get("Verify the release tag against CometAPI") !== "success" ||
conclusions.get(
"Publish with npm Trusted Publishing or alpha.1 bootstrap",
) !== "failure"
) {
reject("The source run does not have the required verify/live success boundary.");
}
const candidates = artifacts.filter(
(artifact) =>
artifact.name === "npm-package-0.1.0-alpha.1" &&
artifact.expired === false,
);
if (candidates.length !== 1 || !candidates[0].digest) {
reject("The source run must contain one unexpired verified alpha.1 artifact.");
}
EOF

echo "release-commit=${release_commit}" >> "$GITHUB_OUTPUT"
echo "source-run-id=${SOURCE_RUN_ID}" >> "$GITHUB_OUTPUT"

recover-publish:
name: Recover the verified alpha.1 npm publication
needs:
- recover-verify
runs-on: ubuntu-latest
timeout-minutes: 15
environment:
name: npm
url: https://www.npmjs.com/package/cometapi/v/0.1.0-alpha.1
permissions:
actions: read
contents: read
id-token: write
steps:
- name: Check out the reviewed recovery implementation
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Set up Node.js 24
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
with:
node-version: 24.x
registry-url: https://registry.npmjs.org
- name: Use a Trusted Publishing-capable npm CLI
run: npm install --global npm@11.12.1
- name: Download the original verified release artifact
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
github-token: ${{ github.token }}
name: npm-package-0.1.0-alpha.1
path: release-artifacts
repository: cometapi-dev/cometapi-node
run-id: ${{ needs.recover-verify.outputs.source-run-id }}
- name: Publish the exact recovered artifact with provenance
env:
ALPHA1_BOOTSTRAP_ENABLED: ${{ vars.NPM_ALPHA1_BOOTSTRAP_ENABLED }}
DIST_TAG: ${{ needs.recover-verify.outputs.dist-tag }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_ALPHA1_BOOTSTRAP_TOKEN }}
VERSION: ${{ needs.recover-verify.outputs.version }}
run: bash scripts/publish-artifact.sh
- name: Verify the recovered public registry artifact
env:
DIST_TAG: ${{ needs.recover-verify.outputs.dist-tag }}
VERSION: ${{ needs.recover-verify.outputs.version }}
shell: bash
run: |
set -euo pipefail
mapfile -t tarballs < <(find "$GITHUB_WORKSPACE/release-artifacts" -maxdepth 1 -type f -name '*.tgz' -print)
if [[ "${#tarballs[@]}" -ne 1 ]]; then
echo "Expected exactly one downloaded artifact for registry verification." >&2
exit 1
fi
local_integrity="$(node -e 'const {createHash}=require("node:crypto");const {readFileSync}=require("node:fs");process.stdout.write("sha512-"+createHash("sha512").update(readFileSync(process.argv[1])).digest("base64"))' "${tarballs[0]}")"
registry_ready="false"
for attempt in {1..12}; do
resolved="$(npm view "cometapi@${VERSION}" version 2>/dev/null || true)"
tagged="$(npm view "cometapi@${DIST_TAG}" version 2>/dev/null || true)"
registry_dist="$(npm view "cometapi@${VERSION}" dist --json 2>/dev/null || true)"
if [[ "$resolved" == "$VERSION" && "$tagged" == "$VERSION" && -n "$registry_dist" ]] && \
REGISTRY_DIST="$registry_dist" LOCAL_INTEGRITY="$local_integrity" node <<'EOF'
let ready = false;
try {
const dist = JSON.parse(process.env.REGISTRY_DIST);
ready =
dist.integrity === process.env.LOCAL_INTEGRITY &&
Boolean(dist.attestations?.url) &&
dist.attestations?.provenance?.predicateType ===
"https://slsa.dev/provenance/v1";
} catch {}
process.exitCode = ready ? 0 : 1;
EOF
then
registry_ready="true"
break
fi
if [[ "$attempt" -lt 12 ]]; then
sleep 10
fi
done
if [[ "$registry_ready" != "true" ]]; then
echo "Registry state did not converge for cometapi@${VERSION}, ${DIST_TAG}, integrity, and provenance." >&2
exit 1
fi

verify_dir="$(mktemp -d)"
cd "$verify_dir"
npm init --yes >/dev/null
npm install --ignore-scripts --no-audit --no-fund \
"openai@6.47.0" "cometapi@${VERSION}"
signatures_verified="false"
for attempt in {1..3}; do
if npm audit signatures; then
signatures_verified="true"
break
fi
if [[ "$attempt" -lt 3 ]]; then
sleep 10
fi
done
if [[ "$signatures_verified" != "true" ]]; then
echo "Registry signature and provenance verification did not converge." >&2
exit 1
fi
npm ls openai --all
if [[ -d node_modules/cometapi/node_modules/openai ]]; then
echo "The registry fixture contains a nested OpenAI installation." >&2
exit 1
fi

node --input-type=module <<'EOF'
import assert from "node:assert/strict";
import { CometAPI } from "cometapi";

const client = new CometAPI({
apiKey: "mock-registry-key",
maxRetries: 0,
fetch: async () =>
new Response(JSON.stringify({ object: "list", data: [] }), {
status: 200,
headers: { "content-type": "application/json" },
}),
});
const models = await client.models.list();
assert.deepEqual(models.data, []);
EOF

node <<'EOF'
const assert = require("node:assert/strict");
const { CometAPI } = require("cometapi");
const { APIError } = require("openai");

const client = new CometAPI({
apiKey: "mock-registry-key",
maxRetries: 0,
fetch: async () =>
new Response(
JSON.stringify({
error: {
message: "mock registry failure",
type: "invalid_request_error",
},
}),
{
status: 400,
headers: { "content-type": "application/json" },
},
),
});

(async () => {
let caught;
try {
await client.models.list();
} catch (error) {
caught = error;
}
assert.ok(caught instanceof APIError);
})().catch((error) => {
console.error(error);
process.exitCode = 1;
});
EOF
1 change: 1 addition & 0 deletions scripts/publish-artifact.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ if [[ "$bootstrap_enabled" == "true" && \
exit 1
fi

artifact_directory="$(cd "$artifact_directory" && pwd -P)"
shopt -s nullglob
tarballs=("$artifact_directory"/*.tgz)
if [[ "${#tarballs[@]}" -ne 1 ]]; then
Expand Down
14 changes: 10 additions & 4 deletions tests/publish-artifact.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
mkdirSync,
mkdtempSync,
readFileSync,
realpathSync,
rmSync,
writeFileSync,
} from "node:fs";
Expand Down Expand Up @@ -54,7 +55,7 @@ function fixture() {
].join("\n"),
);
chmodSync(npm, 0o755);
return { artifacts, bin, log };
return { artifacts, bin, log, root };
}

function runPublish({
Expand All @@ -63,13 +64,14 @@ function runPublish({
token = "",
version = "0.1.0-alpha.1",
} = {}) {
const { artifacts, bin, log } = fixture();
const { bin, log, root } = fixture();
const result = spawnSync("bash", [script], {
cwd: root,
encoding: "utf8",
env: {
...process.env,
ALPHA1_BOOTSTRAP_ENABLED: bootstrapEnabled,
ARTIFACT_DIRECTORY: artifacts,
ARTIFACT_DIRECTORY: "artifacts",
DIST_TAG: distTag,
NODE_AUTH_TOKEN: token,
NPM_CALL_LOG: log,
Expand All @@ -80,6 +82,7 @@ function runPublish({
return {
log: existsSync(log) ? readFileSync(log, "utf8") : "",
result,
root,
};
}

Expand All @@ -91,14 +94,17 @@ describe("publish artifact authentication", () => {
});

it("allows the protected token bootstrap for alpha.1 on next", () => {
const { log, result } = runPublish({
const { log, result, root } = runPublish({
bootstrapEnabled: "true",
token: "opaque",
});
expect(result.status, result.stderr).toBe(0);
expect(log).toMatch(
/^token-present\npublish .* --provenance --tag next\n$/,
);
expect(log).toContain(
`publish ${join(realpathSync(root), "artifacts", "cometapi.tgz")} --access public --provenance --tag next`,
);
});

it.each([
Expand Down
Loading