Skip to content

Commit fbee2d1

Browse files
committed
finalize Public Preview validation
1 parent 1f69eed commit fbee2d1

8 files changed

Lines changed: 307 additions & 108 deletions

File tree

.github/workflows/live-smoke.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ jobs:
1919
vars.LIVE_SMOKE_ENABLED == 'true' &&
2020
github.ref == 'refs/heads/main'
2121
runs-on: ubuntu-latest
22-
# Required repository configuration: protect this environment, its reviewers,
23-
# COMETAPI_KEY secret, and the optional model variable.
22+
# Required repository configuration: protect this environment without required
23+
# reviewers, add COMETAPI_KEY, and optionally set the model variable.
2424
environment: live-smoke
2525
timeout-minutes: 5
2626
steps:

.github/workflows/publish.yml

Lines changed: 6 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,11 @@ jobs:
195195
contents: read
196196
id-token: write
197197
steps:
198+
- name: Check out the verified release commit
199+
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
200+
with:
201+
persist-credentials: false
202+
ref: ${{ needs.verify.outputs.release-commit }}
198203
- name: Set up Node.js 24
199204
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
200205
with:
@@ -213,46 +218,7 @@ jobs:
213218
DIST_TAG: ${{ needs.verify.outputs.dist-tag }}
214219
NODE_AUTH_TOKEN: ${{ vars.NPM_ALPHA1_BOOTSTRAP_ENABLED == 'true' && needs.verify.outputs.version == '0.1.0-alpha.1' && secrets.NPM_ALPHA1_BOOTSTRAP_TOKEN || '' }}
215220
VERSION: ${{ needs.verify.outputs.version }}
216-
shell: bash
217-
run: |
218-
set -euo pipefail
219-
if [[ "$ALPHA1_BOOTSTRAP_ENABLED" == "true" && \
220-
( "$VERSION" != "0.1.0-alpha.1" || "$DIST_TAG" != "next" ) ]]; then
221-
echo "The token bootstrap is restricted to cometapi@0.1.0-alpha.1 on the next dist-tag." >&2
222-
exit 1
223-
fi
224-
225-
mapfile -t tarballs < <(find release-artifacts -maxdepth 1 -type f -name '*.tgz' -print)
226-
if [[ "${#tarballs[@]}" -ne 1 ]]; then
227-
echo "Expected exactly one downloaded artifact, found ${#tarballs[@]}." >&2
228-
exit 1
229-
fi
230-
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]}")"
231-
232-
view_error="$(mktemp)"
233-
set +e
234-
existing_dist="$(npm view "cometapi@${VERSION}" dist --json 2>"$view_error")"
235-
view_status=$?
236-
set -e
237-
if [[ "$view_status" -eq 0 && -n "$existing_dist" ]]; then
238-
EXISTING_DIST="$existing_dist" LOCAL_INTEGRITY="$local_integrity" node <<'EOF'
239-
const dist = JSON.parse(process.env.EXISTING_DIST);
240-
if (dist.integrity !== process.env.LOCAL_INTEGRITY) {
241-
throw new Error("The existing registry version has different integrity.");
242-
}
243-
EOF
244-
echo "cometapi@${VERSION} already matches the verified artifact; resuming checks."
245-
elif grep -q "E404" "$view_error"; then
246-
if [[ "$ALPHA1_BOOTSTRAP_ENABLED" == "true" && -z "$NODE_AUTH_TOKEN" ]]; then
247-
echo "NPM_ALPHA1_BOOTSTRAP_TOKEN is required when the alpha.1 bootstrap is enabled." >&2
248-
exit 1
249-
fi
250-
npm publish "${tarballs[0]}" --access public --provenance --tag "$DIST_TAG"
251-
else
252-
echo "Unable to determine whether cometapi@${VERSION} already exists." >&2
253-
sed -n '1,20p' "$view_error" >&2
254-
exit 1
255-
fi
221+
run: bash scripts/publish-artifact.sh
256222
- name: Verify the public registry artifact
257223
env:
258224
DIST_TAG: ${{ needs.verify.outputs.dist-tag }}

RELEASING.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,9 @@ artifacts. Authorized maintainers must supply or approve:
6666

6767
- Changes to the canonical identity and contact values listed above
6868
- Repository creation and visibility, branch/tag protections, environments,
69-
secrets, and required reviewers
70-
- npm package ownership and Trusted Publisher configuration
69+
secrets, and environment approval policies
70+
- npm package ownership for the maintainer-confirmed `cometapi-team` account
71+
and Trusted Publisher configuration
7172
- A `COMETAPI_KEY`, request budget, and explicit authorization for live smoke
7273
tests
7374
- Immutable public tags, GitHub releases, environment approvals, and npm
@@ -76,6 +77,12 @@ artifacts. Authorized maintainers must supply or approve:
7677
Missing identity, credentials, ownership, or authorization blocks the
7778
corresponding release gate. Do not invent it or replace it with a mock.
7879

80+
The `cometapi` package does not exist in the public registry yet, so npm cannot
81+
verify its owner before the first publication. Registry Alpha owner evidence is
82+
complete only when `npm owner ls cometapi` lists the maintainer-confirmed
83+
`cometapi-team` account after bootstrap; until then this remains a Registry
84+
Alpha prerequisite, not a Public Preview blocker.
85+
7986
For the current milestone, authorized external actions stop at creating the
8087
empty private repository, pushing its sanitized first history, and observing
8188
credential-free CI. Visibility changes and every subsequent external action

scripts/check-public-preview.mjs

Lines changed: 67 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { readFileSync } from "node:fs";
2-
import { join } from "node:path";
2+
import { join, resolve } from "node:path";
3+
import { fileURLToPath } from "node:url";
34

45
import { ROOT } from "./lib.mjs";
56
import {
@@ -8,64 +9,79 @@ import {
89
} from "./release-validation.mjs";
910
import { collectStandaloneContentViolations } from "./standalone-content.mjs";
1011

11-
const inputViolations = [];
12-
const read = (name) => {
13-
try {
14-
return readFileSync(join(ROOT, name), "utf8");
15-
} catch (error) {
16-
const detail = error instanceof Error ? error.message : String(error);
17-
inputViolations.push(`${name} could not be read: ${detail}`);
18-
return undefined;
12+
export function collectPublicPreviewGateViolations(root = ROOT) {
13+
const inputViolations = [];
14+
const read = (name) => {
15+
try {
16+
return readFileSync(join(root, name), "utf8");
17+
} catch (error) {
18+
const detail = error instanceof Error ? error.message : String(error);
19+
inputViolations.push(`${name} could not be read: ${detail}`);
20+
return undefined;
21+
}
22+
};
23+
24+
let sourceManifest;
25+
const packageText = read("package.json");
26+
if (packageText !== undefined) {
27+
try {
28+
sourceManifest = JSON.parse(packageText);
29+
} catch (error) {
30+
const detail = error instanceof Error ? error.message : String(error);
31+
inputViolations.push(`package.json could not be parsed: ${detail}`);
32+
}
1933
}
20-
};
2134

22-
let sourceManifest;
23-
const packageText = read("package.json");
24-
if (packageText !== undefined) {
35+
const contentViolations = collectPublicPreviewViolations({
36+
documents: {
37+
agents: read("AGENTS.md"),
38+
architecture: read("ARCHITECTURE.md"),
39+
changelog: read("CHANGELOG.md"),
40+
compatibility: read("COMPATIBILITY.md"),
41+
conduct: read("CODE_OF_CONDUCT.md"),
42+
contributing: read("CONTRIBUTING.md"),
43+
license: read("LICENSE"),
44+
readme: read("README.md"),
45+
releasing: read("RELEASING.md"),
46+
roadmap: read("ROADMAP.md"),
47+
security: read("SECURITY.md"),
48+
support: read("SUPPORT.md"),
49+
},
50+
sourceManifest,
51+
});
52+
53+
const standaloneContentViolations = [];
2554
try {
26-
sourceManifest = JSON.parse(packageText);
55+
standaloneContentViolations.push(
56+
...collectStandaloneContentViolations(root),
57+
);
2758
} catch (error) {
2859
const detail = error instanceof Error ? error.message : String(error);
29-
inputViolations.push(`package.json could not be parsed: ${detail}`);
60+
standaloneContentViolations.push(
61+
`standalone content could not be checked: ${detail}`,
62+
);
3063
}
31-
}
3264

33-
const violations = collectPublicPreviewViolations({
34-
documents: {
35-
agents: read("AGENTS.md"),
36-
architecture: read("ARCHITECTURE.md"),
37-
changelog: read("CHANGELOG.md"),
38-
compatibility: read("COMPATIBILITY.md"),
39-
conduct: read("CODE_OF_CONDUCT.md"),
40-
contributing: read("CONTRIBUTING.md"),
41-
license: read("LICENSE"),
42-
readme: read("README.md"),
43-
releasing: read("RELEASING.md"),
44-
roadmap: read("ROADMAP.md"),
45-
security: read("SECURITY.md"),
46-
support: read("SUPPORT.md"),
47-
},
48-
sourceManifest,
49-
});
65+
return [
66+
...inputViolations,
67+
...contentViolations,
68+
...standaloneContentViolations,
69+
];
70+
}
5071

51-
const standaloneContentViolations = [];
52-
try {
53-
standaloneContentViolations.push(...collectStandaloneContentViolations(ROOT));
54-
} catch (error) {
55-
const detail = error instanceof Error ? error.message : String(error);
56-
standaloneContentViolations.push(
57-
`standalone content could not be checked: ${detail}`,
58-
);
72+
function main() {
73+
const violations = collectPublicPreviewGateViolations();
74+
if (violations.length > 0) {
75+
console.error(formatPublicPreviewViolations(violations));
76+
process.exitCode = 1;
77+
} else {
78+
console.log("Public Preview content gate passed.");
79+
}
5980
}
6081

61-
const allViolations = [
62-
...inputViolations,
63-
...violations,
64-
...standaloneContentViolations,
65-
];
66-
if (allViolations.length > 0) {
67-
console.error(formatPublicPreviewViolations(allViolations));
68-
process.exitCode = 1;
69-
} else {
70-
console.log("Public Preview content gate passed.");
82+
if (
83+
process.argv[1] !== undefined &&
84+
resolve(process.argv[1]) === fileURLToPath(import.meta.url)
85+
) {
86+
main();
7187
}

scripts/publish-artifact.sh

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#!/usr/bin/env bash
2+
3+
set -euo pipefail
4+
5+
: "${DIST_TAG:?DIST_TAG is required}"
6+
: "${VERSION:?VERSION is required}"
7+
8+
artifact_directory="${ARTIFACT_DIRECTORY:-release-artifacts}"
9+
bootstrap_enabled="${ALPHA1_BOOTSTRAP_ENABLED:-}"
10+
11+
if [[ "$bootstrap_enabled" == "true" && \
12+
( "$VERSION" != "0.1.0-alpha.1" || "$DIST_TAG" != "next" ) ]]; then
13+
echo "The token bootstrap is restricted to cometapi@0.1.0-alpha.1 on the next dist-tag." >&2
14+
exit 1
15+
fi
16+
17+
shopt -s nullglob
18+
tarballs=("$artifact_directory"/*.tgz)
19+
if [[ "${#tarballs[@]}" -ne 1 ]]; then
20+
echo "Expected exactly one downloaded artifact, found ${#tarballs[@]}." >&2
21+
exit 1
22+
fi
23+
24+
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]}")"
25+
view_error="$(mktemp)"
26+
trap 'rm -f "$view_error"' EXIT
27+
28+
set +e
29+
existing_dist="$(npm view "cometapi@${VERSION}" dist --json 2>"$view_error")"
30+
view_status=$?
31+
set -e
32+
33+
if [[ "$view_status" -eq 0 && -n "$existing_dist" ]]; then
34+
EXISTING_DIST="$existing_dist" LOCAL_INTEGRITY="$local_integrity" node <<'EOF'
35+
const dist = JSON.parse(process.env.EXISTING_DIST);
36+
if (dist.integrity !== process.env.LOCAL_INTEGRITY) {
37+
throw new Error("The existing registry version has different integrity.");
38+
}
39+
EOF
40+
echo "cometapi@${VERSION} already matches the verified artifact; resuming checks."
41+
elif grep -q "E404" "$view_error"; then
42+
if [[ "$bootstrap_enabled" == "true" && -z "${NODE_AUTH_TOKEN:-}" ]]; then
43+
echo "NPM_ALPHA1_BOOTSTRAP_TOKEN is required when the alpha.1 bootstrap is enabled." >&2
44+
exit 1
45+
fi
46+
npm publish "${tarballs[0]}" --access public --provenance --tag "$DIST_TAG"
47+
else
48+
echo "Unable to determine whether cometapi@${VERSION} already exists." >&2
49+
sed -n '1,20p' "$view_error" >&2
50+
exit 1
51+
fi

tests/publish-artifact.test.mjs

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import {
2+
chmodSync,
3+
existsSync,
4+
mkdirSync,
5+
mkdtempSync,
6+
readFileSync,
7+
rmSync,
8+
writeFileSync,
9+
} from "node:fs";
10+
import { tmpdir } from "node:os";
11+
import { delimiter, join } from "node:path";
12+
import { spawnSync } from "node:child_process";
13+
import { fileURLToPath, URL } from "node:url";
14+
15+
import { afterEach, describe, expect, it } from "vitest";
16+
17+
const script = fileURLToPath(
18+
new URL("../scripts/publish-artifact.sh", import.meta.url),
19+
);
20+
const temporaryDirectories = [];
21+
22+
afterEach(() => {
23+
for (const directory of temporaryDirectories.splice(0)) {
24+
rmSync(directory, { force: true, recursive: true });
25+
}
26+
});
27+
28+
function fixture() {
29+
const root = mkdtempSync(join(tmpdir(), "cometapi-publish-test-"));
30+
temporaryDirectories.push(root);
31+
const artifacts = join(root, "artifacts");
32+
const bin = join(root, "bin");
33+
const log = join(root, "npm-call.log");
34+
mkdirSync(artifacts);
35+
mkdirSync(bin);
36+
writeFileSync(join(artifacts, "cometapi.tgz"), "artifact\n");
37+
const npm = join(bin, "npm");
38+
writeFileSync(
39+
npm,
40+
[
41+
"#!/usr/bin/env bash",
42+
'if [[ "$1" == "view" ]]; then',
43+
' echo "npm error code E404" >&2',
44+
" exit 1",
45+
"fi",
46+
'if [[ "$1" == "publish" ]]; then',
47+
' printf "%s\\n" "${NODE_AUTH_TOKEN:+token-present}" > "$NPM_CALL_LOG"',
48+
' printf "%s\\n" "$*" >> "$NPM_CALL_LOG"',
49+
" exit 0",
50+
"fi",
51+
'echo "unexpected npm command: $*" >&2',
52+
"exit 2",
53+
"",
54+
].join("\n"),
55+
);
56+
chmodSync(npm, 0o755);
57+
return { artifacts, bin, log };
58+
}
59+
60+
function runPublish({
61+
bootstrapEnabled = "",
62+
distTag = "next",
63+
token = "",
64+
version = "0.1.0-alpha.1",
65+
} = {}) {
66+
const { artifacts, bin, log } = fixture();
67+
const result = spawnSync("bash", [script], {
68+
encoding: "utf8",
69+
env: {
70+
...process.env,
71+
ALPHA1_BOOTSTRAP_ENABLED: bootstrapEnabled,
72+
ARTIFACT_DIRECTORY: artifacts,
73+
DIST_TAG: distTag,
74+
NODE_AUTH_TOKEN: token,
75+
NPM_CALL_LOG: log,
76+
PATH: `${bin}${delimiter}${process.env.PATH ?? ""}`,
77+
VERSION: version,
78+
},
79+
});
80+
return {
81+
log: existsSync(log) ? readFileSync(log, "utf8") : "",
82+
result,
83+
};
84+
}
85+
86+
describe("publish artifact authentication", () => {
87+
it("uses Trusted Publishing without injecting a registry token by default", () => {
88+
const { log, result } = runPublish({ version: "0.1.0-alpha.2" });
89+
expect(result.status, result.stderr).toBe(0);
90+
expect(log).toMatch(/^\npublish .* --provenance --tag next\n$/);
91+
});
92+
93+
it("allows the protected token bootstrap for alpha.1 on next", () => {
94+
const { log, result } = runPublish({
95+
bootstrapEnabled: "true",
96+
token: "opaque",
97+
});
98+
expect(result.status, result.stderr).toBe(0);
99+
expect(log).toMatch(
100+
/^token-present\npublish .* --provenance --tag next\n$/,
101+
);
102+
});
103+
104+
it.each([
105+
["another version", { bootstrapEnabled: "true", version: "0.1.0-alpha.2" }],
106+
["another dist-tag", { bootstrapEnabled: "true", distTag: "latest" }],
107+
["a missing token", { bootstrapEnabled: "true" }],
108+
])("rejects bootstrap mode for %s", (_name, options) => {
109+
const { log, result } = runPublish(options);
110+
expect(result.status).not.toBe(0);
111+
expect(log).toBe("");
112+
});
113+
});

0 commit comments

Comments
 (0)