From 1f81dd23956c2bd1948ab823cd4e3fa15fe3cbe3 Mon Sep 17 00:00:00 2001 From: Azgaar Date: Sat, 7 Mar 2026 16:06:18 +0100 Subject: [PATCH 01/17] feat: implement version bumping script and pre-push hook --- package-lock.json | 10 +- public/modules/dynamic/supporters.js | 4 +- scripts/bump-version.js | 212 +++++++++++++++++++++++++++ scripts/install-hooks.js | 33 +++++ scripts/pre-push | 29 ++++ 5 files changed, 285 insertions(+), 3 deletions(-) create mode 100644 scripts/bump-version.js create mode 100644 scripts/install-hooks.js create mode 100755 scripts/pre-push diff --git a/package-lock.json b/package-lock.json index 2f6fc9ed2..fc45ca854 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/public/modules/dynamic/supporters.js b/public/modules/dynamic/supporters.js index 985e391d3..e0b120f98 100644 --- a/public/modules/dynamic/supporters.js +++ b/public/modules/dynamic/supporters.js @@ -606,4 +606,6 @@ dr_not_sam Mie96 Riley Amber Davis -tomtom1969vlbg`; +tomtom1969vlbg +Eric Knight +Adeline Lefizelier`; diff --git a/scripts/bump-version.js b/scripts/bump-version.js new file mode 100644 index 000000000..5604eb80f --- /dev/null +++ b/scripts/bump-version.js @@ -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); +}); diff --git a/scripts/install-hooks.js b/scripts/install-hooks.js new file mode 100644 index 000000000..d6bd825ec --- /dev/null +++ b/scripts/install-hooks.js @@ -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"); diff --git a/scripts/pre-push b/scripts/pre-push new file mode 100755 index 000000000..6e53cb5f9 --- /dev/null +++ b/scripts/pre-push @@ -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 From 87a9d7cc0a73bfaf2f7d326f12da6298c5dcaab0 Mon Sep 17 00:00:00 2001 From: Azgaar Date: Sat, 7 Mar 2026 16:06:25 +0100 Subject: [PATCH 02/17] chore: bump version to 1.113.5 --- package.json | 2 +- public/versioning.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index c6aa0ee6c..11d2cfc77 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fantasy-map-generator", - "version": "1.113.3", + "version": "1.113.5", "description": "Azgaar's _Fantasy Map Generator_ is a free web application that helps fantasy writers, game masters, and cartographers create and edit fantasy maps.", "homepage": "https://github.com/Azgaar/Fantasy-Map-Generator#readme", "bugs": { diff --git a/public/versioning.js b/public/versioning.js index 528f7cf2e..4dd78cfcb 100644 --- a/public/versioning.js +++ b/public/versioning.js @@ -13,7 +13,7 @@ * Example: 1.102.2 -> Major version 1, Minor version 102, Patch version 2 */ -const VERSION = "1.113.4"; +const VERSION = "1.113.5"; if (parseMapVersion(VERSION) !== VERSION) alert("versioning.js: Invalid format or parsing function"); { From d92aab82ff3414485969b537bacae352c37f30c3 Mon Sep 17 00:00:00 2001 From: Azgaar Date: Sat, 7 Mar 2026 16:06:47 +0100 Subject: [PATCH 03/17] chore: bump version to 1.113.6 --- package.json | 2 +- public/versioning.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 11d2cfc77..99a96119f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fantasy-map-generator", - "version": "1.113.5", + "version": "1.113.6", "description": "Azgaar's _Fantasy Map Generator_ is a free web application that helps fantasy writers, game masters, and cartographers create and edit fantasy maps.", "homepage": "https://github.com/Azgaar/Fantasy-Map-Generator#readme", "bugs": { diff --git a/public/versioning.js b/public/versioning.js index 4dd78cfcb..74f411b77 100644 --- a/public/versioning.js +++ b/public/versioning.js @@ -13,7 +13,7 @@ * Example: 1.102.2 -> Major version 1, Minor version 102, Patch version 2 */ -const VERSION = "1.113.5"; +const VERSION = "1.113.6"; if (parseMapVersion(VERSION) !== VERSION) alert("versioning.js: Invalid format or parsing function"); { From a7ad7055e0c7b9db065e68f1a4117ec0c254eded Mon Sep 17 00:00:00 2001 From: Azgaar Date: Sat, 7 Mar 2026 16:07:00 +0100 Subject: [PATCH 04/17] chore: bump version to 1.113.7 --- package.json | 2 +- public/versioning.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 99a96119f..318a1898d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fantasy-map-generator", - "version": "1.113.6", + "version": "1.113.7", "description": "Azgaar's _Fantasy Map Generator_ is a free web application that helps fantasy writers, game masters, and cartographers create and edit fantasy maps.", "homepage": "https://github.com/Azgaar/Fantasy-Map-Generator#readme", "bugs": { diff --git a/public/versioning.js b/public/versioning.js index 74f411b77..626e47edb 100644 --- a/public/versioning.js +++ b/public/versioning.js @@ -13,7 +13,7 @@ * Example: 1.102.2 -> Major version 1, Minor version 102, Patch version 2 */ -const VERSION = "1.113.6"; +const VERSION = "1.113.7"; if (parseMapVersion(VERSION) !== VERSION) alert("versioning.js: Invalid format or parsing function"); { From 0889fd7dffd4182c81a01c020a0846388acebb0a Mon Sep 17 00:00:00 2001 From: Azgaar Date: Sat, 7 Mar 2026 16:07:27 +0100 Subject: [PATCH 05/17] chore: bump version to 1.113.8 --- package.json | 2 +- public/versioning.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 318a1898d..ebd47b13f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fantasy-map-generator", - "version": "1.113.7", + "version": "1.113.8", "description": "Azgaar's _Fantasy Map Generator_ is a free web application that helps fantasy writers, game masters, and cartographers create and edit fantasy maps.", "homepage": "https://github.com/Azgaar/Fantasy-Map-Generator#readme", "bugs": { diff --git a/public/versioning.js b/public/versioning.js index 626e47edb..e4b4d7aec 100644 --- a/public/versioning.js +++ b/public/versioning.js @@ -13,7 +13,7 @@ * Example: 1.102.2 -> Major version 1, Minor version 102, Patch version 2 */ -const VERSION = "1.113.7"; +const VERSION = "1.113.8"; if (parseMapVersion(VERSION) !== VERSION) alert("versioning.js: Invalid format or parsing function"); { From a8d27c19bb1506f70c78f2c44a955d4122521d4f Mon Sep 17 00:00:00 2001 From: Azgaar Date: Sat, 7 Mar 2026 16:10:17 +0100 Subject: [PATCH 06/17] chore: bump version to 1.113.9 --- package.json | 2 +- public/versioning.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index ebd47b13f..5585c9f7e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fantasy-map-generator", - "version": "1.113.8", + "version": "1.113.9", "description": "Azgaar's _Fantasy Map Generator_ is a free web application that helps fantasy writers, game masters, and cartographers create and edit fantasy maps.", "homepage": "https://github.com/Azgaar/Fantasy-Map-Generator#readme", "bugs": { diff --git a/public/versioning.js b/public/versioning.js index e4b4d7aec..7f73383a0 100644 --- a/public/versioning.js +++ b/public/versioning.js @@ -13,7 +13,7 @@ * Example: 1.102.2 -> Major version 1, Minor version 102, Patch version 2 */ -const VERSION = "1.113.8"; +const VERSION = "1.113.9"; if (parseMapVersion(VERSION) !== VERSION) alert("versioning.js: Invalid format or parsing function"); { From bbd3a907c2a6a8b8b184cd828bb23eb775329584 Mon Sep 17 00:00:00 2001 From: Azgaar Date: Sat, 7 Mar 2026 16:10:43 +0100 Subject: [PATCH 07/17] chore: bump version to 1.113.10 --- package.json | 2 +- public/versioning.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 5585c9f7e..190974d65 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fantasy-map-generator", - "version": "1.113.9", + "version": "1.113.10", "description": "Azgaar's _Fantasy Map Generator_ is a free web application that helps fantasy writers, game masters, and cartographers create and edit fantasy maps.", "homepage": "https://github.com/Azgaar/Fantasy-Map-Generator#readme", "bugs": { diff --git a/public/versioning.js b/public/versioning.js index 7f73383a0..825a6306d 100644 --- a/public/versioning.js +++ b/public/versioning.js @@ -13,7 +13,7 @@ * Example: 1.102.2 -> Major version 1, Minor version 102, Patch version 2 */ -const VERSION = "1.113.9"; +const VERSION = "1.113.10"; if (parseMapVersion(VERSION) !== VERSION) alert("versioning.js: Invalid format or parsing function"); { From 274e9927997d5f8522a13e3f67b58c0b38963a3f Mon Sep 17 00:00:00 2001 From: Azgaar Date: Sat, 7 Mar 2026 16:37:31 +0100 Subject: [PATCH 08/17] chore: bump version to 1.113.3 and update versioning process --- .github/workflows/bump-version.yml | 68 ++++++++++++++++++++++++++++++ package-lock.json | 4 +- package.json | 2 +- public/versioning.js | 39 ++++++++++------- scripts/bump-version.js | 4 +- scripts/install-hooks.js | 33 --------------- scripts/pre-push | 29 ------------- 7 files changed, 96 insertions(+), 83 deletions(-) create mode 100644 .github/workflows/bump-version.yml delete mode 100644 scripts/install-hooks.js delete mode 100755 scripts/pre-push diff --git a/.github/workflows/bump-version.yml b/.github/workflows/bump-version.yml new file mode 100644 index 000000000..de50d24c0 --- /dev/null +++ b/.github/workflows/bump-version.yml @@ -0,0 +1,68 @@ +name: Bump version on PR merge + +on: + pull_request: + branches: [master] + types: [closed] + +permissions: + contents: write + +jobs: + bump: + # Only run when the PR was actually merged (not just closed) + if: github.event.pull_request.merged == true + + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v5 + with: + # fetch-depth 2 so git diff HEAD~1 HEAD works for detecting changed files + fetch-depth: 2 + # Use a token so the pushed commit triggers deploy.yml + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Node + uses: actions/setup-node@v6 + with: + node-version: lts/* + cache: npm + + - name: Determine bump type from PR labels + id: bump + run: | + LABELS='${{ toJSON(github.event.pull_request.labels.*.name) }}' + if echo "$LABELS" | grep -q '"major"'; then + echo "type=major" >> "$GITHUB_OUTPUT" + elif echo "$LABELS" | grep -q '"minor"'; then + echo "type=minor" >> "$GITHUB_OUTPUT" + else + echo "type=patch" >> "$GITHUB_OUTPUT" + fi + + - name: Run version bump script + run: node scripts/bump-version.js ${{ steps.bump.outputs.type }} + + - name: Commit and push bump + run: | + NEW_VERSION=$(node -e " + const m = require('fs') + .readFileSync('public/versioning.js', 'utf8') + .match(/const VERSION = \"([\d.]+)\"/); + console.log(m[1]); + ") + + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add . + + # Only commit if something actually changed + if ! git diff --cached --quiet; then + git commit -m "chore: bump version to $NEW_VERSION" + git push + echo "Pushed version bump → $NEW_VERSION" + else + echo "Nothing changed, skipping commit." + fi diff --git a/package-lock.json b/package-lock.json index fc45ca854..e607bc5cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "fantasy-map-generator", - "version": "1.113.6", + "version": "1.113.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "fantasy-map-generator", - "version": "1.113.6", + "version": "1.113.3", "license": "MIT", "dependencies": { "alea": "^1.0.1", diff --git a/package.json b/package.json index 190974d65..c6aa0ee6c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fantasy-map-generator", - "version": "1.113.10", + "version": "1.113.3", "description": "Azgaar's _Fantasy Map Generator_ is a free web application that helps fantasy writers, game masters, and cartographers create and edit fantasy maps.", "homepage": "https://github.com/Azgaar/Fantasy-Map-Generator#readme", "bugs": { diff --git a/public/versioning.js b/public/versioning.js index 825a6306d..9639d4dce 100644 --- a/public/versioning.js +++ b/public/versioning.js @@ -5,15 +5,18 @@ * We use Semantic Versioning: major.minor.patch. Refer to https://semver.org * Our .map file format is considered the public API. * - * Update the version MANUALLY on each merge to main: + * Update the version on each merge to main: * 1. MAJOR version: Incompatible changes that break existing maps * 2. MINOR version: Additions or changes that are backward-compatible but may require old .map files to be updated - * 3. PATCH version: Backward-compatible bug fixes and small features that do not affect the .map file format + * 3. PATCH version: Backward-compatible bug fixes and small features that don't affect the .map file format * * Example: 1.102.2 -> Major version 1, Minor version 102, Patch version 2 + * Version bumping is automated via GitHub Actions on PR merge. + * + * For the changes that may be interesting to end users, update the `latestPublicChanges` array below (new changes on top). */ -const VERSION = "1.113.10"; +const VERSION = "1.113.3"; if (parseMapVersion(VERSION) !== VERSION) alert("versioning.js: Invalid format or parsing function"); { @@ -26,6 +29,22 @@ if (parseMapVersion(VERSION) !== VERSION) alert("versioning.js: Invalid format o setTimeout(showUpdateWindow, 6000); } + const latestPublicChanges = [ + "Search input in Overview dialogs", + "Custom burg grouping and icon selection", + "Ability to set custom image as Marker or Regiment icon", + "Submap and Transform tools rework", + "Azgaar Bot to answer questions and provide help", + "Labels: ability to set letter spacing", + "Zones performance improvement", + "Notes Editor: on-demand AI text generation", + "New style preset: Dark Seas", + "New routes generation algorithm", + "Routes overview tool", + "Configurable longitude", + "Export zones to GeoJSON" + ]; + function showUpdateWindow() { const changelog = "https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog"; const reddit = "https://www.reddit.com/r/FantasyMapGenerator"; @@ -37,19 +56,7 @@ if (parseMapVersion(VERSION) !== VERSION) alert("versioning.js: Invalid format o
    Latest changes: -
  • Search input in Overview dialogs
  • -
  • Custom burg grouping and icon selection
  • -
  • Ability to set custom image as Marker or Regiment icon
  • -
  • Submap and Transform tools rework
  • -
  • Azgaar Bot to answer questions and provide help
  • -
  • Labels: ability to set letter spacing
  • -
  • Zones performance improvement
  • -
  • Notes Editor: on-demand AI text generation
  • -
  • New style preset: Dark Seas
  • -
  • New routes generation algorithm
  • -
  • Routes overview tool
  • -
  • Configurable longitude
  • -
  • Export zones to GeoJSON
  • + ${latestPublicChanges.map(change => `
  • ${change}
  • `).join("")}

Join our Discord server and Reddit community to ask questions, share maps, discuss the Generator and Worlbuilding, report bugs and propose new features.

diff --git a/scripts/bump-version.js b/scripts/bump-version.js index 5604eb80f..af4c35a33 100644 --- a/scripts/bump-version.js +++ b/scripts/bump-version.js @@ -12,8 +12,8 @@ * 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 minor # non-interactive + * node scripts/bump-version.js major # non-interactive * node scripts/bump-version.js --dry-run # preview only, no writes */ diff --git a/scripts/install-hooks.js b/scripts/install-hooks.js deleted file mode 100644 index d6bd825ec..000000000 --- a/scripts/install-hooks.js +++ /dev/null @@ -1,33 +0,0 @@ -#!/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"); diff --git a/scripts/pre-push b/scripts/pre-push deleted file mode 100755 index 6e53cb5f9..000000000 --- a/scripts/pre-push +++ /dev/null @@ -1,29 +0,0 @@ -#!/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 From 71ad41b4fcd9603619632cb8f23395a8a9298049 Mon Sep 17 00:00:00 2001 From: Azgaar Date: Sat, 7 Mar 2026 16:59:08 +0100 Subject: [PATCH 09/17] chore: enhance version bump process with base version check --- .github/workflows/bump-version.yml | 12 ++++++++++- scripts/bump-version.js | 32 +++++++++++++++++++++++++++++- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/.github/workflows/bump-version.yml b/.github/workflows/bump-version.yml index de50d24c0..8d5c8508e 100644 --- a/.github/workflows/bump-version.yml +++ b/.github/workflows/bump-version.yml @@ -42,8 +42,18 @@ jobs: echo "type=patch" >> "$GITHUB_OUTPUT" fi + - name: Extract base version (master before this PR merged) + id: base + run: | + BASE=$(git show HEAD~1:public/versioning.js \ + | grep -oP 'const VERSION = "\K[\d.]+') + echo "version=$BASE" >> "$GITHUB_OUTPUT" + echo "Base version on master before merge: $BASE" + - name: Run version bump script - run: node scripts/bump-version.js ${{ steps.bump.outputs.type }} + run: | + node scripts/bump-version.js ${{ steps.bump.outputs.type }} \ + --base-version ${{ steps.base.outputs.version }} - name: Commit and push bump run: | diff --git a/scripts/bump-version.js b/scripts/bump-version.js index af4c35a33..e9859e820 100644 --- a/scripts/bump-version.js +++ b/scripts/bump-version.js @@ -61,6 +61,17 @@ function escapeRegExp(str) { return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } +/** Returns true if versionA is strictly greater than versionB (semver). */ +function isVersionGreater(versionA, versionB) { + const a = versionA.split(".").map(Number); + const b = versionB.split(".").map(Number); + for (let i = 0; i < 3; i++) { + if (a[i] > b[i]) return true; + if (a[i] < b[i]) return false; + } + return false; // equal +} + /** * Returns public/*.js paths (relative to repo root) that have changed. * Checks (in order, deduplicating): @@ -180,13 +191,32 @@ function promptBumpType(currentVersion) { // --------------------------------------------------------------------------- async function main() { - const args = process.argv.slice(2).map(a => a.toLowerCase()); + const argv = process.argv.slice(2); + const args = argv.map(a => a.toLowerCase()); const dry = args.includes("--dry-run"); + // --base-version X.Y.Z — version on master before this PR was merged. + // When provided, the script checks whether the developer already bumped + // the version manually in their branch. If so, the increment is skipped + // and only the ?v= hashes in index.html are refreshed. + const baseVersionFlagIdx = argv.findIndex(a => a === "--base-version"); + const baseVersion = baseVersionFlagIdx !== -1 ? argv[baseVersionFlagIdx + 1] : null; + if (dry) console.log("\n[bump-version] DRY RUN — no files will be changed\n"); const currentVersion = parseCurrentVersion(); + if (baseVersion && isVersionGreater(currentVersion, baseVersion)) { + // Developer already bumped the version manually in their branch. + console.log( + `\n[bump-version] Version already updated manually: ${baseVersion} → ${currentVersion} (base was ${baseVersion})\n` + ); + console.log(" Skipping version increment — updating ?v= hashes only.\n"); + updateIndexHtmlHashes(currentVersion, dry); + console.log(`\n[bump-version] ${dry ? "(dry run) " : ""}done.\n`); + return; + } + // Determine bump type: CLI arg → stdin prompt → default patch let bumpType; if (args.includes("major")) bumpType = "major"; From f43fd9af3cefbd674c517a1b1d49360d7a1ee316 Mon Sep 17 00:00:00 2001 From: Azgaar Date: Sat, 7 Mar 2026 17:00:33 +0100 Subject: [PATCH 10/17] Update .github/workflows/bump-version.yml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/workflows/bump-version.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/bump-version.yml b/.github/workflows/bump-version.yml index 8d5c8508e..e2b436aeb 100644 --- a/.github/workflows/bump-version.yml +++ b/.github/workflows/bump-version.yml @@ -21,8 +21,8 @@ jobs: with: # fetch-depth 2 so git diff HEAD~1 HEAD works for detecting changed files fetch-depth: 2 - # Use a token so the pushed commit triggers deploy.yml - token: ${{ secrets.GITHUB_TOKEN }} + # Use a PAT/GitHub App token so the pushed commit can trigger deploy.yml and other workflows + token: ${{ secrets.RELEASE_BOT_TOKEN }} - name: Set up Node uses: actions/setup-node@v6 From 2eeb016c9e81a1e593e099820e02d0a3c756c7f8 Mon Sep 17 00:00:00 2001 From: Azgaar Date: Sat, 7 Mar 2026 17:02:58 +0100 Subject: [PATCH 11/17] Update .github/workflows/bump-version.yml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/workflows/bump-version.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/bump-version.yml b/.github/workflows/bump-version.yml index e2b436aeb..ea2c9076e 100644 --- a/.github/workflows/bump-version.yml +++ b/.github/workflows/bump-version.yml @@ -27,7 +27,7 @@ jobs: - name: Set up Node uses: actions/setup-node@v6 with: - node-version: lts/* + node-version: '24.x' cache: npm - name: Determine bump type from PR labels From 7b498020048089895bfb6ce4dfde07b0cf1d4848 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Mar 2026 17:03:34 +0100 Subject: [PATCH 12/17] fix: sync package-lock.json version fields during automated version bump (#1345) * Initial plan * fix: update package-lock.json version fields during version bump Co-authored-by: Azgaar <26469650+Azgaar@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Azgaar <26469650+Azgaar@users.noreply.github.com> --- scripts/bump-version.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/scripts/bump-version.js b/scripts/bump-version.js index e9859e820..e11ba058e 100644 --- a/scripts/bump-version.js +++ b/scripts/bump-version.js @@ -7,6 +7,7 @@ * Updates: * - public/versioning.js — VERSION constant * - package.json — "version" field + * - package-lock.json — top-level "version" and packages[""].version fields * - src/index.html — ?v= cache-busting hashes for changed public/*.js files * * Usage: @@ -28,6 +29,7 @@ const {execSync} = require("child_process"); const repoRoot = path.resolve(__dirname, ".."); const packageJsonPath = path.join(repoRoot, "package.json"); +const packageLockJsonPath = path.join(repoRoot, "package-lock.json"); const versioningPath = path.join(repoRoot, "public", "versioning.js"); const indexHtmlPath = path.join(repoRoot, "src", "index.html"); @@ -138,6 +140,22 @@ function updatePackageJson(newVersion, dry) { console.log(` package.json ${oldVersion} → ${newVersion}`); } +function updatePackageLockJson(newVersion, dry) { + if (!fs.existsSync(packageLockJsonPath)) { + console.log(" package-lock.json (not found, skipping)"); + return; + } + const original = readFile(packageLockJsonPath); + const lock = JSON.parse(original); + const oldVersion = lock.version; + lock.version = newVersion; + if (lock.packages && lock.packages[""]) { + lock.packages[""].version = newVersion; + } + if (!dry) writeFile(packageLockJsonPath, `${JSON.stringify(lock, null, 2)}\n`); + console.log(` package-lock.json ${oldVersion} → ${newVersion}`); +} + function updateIndexHtmlHashes(newVersion, dry) { const changedFiles = getChangedPublicJsFiles(); @@ -231,6 +249,7 @@ async function main() { updateVersioningJs(newVersion, dry); updatePackageJson(newVersion, dry); + updatePackageLockJson(newVersion, dry); updateIndexHtmlHashes(newVersion, dry); console.log(`\n[bump-version] ${dry ? "(dry run) " : ""}done.\n`); From ab445d910e2a5a171dfe8e7488240f6f7e9d58de Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Mar 2026 17:03:50 +0100 Subject: [PATCH 13/17] =?UTF-8?q?Fix=20branch=20name=20in=20versioning.js?= =?UTF-8?q?=20comment:=20'main'=20=E2=86=92=20'master'=20(#1346)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Initial plan * fix: update branch name in versioning.js comment from 'main' to 'master' Co-authored-by: Azgaar <26469650+Azgaar@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Azgaar <26469650+Azgaar@users.noreply.github.com> --- public/versioning.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/versioning.js b/public/versioning.js index 9639d4dce..e6660a668 100644 --- a/public/versioning.js +++ b/public/versioning.js @@ -5,7 +5,7 @@ * We use Semantic Versioning: major.minor.patch. Refer to https://semver.org * Our .map file format is considered the public API. * - * Update the version on each merge to main: + * Update the version on each merge to master: * 1. MAJOR version: Incompatible changes that break existing maps * 2. MINOR version: Additions or changes that are backward-compatible but may require old .map files to be updated * 3. PATCH version: Backward-compatible bug fixes and small features that don't affect the .map file format From 9a92a481d8111c5c102f4f951243a93346d22158 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Mar 2026 17:21:15 +0100 Subject: [PATCH 14/17] Extend bump-version.js to update ?v= cache-busting hashes in public/**/*.js dynamic imports (#1347) * Initial plan * feat: extend bump-version.js to update ?v= hashes in public/**/*.js dynamic imports Co-authored-by: Azgaar <26469650+Azgaar@users.noreply.github.com> * chore: merge base branch changes (package-lock.json sync, RELEASE_BOT_TOKEN, node 24.x, comment fix) Co-authored-by: Azgaar <26469650+Azgaar@users.noreply.github.com> * Update scripts/bump-version.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update scripts/bump-version.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Azgaar <26469650+Azgaar@users.noreply.github.com> Co-authored-by: Azgaar Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- scripts/bump-version.js | 106 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 100 insertions(+), 6 deletions(-) diff --git a/scripts/bump-version.js b/scripts/bump-version.js index e11ba058e..de7c8f457 100644 --- a/scripts/bump-version.js +++ b/scripts/bump-version.js @@ -9,6 +9,7 @@ * - package.json — "version" field * - package-lock.json — top-level "version" and packages[""].version fields * - src/index.html — ?v= cache-busting hashes for changed public/*.js files + * - public/**\/*.js — ?v= cache-busting hashes in dynamic import() calls * * Usage: * node scripts/bump-version.js # interactive prompt @@ -156,9 +157,7 @@ function updatePackageLockJson(newVersion, dry) { console.log(` package-lock.json ${oldVersion} → ${newVersion}`); } -function updateIndexHtmlHashes(newVersion, dry) { - const changedFiles = getChangedPublicJsFiles(); - +function updateIndexHtmlHashes(changedFiles, newVersion, dry) { if (changedFiles.length === 0) { console.log(" src/index.html (no changed public/*.js files detected)"); return; @@ -181,11 +180,102 @@ function updateIndexHtmlHashes(newVersion, dry) { 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(", ")})` + const publicRoot = path.join(repoRoot, "public"); + + function walk(dir) { + for (const entry of fs.readdirSync(dir, {withFileTypes: true})) { + const full = path.join(dir, entry.name); + if (entry.isDirectory()) { + // Skip third-party vendor bundles under public/libs/** + const relFromPublic = path + .relative(publicRoot, full) + .replace(/\\/g, "/"); + if (relFromPublic === "libs" || relFromPublic.startsWith("libs/")) { + continue; + } + walk(full); + } else if (entry.isFile() && entry.name.endsWith(".js")) { + results.push(path.relative(repoRoot, full).replace(/\\/g, "/")); + } + } + } + + walk(publicRoot); ); } } +/** Returns all .js file paths (relative to repo root, with forward slashes) under public/. */ +function getAllPublicJsFiles() { + const results = []; + function walk(dir) { + for (const entry of fs.readdirSync(dir, {withFileTypes: true})) { + const full = path.join(dir, entry.name); + if (entry.isDirectory()) { + walk(full); + } else if (entry.isFile() && entry.name.endsWith(".js")) { + results.push(path.relative(repoRoot, full).replace(/\\/g, "/")); + } + } + } + walk(path.join(repoRoot, "public")); + return results; +} + +/** + * Scans all public/**\/*.js files for relative dynamic-import ?v= references + * (e.g. import("../dynamic/supporters.js?v=1.97.14")) and updates any that + * point to one of the changed files. + */ +function updatePublicJsDynamicImportHashes(changedFiles, newVersion, dry) { + if (changedFiles.length === 0) { + console.log(" public/**/*.js (no changed public/*.js files detected)"); + return; + const replacement = `${quote}${relImportPath}?v=${newVersion}${quote}`; + // Only record and apply an update if the version actually changes + if (match === replacement) { + return match; + } + if (!updatedMap[relJsFile]) updatedMap[relJsFile] = []; + updatedMap[relJsFile].push(relImportPath); + return replacement; + const publicJsFiles = getAllPublicJsFiles(); + const updatedMap = {}; + + for (const relJsFile of publicJsFiles) { + const absJsFile = path.join(repoRoot, relJsFile); + const content = readFile(absJsFile); + const dir = path.dirname(absJsFile); + + const pattern = /(['"])(\.\.?\/[^'"]*)\?v=[0-9.]+\1/g; + const newContent = content.replace(pattern, (match, quote, relImportPath) => { + // Strip any query string defensively before resolving the path + const cleanImportPath = relImportPath.split("?")[0]; + const absImport = path.resolve(dir, cleanImportPath); + const repoRelImport = path.relative(repoRoot, absImport).replace(/\\/g, "/"); + if (changedSet.has(repoRelImport)) { + if (!updatedMap[relJsFile]) updatedMap[relJsFile] = []; + updatedMap[relJsFile].push(relImportPath); + return `${quote}${relImportPath}?v=${newVersion}${quote}`; + } + return match; + }); + + if (updatedMap[relJsFile] && !dry) { + writeFile(absJsFile, newContent); + } + } + + if (Object.keys(updatedMap).length > 0) { + const lines = Object.entries(updatedMap) + .map(([file, refs]) => ` ${file}:\n - ${refs.join("\n - ")}`) + .join("\n"); + console.log(` public/**/*.js hashes updated:\n${lines}`); + } else { + console.log(" public/**/*.js (no dynamic import ?v= hashes needed updating)"); + } +} + // --------------------------------------------------------------------------- // Prompt // --------------------------------------------------------------------------- @@ -230,7 +320,9 @@ async function main() { `\n[bump-version] Version already updated manually: ${baseVersion} → ${currentVersion} (base was ${baseVersion})\n` ); console.log(" Skipping version increment — updating ?v= hashes only.\n"); - updateIndexHtmlHashes(currentVersion, dry); + const changedFiles = getChangedPublicJsFiles(); + updateIndexHtmlHashes(changedFiles, currentVersion, dry); + updatePublicJsDynamicImportHashes(changedFiles, currentVersion, dry); console.log(`\n[bump-version] ${dry ? "(dry run) " : ""}done.\n`); return; } @@ -247,10 +339,12 @@ async function main() { console.log(`\n[bump-version] ${bumpType}: ${currentVersion} → ${newVersion}\n`); + const changedFiles = getChangedPublicJsFiles(); updateVersioningJs(newVersion, dry); updatePackageJson(newVersion, dry); updatePackageLockJson(newVersion, dry); - updateIndexHtmlHashes(newVersion, dry); + updateIndexHtmlHashes(changedFiles, newVersion, dry); + updatePublicJsDynamicImportHashes(changedFiles, newVersion, dry); console.log(`\n[bump-version] ${dry ? "(dry run) " : ""}done.\n`); } From 8b9849aeedc05ea74e28ff70e0ce8090c6a8c8fd Mon Sep 17 00:00:00 2001 From: Azgaar Date: Sat, 7 Mar 2026 17:32:14 +0100 Subject: [PATCH 15/17] refactor: streamline dynamic import hash updates for public JS files --- scripts/bump-version.js | 109 +++++++++++++++------------------------- 1 file changed, 40 insertions(+), 69 deletions(-) diff --git a/scripts/bump-version.js b/scripts/bump-version.js index de7c8f457..fffa86edb 100644 --- a/scripts/bump-version.js +++ b/scripts/bump-version.js @@ -180,99 +180,70 @@ function updateIndexHtmlHashes(changedFiles, newVersion, dry) { console.log(` src/index.html hashes updated for:\n - ${updated.join("\n - ")}`); } else { console.log( - const publicRoot = path.join(repoRoot, "public"); - - function walk(dir) { - for (const entry of fs.readdirSync(dir, {withFileTypes: true})) { - const full = path.join(dir, entry.name); - if (entry.isDirectory()) { - // Skip third-party vendor bundles under public/libs/** - const relFromPublic = path - .relative(publicRoot, full) - .replace(/\\/g, "/"); - if (relFromPublic === "libs" || relFromPublic.startsWith("libs/")) { - continue; - } - walk(full); - } else if (entry.isFile() && entry.name.endsWith(".js")) { - results.push(path.relative(repoRoot, full).replace(/\\/g, "/")); - } - } - } - - walk(publicRoot); + ` src/index.html (changed files have no ?v= entry: ${changedFiles.map(f => f.replace("public/", "")).join(", ")}` ); } } -/** Returns all .js file paths (relative to repo root, with forward slashes) under public/. */ -function getAllPublicJsFiles() { - const results = []; - function walk(dir) { +/** + * For each changed public JS file, scans ALL other public JS files for + * dynamic import() calls that reference it via a relative ?v= path, and + * updates the hash to newVersion. + * + * Example: public/modules/dynamic/installation.js changed → + * main.js: import("./modules/dynamic/installation.js?v=1.89.19") + * → import("./modules/dynamic/installation.js?v=1.113.4") + */ +function updatePublicJsDynamicImportHashes(changedFiles, newVersion, dry) { + if (changedFiles.length === 0) { + console.log(" public/**/*.js (no changed files, skipping dynamic import hashes)"); + return; + } + + // Absolute paths of every changed file for O(1) lookup + const changedAbsPaths = new Set(changedFiles.map(f => path.join(repoRoot, f))); + + // Collect all public JS files, skipping public/libs (third-party) + const publicRoot = path.join(repoRoot, "public"); + const allJsFiles = []; + (function walk(dir) { for (const entry of fs.readdirSync(dir, {withFileTypes: true})) { const full = path.join(dir, entry.name); if (entry.isDirectory()) { + if (path.relative(publicRoot, full).replace(/\\/g, "/") === "libs") continue; walk(full); } else if (entry.isFile() && entry.name.endsWith(".js")) { - results.push(path.relative(repoRoot, full).replace(/\\/g, "/")); + allJsFiles.push(full); } } - } - walk(path.join(repoRoot, "public")); - return results; -} + })(publicRoot); -/** - * Scans all public/**\/*.js files for relative dynamic-import ?v= references - * (e.g. import("../dynamic/supporters.js?v=1.97.14")) and updates any that - * point to one of the changed files. - */ -function updatePublicJsDynamicImportHashes(changedFiles, newVersion, dry) { - if (changedFiles.length === 0) { - console.log(" public/**/*.js (no changed public/*.js files detected)"); - return; - const replacement = `${quote}${relImportPath}?v=${newVersion}${quote}`; - // Only record and apply an update if the version actually changes - if (match === replacement) { - return match; - } - if (!updatedMap[relJsFile]) updatedMap[relJsFile] = []; - updatedMap[relJsFile].push(relImportPath); - return replacement; - const publicJsFiles = getAllPublicJsFiles(); const updatedMap = {}; - for (const relJsFile of publicJsFiles) { - const absJsFile = path.join(repoRoot, relJsFile); + for (const absJsFile of allJsFiles) { const content = readFile(absJsFile); - const dir = path.dirname(absJsFile); - - const pattern = /(['"])(\.\.?\/[^'"]*)\?v=[0-9.]+\1/g; + // Matches: import("../path/file.js?v=1.2.3") or import('../path/file.js?v=1.2.3') + const pattern = /(['"])(\.{1,2}\/[^'"?]+)\?v=[0-9.]+\1/g; + let anyChanged = false; const newContent = content.replace(pattern, (match, quote, relImportPath) => { - // Strip any query string defensively before resolving the path - const cleanImportPath = relImportPath.split("?")[0]; - const absImport = path.resolve(dir, cleanImportPath); - const repoRelImport = path.relative(repoRoot, absImport).replace(/\\/g, "/"); - if (changedSet.has(repoRelImport)) { - if (!updatedMap[relJsFile]) updatedMap[relJsFile] = []; - updatedMap[relJsFile].push(relImportPath); - return `${quote}${relImportPath}?v=${newVersion}${quote}`; - } - return match; + const absImport = path.resolve(path.dirname(absJsFile), relImportPath); + if (!changedAbsPaths.has(absImport)) return match; + const repoRelFile = path.relative(repoRoot, absJsFile).replace(/\\/g, "/"); + if (!updatedMap[repoRelFile]) updatedMap[repoRelFile] = []; + updatedMap[repoRelFile].push(relImportPath); + anyChanged = true; + return `${quote}${relImportPath}?v=${newVersion}${quote}`; }); - - if (updatedMap[relJsFile] && !dry) { - writeFile(absJsFile, newContent); - } + if (anyChanged && !dry) writeFile(absJsFile, newContent); } if (Object.keys(updatedMap).length > 0) { const lines = Object.entries(updatedMap) .map(([file, refs]) => ` ${file}:\n - ${refs.join("\n - ")}`) .join("\n"); - console.log(` public/**/*.js hashes updated:\n${lines}`); + console.log(` public/**/*.js dynamic import hashes updated:\n${lines}`); } else { - console.log(" public/**/*.js (no dynamic import ?v= hashes needed updating)"); + console.log(" public/**/*.js (no dynamic import ?v= hashes to update)"); } } From f6837c09fa5d85e6847bfc4cfd2c6c83efc4b1a4 Mon Sep 17 00:00:00 2001 From: Azgaar Date: Sat, 7 Mar 2026 17:49:11 +0100 Subject: [PATCH 16/17] refactor: enhance version bump detection using AI analysis of PR diffs --- .github/workflows/bump-version.yml | 29 +++--- scripts/detect-bump-type.js | 158 +++++++++++++++++++++++++++++ 2 files changed, 172 insertions(+), 15 deletions(-) create mode 100644 scripts/detect-bump-type.js diff --git a/.github/workflows/bump-version.yml b/.github/workflows/bump-version.yml index ea2c9076e..aca8bde65 100644 --- a/.github/workflows/bump-version.yml +++ b/.github/workflows/bump-version.yml @@ -19,28 +19,28 @@ jobs: - name: Checkout uses: actions/checkout@v5 with: - # fetch-depth 2 so git diff HEAD~1 HEAD works for detecting changed files + # fetch-depth 2 so HEAD~1 resolves to the pre-merge master commit fetch-depth: 2 - # Use a PAT/GitHub App token so the pushed commit can trigger deploy.yml and other workflows + # Use a PAT so the pushed bump commit can trigger deploy.yml token: ${{ secrets.RELEASE_BOT_TOKEN }} - name: Set up Node uses: actions/setup-node@v6 with: - node-version: '24.x' + node-version: "24.x" cache: npm - - name: Determine bump type from PR labels + - name: Get PR diff + run: git diff HEAD~1 HEAD > /tmp/pr.diff + + - name: AI-detect bump type from diff id: bump + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - LABELS='${{ toJSON(github.event.pull_request.labels.*.name) }}' - if echo "$LABELS" | grep -q '"major"'; then - echo "type=major" >> "$GITHUB_OUTPUT" - elif echo "$LABELS" | grep -q '"minor"'; then - echo "type=minor" >> "$GITHUB_OUTPUT" - else - echo "type=patch" >> "$GITHUB_OUTPUT" - fi + TYPE=$(node scripts/detect-bump-type.js --diff-file /tmp/pr.diff) + echo "type=$TYPE" >> "$GITHUB_OUTPUT" + echo "AI-determined bump type: $TYPE" - name: Extract base version (master before this PR merged) id: base @@ -48,7 +48,7 @@ jobs: BASE=$(git show HEAD~1:public/versioning.js \ | grep -oP 'const VERSION = "\K[\d.]+') echo "version=$BASE" >> "$GITHUB_OUTPUT" - echo "Base version on master before merge: $BASE" + echo "Base version: $BASE" - name: Run version bump script run: | @@ -66,9 +66,8 @@ jobs: git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - git add . + git add public/versioning.js package.json package-lock.json src/index.html - # Only commit if something actually changed if ! git diff --cached --quiet; then git commit -m "chore: bump version to $NEW_VERSION" git push diff --git a/scripts/detect-bump-type.js b/scripts/detect-bump-type.js new file mode 100644 index 000000000..44a17c0f9 --- /dev/null +++ b/scripts/detect-bump-type.js @@ -0,0 +1,158 @@ +#!/usr/bin/env node +"use strict"; + +/** + * Uses the GitHub Models API (gpt-4o-mini, no extra secrets required — + * GITHUB_TOKEN is enough when running inside GitHub Actions) to analyse + * a PR diff and decide whether the release is a patch, minor, or major bump. + * + * Versioning rules (from public/versioning.js): + * MAJOR — incompatible changes that break existing .map files + * MINOR — backward-compatible additions or changes that may require + * old .map files to be updated / migrated + * PATCH — backward-compatible bug fixes and small features that do + * NOT affect the .map file format at all + * + * Usage (called by bump-version.yml): + * node scripts/detect-bump-type.js --diff-file + * + * Output: prints exactly one of: patch | minor | major + * Exit 0 always (falls back to "patch" on any error). + */ + +const fs = require("fs"); +const https = require("https"); +const path = require("path"); + +// --------------------------------------------------------------------------- +// Config +// --------------------------------------------------------------------------- + +const MODEL = "gpt-4o-mini"; +const ENDPOINT_HOST = "models.inference.ai.azure.com"; +const ENDPOINT_PATH = "/chat/completions"; +// Keep the diff under ~20 000 chars to stay within the model's context window. +const MAX_DIFF_CHARS = 20_000; + +// --------------------------------------------------------------------------- +// System prompt — contains the project-specific versioning rules +// --------------------------------------------------------------------------- + +const SYSTEM_PROMPT = `\ +You are a semantic-version expert for Azgaar's Fantasy Map Generator. + +The project uses semantic versioning where the PUBLIC API is the .map save-file format. + +Rules: +• MAJOR — any change that makes existing .map files incompatible or unreadable + (e.g. removing or renaming top-level save-data fields, changing data types of + stored values, restructuring the save format) +• MINOR — backward-compatible additions or changes that may require old .map + files to be silently migrated on load (e.g. adding new optional fields to the + save format, changing default values that affect saved maps, adding new + generators that store new data) +• PATCH — everything else: UI improvements, bug fixes, refactors, new features + that do not touch the .map file format at all, dependency updates, docs + +Respond with EXACTLY one lowercase word: patch | minor | major +No explanation, no punctuation, no extra words.`; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function httpsPost(host, pathStr, headers, body) { + return new Promise((resolve, reject) => { + const data = JSON.stringify(body); + const req = https.request( + {host, path: pathStr, method: "POST", headers: {...headers, "Content-Length": Buffer.byteLength(data)}}, + res => { + let raw = ""; + res.on("data", c => (raw += c)); + res.on("end", () => { + if (res.statusCode >= 200 && res.statusCode < 300) resolve(raw); + else reject(new Error(`HTTP ${res.statusCode}: ${raw.slice(0, 300)}`)); + }); + } + ); + req.on("error", reject); + req.write(data); + req.end(); + }); +} + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- + +async function main() { + const args = process.argv.slice(2); + const diffFileIdx = args.indexOf("--diff-file"); + const diffFile = diffFileIdx !== -1 ? args[diffFileIdx + 1] : null; + + if (!diffFile || !fs.existsSync(diffFile)) { + console.error("[detect-bump-type] --diff-file is required and must exist."); + process.stdout.write("patch\n"); + process.exit(0); + } + + const token = process.env.GITHUB_TOKEN; + if (!token) { + console.error("[detect-bump-type] GITHUB_TOKEN not set — falling back to patch."); + process.stdout.write("patch\n"); + process.exit(0); + } + + let diff = fs.readFileSync(diffFile, "utf8").trim(); + if (!diff) { + console.error("[detect-bump-type] Diff is empty — falling back to patch."); + process.stdout.write("patch\n"); + process.exit(0); + } + + // Trim diff to avoid exceeding context window + if (diff.length > MAX_DIFF_CHARS) { + diff = diff.slice(0, MAX_DIFF_CHARS) + "\n\n[diff truncated]"; + console.error(`[detect-bump-type] Diff truncated to ${MAX_DIFF_CHARS} chars.`); + } + + const userMessage = `Analyse this git diff and respond with exactly one word (patch, minor, or major):\n\n${diff}`; + + try { + const raw = await httpsPost( + ENDPOINT_HOST, + ENDPOINT_PATH, + { + "Content-Type": "application/json", + Authorization: `Bearer ${token}` + }, + { + model: MODEL, + messages: [ + {role: "system", content: SYSTEM_PROMPT}, + {role: "user", content: userMessage} + ], + temperature: 0, + max_tokens: 5 + } + ); + + const json = JSON.parse(raw); + const answer = json.choices?.[0]?.message?.content?.trim().toLowerCase() ?? "patch"; + + if (answer === "major" || answer === "minor" || answer === "patch") { + console.error(`[detect-bump-type] AI decision: ${answer}`); + process.stdout.write(`${answer}\n`); + } else { + console.error(`[detect-bump-type] Unexpected AI response "${answer}" — defaulting to patch.`); + process.stdout.write("patch\n"); + } + } catch (err) { + console.error(`[detect-bump-type] API error: ${err.message} — falling back to patch.`); + process.stdout.write("patch\n"); + } + + process.exit(0); +} + +main(); From b1dbac0c4a651d14bd766e60a6122803bb08e8fa Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Mar 2026 18:03:41 +0100 Subject: [PATCH 17/17] Auto versioning (#1348) * Initial plan * fix: add ref to checkout step and stage public/**/*.js in bump workflow Co-authored-by: Azgaar <26469650+Azgaar@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Azgaar <26469650+Azgaar@users.noreply.github.com> --- .github/workflows/bump-version.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/bump-version.yml b/.github/workflows/bump-version.yml index aca8bde65..264d9bb3d 100644 --- a/.github/workflows/bump-version.yml +++ b/.github/workflows/bump-version.yml @@ -19,6 +19,9 @@ jobs: - name: Checkout uses: actions/checkout@v5 with: + # Check out the base branch (master) so we are on a real branch, not + # a detached PR merge ref, which makes git push work without arguments. + ref: ${{ github.event.pull_request.base.ref }} # fetch-depth 2 so HEAD~1 resolves to the pre-merge master commit fetch-depth: 2 # Use a PAT so the pushed bump commit can trigger deploy.yml @@ -66,7 +69,10 @@ jobs: git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" + # Stage versioning files + any public/**/*.js whose dynamic import + # hashes may have been refreshed by updatePublicJsDynamicImportHashes git add public/versioning.js package.json package-lock.json src/index.html + git add public/ 2>/dev/null || true if ! git diff --cached --quiet; then git commit -m "chore: bump version to $NEW_VERSION"