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
36 changes: 18 additions & 18 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,24 @@ To test individual modules, cd into them and run `yarn test`. For example, runni

# Publishing a Release

### Release Branch

1. Checkout main locally
2. Increment the version number in modules/react-arborist/package.json
3. Create a branch called release/v0.0.0
4. Open a PR to main
5. Test, review, and merge, delete branch

### Create Github Release

1. Create a release based on main
2. Assign a new tag to be created with v0.0.0
3. Title the release "Version 0.0.0"
4. Write release notes
5. Publish
6. Check that it successfully published to npmjs

The Github actions workflow will publish to npm.
Releases are driven by `bin/release.mjs`, invoked via `yarn release`. The script bumps the version, runs tests, and pushes a `v*` tag. The tag push triggers `.github/workflows/publish.yml`, which publishes to npm via Trusted Publishing (OIDC) — no token needed.

1. On `main`, with a clean working tree, update `CHANGELOG.md` with a new section for the upcoming version. Commit and push.
2. Run `yarn release <patch|minor|major|X.Y.Z>` from the repo root. The script will:
- Verify you're on `main`, the working tree is clean, and you're in sync with the remote
- Run tests and build
- Bump `modules/react-arborist/package.json`, commit it, and tag `vX.Y.Z`
- Push the commit and tag to `origin`
- Open a GitHub Release draft in your browser (via `gh`)
3. Edit the release draft, paste in the changelog entry, and publish.
4. Watch `gh run watch` — the publish workflow will build and `npm publish` via OIDC. Confirm the new version on https://www.npmjs.com/package/react-arborist.

Flags:

- `--preview` — dry-run; the script still reads git state and builds, but no commit, tag, push, or release draft is created.
- `--any-branch` — skip both the `main` branch check and the remote sync check (useful for testing on a branch that isn't pushed).
- `--no-tests` — skip `yarn test`.
- `--yes` — skip the interactive confirmation.

# Publish the Demo Site

Expand Down
130 changes: 130 additions & 0 deletions bin/release.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
#!/usr/bin/env node
import { execSync } from "node:child_process";
import { readFileSync, writeFileSync } from "node:fs";
import { createInterface } from "node:readline/promises";
import { stdin as input, stdout as output } from "node:process";
import path from "node:path";
import { fileURLToPath } from "node:url";

const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const pkgPath = path.join(rootDir, "modules/react-arborist/package.json");

const args = process.argv.slice(2);
const flags = {
preview: args.includes("--preview"),
anyBranch: args.includes("--any-branch"),
skipTests: args.includes("--no-tests"),
yes: args.includes("--yes") || args.includes("-y"),
};
const versionArg = args.find((a) => !a.startsWith("-"));

function out(cmd) {
return execSync(cmd, { cwd: rootDir, encoding: "utf8" }).trim();
}

function run(cmd) {
if (flags.preview) {
console.log(` [preview] ${cmd}`);
return;
}
execSync(cmd, { cwd: rootDir, stdio: "inherit" });
}

function step(name) {
console.log(`\n→ ${name}`);
}

function fail(msg) {
console.error(`✖ ${msg}`);
process.exit(1);
}

function bump(current, kind) {
if (/^\d+\.\d+\.\d+$/.test(kind)) return kind;
const [maj, min, pat] = current.split(".").map(Number);
if (kind === "patch") return `${maj}.${min}.${pat + 1}`;
if (kind === "minor") return `${maj}.${min + 1}.0`;
if (kind === "major") return `${maj + 1}.0.0`;
fail(`Invalid version: "${kind}". Use patch, minor, major, or X.Y.Z.`);
}

if (!versionArg) {
fail("Usage: yarn release <patch|minor|major|X.Y.Z> [--preview] [--any-branch] [--no-tests] [--yes]");
}

step("Checking branch");
const branch = out("git rev-parse --abbrev-ref HEAD");
if (branch !== "main" && !flags.anyBranch) {
fail(`Not on main (currently on ${branch}). Use --any-branch to override.`);
}
console.log(` on ${branch}`);

step("Checking working tree");
if (out("git status --porcelain")) {
fail("Working tree not clean. Commit or stash first.");
}
console.log(" clean");

if (flags.anyBranch) {
console.log("\n→ Skipping remote sync check (--any-branch)");
} else {
step("Fetching origin");
execSync("git fetch origin", { cwd: rootDir, stdio: "inherit" });
const local = out(`git rev-parse ${branch}`);
const remote = out(`git rev-parse origin/${branch}`);
if (local !== remote) {
fail(`Local ${branch} (${local.slice(0, 7)}) differs from origin/${branch} (${remote.slice(0, 7)}).`);
}
console.log(" in sync");
}

if (!flags.skipTests) {
step("Running tests");
run("yarn test");
}

step("Building library");
run("yarn build-lib");

const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
const oldVersion = pkg.version;
const newVersion = bump(oldVersion, versionArg);
const tag = `v${newVersion}`;

console.log(`\nVersion: ${oldVersion} → ${newVersion} (tag: ${tag})`);

if (out(`git tag -l ${tag}`)) {
fail(`Tag ${tag} already exists.`);
}

if (!flags.preview && !flags.yes) {
const rl = createInterface({ input, output });
const answer = await rl.question(`Continue? (y/N) `);
rl.close();
if (answer.trim().toLowerCase() !== "y") {
console.log("Aborted.");
process.exit(0);
}
}

step(`Bumping ${pkgPath} to ${newVersion}`);
if (flags.preview) {
console.log(` [preview] write version=${newVersion}`);
} else {
pkg.version = newVersion;
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
}

step("Committing");
run(`git commit -am ${tag}`);

step(`Tagging ${tag}`);
run(`git tag ${tag}`);

step("Pushing commit + tag");
run(`git push origin ${branch} --follow-tags`);

step("Opening GitHub release draft");
run(`gh release create ${tag} --draft --title ${tag} --notes "" --web`);

console.log(`\nReleased ${tag}. Watch the publish workflow with: gh run watch`);
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"clean": "yarn workspace react-arborist clean",
"showcase": "yarn workspace showcase start",
"start": "run-s clean build-lib && run-p watch showcase",
"publish": "sh bin/publish"
"publish": "sh bin/publish",
"release": "node bin/release.mjs"
},
"private": true,
"packageManager": "[email protected]",
Expand Down
Loading