Skip to content

Commit e1ccc2d

Browse files
committed
feat: add automated npm publishing script with changelog generation
1 parent 898c4c2 commit e1ccc2d

File tree

1 file changed

+138
-0
lines changed

1 file changed

+138
-0
lines changed

scripts/publish.ts

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
#!/usr/bin/env bun
2+
3+
import { $ } from "bun";
4+
import { generateChangelog, getContributors } from "./generate-changelog";
5+
6+
const PACKAGE_NAME = "cc-safety-net";
7+
8+
const bump = process.env.BUMP as "major" | "minor" | "patch" | undefined;
9+
const versionOverride = process.env.VERSION;
10+
11+
console.log("=== Publishing cc-safety-net ===\n");
12+
13+
async function fetchPreviousVersion(): Promise<string> {
14+
try {
15+
const res = await fetch(
16+
`https://registry.npmjs.org/${PACKAGE_NAME}/latest`,
17+
);
18+
if (!res.ok) throw new Error(`Failed to fetch: ${res.statusText}`);
19+
const data = (await res.json()) as { version: string };
20+
console.log(`Previous version: ${data.version}`);
21+
return data.version;
22+
} catch {
23+
console.log("No previous version found, starting from 0.0.0");
24+
return "0.0.0";
25+
}
26+
}
27+
28+
function bumpVersion(
29+
version: string,
30+
type: "major" | "minor" | "patch",
31+
): string {
32+
const parts = version.split(".").map((part) => Number(part));
33+
const major = parts[0] ?? 0;
34+
const minor = parts[1] ?? 0;
35+
const patch = parts[2] ?? 0;
36+
switch (type) {
37+
case "major":
38+
return `${major + 1}.0.0`;
39+
case "minor":
40+
return `${major}.${minor + 1}.0`;
41+
case "patch":
42+
return `${major}.${minor}.${patch + 1}`;
43+
}
44+
}
45+
46+
async function updatePackageVersion(newVersion: string): Promise<void> {
47+
const pkgPath = new URL("../package.json", import.meta.url).pathname;
48+
let pkg = await Bun.file(pkgPath).text();
49+
pkg = pkg.replace(/"version": "[^"]+"/, `"version": "${newVersion}"`);
50+
await Bun.write(pkgPath, pkg);
51+
console.log(`Updated: ${pkgPath}`);
52+
}
53+
54+
async function buildAndPublish(): Promise<void> {
55+
console.log("\nPublishing to npm...");
56+
// --ignore-scripts: workflow already built, skip prepublishOnly
57+
if (process.env.CI) {
58+
await $`npm publish --access public --provenance --ignore-scripts`;
59+
} else {
60+
await $`npm publish --access public --ignore-scripts`;
61+
}
62+
}
63+
64+
async function gitTagAndRelease(
65+
newVersion: string,
66+
notes: string[],
67+
): Promise<void> {
68+
if (!process.env.CI) return;
69+
70+
console.log("\nCommitting and tagging...");
71+
await $`git config user.email "github-actions[bot]@users.noreply.github.com"`;
72+
await $`git config user.name "github-actions[bot]"`;
73+
await $`git add package.json assets/cc-safety-net.schema.json`;
74+
75+
const hasStagedChanges = await $`git diff --cached --quiet`.nothrow();
76+
if (hasStagedChanges.exitCode !== 0) {
77+
await $`git commit -m "release: v${newVersion}"`;
78+
} else {
79+
console.log("No changes to commit (version already updated)");
80+
}
81+
82+
const tagExists = await $`git rev-parse v${newVersion}`.nothrow();
83+
if (tagExists.exitCode !== 0) {
84+
await $`git tag v${newVersion}`;
85+
} else {
86+
console.log(`Tag v${newVersion} already exists`);
87+
}
88+
89+
await $`git push origin HEAD --tags`;
90+
91+
console.log("\nCreating GitHub release...");
92+
const releaseNotes =
93+
notes.length > 0 ? notes.join("\n") : "No notable changes";
94+
const releaseExists = await $`gh release view v${newVersion}`.nothrow();
95+
if (releaseExists.exitCode !== 0) {
96+
await $`gh release create v${newVersion} --title "v${newVersion}" --notes ${releaseNotes}`;
97+
} else {
98+
console.log(`Release v${newVersion} already exists`);
99+
}
100+
}
101+
102+
async function checkVersionExists(version: string): Promise<boolean> {
103+
try {
104+
const res = await fetch(
105+
`https://registry.npmjs.org/${PACKAGE_NAME}/${version}`,
106+
);
107+
return res.ok;
108+
} catch {
109+
return false;
110+
}
111+
}
112+
113+
async function main(): Promise<void> {
114+
const previous = await fetchPreviousVersion();
115+
const newVersion =
116+
versionOverride ||
117+
(bump ? bumpVersion(previous, bump) : bumpVersion(previous, "patch"));
118+
console.log(`New version: ${newVersion}\n`);
119+
120+
if (await checkVersionExists(newVersion)) {
121+
console.log(
122+
`Version ${newVersion} already exists on npm. Skipping publish.`,
123+
);
124+
process.exit(0);
125+
}
126+
127+
await updatePackageVersion(newVersion);
128+
const changelog = await generateChangelog(`v${previous}`);
129+
const contributors = await getContributors(`v${previous}`);
130+
const notes = [...changelog, ...contributors];
131+
132+
await buildAndPublish();
133+
await gitTagAndRelease(newVersion, notes);
134+
135+
console.log(`\n=== Successfully published ${PACKAGE_NAME}@${newVersion} ===`);
136+
}
137+
138+
main();

0 commit comments

Comments
 (0)