Skip to content

Publish

Publish #10

Workflow file for this run

name: Publish
on:
workflow_run:
workflows:
- Release Please
types:
- completed
permissions:
actions: read
contents: read
concurrency:
group: npm-publish
cancel-in-progress: false
jobs:
verify:
name: Verify the immutable release artifact
if: >-
vars.RELEASE_PLEASE_ENABLED == 'true' &&
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'push' &&
github.event.workflow_run.head_branch == 'main'
runs-on: ubuntu-latest
timeout-minutes: 30
outputs:
artifact-name: ${{ steps.artifact-name.outputs.name }}
dist-tag: ${{ steps.version.outputs.dist-tag }}
release-commit: ${{ steps.trust.outputs.release-commit }}
release-tag: ${{ steps.trust.outputs.release-tag }}
version: ${{ steps.version.outputs.version }}
steps:
- name: Check out the current main branch
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
ref: refs/heads/main
- name: Download the exact Release Please result
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: release-please-result-${{ github.event.workflow_run.id }}-${{ github.event.workflow_run.run_attempt }}
path: release-please-result
github-token: ${{ github.token }}
repository: ${{ github.repository }}
run-id: ${{ github.event.workflow_run.id }}
- name: Reject an untrusted Release Please workflow run
id: trust
env:
EXPECTED_BUGS_URL: https://github.com/cometapi-dev/cometapi-node/issues
EXPECTED_REPOSITORY: cometapi-dev/cometapi-node
EXPECTED_REPOSITORY_URL: git+https://github.com/cometapi-dev/cometapi-node.git
EXPECTED_WORKFLOW: Release Please
EXPECTED_WORKFLOW_PATH: .github/workflows/release-please.yml
RELEASE_RESULT: release-please-result/result.json
WORKFLOW_SHA: ${{ github.event.workflow_run.head_sha }}
shell: bash
run: |
set -euo pipefail
head_commit="$(git rev-parse HEAD)"
if [[ "$head_commit" != "$WORKFLOW_SHA" ]]; then
echo "The successful Release Please SHA is no longer the exact main tip." >&2
exit 1
fi
git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
if [[ "$(git rev-parse refs/remotes/origin/main)" != "$WORKFLOW_SHA" ]]; then
echo "origin/main moved after the successful Release Please run." >&2
exit 1
fi
node --input-type=module <<'EOF'
import { appendFileSync, readFileSync } from "node:fs";
import {
validateReleasePleaseActionResult,
validateReleaseWorkflowRun,
} from "./scripts/release-workflow-validation.mjs";
const event = JSON.parse(readFileSync(process.env.GITHUB_EVENT_PATH, "utf8"));
const run = validateReleaseWorkflowRun(event, {
checkedOutSha: process.env.WORKFLOW_SHA,
repository: process.env.EXPECTED_REPOSITORY,
workflowName: process.env.EXPECTED_WORKFLOW,
workflowPath: process.env.EXPECTED_WORKFLOW_PATH,
});
const manifest = JSON.parse(readFileSync("package.json", "utf8"));
if (
manifest.repository?.type !== "git" ||
manifest.repository?.url !== process.env.EXPECTED_REPOSITORY_URL
) {
throw new Error(
`package.json repository must equal ${process.env.EXPECTED_REPOSITORY_URL}.`,
);
}
if (manifest.bugs?.url !== process.env.EXPECTED_BUGS_URL) {
throw new Error(`package.json bugs.url must equal ${process.env.EXPECTED_BUGS_URL}.`);
}
const actionResult = JSON.parse(
readFileSync(process.env.RELEASE_RESULT, "utf8"),
);
const release = validateReleasePleaseActionResult(actionResult, {
releaseCommit: run.releaseCommit,
repository: process.env.EXPECTED_REPOSITORY,
runAttempt: run.runAttempt,
runId: run.runId,
version: manifest.version,
workflowName: process.env.EXPECTED_WORKFLOW,
workflowPath: process.env.EXPECTED_WORKFLOW_PATH,
});
appendFileSync(
process.env.GITHUB_OUTPUT,
[
`release-commit=${release.releaseCommit}`,
`release-tag=${release.tag}`,
`release-url=${release.htmlUrl}`,
`release-version=${release.version}`,
"",
].join("\n"),
);
EOF
- name: Set up Node.js 24
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24.x
cache: npm
- name: Install validation dependencies without lifecycle scripts
run: npm ci --ignore-scripts
- name: Verify release metadata and derive the npm dist-tag
id: version
env:
RELEASE_TAG: ${{ steps.trust.outputs.release-tag }}
shell: bash
run: |
set -euo pipefail
node scripts/validate-release.mjs \
--tag "$RELEASE_TAG" \
--require-final \
--require-releasable-docs >> "$GITHUB_OUTPUT"
- name: Verify the exact immutable GitHub release and tag
id: release
env:
GH_TOKEN: ${{ github.token }}
RELEASE_COMMIT: ${{ steps.trust.outputs.release-commit }}
RELEASE_HTML_URL: ${{ steps.trust.outputs.release-url }}
RELEASE_TAG: ${{ steps.trust.outputs.release-tag }}
shell: bash
run: |
set -euo pipefail
release_json="$RUNNER_TEMP/github-release.json"
gh api "repos/${GITHUB_REPOSITORY}/releases/tags/${RELEASE_TAG}" > "$release_json"
git fetch --no-tags origin \
"+refs/tags/${RELEASE_TAG}:refs/tags/${RELEASE_TAG}"
tag_commit="$(git rev-parse --verify "refs/tags/${RELEASE_TAG}^{commit}")"
RELEASE_JSON="$release_json" TAG_COMMIT="$tag_commit" \
node --input-type=module <<'EOF'
import { readFileSync } from "node:fs";
import {
extractReleaseNotesFromChangelog,
validateGitHubRelease,
} from "./scripts/release-workflow-validation.mjs";
const release = JSON.parse(readFileSync(process.env.RELEASE_JSON, "utf8"));
const version = JSON.parse(readFileSync("package.json", "utf8")).version;
validateGitHubRelease(release, {
expectedBody: extractReleaseNotesFromChangelog(
readFileSync("CHANGELOG.md", "utf8"),
version,
),
htmlUrl: process.env.RELEASE_HTML_URL,
releaseCommit: process.env.RELEASE_COMMIT,
tag: process.env.RELEASE_TAG,
tagCommit: process.env.TAG_COMMIT,
});
EOF
- name: Use a Trusted Publishing-capable npm CLI
run: npm install --global npm@11.12.1
- name: Install locked dependencies
run: npm ci
- name: Run release checks
run: |
npm run format:check
npm run lint
npm run typecheck
npm test
npm run test:secrets
npm run build
npm run test:package
npm run test:live-contract
npm run test:compat -- --lane locked
npm run check:self-contained
- name: Pack the exact release artifact
id: pack
shell: bash
run: |
set -euo pipefail
mkdir -p release-artifacts
npm pack --pack-destination release-artifacts
mapfile -t tarballs < <(find release-artifacts -maxdepth 1 -type f -name '*.tgz' -print)
if [[ "${#tarballs[@]}" -ne 1 ]]; then
echo "Expected exactly one packed artifact, found ${#tarballs[@]}." >&2
exit 1
fi
echo "tarball=${tarballs[0]}" >> "$GITHUB_OUTPUT"
- name: Test consumers against the exact release artifact
run: |
npm run test:package -- \
--tarball "${{ steps.pack.outputs.tarball }}" \
--tag "${{ steps.trust.outputs.release-tag }}"
npm run test:examples -- --tarball "${{ steps.pack.outputs.tarball }}"
npm run test:fixtures -- --tarball "${{ steps.pack.outputs.tarball }}"
- name: Name the attempt-qualified release artifact
id: artifact-name
shell: bash
run: |
set -euo pipefail
echo "name=npm-package-${{ steps.version.outputs.version }}-${{ github.run_id }}-${{ github.run_attempt }}" >> "$GITHUB_OUTPUT"
- name: Upload the verified release artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ steps.artifact-name.outputs.name }}
path: ${{ steps.pack.outputs.tarball }}
if-no-files-found: error
retention-days: 30
live-smoke:
name: Verify the release tag against CometAPI
needs:
- verify
concurrency:
group: live-smoke
cancel-in-progress: false
runs-on: ubuntu-latest
timeout-minutes: 5
# Required repository configuration: configure the protected live-smoke environment
# without required reviewers and add COMETAPI_KEY before publishing a release.
environment: live-smoke
steps:
- name: Check out the verified release tag
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
ref: ${{ needs.verify.outputs.release-commit }}
- name: Set up Node.js 24
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24.x
cache: npm
- name: Install locked dependencies
run: npm ci
- name: Build the release tag
run: npm run build
- name: Run the bounded live smoke
env:
COMETAPI_KEY: ${{ secrets.COMETAPI_KEY }}
COMETAPI_LIVE_SMOKE: "1"
COMETAPI_SMOKE_MODEL: ${{ vars.COMETAPI_SMOKE_MODEL || 'gpt-5.4' }}
COMETAPI_LIVE_REQUEST_LIMIT: "3"
COMETAPI_LIVE_MAX_OUTPUT_TOKENS: "16"
COMETAPI_LIVE_REQUEST_TIMEOUT_MS: "60000"
COMETAPI_LIVE_CONCURRENCY: "1"
run: npm run test:live
publish:
name: Publish with npm Trusted Publishing
needs:
- live-smoke
- verify
runs-on: ubuntu-latest
timeout-minutes: 15
# Required repository configuration: configure the protected npm environment with
# approval by the current release approver and self-review allowed, plus package ownership.
environment:
name: npm
url: https://www.npmjs.com/package/cometapi/v/${{ needs.verify.outputs.version }}
permissions:
contents: read
id-token: write
steps:
- name: Check out the verified release commit
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
ref: ${{ needs.verify.outputs.release-commit }}
- name: Set up Node.js 24
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24.x
package-manager-cache: false
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 verified release artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: ${{ needs.verify.outputs.artifact-name }}
path: release-artifacts
- name: Publish the exact artifact with provenance
env:
DIST_TAG: ${{ needs.verify.outputs.dist-tag }}
VERSION: ${{ needs.verify.outputs.version }}
run: bash scripts/publish-artifact.sh
- name: Verify the public registry artifact
env:
DIST_TAG: ${{ needs.verify.outputs.dist-tag }}
VERSION: ${{ needs.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