Skip to content

Commit 3cbaeec

Browse files
committed
chore: add release script for version bumping and tagging
1 parent ecbdcec commit 3cbaeec

4 files changed

Lines changed: 87 additions & 7 deletions

File tree

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,11 @@ Pushing changes under `docs/` to `main` triggers [`.github/workflows/deploy-docs
6060

6161
## Releasing
6262

63-
Pushing a tag matching `v*` (e.g. `v1.1.0`) triggers [`.github/workflows/release.yml`](.github/workflows/release.yml), which type-checks, lints, and tests the project, sets `package.json`'s version to match the tag, builds & zips both the Chrome and Firefox extensions, and publishes them as assets on a GitHub Release (auto-generated release notes from commits/PRs since the last tag).
64-
6563
```sh
66-
git tag v1.1.0
67-
git push origin v1.1.0
64+
pnpm release # prompts for the version
65+
pnpm release 1.2.0 # or pass it directly
6866
```
6967

70-
`package.json`'s `version` is the single source of truth for the extension version — `wxt.config.ts` intentionally has no `version` field, so the built manifest always reflects it (and the release workflow keeps it in sync with the tag).
68+
[`scripts/release.mjs`](scripts/release.mjs) bumps `package.json`'s `version`, commits it, tags it (`v<version>`), and pushes the commit + tag to `origin/main` (after confirming you're on `main`, the working tree is clean, and `main` is up to date with `origin/main`). Pushing the tag triggers [`.github/workflows/release.yml`](.github/workflows/release.yml), which type-checks, lints, and tests the project, builds & zips both the Chrome and Firefox extensions, publishes them as assets on a GitHub Release (auto-generated release notes from commits/PRs since the last tag), and commits an update to `docs/changelog.md`.
69+
70+
`package.json`'s `version` is the single source of truth for the extension version — `wxt.config.ts` intentionally has no `version` field, so the built manifest always reflects it.

eslint.config.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,16 @@ export default tseslint.config(
66
js.configs.recommended,
77
...tseslint.configs.recommended,
88
eslintConfigPrettier,
9-
{ ignores: ['.wxt/', '.output/', 'docs/.vitepress/cache/', 'docs/.vitepress/dist/'] },
109
{
11-
files: ['.github/scripts/**/*.mjs'],
10+
ignores: [
11+
'.wxt/',
12+
'.output/',
13+
'docs/.vitepress/cache/',
14+
'docs/.vitepress/dist/',
15+
],
16+
},
17+
{
18+
files: ['.github/scripts/**/*.mjs', 'scripts/**/*.mjs'],
1219
languageOptions: {
1320
globals: { process: 'readonly', console: 'readonly' },
1421
},

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"lint": "eslint .",
1717
"format": "prettier --write .",
1818
"test": "vitest run",
19+
"release": "node scripts/release.mjs",
1920
"postinstall": "wxt prepare",
2021
"docs:dev": "vitepress dev docs",
2122
"docs:build": "vitepress build docs",

scripts/release.mjs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
#!/usr/bin/env node
2+
// Bumps package.json's version, commits it, tags it, and pushes — triggering
3+
// .github/workflows/release.yml. See README.md's "Releasing" section.
4+
import { execSync } from 'node:child_process';
5+
import { createInterface } from 'node:readline/promises';
6+
import { stdin, stdout } from 'node:process';
7+
8+
function run(cmd) {
9+
execSync(cmd, { stdio: 'inherit' });
10+
}
11+
12+
function capture(cmd) {
13+
return execSync(cmd, { encoding: 'utf8' }).trim();
14+
}
15+
16+
async function prompt(question) {
17+
const rl = createInterface({ input: stdin, output: stdout });
18+
try {
19+
return (await rl.question(question)).trim();
20+
} finally {
21+
rl.close();
22+
}
23+
}
24+
25+
async function main() {
26+
const branch = capture('git rev-parse --abbrev-ref HEAD');
27+
if (branch !== 'main') {
28+
throw new Error(`Refusing to release from branch "${branch}" — switch to main first.`);
29+
}
30+
31+
if (capture('git status --porcelain')) {
32+
throw new Error('Working tree is not clean — commit or stash changes before releasing.');
33+
}
34+
35+
run('git fetch origin main --quiet');
36+
if (capture('git rev-list HEAD..origin/main --count') !== '0') {
37+
throw new Error('Local main is behind origin/main — pull before releasing.');
38+
}
39+
40+
let version = process.argv[2] ?? (await prompt('Enter release version (e.g. 1.2.0): '));
41+
version = version.replace(/^v/, '').trim();
42+
if (!/^\d+\.\d+\.\d+(-[0-9A-Za-z-.]+)?$/.test(version)) {
43+
throw new Error(`Invalid version "${version}" — expected semver like 1.2.0`);
44+
}
45+
46+
const tag = `v${version}`;
47+
if (capture(`git tag -l ${tag}`)) {
48+
throw new Error(`Tag ${tag} already exists locally.`);
49+
}
50+
51+
const proceed = await prompt(
52+
`About to bump package.json to ${version}, commit, tag ${tag}, and push both to origin/main. Continue? [y/N] `,
53+
);
54+
if (!/^y(es)?$/i.test(proceed)) {
55+
console.log('Aborted.');
56+
return;
57+
}
58+
59+
run(`pnpm pkg set version="${version}"`);
60+
run('git add package.json');
61+
run(`git commit -m "chore: release ${tag}"`);
62+
run(`git tag ${tag}`);
63+
run('git push origin main');
64+
run(`git push origin ${tag}`);
65+
66+
console.log(`Pushed ${tag} — release workflow will build, publish, and update the changelog.`);
67+
}
68+
69+
main().catch((err) => {
70+
console.error(err.message ?? err);
71+
process.exit(1);
72+
});

0 commit comments

Comments
 (0)