Skip to content

Commit d31e313

Browse files
authored
Merge pull request #21 from proxymesh/release/0.2.1
Release 0.2.1
2 parents cecb877 + fc8a893 commit d31e313

5 files changed

Lines changed: 272 additions & 2 deletions

File tree

.cursor/skills/release/SKILL.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
---
2+
name: release
3+
description: >-
4+
Bumps package.json and jsr.json, pushes branch release/VERSION, opens a PR to
5+
main with auto-merge, and relies on CI to publish a GitHub release after merge.
6+
Use when the user invokes /release or /release VERSION, asks for a release PR,
7+
or wants to ship a new semver version.
8+
---
9+
10+
# Release (`/release` [VERSION])
11+
12+
In chat, users may type **`/release`** (patch bump) or **`/release 1.2.3`** (explicit semver). Treat that as this workflow.
13+
14+
## Goal
15+
16+
Cut a **release PR** from `release/<VERSION>``main` with version bumps in `package.json` and `jsr.json`. Optional **VERSION** defaults to **patch bump** from the current `package.json` version.
17+
18+
## Preconditions
19+
20+
- Clean git working tree (no uncommitted changes).
21+
- `git` and [`gh`](https://cli.github.com/) installed and authenticated (`gh auth login`).
22+
- Repo default branch is `main`.
23+
- Remote branch `release/<VERSION>` must not already exist.
24+
25+
## Steps
26+
27+
1. **Resolve VERSION** (if the user did not pass one): read `package.json` `version`, bump **patch** (e.g. `0.2.1``0.2.2`).
28+
2. **Run the automation script** from the repo root (preferred):
29+
30+
```bash
31+
npm run release:pr -- [VERSION]
32+
```
33+
34+
Or: `node scripts/create-release-pr.mjs [VERSION]`. Omit `[VERSION]` for patch bump.
35+
36+
3. If the script cannot enable auto-merge, tell the user to merge the PR manually once CI passes.
37+
38+
## After merge
39+
40+
Merging into `main` triggers `.github/workflows/release-on-merge.yml`, which creates GitHub release tag `v-<VERSION>` and release notes. That **published** release runs `publish.yml` (npm + JSR).
41+
42+
## Tag convention
43+
44+
GitHub release tag: `v-${VERSION}` (e.g. `0.2.2` → tag `v-0.2.2`), matching `publish.yml` expectations.
45+
46+
## Do not
47+
48+
- Commit unrelated files (e.g. stray `site/` or local-only dirs) on the release branch.
49+
- Bump only one of `package.json` / `jsr.json`; both must match for publish.
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
name: Release on merge
2+
3+
# When a release/* PR merges into main, create a GitHub release (tag v-<version>).
4+
# The published release triggers publish.yml (npm + JSR).
5+
6+
on:
7+
pull_request:
8+
types: [closed]
9+
branches:
10+
- main
11+
12+
concurrency:
13+
group: release-on-merge-${{ github.event.pull_request.number }}
14+
cancel-in-progress: false
15+
16+
permissions:
17+
contents: write
18+
19+
jobs:
20+
github-release:
21+
if: >-
22+
github.event.pull_request.merged == true &&
23+
startsWith(github.head_ref, 'release/') &&
24+
github.event.pull_request.head.repo.full_name == github.repository
25+
runs-on: ubuntu-latest
26+
steps:
27+
- name: Checkout merge commit
28+
uses: actions/checkout@v6
29+
with:
30+
ref: ${{ github.event.pull_request.merge_commit_sha }}
31+
32+
- name: Read version
33+
id: ver
34+
run: |
35+
VERSION="$(node -p "require('./package.json').version")"
36+
echo "version=${VERSION}" >> "${GITHUB_OUTPUT}"
37+
38+
- name: Create GitHub release
39+
env:
40+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
41+
run: |
42+
set -euo pipefail
43+
VERSION="${{ steps.ver.outputs.version }}"
44+
TAG="v-${VERSION}"
45+
if gh release view "${TAG}" --repo "${{ github.repository }}" >/dev/null 2>&1; then
46+
echo "Release ${TAG} already exists; skipping."
47+
exit 0
48+
fi
49+
gh release create "${TAG}" \
50+
--repo "${{ github.repository }}" \
51+
--title "${VERSION}" \
52+
--generate-notes

jsr.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@proxymesh/javascript-proxy-headers",
3-
"version": "0.2.0",
3+
"version": "0.2.1",
44
"license": "MIT",
55
"exports": "./mod.ts"
66
}

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "javascript-proxy-headers",
3-
"version": "0.2.0",
3+
"version": "0.2.1",
44
"description": "Extensions for JavaScript HTTP libraries to support sending and receiving custom proxy headers during HTTPS CONNECT tunneling",
55
"type": "module",
66
"main": "index.js",
@@ -59,6 +59,7 @@
5959
"LICENSE"
6060
],
6161
"scripts": {
62+
"release:pr": "node scripts/create-release-pr.mjs",
6263
"test": "node test/test_proxy_headers.js core axios node-fetch got undici superagent ky wretch make-fetch-happen needle typed-rest-client",
6364
"test:run": "node run_tests.js",
6465
"test:verbose": "node test/test_proxy_headers.js -v",

scripts/create-release-pr.mjs

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Bumps package.json + jsr.json, opens release/VERSION → main PR, enables auto-merge.
4+
* Usage: node scripts/create-release-pr.mjs [VERSION]
5+
* If VERSION is omitted, bumps the patch segment of the current package.json version.
6+
*/
7+
import { execFileSync, execSync } from "node:child_process";
8+
import { readFileSync, writeFileSync } from "node:fs";
9+
import { dirname, join } from "node:path";
10+
import { fileURLToPath } from "node:url";
11+
12+
const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
13+
14+
function sh(cmd, inherit = false) {
15+
return execSync(cmd, {
16+
cwd: repoRoot,
17+
encoding: "utf8",
18+
stdio: inherit ? "inherit" : "pipe",
19+
}).trim();
20+
}
21+
22+
function gh(args, inherit = false) {
23+
if (inherit) {
24+
execFileSync("gh", args, { cwd: repoRoot, stdio: "inherit" });
25+
return "";
26+
}
27+
return execFileSync("gh", args, { cwd: repoRoot, encoding: "utf8" }).trim();
28+
}
29+
30+
function ensureCleanWorkingTree() {
31+
const out = sh("git status --porcelain");
32+
if (out) {
33+
throw new Error(
34+
"Working tree is not clean. Commit or stash changes before running this script.",
35+
);
36+
}
37+
}
38+
39+
function refExists(ref) {
40+
try {
41+
execSync(`git rev-parse --verify --quiet "${ref}"`, {
42+
cwd: repoRoot,
43+
stdio: "ignore",
44+
});
45+
return true;
46+
} catch {
47+
return false;
48+
}
49+
}
50+
51+
function remoteHeadsHas(branch) {
52+
const out = sh(`git ls-remote --heads origin "${branch}"`);
53+
return out.length > 0;
54+
}
55+
56+
function parseSemver(v) {
57+
const m = String(v).match(/^(\d+)\.(\d+)\.(\d+)$/);
58+
if (!m) {
59+
throw new Error(`VERSION must be semver X.Y.Z (got ${JSON.stringify(v)})`);
60+
}
61+
return [Number(m[1]), Number(m[2]), Number(m[3])];
62+
}
63+
64+
function bumpPatch(version) {
65+
const [a, b, c] = parseSemver(version);
66+
return `${a}.${b}.${c + 1}`;
67+
}
68+
69+
function cmpVersion(a, b) {
70+
const pa = parseSemver(a);
71+
const pb = parseSemver(b);
72+
for (let i = 0; i < 3; i++) {
73+
if (pa[i] > pb[i]) return 1;
74+
if (pa[i] < pb[i]) return -1;
75+
}
76+
return 0;
77+
}
78+
79+
function readJson(path) {
80+
return JSON.parse(readFileSync(path, "utf8"));
81+
}
82+
83+
function writeJson(path, obj) {
84+
writeFileSync(path, `${JSON.stringify(obj, null, 2)}\n`, "utf8");
85+
}
86+
87+
function main() {
88+
process.chdir(repoRoot);
89+
90+
const arg = process.argv[2];
91+
const pkgPath = join(repoRoot, "package.json");
92+
const jsrPath = join(repoRoot, "jsr.json");
93+
94+
ensureCleanWorkingTree();
95+
96+
const pkg = readJson(pkgPath);
97+
const current = pkg.version;
98+
const target = arg ? String(arg).trim() : bumpPatch(current);
99+
100+
parseSemver(target);
101+
if (cmpVersion(target, current) <= 0) {
102+
throw new Error(
103+
`New version must be greater than current ${current} (got ${target})`,
104+
);
105+
}
106+
107+
sh("git fetch origin main", true);
108+
sh("git checkout main", true);
109+
sh("git pull --ff-only origin main", true);
110+
111+
const branch = `release/${target}`;
112+
if (refExists(`refs/heads/${branch}`)) {
113+
throw new Error(
114+
`Local branch ${branch} already exists. Delete it or pick another version.`,
115+
);
116+
}
117+
if (remoteHeadsHas(branch)) {
118+
throw new Error(
119+
`Remote branch origin/${branch} already exists. Delete it or pick another version.`,
120+
);
121+
}
122+
123+
sh(`git checkout -b "${branch}"`, true);
124+
125+
pkg.version = target;
126+
writeJson(pkgPath, pkg);
127+
128+
const jsr = readJson(jsrPath);
129+
jsr.version = target;
130+
writeJson(jsrPath, jsr);
131+
132+
sh(`git add package.json jsr.json`, true);
133+
sh(`git commit -m "chore(release): bump version to ${target}"`, true);
134+
sh(`git push -u origin "${branch}"`, true);
135+
136+
const body = [
137+
"Automated release PR.",
138+
"",
139+
`- Bumps \`package.json\` and \`jsr.json\` to **${target}**`,
140+
`- Merging publishes GitHub release \`v-${target}\` and triggers npm/JSR publish (see \`.github/workflows/\`).`,
141+
].join("\n");
142+
143+
const prUrl = gh([
144+
"pr",
145+
"create",
146+
"--base",
147+
"main",
148+
"--head",
149+
branch,
150+
"--title",
151+
`Release ${target}`,
152+
"--body",
153+
body,
154+
]);
155+
156+
console.log(prUrl);
157+
158+
try {
159+
gh(["pr", "merge", prUrl, "--auto", "--merge"], true);
160+
console.log("Auto-merge enabled; PR will merge when required checks pass.");
161+
} catch {
162+
console.warn(
163+
"\nCould not enable auto-merge (repo may not allow it, or checks are pending). Merge the PR manually when ready.",
164+
);
165+
}
166+
}
167+
168+
main();

0 commit comments

Comments
 (0)