mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2026-03-23 07:37:24 +01:00
Auto versioning (#1344)
* feat: implement version bumping script and pre-push hook * chore: bump version to 1.113.5 * chore: bump version to 1.113.6 * chore: bump version to 1.113.7 * chore: bump version to 1.113.8 * chore: bump version to 1.113.9 * chore: bump version to 1.113.10 * chore: bump version to 1.113.3 and update versioning process * chore: enhance version bump process with base version check * Update .github/workflows/bump-version.yml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update .github/workflows/bump-version.yml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * 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> * Fix branch name in versioning.js comment: 'main' → 'master' (#1346) * 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> * 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 <maxganiev@yandex.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * refactor: streamline dynamic import hash updates for public JS files * refactor: enhance version bump detection using AI analysis of PR diffs * 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> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: Azgaar <26469650+Azgaar@users.noreply.github.com>
This commit is contained in:
parent
a66b60b5a7
commit
d7555ff1b2
6 changed files with 601 additions and 19 deletions
83
.github/workflows/bump-version.yml
vendored
Normal file
83
.github/workflows/bump-version.yml
vendored
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
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:
|
||||
# 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
|
||||
token: ${{ secrets.RELEASE_BOT_TOKEN }}
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "24.x"
|
||||
cache: npm
|
||||
|
||||
- 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: |
|
||||
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
|
||||
run: |
|
||||
BASE=$(git show HEAD~1:public/versioning.js \
|
||||
| grep -oP 'const VERSION = "\K[\d.]+')
|
||||
echo "version=$BASE" >> "$GITHUB_OUTPUT"
|
||||
echo "Base version: $BASE"
|
||||
|
||||
- name: Run version bump script
|
||||
run: |
|
||||
node scripts/bump-version.js ${{ steps.bump.outputs.type }} \
|
||||
--base-version ${{ steps.base.outputs.version }}
|
||||
|
||||
- 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"
|
||||
# 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"
|
||||
git push
|
||||
echo "Pushed version bump → $NEW_VERSION"
|
||||
else
|
||||
echo "Nothing changed, skipping commit."
|
||||
fi
|
||||
10
package-lock.json
generated
10
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "fantasy-map-generator",
|
||||
"version": "1.113.2",
|
||||
"version": "1.113.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "fantasy-map-generator",
|
||||
"version": "1.113.2",
|
||||
"version": "1.113.3",
|
||||
"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",
|
||||
|
|
|
|||
|
|
@ -606,4 +606,6 @@ dr_not_sam
|
|||
Mie96
|
||||
Riley
|
||||
Amber Davis
|
||||
tomtom1969vlbg`;
|
||||
tomtom1969vlbg
|
||||
Eric Knight
|
||||
Adeline Lefizelier`;
|
||||
|
|
|
|||
|
|
@ -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 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 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.4";
|
||||
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
|
|||
|
||||
<ul>
|
||||
<strong>Latest changes:</strong>
|
||||
<li>Search input in Overview dialogs</li>
|
||||
<li>Custom burg grouping and icon selection</li>
|
||||
<li>Ability to set custom image as Marker or Regiment icon</li>
|
||||
<li>Submap and Transform tools rework</li>
|
||||
<li>Azgaar Bot to answer questions and provide help</li>
|
||||
<li>Labels: ability to set letter spacing</li>
|
||||
<li>Zones performance improvement</li>
|
||||
<li>Notes Editor: on-demand AI text generation</li>
|
||||
<li>New style preset: Dark Seas</li>
|
||||
<li>New routes generation algorithm</li>
|
||||
<li>Routes overview tool</li>
|
||||
<li>Configurable longitude</li>
|
||||
<li>Export zones to GeoJSON</li>
|
||||
${latestPublicChanges.map(change => `<li>${change}</li>`).join("")}
|
||||
</ul>
|
||||
|
||||
<p>Join our <a href="${discord}" target="_blank">Discord server</a> and <a href="${reddit}" target="_blank">Reddit community</a> to ask questions, share maps, discuss the Generator and Worlbuilding, report bugs and propose new features.</p>
|
||||
|
|
|
|||
326
scripts/bump-version.js
Normal file
326
scripts/bump-version.js
Normal file
|
|
@ -0,0 +1,326 @@
|
|||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Bump the project version (patch / minor / major).
|
||||
*
|
||||
* 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
|
||||
* - public/**\/*.js — ?v= cache-busting hashes in dynamic import() calls
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/bump-version.js # interactive prompt
|
||||
* node scripts/bump-version.js patch # non-interactive
|
||||
* 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
|
||||
*/
|
||||
|
||||
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 packageLockJsonPath = path.join(repoRoot, "package-lock.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 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):
|
||||
* 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 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(changedFiles, newVersion, dry) {
|
||||
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 have no ?v= entry: ${changedFiles.map(f => f.replace("public/", "")).join(", ")}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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")) {
|
||||
allJsFiles.push(full);
|
||||
}
|
||||
}
|
||||
})(publicRoot);
|
||||
|
||||
const updatedMap = {};
|
||||
|
||||
for (const absJsFile of allJsFiles) {
|
||||
const content = readFile(absJsFile);
|
||||
// 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) => {
|
||||
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 (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 dynamic import hashes updated:\n${lines}`);
|
||||
} else {
|
||||
console.log(" public/**/*.js (no dynamic import ?v= hashes to update)");
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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 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");
|
||||
const changedFiles = getChangedPublicJsFiles();
|
||||
updateIndexHtmlHashes(changedFiles, currentVersion, dry);
|
||||
updatePublicJsDynamicImportHashes(changedFiles, 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";
|
||||
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`);
|
||||
|
||||
const changedFiles = getChangedPublicJsFiles();
|
||||
updateVersioningJs(newVersion, dry);
|
||||
updatePackageJson(newVersion, dry);
|
||||
updatePackageLockJson(newVersion, dry);
|
||||
updateIndexHtmlHashes(changedFiles, newVersion, dry);
|
||||
updatePublicJsDynamicImportHashes(changedFiles, 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);
|
||||
});
|
||||
158
scripts/detect-bump-type.js
Normal file
158
scripts/detect-bump-type.js
Normal file
|
|
@ -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 <path>
|
||||
*
|
||||
* 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 <path> 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();
|
||||
Loading…
Add table
Add a link
Reference in a new issue