feat: implement version bumping script and pre-push hook

This commit is contained in:
Azgaar 2026-03-07 16:06:18 +01:00
parent a66b60b5a7
commit 1f81dd2395
5 changed files with 285 additions and 3 deletions

10
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "fantasy-map-generator",
"version": "1.113.2",
"version": "1.113.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "fantasy-map-generator",
"version": "1.113.2",
"version": "1.113.6",
"license": "MIT",
"dependencies": {
"alea": "^1.0.1",
@ -1353,6 +1353,7 @@
"integrity": "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~7.16.0"
}
@ -1393,6 +1394,7 @@
"integrity": "sha512-gfajTHVCiwpxRj1qh0Sh/5bbGLG4F/ZH/V9xvFVoFddpITfMta9YGow0W6ZpTTORv2vdJuz9TnrNSmjKvpOf4g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@vitest/browser": "4.0.18",
"@vitest/mocker": "4.0.18",
@ -1874,6 +1876,7 @@
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
"license": "ISC",
"peer": true,
"engines": {
"node": ">=12"
}
@ -2160,6 +2163,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@ -2471,6 +2475,7 @@
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.27.0",
"fdir": "^6.5.0",
@ -2546,6 +2551,7 @@
"integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@vitest/expect": "4.0.18",
"@vitest/mocker": "4.0.18",

View file

@ -606,4 +606,6 @@ dr_not_sam
Mie96
Riley
Amber Davis
tomtom1969vlbg`;
tomtom1969vlbg
Eric Knight
Adeline Lefizelier`;

212
scripts/bump-version.js Normal file
View file

@ -0,0 +1,212 @@
#!/usr/bin/env node
"use strict";
/**
* Bump the project version (patch / minor / major).
*
* Updates:
* - public/versioning.js VERSION constant
* - package.json "version" field
* - src/index.html ?v= cache-busting hashes for changed public/*.js files
*
* Usage:
* node scripts/bump-version.js # interactive prompt
* node scripts/bump-version.js patch # non-interactive
* node scripts/bump-version.js minor
* node scripts/bump-version.js major
* node scripts/bump-version.js --dry-run # preview only, no writes
*/
const fs = require("fs");
const path = require("path");
const readline = require("readline");
const {execSync} = require("child_process");
// ---------------------------------------------------------------------------
// Paths
// ---------------------------------------------------------------------------
const repoRoot = path.resolve(__dirname, "..");
const packageJsonPath = path.join(repoRoot, "package.json");
const versioningPath = path.join(repoRoot, "public", "versioning.js");
const indexHtmlPath = path.join(repoRoot, "src", "index.html");
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function readFile(filePath) {
return fs.readFileSync(filePath, "utf8");
}
function writeFile(filePath, content) {
fs.writeFileSync(filePath, content, "utf8");
}
function parseCurrentVersion() {
const content = readFile(versioningPath);
const match = content.match(/const VERSION = "(\d+\.\d+\.\d+)";/);
if (!match) throw new Error("Could not find VERSION constant in public/versioning.js");
return match[1];
}
function bumpVersion(version, type) {
const [major, minor, patch] = version.split(".").map(Number);
if (type === "major") return `${major + 1}.0.0`;
if (type === "minor") return `${major}.${minor + 1}.0`;
return `${major}.${minor}.${patch + 1}`;
}
function escapeRegExp(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
/**
* Returns public/*.js paths (relative to repo root) that have changed.
* Checks (in order, deduplicating):
* 1. Upstream branch diff catches everything on a feature/PR branch
* 2. Staged (index) diff catches files staged but not yet committed
* 3. Last-commit diff fallback for main / detached HEAD
*/
function getChangedPublicJsFiles() {
const run = cmd => execSync(cmd, {encoding: "utf8", cwd: repoRoot});
const parseFiles = output =>
output
.split("\n")
.map(f => f.trim())
.filter(f => f.startsWith("public/") && f.endsWith(".js"));
const seen = new Set();
const collect = files => files.forEach(f => seen.add(f));
// 1. Upstream branch diff
try {
const upstream = run("git rev-parse --abbrev-ref --symbolic-full-name @{upstream}").trim();
collect(parseFiles(run(`git diff --name-only ${upstream}...HEAD`)));
} catch {
/* no upstream */
}
// 2. Staged changes (useful when building before committing)
try {
collect(parseFiles(run("git diff --name-only --cached")));
} catch {
/* ignore */
}
if (seen.size > 0) return [...seen];
// 3. Fallback: last commit diff
try {
return parseFiles(run("git diff --name-only HEAD~1 HEAD"));
} catch {
/* shallow / single-commit repo */
}
return [];
}
// ---------------------------------------------------------------------------
// File updaters
// ---------------------------------------------------------------------------
function updateVersioningJs(newVersion, dry) {
const original = readFile(versioningPath);
const updated = original.replace(/const VERSION = "\d+\.\d+\.\d+";/, `const VERSION = "${newVersion}";`);
if (original === updated) throw new Error("Failed to update VERSION in public/versioning.js");
if (!dry) writeFile(versioningPath, updated);
console.log(` public/versioning.js → ${newVersion}`);
}
function updatePackageJson(newVersion, dry) {
const original = readFile(packageJsonPath);
const pkg = JSON.parse(original);
const oldVersion = pkg.version;
pkg.version = newVersion;
if (!dry) writeFile(packageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`);
console.log(` package.json ${oldVersion}${newVersion}`);
}
function updateIndexHtmlHashes(newVersion, dry) {
const changedFiles = getChangedPublicJsFiles();
if (changedFiles.length === 0) {
console.log(" src/index.html (no changed public/*.js files detected)");
return;
}
let html = readFile(indexHtmlPath);
const updated = [];
for (const publicPath of changedFiles) {
const htmlPath = publicPath.replace(/^public\//, "");
const pattern = new RegExp(`${escapeRegExp(htmlPath)}\\?v=[0-9.]+`, "g");
if (pattern.test(html)) {
html = html.replace(pattern, `${htmlPath}?v=${newVersion}`);
updated.push(htmlPath);
}
}
if (updated.length > 0) {
if (!dry) writeFile(indexHtmlPath, html);
console.log(` src/index.html hashes updated for:\n - ${updated.join("\n - ")}`);
} else {
console.log(
` src/index.html (changed files not referenced: ${changedFiles.map(f => f.replace("public/", "")).join(", ")})`
);
}
}
// ---------------------------------------------------------------------------
// Prompt
// ---------------------------------------------------------------------------
function promptBumpType(currentVersion) {
return new Promise(resolve => {
const rl = readline.createInterface({input: process.stdin, output: process.stdout});
process.stdout.write(`\nCurrent version: ${currentVersion}\nBump type (patch / minor / major) [patch]: `);
rl.once("line", answer => {
rl.close();
const input = answer.trim().toLowerCase();
if (input === "minor" || input === "mi") return resolve("minor");
if (input === "major" || input === "maj") return resolve("major");
resolve("patch");
});
});
}
// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
async function main() {
const args = process.argv.slice(2).map(a => a.toLowerCase());
const dry = args.includes("--dry-run");
if (dry) console.log("\n[bump-version] DRY RUN — no files will be changed\n");
const currentVersion = parseCurrentVersion();
// Determine bump type: CLI arg → stdin prompt → default patch
let bumpType;
if (args.includes("major")) bumpType = "major";
else if (args.includes("minor")) bumpType = "minor";
else if (args.includes("patch")) bumpType = "patch";
else if (process.stdin.isTTY) bumpType = await promptBumpType(currentVersion);
else bumpType = "patch"; // non-interactive (CI / pipe)
const newVersion = bumpVersion(currentVersion, bumpType);
console.log(`\n[bump-version] ${bumpType}: ${currentVersion}${newVersion}\n`);
updateVersioningJs(newVersion, dry);
updatePackageJson(newVersion, dry);
updateIndexHtmlHashes(newVersion, dry);
console.log(`\n[bump-version] ${dry ? "(dry run) " : ""}done.\n`);
}
main().catch(err => {
console.error("\n[bump-version] Error:", err.message || err);
process.exit(1);
});

33
scripts/install-hooks.js Normal file
View file

@ -0,0 +1,33 @@
#!/usr/bin/env node
// Installs scripts/pre-push as .git/hooks/pre-push.
// Runs automatically via the `prepare` npm lifecycle hook (npm install).
const fs = require("fs");
const path = require("path");
const repoRoot = path.resolve(__dirname, "..");
const hooksDir = path.join(repoRoot, ".git", "hooks");
const source = path.join(repoRoot, "scripts", "pre-push");
const target = path.join(hooksDir, "pre-push");
if (!fs.existsSync(path.join(repoRoot, ".git"))) {
// Not a git repo (e.g. Docker / CI build from tarball) — skip silently.
process.exit(0);
}
if (!fs.existsSync(hooksDir)) {
fs.mkdirSync(hooksDir, {recursive: true});
}
try {
// Symlink so changes to scripts/pre-push are reflected immediately.
if (fs.existsSync(target) || fs.lstatSync(target).isSymbolicLink()) {
fs.unlinkSync(target);
}
} catch {
// Target doesn't exist yet — that's fine.
}
fs.symlinkSync(source, target);
fs.chmodSync(source, 0o755);
console.log("[prepare] Installed git pre-push hook → .git/hooks/pre-push");

29
scripts/pre-push Executable file
View file

@ -0,0 +1,29 @@
#!/usr/bin/env sh
# Git pre-push hook — automatically bumps the version before pushing.
# Installed by: npm run prepare (scripts/install-hooks.js)
#
# Prompts for patch / minor / major (default: patch), updates:
# - public/versioning.js
# - package.json
# - src/index.html (cache-busting ?v= hashes for changed modules)
# then commits those changes so they are included in the push.
set -e
REPO_ROOT="$(git rev-parse --show-toplevel)"
echo ""
node "$REPO_ROOT/scripts/bump-version.js"
# Stage files that may have been modified by the bump
git add \
"$REPO_ROOT/public/versioning.js" \
"$REPO_ROOT/package.json" \
"$REPO_ROOT/src/index.html" 2>/dev/null || true
# Only commit if there are staged changes from the bump
if ! git diff --cached --quiet; then
NEW_VERSION=$(node -e "const f=require('fs');const m=f.readFileSync('$REPO_ROOT/public/versioning.js','utf8').match(/const VERSION = \"([\d.]+)\"/);console.log(m[1])")
git commit -m "chore: bump version to $NEW_VERSION"
echo "[pre-push] Committed version bump → $NEW_VERSION"
fi