Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions .cursor/skills/release/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
---
name: release
description: >-
Bumps package.json and jsr.json, pushes branch release/VERSION, opens a PR to
main with auto-merge, and relies on CI to publish a GitHub release after merge.
Use when the user invokes /release or /release VERSION, asks for a release PR,
or wants to ship a new semver version.
---

# Release (`/release` [VERSION])

In chat, users may type **`/release`** (patch bump) or **`/release 1.2.3`** (explicit semver). Treat that as this workflow.

## Goal

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.

## Preconditions

- Clean git working tree (no uncommitted changes).
- `git` and [`gh`](https://cli.github.com/) installed and authenticated (`gh auth login`).
- Repo default branch is `main`.
- Remote branch `release/<VERSION>` must not already exist.

## Steps

1. **Resolve VERSION** (if the user did not pass one): read `package.json` `version`, bump **patch** (e.g. `0.2.1` → `0.2.2`).
2. **Run the automation script** from the repo root (preferred):

```bash
npm run release:pr -- [VERSION]
```

Or: `node scripts/create-release-pr.mjs [VERSION]`. Omit `[VERSION]` for patch bump.

3. If the script cannot enable auto-merge, tell the user to merge the PR manually once CI passes.

## After merge

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).

## Tag convention

GitHub release tag: `v-${VERSION}` (e.g. `0.2.2` → tag `v-0.2.2`), matching `publish.yml` expectations.

## Do not

- Commit unrelated files (e.g. stray `site/` or local-only dirs) on the release branch.
- Bump only one of `package.json` / `jsr.json`; both must match for publish.
52 changes: 52 additions & 0 deletions .github/workflows/release-on-merge.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: Release on merge

# When a release/* PR merges into main, create a GitHub release (tag v-<version>).
# The published release triggers publish.yml (npm + JSR).

on:
pull_request:
types: [closed]
branches:
- main

concurrency:
group: release-on-merge-${{ github.event.pull_request.number }}
cancel-in-progress: false

permissions:
contents: write

jobs:
github-release:
if: >-
github.event.pull_request.merged == true &&
startsWith(github.head_ref, 'release/') &&
github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
steps:
- name: Checkout merge commit
uses: actions/checkout@v6
with:
ref: ${{ github.event.pull_request.merge_commit_sha }}

- name: Read version
id: ver
run: |
VERSION="$(node -p "require('./package.json').version")"
echo "version=${VERSION}" >> "${GITHUB_OUTPUT}"

- name: Create GitHub release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
VERSION="${{ steps.ver.outputs.version }}"
TAG="v-${VERSION}"
if gh release view "${TAG}" --repo "${{ github.repository }}" >/dev/null 2>&1; then
echo "Release ${TAG} already exists; skipping."
exit 0
fi
gh release create "${TAG}" \
--repo "${{ github.repository }}" \
--title "${VERSION}" \
--generate-notes
2 changes: 1 addition & 1 deletion jsr.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@proxymesh/javascript-proxy-headers",
"version": "0.2.0",
"version": "0.2.1",
"license": "MIT",
"exports": "./mod.ts"
}
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "javascript-proxy-headers",
"version": "0.2.0",
"version": "0.2.1",
"description": "Extensions for JavaScript HTTP libraries to support sending and receiving custom proxy headers during HTTPS CONNECT tunneling",
"type": "module",
"main": "index.js",
Expand Down Expand Up @@ -59,6 +59,7 @@
"LICENSE"
],
"scripts": {
"release:pr": "node scripts/create-release-pr.mjs",
"test": "node test/test_proxy_headers.js core axios node-fetch got undici superagent ky wretch make-fetch-happen needle typed-rest-client",
"test:run": "node run_tests.js",
"test:verbose": "node test/test_proxy_headers.js -v",
Expand Down
168 changes: 168 additions & 0 deletions scripts/create-release-pr.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
#!/usr/bin/env node
/**
* Bumps package.json + jsr.json, opens release/VERSION → main PR, enables auto-merge.
* Usage: node scripts/create-release-pr.mjs [VERSION]
* If VERSION is omitted, bumps the patch segment of the current package.json version.
*/
import { execFileSync, execSync } from "node:child_process";
import { readFileSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";

const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "..");

function sh(cmd, inherit = false) {
return execSync(cmd, {
cwd: repoRoot,
encoding: "utf8",
stdio: inherit ? "inherit" : "pipe",
}).trim();
}

function gh(args, inherit = false) {
if (inherit) {
execFileSync("gh", args, { cwd: repoRoot, stdio: "inherit" });
return "";
}
return execFileSync("gh", args, { cwd: repoRoot, encoding: "utf8" }).trim();
}

function ensureCleanWorkingTree() {
const out = sh("git status --porcelain");
if (out) {
throw new Error(
"Working tree is not clean. Commit or stash changes before running this script.",
);
}
}

function refExists(ref) {
try {
execSync(`git rev-parse --verify --quiet "${ref}"`, {
cwd: repoRoot,
stdio: "ignore",
});
return true;
} catch {
return false;
}
}

function remoteHeadsHas(branch) {
const out = sh(`git ls-remote --heads origin "${branch}"`);
return out.length > 0;
}

function parseSemver(v) {
const m = String(v).match(/^(\d+)\.(\d+)\.(\d+)$/);
if (!m) {
throw new Error(`VERSION must be semver X.Y.Z (got ${JSON.stringify(v)})`);
}
return [Number(m[1]), Number(m[2]), Number(m[3])];
}

function bumpPatch(version) {
const [a, b, c] = parseSemver(version);
return `${a}.${b}.${c + 1}`;
}

function cmpVersion(a, b) {
const pa = parseSemver(a);
const pb = parseSemver(b);
for (let i = 0; i < 3; i++) {
if (pa[i] > pb[i]) return 1;
if (pa[i] < pb[i]) return -1;
}
return 0;
}

function readJson(path) {
return JSON.parse(readFileSync(path, "utf8"));
}

function writeJson(path, obj) {
writeFileSync(path, `${JSON.stringify(obj, null, 2)}\n`, "utf8");
}

function main() {
process.chdir(repoRoot);

const arg = process.argv[2];
const pkgPath = join(repoRoot, "package.json");
const jsrPath = join(repoRoot, "jsr.json");

ensureCleanWorkingTree();

const pkg = readJson(pkgPath);
const current = pkg.version;
const target = arg ? String(arg).trim() : bumpPatch(current);

parseSemver(target);
if (cmpVersion(target, current) <= 0) {
throw new Error(
`New version must be greater than current ${current} (got ${target})`,
);
}

sh("git fetch origin main", true);
sh("git checkout main", true);
sh("git pull --ff-only origin main", true);

const branch = `release/${target}`;
if (refExists(`refs/heads/${branch}`)) {
throw new Error(
`Local branch ${branch} already exists. Delete it or pick another version.`,
);
}
if (remoteHeadsHas(branch)) {
throw new Error(
`Remote branch origin/${branch} already exists. Delete it or pick another version.`,
);
}

sh(`git checkout -b "${branch}"`, true);

pkg.version = target;
writeJson(pkgPath, pkg);

const jsr = readJson(jsrPath);
jsr.version = target;
writeJson(jsrPath, jsr);

sh(`git add package.json jsr.json`, true);
sh(`git commit -m "chore(release): bump version to ${target}"`, true);
sh(`git push -u origin "${branch}"`, true);

const body = [
"Automated release PR.",
"",
`- Bumps \`package.json\` and \`jsr.json\` to **${target}**`,
`- Merging publishes GitHub release \`v-${target}\` and triggers npm/JSR publish (see \`.github/workflows/\`).`,
].join("\n");

const prUrl = gh([
"pr",
"create",
"--base",
"main",
"--head",
branch,
"--title",
`Release ${target}`,
"--body",
body,
]);

console.log(prUrl);

try {
gh(["pr", "merge", prUrl, "--auto", "--merge"], true);
console.log("Auto-merge enabled; PR will merge when required checks pass.");
} catch {
console.warn(
"\nCould not enable auto-merge (repo may not allow it, or checks are pending). Merge the PR manually when ready.",
);
}
}

main();
Loading