forked from CopilotKit/CopilotKit
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpublish-release.ts
More file actions
201 lines (184 loc) · 6.49 KB
/
Copy pathpublish-release.ts
File metadata and controls
201 lines (184 loc) · 6.49 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
/**
* Publish a stable release (runs after merge of a release PR).
*
* 1. Reads the scope and current version from package.json (already bumped by the release PR)
* 2. Optionally reads the Notion draft for the final release notes
* 3. Publishes pre-built packages to npm with "latest" tag
* 4. Outputs the version for downstream steps (git tag, GitHub Release)
*
* NOTE: Build is handled by the separate CI build job (no publish secrets).
* This script receives pre-built artifacts and only performs the publish step.
*
* Env vars:
* NOTION_API_KEY — for reading edited release notes from Notion (optional)
* GITHUB_OUTPUT — CI output file
*
* Auth: Uses npm OIDC trusted publishers (id-token: write) via npx npm@11.
* No NPM_TOKEN needed — NODE_AUTH_TOKEN must be empty to avoid blocking OIDC.
*
* Usage: tsx scripts/release/publish-release.ts --scope <monorepo|angular>
*/
import fs from "fs";
import path from "path";
import { spawnSync } from "child_process";
import {
getCurrentVersion,
getPackagesForScope,
parseSemver,
} from "./lib/versions.js";
import { readReleaseDraft } from "./lib/notion.js";
import { ROOT, getScopeConfig, type ReleaseScope } from "./lib/config.js";
function run(cmd: string, args: string[], opts?: { cwd?: string }) {
const result = spawnSync(cmd, args, {
cwd: opts?.cwd ?? ROOT,
stdio: "inherit",
encoding: "utf8",
});
if (result.status !== 0) {
throw new Error(`Command failed: ${cmd} ${args.join(" ")}`);
}
return result;
}
function getPublishedVersion(packageName: string): string | null {
const result = spawnSync("npm", ["view", packageName, "version"], {
encoding: "utf8",
timeout: 15000,
});
if (result.status === 0) {
return result.stdout.trim() || null;
}
// E404 means package doesn't exist on registry — genuinely not published
if (
result.stderr?.includes("E404") ||
result.stderr?.includes("is not in this registry")
) {
return null;
}
// Any other error (network, auth, rate limit) should stop the release
throw new Error(
`npm registry check failed for ${packageName}: ${result.stderr?.trim() || "unknown error"}`,
);
}
function isGreaterVersion(next: string, current: string): boolean {
const a = parseSemver(next);
const b = parseSemver(current);
if (a.major !== b.major) return a.major > b.major;
if (a.minor !== b.minor) return a.minor > b.minor;
return a.patch > b.patch;
}
const VALID_SCOPES = ["monorepo", "angular"];
async function main() {
const argv = process.argv.slice(2);
const scopeIdx = argv.indexOf("--scope");
const scope = (
scopeIdx !== -1 ? argv[scopeIdx + 1] : null
) as ReleaseScope | null;
if (!scope || !VALID_SCOPES.includes(scope)) {
console.error(
`Usage: publish-release.ts --scope <${VALID_SCOPES.join("|")}>`,
);
process.exit(1);
}
const version = getCurrentVersion(scope);
const scopeConfig = getScopeConfig(scope);
console.log(`Scope: ${scope}`);
console.log(`Publishing version: ${version}`);
// Safety check: only allow clean semver (no prerelease suffixes like -canary.123)
const v = parseSemver(version);
if (v.prerelease) {
console.error(
`Refusing to publish: ${version} contains a prerelease suffix. ` +
`Stable releases must be clean semver (e.g. 1.55.3).`,
);
process.exit(1);
}
// Safety check: refuse to publish if version isn't greater than what's on npm
let published: string | null;
try {
published = getPublishedVersion(scopeConfig.versionSource);
} catch (err: any) {
console.error(
`Registry check failed — aborting release to avoid publishing over an existing version.`,
);
console.error(err.message);
process.exit(1);
}
if (published) {
console.log(`Currently published version: ${published}`);
if (!isGreaterVersion(version, published)) {
console.error(
`Refusing to publish: ${version} is not greater than the currently published ${published}.`,
);
process.exit(1);
}
}
// Try to read edited release notes from Notion
const notionRefPath = path.join(ROOT, "release-notes-notion.json");
const releaseNotesPath = path.join(ROOT, "release-notes.md");
if (fs.existsSync(notionRefPath)) {
try {
const ref = JSON.parse(fs.readFileSync(notionRefPath, "utf8"));
if (ref.pageId && process.env.NOTION_API_KEY) {
console.log("Reading edited release notes from Notion...");
const notionContent = await readReleaseDraft(ref.pageId);
if (notionContent.trim()) {
fs.writeFileSync(releaseNotesPath, notionContent);
console.log("Release notes updated from Notion draft.");
}
}
} catch (err: any) {
console.error(`Failed to read Notion draft: ${err.message}`);
console.log("Using release notes from the PR branch.");
}
}
// NOTE: Build is handled by the CI build job (no secrets).
// The publish job receives pre-built artifacts via download-artifact.
// We intentionally do NOT rebuild here to keep NPM_TOKEN out of the
// build process tree.
// Publish each package in scope.
// Uses pnpm pack (workspace-aware) + npx npm@11 publish (OIDC-aware).
// npm 11 uses GitHub Actions OIDC tokens for auth when id-token: write
// is granted, eliminating the need for long-lived NPM_TOKEN secrets.
// Skips packages already published at this version (idempotent retries).
console.log("\nPublishing packages...");
let skipped = 0;
for (const p of getPackagesForScope(scope)) {
const pubVersion = getPublishedVersion(p.name);
if (pubVersion === version) {
console.log(` Skipping ${p.name}@${version} (already published)`);
skipped++;
continue;
}
console.log(` Publishing ${p.name}@${version}...`);
run("pnpm", ["pack"], { cwd: p.dir });
const tarball = `${p.name.replace("@", "").replace("/", "-")}-${version}.tgz`;
run(
"npx",
[
"--yes",
"npm@11.15.0",
"publish",
tarball,
"--tag",
"latest",
"--access",
"public",
],
{ cwd: p.dir },
);
}
if (skipped > 0) {
console.log(`\n${skipped} package(s) skipped (already at ${version}).`);
}
// Output version for downstream steps
const outputPath = process.env.GITHUB_OUTPUT;
if (outputPath) {
fs.appendFileSync(outputPath, `version=${version}\n`);
fs.appendFileSync(outputPath, `scope=${scope}\n`);
}
console.log(`\nRelease published: ${version} (${scope})`);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});