mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 06:05:24 +00:00
feat: beta channels for core and plugins with one stable/beta release workflow
This commit is contained in:
1 parent
6a855c0fa8
commit
f73c4b5ad6
55 files changed
+16200
-460
No files matched your search
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Works out beta numbers from a list of tags on stdin.
|
||||
*
|
||||
* node scripts/beta-version.cjs 26.11.0 < tags.txt -> 26.11.0-beta.3
|
||||
* node scripts/beta-version.cjs 26.11.0 --latest < tags.txt -> v26.11.0-beta.2
|
||||
*/
|
||||
|
||||
const fs = require("fs");
|
||||
|
||||
function betaNumbers(base, tags) {
|
||||
const prefix = `v${base}-beta.`;
|
||||
return tags
|
||||
.map((tag) => tag.trim())
|
||||
.filter((tag) => tag.startsWith(prefix))
|
||||
.map((tag) => Number(tag.slice(prefix.length)))
|
||||
.filter((n) => Number.isInteger(n) && n > 0)
|
||||
.sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
function nextBeta(base, tags) {
|
||||
const numbers = betaNumbers(base, tags);
|
||||
return `${base}-beta.${(numbers.at(-1) ?? 0) + 1}`;
|
||||
}
|
||||
|
||||
function latestBetaTag(base, tags) {
|
||||
const numbers = betaNumbers(base, tags);
|
||||
return numbers.length ? `v${base}-beta.${numbers.at(-1)}` : "";
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
const base = process.argv[2];
|
||||
if (!base || !/^\d+\.\d+\.\d+$/.test(base)) {
|
||||
console.error("beta-version: pass a version like 26.11.0");
|
||||
process.exit(1);
|
||||
}
|
||||
const tags = fs.readFileSync(0, "utf8").split("\n");
|
||||
console.log(
|
||||
process.argv.includes("--latest")
|
||||
? latestBetaTag(base, tags)
|
||||
: nextBeta(base, tags),
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = { latestBetaTag, nextBeta };
|
||||
@@ -0,0 +1,26 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createRequire } from "module";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const { latestBetaTag, nextBeta } = require("./beta-version.cjs");
|
||||
|
||||
const TAGS = [
|
||||
"v26.10.0",
|
||||
"v26.11.0-beta.1",
|
||||
"v26.11.0-beta.10",
|
||||
"v26.11.0-beta.2",
|
||||
"v26.11.0-beta.x",
|
||||
"v26.12.0-beta.4",
|
||||
];
|
||||
|
||||
describe("beta-version", () => {
|
||||
it("numbers the next beta after the highest one, not the last listed", () => {
|
||||
expect(nextBeta("26.11.0", TAGS)).toBe("26.11.0-beta.11");
|
||||
expect(latestBetaTag("26.11.0", TAGS)).toBe("v26.11.0-beta.10");
|
||||
});
|
||||
|
||||
it("starts at 1 for a version with no betas", () => {
|
||||
expect(nextBeta("27.1.0", TAGS)).toBe("27.1.0-beta.1");
|
||||
expect(latestBetaTag("27.1.0", TAGS)).toBe("");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,133 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Writes the body of a beta release: what a beta is, how to get it, how to
|
||||
* leave, and the commits since the previous beta (or stable release) grouped
|
||||
* by their feat:/fix: prefix.
|
||||
*
|
||||
* node scripts/generate-beta-body.cjs --version 26.11.0-beta.2 \
|
||||
* --branch dev-26.11.0 --sha <head> [--prev <ref>]
|
||||
*/
|
||||
|
||||
const { execFileSync } = require("child_process");
|
||||
|
||||
const REPO = "Termix-SSH/Termix";
|
||||
const MAX_COMMITS = 150;
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = {};
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
if (!argv[i].startsWith("--")) continue;
|
||||
const next = argv[i + 1];
|
||||
if (next === undefined || next.startsWith("--")) {
|
||||
args[argv[i].slice(2)] = true;
|
||||
} else {
|
||||
args[argv[i].slice(2)] = next;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
/** Commit subjects sorted into features, fixes and everything else. */
|
||||
function groupCommits(subjects) {
|
||||
const groups = { features: [], fixes: [], other: [] };
|
||||
for (const subject of subjects) {
|
||||
const text = subject.trim();
|
||||
if (!text) continue;
|
||||
const match = /^(\w+)(\([^)]*\))?!?:\s*(.+)$/.exec(text);
|
||||
const type = match?.[1].toLowerCase();
|
||||
const message = match ? match[3] : text;
|
||||
if (type === "feat") groups.features.push(message);
|
||||
else if (type === "fix") groups.fixes.push(message);
|
||||
else groups.other.push(message);
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
/** "26.11.0-beta.2" reads as "26.11.0 Beta 2". */
|
||||
function betaTitle(version) {
|
||||
const match = /^(.+)-beta\.(\d+)$/.exec(version);
|
||||
return match ? `${match[1]} Beta ${match[2]}` : version;
|
||||
}
|
||||
|
||||
function renderBody({ version, branch, sha, prev, subjects, repo = REPO }) {
|
||||
const shown = subjects.slice(0, MAX_COMMITS);
|
||||
const groups = groupCommits(shown);
|
||||
const list = (items) => items.map((item) => `- ${item}`).join("\n");
|
||||
const sections = [
|
||||
["Features", groups.features],
|
||||
["Fixes", groups.fixes],
|
||||
["Other changes", groups.other],
|
||||
]
|
||||
.filter(([, items]) => items.length > 0)
|
||||
.map(([title, items]) => `### ${title}\n\n${list(items)}`);
|
||||
|
||||
let changes;
|
||||
if (!prev) {
|
||||
changes = "First beta of this version.";
|
||||
} else if (sections.length === 0) {
|
||||
changes = "No changes since the last release.";
|
||||
} else {
|
||||
changes = sections.join("\n\n");
|
||||
if (subjects.length > MAX_COMMITS) {
|
||||
changes += `\n\n...and ${subjects.length - MAX_COMMITS} more.`;
|
||||
}
|
||||
changes += `\n\n[Full diff](https://github.com/${repo}/compare/${prev}...${sha})`;
|
||||
}
|
||||
|
||||
return `> [!WARNING]
|
||||
> This is a beta of the next Termix release, built from \`${branch}\`. It can have bugs, so keep a backup and don't use it where you can't afford problems.
|
||||
|
||||
**Found a bug or have feedback?** [Send beta feedback](https://github.com/${repo}/issues/new?template=beta_feedback.yml&termix-version=${encodeURIComponent(version)}). It is the fastest way to get it fixed before the stable release.
|
||||
|
||||
### How to get it
|
||||
|
||||
- **Docker:** \`ghcr.io/termix-ssh/termix:beta\` follows every beta. \`ghcr.io/termix-ssh/termix:${version}\` stays on this one.
|
||||
- **Desktop:** download an installer from the assets below.
|
||||
- **Going back:** switch to the \`latest\` image or a stable installer. Keep a backup of your data folder, since a beta can change the database.
|
||||
|
||||
## Changes in ${betaTitle(version)}
|
||||
|
||||
${changes}
|
||||
`;
|
||||
}
|
||||
|
||||
function readSubjects(prev, sha) {
|
||||
if (!prev) return [];
|
||||
const out = execFileSync(
|
||||
"git",
|
||||
[
|
||||
"log",
|
||||
"--no-merges",
|
||||
"--format=%s",
|
||||
`${prev}..${sha}`,
|
||||
"--",
|
||||
".",
|
||||
":!package-lock.json",
|
||||
],
|
||||
{ encoding: "utf8" },
|
||||
);
|
||||
return out.split("\n").filter(Boolean);
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
if (!args.version || !args.branch || !args.sha) {
|
||||
console.error(
|
||||
"generate-beta-body: --version, --branch and --sha are required",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
const prev = typeof args.prev === "string" ? args.prev : "";
|
||||
process.stdout.write(
|
||||
renderBody({
|
||||
version: args.version,
|
||||
branch: args.branch,
|
||||
sha: args.sha,
|
||||
prev,
|
||||
subjects: readSubjects(prev, args.sha),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = { betaTitle, groupCommits, renderBody };
|
||||
@@ -0,0 +1,57 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createRequire } from "module";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const {
|
||||
betaTitle,
|
||||
groupCommits,
|
||||
renderBody,
|
||||
} = require("./generate-beta-body.cjs");
|
||||
|
||||
describe("generate-beta-body", () => {
|
||||
it("groups commits by their prefix", () => {
|
||||
expect(
|
||||
groupCommits([
|
||||
"feat: add beta channel",
|
||||
"fix(sync): keep links",
|
||||
"chore: bump deps",
|
||||
"Plain message",
|
||||
]),
|
||||
).toEqual({
|
||||
features: ["add beta channel"],
|
||||
fixes: ["keep links"],
|
||||
other: ["bump deps", "Plain message"],
|
||||
});
|
||||
});
|
||||
|
||||
it("names the beta", () => {
|
||||
expect(betaTitle("26.11.0-beta.2")).toBe("26.11.0 Beta 2");
|
||||
});
|
||||
|
||||
it("links feedback with the version and lists the changes", () => {
|
||||
const body = renderBody({
|
||||
version: "26.11.0-beta.2",
|
||||
branch: "dev-26.11.0",
|
||||
sha: "abc",
|
||||
prev: "v26.11.0-beta.1",
|
||||
subjects: ["feat: one", "fix: two"],
|
||||
});
|
||||
expect(body).toContain("termix-version=26.11.0-beta.2");
|
||||
expect(body).toContain("## Changes in 26.11.0 Beta 2");
|
||||
expect(body).toContain("### Features\n\n- one");
|
||||
expect(body).toContain("### Fixes\n\n- two");
|
||||
expect(body).toContain("compare/v26.11.0-beta.1...abc");
|
||||
expect(body).not.toContain("—");
|
||||
});
|
||||
|
||||
it("says so when there is nothing to compare against", () => {
|
||||
const body = renderBody({
|
||||
version: "26.11.0-beta.1",
|
||||
branch: "dev-26.11.0",
|
||||
sha: "abc",
|
||||
prev: "",
|
||||
subjects: [],
|
||||
});
|
||||
expect(body).toContain("First beta of this version.");
|
||||
});
|
||||
});
|
||||
@@ -110,6 +110,12 @@ function adminSections() {
|
||||
"admin-ssl": keysIn(functionBody(settings, "AdminSSLSection")),
|
||||
"admin-api-keys": keysIn(read("AdminApiKeysSection.tsx")),
|
||||
"admin-audit-log": keysIn(read("AdminAuditLogSection.tsx")),
|
||||
updates: keysIn(
|
||||
fs.readFileSync(
|
||||
path.join(ROOT, "src", "ui", "settings", "UpdatesSettings.tsx"),
|
||||
"utf8",
|
||||
),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user