mirror of
https://github.com/tale/headplane.git
synced 2026-08-30 08:49:30 +00:00
chore: add a release automation script
This commit is contained in:
@@ -77,3 +77,30 @@ jobs:
|
||||
subject-name: ghcr.io/${{ github.repository }}
|
||||
subject-digest: ${{ steps.push.outputs.digest }}
|
||||
push-to-registry: true
|
||||
|
||||
release:
|
||||
name: GitHub Release
|
||||
needs: docker
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: package.json
|
||||
|
||||
- name: Publish the release
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
TAG: ${{ github.ref_name }}
|
||||
run: |
|
||||
node --disable-warning=ExperimentalWarning scripts/release.ts notes "$TAG" > "$RUNNER_TEMP/notes.md"
|
||||
if [[ "$TAG" == *-* ]]; then
|
||||
gh release create "$TAG" --title "$TAG" --notes-file "$RUNNER_TEMP/notes.md" --prerelease
|
||||
else
|
||||
gh release create "$TAG" --title "$TAG" --notes-file "$RUNNER_TEMP/notes.md" --latest
|
||||
fi
|
||||
|
||||
@@ -53,3 +53,15 @@ The project has a documentation site available at the `docs/` directory built
|
||||
with VitePress. The documentation is written in Markdown and can be easily
|
||||
edited and extended. If making changes to staple features, please take care to
|
||||
also update the documentation to reflect any changes in functionality or usage.
|
||||
|
||||
## Releases
|
||||
|
||||
Every user-facing change adds a line to the `# Next` section of `CHANGELOG.md`,
|
||||
under `## Changes` or `## Fixes`. That section is what nightly users read on the
|
||||
docs site, so write it as prose for them, not as a commit summary. Prose placed
|
||||
directly under `# Next`, above the subheadings, becomes the release preamble.
|
||||
Use it for compatibility notes and upgrade warnings.
|
||||
|
||||
To cut a release, run `pnpm release cut <version>`. It renames `# Next` to the
|
||||
version, bumps `package.json`, commits, and tags. Pushing the tag builds the
|
||||
images and publishes the GitHub release from that changelog section.
|
||||
|
||||
+12
-1
@@ -1,8 +1,19 @@
|
||||
# Next
|
||||
|
||||
## Changes
|
||||
|
||||
- **Rebuilt the Browser SSH module to use Tailscale's `tsconnect`**, which should result in fewer bugs and better compatibility with future Tailscale releases.
|
||||
- Added `integration.agent.tailscale_netns`, an agent-only opt-out from Tailscale's routing-loop socket handling for deployments where its fallback pins the agent's Headscale connection to the wrong interface. Existing behavior remains enabled by default.
|
||||
- Added a Disable/Enable key expiry action to the machine menu (via [#554](https://github.com/tale/headplane/pull/554)). Headscale does not keep the toggle state apart from the expiry date, so re-enabling expiry marks the node expired as of that moment.
|
||||
- Headplane now supports Docker API version 1.24+ (Engine 1.12+, including Podman's Docker-compatible socket).
|
||||
- Usernames are now validated before a user is created or renamed, so Headplane rejects names that Headscale accepts but ACL policy can never match (closes [#502](https://github.com/tale/headplane/issues/502)).
|
||||
|
||||
## Fixes
|
||||
|
||||
- Fixed untagged Headscale builds being read as version 0.0.0. The per-commit `main-*` and `development` images report a Go pseudo-version from `/version`, which is now treated as an unknown version instead of an ancient one (via [#590](https://github.com/tale/headplane/pull/590)).
|
||||
- Fixed the Browser SSH WASM module not building under Nix (via [#588](https://github.com/tale/headplane/pull/588)).
|
||||
- Fixed `server.data_path`, `headscale.config_path`, `headscale.dns_records_path` and `headscale.tls_cert_path` being silently lowercased, which pointed Headplane at a different location for any path containing a capital letter (closes [#612](https://github.com/tale/headplane/issues/612)).
|
||||
- Fixed the Headplane agent falling back to an interactive Tailscale login. The agent now starts with a pre-auth-key, preserves its existing state across restarts, and auto-approves itself when Headscale requires manual approval (closes [#582](https://github.com/tale/headplane/issues/582)).
|
||||
- Added `integration.agent.tailscale_netns`, an agent-only opt-out from Tailscale's routing-loop socket handling for deployments where its fallback pins the agent's Headscale connection to the wrong interface. Existing behavior remains enabled by default.
|
||||
- Fixed creating pre-auth keys with an expiry of 1000 days or more. The number input submitted its locale-formatted value (`365,000`, `365 000`, `365.000`), which either failed with a 500 or silently created a key with a truncated expiry. The raw value is now submitted and the server rejects malformed expiries with a 400 (closes [#596](https://github.com/tale/headplane/issues/596)).
|
||||
|
||||
# 0.7.0
|
||||
|
||||
+2
-1
@@ -18,7 +18,8 @@
|
||||
"docs:build": "vitepress build docs",
|
||||
"docs:preview": "vitepress preview docs",
|
||||
"lint": "oxlint",
|
||||
"format": "oxfmt"
|
||||
"format": "oxfmt",
|
||||
"release": "node --disable-warning=ExperimentalWarning scripts/release.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@base-ui/react": "^1.7.0",
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
|
||||
type Section = {
|
||||
body: string;
|
||||
start: number;
|
||||
title: string;
|
||||
};
|
||||
|
||||
const CHANGELOG = new URL("../CHANGELOG.md", import.meta.url);
|
||||
const PACKAGE = new URL("../package.json", import.meta.url);
|
||||
const SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.]+)?$/;
|
||||
const BETA = "> This is a beta release. Please report any issues you encounter.";
|
||||
|
||||
function die(message: string): never {
|
||||
console.error(`release: ${message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function git(...args: string[]) {
|
||||
try {
|
||||
return execFileSync("git", args, {
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "inherit"],
|
||||
}).trim();
|
||||
} catch {
|
||||
die(`git ${args.join(" ")} failed`);
|
||||
}
|
||||
}
|
||||
|
||||
function readSections(text: string): Section[] {
|
||||
const heads = [...text.matchAll(/^# (.+)$/gm)];
|
||||
if (heads.length === 0) die("CHANGELOG.md has no sections");
|
||||
|
||||
return heads.map((head, index) => ({
|
||||
body: text.slice(head.index + head[0].length, heads[index + 1]?.index),
|
||||
start: head.index,
|
||||
title: head[1],
|
||||
}));
|
||||
}
|
||||
|
||||
function cut(version: string) {
|
||||
if (!SEMVER.test(version)) die(`\`${version}\` is not a version`);
|
||||
|
||||
const tag = `v${version}`;
|
||||
if (git("status", "--porcelain", "--untracked-files=no")) die("the working tree is dirty");
|
||||
if (git("tag", "--list", tag)) die(`${tag} already exists`);
|
||||
|
||||
const text = readFileSync(CHANGELOG, "utf8");
|
||||
const [next, ...released] = readSections(text);
|
||||
if (next.title !== "Next") die("CHANGELOG.md must open with a `# Next` section");
|
||||
|
||||
const notes = next.body.trim();
|
||||
if (!notes) die("`# Next` is empty");
|
||||
|
||||
const date = new Date().toLocaleDateString("en-US", {
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
});
|
||||
|
||||
const beta = version.includes("-") ? `${BETA}\n\n` : "";
|
||||
const section = `# ${version} (${date})\n\n${beta}${notes}\n\n---\n\n`;
|
||||
writeFileSync(CHANGELOG, `# Next\n\n${section}${text.slice(released[0].start)}`);
|
||||
|
||||
const pkg = readFileSync(PACKAGE, "utf8");
|
||||
const bumped = pkg.replace(/^(\s*"version": ")[^"]+(",)$/m, `$1${version}$2`);
|
||||
if (bumped === pkg) die("could not find the version field in package.json");
|
||||
writeFileSync(PACKAGE, bumped);
|
||||
|
||||
git("add", "CHANGELOG.md", "package.json");
|
||||
git("commit", "-m", `chore: ${tag}`);
|
||||
git("tag", "-m", tag, tag);
|
||||
|
||||
console.log(`${tag} is committed and tagged. To ship it:\n`);
|
||||
console.log(" git push origin main --follow-tags");
|
||||
}
|
||||
|
||||
function notes(tag?: string) {
|
||||
const text = readFileSync(CHANGELOG, "utf8");
|
||||
const section = readSections(text).find((entry) => /^\d/.test(entry.title));
|
||||
if (!section) die("CHANGELOG.md has no released version");
|
||||
|
||||
const version = section.title.split(" ")[0];
|
||||
if (tag && tag !== `v${version}`) die(`${tag} is not the top section (${version})`);
|
||||
|
||||
console.log(section.body.replace(/---\s*$/, "").trim());
|
||||
}
|
||||
|
||||
const [command, argument] = process.argv.slice(2);
|
||||
|
||||
switch (command) {
|
||||
case "cut":
|
||||
if (!argument) die("usage: release.ts cut <version>");
|
||||
cut(argument);
|
||||
break;
|
||||
|
||||
case "notes":
|
||||
notes(argument);
|
||||
break;
|
||||
|
||||
default:
|
||||
die("usage: release.ts <cut|notes> [tag]");
|
||||
}
|
||||
Reference in New Issue
Block a user