|
4 | 4 | release: |
5 | 5 | types: |
6 | 6 | - published |
7 | | - workflow_dispatch: |
8 | | - inputs: |
9 | | - source_run_id: |
10 | | - description: Failed v0.1.0-alpha.1 Publish run containing the verified artifact |
11 | | - required: true |
12 | | - type: string |
13 | 7 |
|
14 | 8 | permissions: |
15 | 9 | contents: read |
@@ -189,7 +183,7 @@ jobs: |
189 | 183 | run: npm run test:live |
190 | 184 |
|
191 | 185 | publish: |
192 | | - name: Publish with npm Trusted Publishing or alpha.1 bootstrap |
| 186 | + name: Publish with npm Trusted Publishing |
193 | 187 | needs: |
194 | 188 | - live-smoke |
195 | 189 | - verify |
@@ -223,9 +217,7 @@ jobs: |
223 | 217 | path: release-artifacts |
224 | 218 | - name: Publish the exact artifact with provenance |
225 | 219 | env: |
226 | | - ALPHA1_BOOTSTRAP_ENABLED: ${{ vars.NPM_ALPHA1_BOOTSTRAP_ENABLED }} |
227 | 220 | DIST_TAG: ${{ needs.verify.outputs.dist-tag }} |
228 | | - NODE_AUTH_TOKEN: ${{ vars.NPM_ALPHA1_BOOTSTRAP_ENABLED == 'true' && needs.verify.outputs.version == '0.1.0-alpha.1' && secrets.NPM_ALPHA1_BOOTSTRAP_TOKEN || '' }} |
229 | 221 | VERSION: ${{ needs.verify.outputs.version }} |
230 | 222 | run: bash scripts/publish-artifact.sh |
231 | 223 | - name: Verify the public registry artifact |
@@ -350,277 +342,3 @@ jobs: |
350 | 342 | process.exitCode = 1; |
351 | 343 | }); |
352 | 344 | EOF |
353 | | -
|
354 | | - recover-verify: |
355 | | - name: Verify the failed alpha.1 publication source |
356 | | - if: github.event_name == 'workflow_dispatch' |
357 | | - runs-on: ubuntu-latest |
358 | | - timeout-minutes: 10 |
359 | | - outputs: |
360 | | - dist-tag: next |
361 | | - release-commit: ${{ steps.trust.outputs.release-commit }} |
362 | | - source-run-id: ${{ steps.trust.outputs.source-run-id }} |
363 | | - version: 0.1.0-alpha.1 |
364 | | - permissions: |
365 | | - actions: read |
366 | | - contents: read |
367 | | - steps: |
368 | | - - name: Check out the recovery implementation |
369 | | - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 |
370 | | - with: |
371 | | - fetch-depth: 0 |
372 | | - persist-credentials: false |
373 | | - - name: Reject an untrusted recovery source |
374 | | - id: trust |
375 | | - env: |
376 | | - EXPECTED_REPOSITORY: cometapi-dev/cometapi-node |
377 | | - EXPECTED_TAG: v0.1.0-alpha.1 |
378 | | - GH_TOKEN: ${{ github.token }} |
379 | | - SOURCE_RUN_ID: ${{ inputs.source_run_id }} |
380 | | - shell: bash |
381 | | - run: | |
382 | | - set -euo pipefail |
383 | | - if [[ "$GITHUB_REPOSITORY" != "$EXPECTED_REPOSITORY" || \ |
384 | | - "$GITHUB_REF" != "refs/heads/main" ]]; then |
385 | | - echo "Recovery is restricted to the canonical repository's main branch." >&2 |
386 | | - exit 1 |
387 | | - fi |
388 | | - if [[ ! "$SOURCE_RUN_ID" =~ ^[1-9][0-9]*$ ]]; then |
389 | | - echo "source_run_id must be a positive integer." >&2 |
390 | | - exit 1 |
391 | | - fi |
392 | | -
|
393 | | - git fetch --no-tags origin \ |
394 | | - "+refs/tags/${EXPECTED_TAG}:refs/tags/${EXPECTED_TAG}" \ |
395 | | - "+refs/heads/main:refs/remotes/origin/main" |
396 | | - release_commit="$(git rev-parse --verify "refs/tags/${EXPECTED_TAG}^{commit}")" |
397 | | - if ! git merge-base --is-ancestor "$release_commit" refs/remotes/origin/main; then |
398 | | - echo "The immutable release tag is not reachable from origin/main." >&2 |
399 | | - exit 1 |
400 | | - fi |
401 | | -
|
402 | | - release_json="$(gh api \ |
403 | | - "repos/${GITHUB_REPOSITORY}/releases/tags/${EXPECTED_TAG}")" |
404 | | - run_json="$(gh api \ |
405 | | - "repos/${GITHUB_REPOSITORY}/actions/runs/${SOURCE_RUN_ID}")" |
406 | | - jobs_json="$(gh api \ |
407 | | - "repos/${GITHUB_REPOSITORY}/actions/runs/${SOURCE_RUN_ID}/jobs?per_page=100")" |
408 | | - artifacts_json="$(gh api \ |
409 | | - "repos/${GITHUB_REPOSITORY}/actions/runs/${SOURCE_RUN_ID}/artifacts?per_page=100")" |
410 | | -
|
411 | | - RELEASE_JSON="$release_json" RUN_JSON="$run_json" \ |
412 | | - JOBS_JSON="$jobs_json" ARTIFACTS_JSON="$artifacts_json" \ |
413 | | - RELEASE_COMMIT="$release_commit" EXPECTED_TAG="$EXPECTED_TAG" node <<'EOF' |
414 | | - const release = JSON.parse(process.env.RELEASE_JSON); |
415 | | - const run = JSON.parse(process.env.RUN_JSON); |
416 | | - const jobs = JSON.parse(process.env.JOBS_JSON).jobs; |
417 | | - const artifacts = JSON.parse(process.env.ARTIFACTS_JSON).artifacts; |
418 | | - const expectedCommit = process.env.RELEASE_COMMIT; |
419 | | - const expectedTag = process.env.EXPECTED_TAG; |
420 | | -
|
421 | | - const reject = (message) => { |
422 | | - throw new Error(message); |
423 | | - }; |
424 | | - if ( |
425 | | - release.tag_name !== expectedTag || |
426 | | - release.draft !== false || |
427 | | - release.prerelease !== true || |
428 | | - release.immutable !== true |
429 | | - ) { |
430 | | - reject("Recovery requires the published immutable alpha.1 prerelease."); |
431 | | - } |
432 | | - if ( |
433 | | - run.event !== "release" || |
434 | | - run.path !== ".github/workflows/publish.yml" || |
435 | | - run.head_branch !== expectedTag || |
436 | | - run.head_sha !== expectedCommit || |
437 | | - run.status !== "completed" || |
438 | | - run.conclusion !== "failure" |
439 | | - ) { |
440 | | - reject("The source run does not match the failed alpha.1 release workflow."); |
441 | | - } |
442 | | - const conclusions = new Map(jobs.map((job) => [job.name, job.conclusion])); |
443 | | - if ( |
444 | | - conclusions.get("Verify the immutable release artifact") !== "success" || |
445 | | - conclusions.get("Verify the release tag against CometAPI") !== "success" || |
446 | | - conclusions.get( |
447 | | - "Publish with npm Trusted Publishing or alpha.1 bootstrap", |
448 | | - ) !== "failure" |
449 | | - ) { |
450 | | - reject("The source run does not have the required verify/live success boundary."); |
451 | | - } |
452 | | - const candidates = artifacts.filter( |
453 | | - (artifact) => |
454 | | - artifact.name === "npm-package-0.1.0-alpha.1" && |
455 | | - artifact.expired === false, |
456 | | - ); |
457 | | - if (candidates.length !== 1 || !candidates[0].digest) { |
458 | | - reject("The source run must contain one unexpired verified alpha.1 artifact."); |
459 | | - } |
460 | | - EOF |
461 | | -
|
462 | | - echo "release-commit=${release_commit}" >> "$GITHUB_OUTPUT" |
463 | | - echo "source-run-id=${SOURCE_RUN_ID}" >> "$GITHUB_OUTPUT" |
464 | | -
|
465 | | - recover-publish: |
466 | | - name: Recover the verified alpha.1 npm publication |
467 | | - needs: |
468 | | - - recover-verify |
469 | | - runs-on: ubuntu-latest |
470 | | - timeout-minutes: 15 |
471 | | - environment: |
472 | | - name: npm |
473 | | - url: https://www.npmjs.com/package/cometapi/v/0.1.0-alpha.1 |
474 | | - permissions: |
475 | | - actions: read |
476 | | - contents: read |
477 | | - id-token: write |
478 | | - steps: |
479 | | - - name: Check out the reviewed recovery implementation |
480 | | - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 |
481 | | - with: |
482 | | - persist-credentials: false |
483 | | - - name: Set up Node.js 24 |
484 | | - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 |
485 | | - with: |
486 | | - node-version: 24.x |
487 | | - registry-url: https://registry.npmjs.org |
488 | | - - name: Use a Trusted Publishing-capable npm CLI |
489 | | - run: npm install --global npm@11.12.1 |
490 | | - - name: Download the original verified release artifact |
491 | | - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 |
492 | | - with: |
493 | | - github-token: ${{ github.token }} |
494 | | - name: npm-package-0.1.0-alpha.1 |
495 | | - path: release-artifacts |
496 | | - repository: cometapi-dev/cometapi-node |
497 | | - run-id: ${{ needs.recover-verify.outputs.source-run-id }} |
498 | | - - name: Publish the exact recovered artifact with provenance |
499 | | - env: |
500 | | - ALPHA1_BOOTSTRAP_ENABLED: ${{ vars.NPM_ALPHA1_BOOTSTRAP_ENABLED }} |
501 | | - DIST_TAG: ${{ needs.recover-verify.outputs.dist-tag }} |
502 | | - NODE_AUTH_TOKEN: ${{ secrets.NPM_ALPHA1_BOOTSTRAP_TOKEN }} |
503 | | - VERSION: ${{ needs.recover-verify.outputs.version }} |
504 | | - run: bash scripts/publish-artifact.sh |
505 | | - - name: Verify the recovered public registry artifact |
506 | | - env: |
507 | | - DIST_TAG: ${{ needs.recover-verify.outputs.dist-tag }} |
508 | | - VERSION: ${{ needs.recover-verify.outputs.version }} |
509 | | - shell: bash |
510 | | - run: | |
511 | | - set -euo pipefail |
512 | | - mapfile -t tarballs < <(find "$GITHUB_WORKSPACE/release-artifacts" -maxdepth 1 -type f -name '*.tgz' -print) |
513 | | - if [[ "${#tarballs[@]}" -ne 1 ]]; then |
514 | | - echo "Expected exactly one downloaded artifact for registry verification." >&2 |
515 | | - exit 1 |
516 | | - fi |
517 | | - 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]}")" |
518 | | - registry_ready="false" |
519 | | - for attempt in {1..12}; do |
520 | | - resolved="$(npm view "cometapi@${VERSION}" version 2>/dev/null || true)" |
521 | | - tagged="$(npm view "cometapi@${DIST_TAG}" version 2>/dev/null || true)" |
522 | | - registry_dist="$(npm view "cometapi@${VERSION}" dist --json 2>/dev/null || true)" |
523 | | - if [[ "$resolved" == "$VERSION" && "$tagged" == "$VERSION" && -n "$registry_dist" ]] && \ |
524 | | - REGISTRY_DIST="$registry_dist" LOCAL_INTEGRITY="$local_integrity" node <<'EOF' |
525 | | - let ready = false; |
526 | | - try { |
527 | | - const dist = JSON.parse(process.env.REGISTRY_DIST); |
528 | | - ready = |
529 | | - dist.integrity === process.env.LOCAL_INTEGRITY && |
530 | | - Boolean(dist.attestations?.url) && |
531 | | - dist.attestations?.provenance?.predicateType === |
532 | | - "https://slsa.dev/provenance/v1"; |
533 | | - } catch {} |
534 | | - process.exitCode = ready ? 0 : 1; |
535 | | - EOF |
536 | | - then |
537 | | - registry_ready="true" |
538 | | - break |
539 | | - fi |
540 | | - if [[ "$attempt" -lt 12 ]]; then |
541 | | - sleep 10 |
542 | | - fi |
543 | | - done |
544 | | - if [[ "$registry_ready" != "true" ]]; then |
545 | | - echo "Registry state did not converge for cometapi@${VERSION}, ${DIST_TAG}, integrity, and provenance." >&2 |
546 | | - exit 1 |
547 | | - fi |
548 | | -
|
549 | | - verify_dir="$(mktemp -d)" |
550 | | - cd "$verify_dir" |
551 | | - npm init --yes >/dev/null |
552 | | - npm install --ignore-scripts --no-audit --no-fund \ |
553 | | - "openai@6.47.0" "cometapi@${VERSION}" |
554 | | - signatures_verified="false" |
555 | | - for attempt in {1..3}; do |
556 | | - if npm audit signatures; then |
557 | | - signatures_verified="true" |
558 | | - break |
559 | | - fi |
560 | | - if [[ "$attempt" -lt 3 ]]; then |
561 | | - sleep 10 |
562 | | - fi |
563 | | - done |
564 | | - if [[ "$signatures_verified" != "true" ]]; then |
565 | | - echo "Registry signature and provenance verification did not converge." >&2 |
566 | | - exit 1 |
567 | | - fi |
568 | | - npm ls openai --all |
569 | | - if [[ -d node_modules/cometapi/node_modules/openai ]]; then |
570 | | - echo "The registry fixture contains a nested OpenAI installation." >&2 |
571 | | - exit 1 |
572 | | - fi |
573 | | -
|
574 | | - node --input-type=module <<'EOF' |
575 | | - import assert from "node:assert/strict"; |
576 | | - import { CometAPI } from "cometapi"; |
577 | | -
|
578 | | - const client = new CometAPI({ |
579 | | - apiKey: "mock-registry-key", |
580 | | - maxRetries: 0, |
581 | | - fetch: async () => |
582 | | - new Response(JSON.stringify({ object: "list", data: [] }), { |
583 | | - status: 200, |
584 | | - headers: { "content-type": "application/json" }, |
585 | | - }), |
586 | | - }); |
587 | | - const models = await client.models.list(); |
588 | | - assert.deepEqual(models.data, []); |
589 | | - EOF |
590 | | -
|
591 | | - node <<'EOF' |
592 | | - const assert = require("node:assert/strict"); |
593 | | - const { CometAPI } = require("cometapi"); |
594 | | - const { APIError } = require("openai"); |
595 | | -
|
596 | | - const client = new CometAPI({ |
597 | | - apiKey: "mock-registry-key", |
598 | | - maxRetries: 0, |
599 | | - fetch: async () => |
600 | | - new Response( |
601 | | - JSON.stringify({ |
602 | | - error: { |
603 | | - message: "mock registry failure", |
604 | | - type: "invalid_request_error", |
605 | | - }, |
606 | | - }), |
607 | | - { |
608 | | - status: 400, |
609 | | - headers: { "content-type": "application/json" }, |
610 | | - }, |
611 | | - ), |
612 | | - }); |
613 | | -
|
614 | | - (async () => { |
615 | | - let caught; |
616 | | - try { |
617 | | - await client.models.list(); |
618 | | - } catch (error) { |
619 | | - caught = error; |
620 | | - } |
621 | | - assert.ok(caught instanceof APIError); |
622 | | - })().catch((error) => { |
623 | | - console.error(error); |
624 | | - process.exitCode = 1; |
625 | | - }); |
626 | | - EOF |
0 commit comments