1
0
Fork 0

feat(version): add version bump script for CI deployments

This commit is contained in:
Jason Fraley 2026-08-26 22:13:24 -04:00
parent 2922df23dd
commit b99520bc64
3 changed files with 141 additions and 0 deletions

View file

@ -60,6 +60,7 @@ Dev credentials: `dev` / `Test123` (if seed data has been applied).
| `bun run payload mission-tick` | Auto-complete missions whose scheduled day has passed | | `bun run payload mission-tick` | Auto-complete missions whose scheduled day has passed |
| `bun run payload server-tick` | Mark game servers without a recent heartbeat offline | | `bun run payload server-tick` | Mark game servers without a recent heartbeat offline |
| `bun run deploy` | Bump patch version, build, and deploy | | `bun run deploy` | Bump patch version, build, and deploy |
| `bun run version:bump -- --base <sha> --head <sha>` | Choose and apply a patch/minor bump from a git range |
Note: `game-tick` and `market-tick` are Payload bins registered in `payload.config.ts`, not npm scripts. Run via `bun run payload`. Note: `game-tick` and `market-tick` are Payload bins registered in `payload.config.ts`, not npm scripts. Run via `bun run payload`.
@ -67,6 +68,10 @@ All five bin scripts also log to daily files under `logs/<bin-key>/` (e.g. `logs
For an Arma 3 unit deployment we self-host on **Coolify** with one container per environment (dev / stg / prod), a Coolify-managed PostgreSQL service per environment, a persistent volume for `/app/media`, and `docker exec` scheduled jobs for the game tick. The Dockerfile is Bun-based (multi-stage, Next.js standalone runtime + Payload CLI kept at runtime), and `docker-compose.yml` mirrors the same shape for local full-stack dev. For an Arma 3 unit deployment we self-host on **Coolify** with one container per environment (dev / stg / prod), a Coolify-managed PostgreSQL service per environment, a persistent volume for `/app/media`, and `docker exec` scheduled jobs for the game tick. The Dockerfile is Bun-based (multi-stage, Next.js standalone runtime + Payload CLI kept at runtime), and `docker-compose.yml` mirrors the same shape for local full-stack dev.
### CI version bumps
Run `bun run version:bump -- --base "$CI_PREVIOUS_SHA" --head "$CI_SHA"` before the production build. The script bumps minor for product changes under `src/app`, `src/components/frontend`, `src/lib`, `src/collections`, `src/bot`, `src/hooks`, `src/migrations`, or `src/utils`, and patch for other deployable changes. Use `--dry-run` to inspect the decision. Commit the changed `package.json` back to the deployment branch (for example, with a `[skip ci]` commit if your CI provider supports it); the existing `generate:version-info` build step will publish the new version to the app.
For the full setup — per-env Postgres provisioning, env vars, scheduled jobs, persistent media volume, SSE single-instance caveat, rollbacks, troubleshooting — see **[DEPLOYMENT.md](./DEPLOYMENT.md)**. For the full setup — per-env Postgres provisioning, env vars, scheduled jobs, persistent media volume, SSE single-instance caveat, rollbacks, troubleshooting — see **[DEPLOYMENT.md](./DEPLOYMENT.md)**.
## Project structure ## Project structure

View file

@ -13,6 +13,7 @@
"generate:importmap": "cross-env NODE_OPTIONS=--no-deprecation payload generate:importmap", "generate:importmap": "cross-env NODE_OPTIONS=--no-deprecation payload generate:importmap",
"generate:types": "cross-env NODE_OPTIONS=--no-deprecation payload generate:types", "generate:types": "cross-env NODE_OPTIONS=--no-deprecation payload generate:types",
"generate:version-info": "bun run src/scripts/generateVersionInfo.ts", "generate:version-info": "bun run src/scripts/generateVersionInfo.ts",
"version:bump": "bun run src/scripts/bumpVersion.ts",
"lint": "cross-env NODE_OPTIONS=--no-deprecation next lint", "lint": "cross-env NODE_OPTIONS=--no-deprecation next lint",
"payload": "cross-env NODE_OPTIONS=--no-deprecation payload", "payload": "cross-env NODE_OPTIONS=--no-deprecation payload",
"migrate": "bun run src/scripts/run-migrations.ts", "migrate": "bun run src/scripts/run-migrations.ts",

135
src/scripts/bumpVersion.ts Normal file
View file

@ -0,0 +1,135 @@
/**
* Bumps the application version for a deployable change.
*
* The script is intentionally dependency-free so it can run in CI before the
* production build. It changes package.json only; generated version metadata
* is refreshed by the existing generate:version-info script.
*
* Usage:
* bun run version:bump -- --base <sha> --head <sha>
* bun run version:bump -- --dry-run
*/
import { execFileSync } from "node:child_process";
import { readFileSync, writeFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
type Bump = "patch" | "minor" | "none";
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const packagePath = path.join(projectRoot, "package.json");
// Changes in these areas affect the product shipped to users and warrant a
// minor release. Everything else defaults to a patch release so every real
// production deployment still gets a distinct version.
const MINOR_PATHS = [
"src/app/",
"src/bot/",
"src/collections/",
"src/components/frontend/",
"src/hooks/",
"src/lib/",
"src/migrations/",
"src/utils/",
];
const VERSION_ONLY_PATHS = new Set([
"package.json",
"src/generated/versionInfo.json",
]);
function git(args: string[]): string {
return execFileSync("git", args, { cwd: projectRoot, encoding: "utf8" }).trim();
}
function option(name: string): string | undefined {
const index = process.argv.indexOf(name);
return index === -1 ? undefined : process.argv[index + 1];
}
function hasFlag(name: string): boolean {
return process.argv.includes(name);
}
function resolveRange(): [string, string] {
const head = option("--head") ?? process.env.GITHUB_SHA ?? "HEAD";
const explicitBase = option("--base") ?? process.env.GITHUB_EVENT_BEFORE;
if (explicitBase && !/^0+$/.test(explicitBase)) return [explicitBase, head];
try {
return [git(["rev-parse", `${head}^`]), head];
} catch {
// An initial commit has no parent. Comparing it to the empty tree makes
// the first deployment behave like any other deployable change.
return ["4b825dc642cb6eb9a060e54bf8d69288fbee4904", head];
}
}
function changedFiles(base: string, head: string): string[] {
return git(["diff", "--name-only", "--diff-filter=ACMRTUXB", base, head])
.split("\n")
.map((file) => file.trim())
.filter(Boolean);
}
function commitSubjects(base: string, head: string): string[] {
return git(["log", "--format=%s", `${base}..${head}`])
.split("\n")
.map((subject) => subject.trim())
.filter(Boolean);
}
export function chooseBump(files: string[], subjects: string[]): Bump {
const meaningfulFiles = files.filter((file) => !VERSION_ONLY_PATHS.has(file));
if (meaningfulFiles.length === 0) return "none";
// Conventional commits provide an explicit signal when the changed paths
// alone do not capture the scope of a feature.
if (subjects.some((subject) => /^(feat|feature)(\(.+\))?!?:/i.test(subject))) {
return "minor";
}
return meaningfulFiles.some((file) => MINOR_PATHS.some((prefix) => file.startsWith(prefix)))
? "minor"
: "patch";
}
function increment(version: string, bump: Exclude<Bump, "none">): string {
const match = /^(\d+)\.(\d+)\.(\d+)(?:-.+)?$/.exec(version);
if (!match) throw new Error(`package.json has an unsupported version: ${version}`);
const [, major, minor, patch] = match;
return bump === "minor"
? `${major}.${Number(minor) + 1}.0`
: `${major}.${minor}.${Number(patch) + 1}`;
}
function main(): void {
const [base, head] = resolveRange();
const files = changedFiles(base, head);
const subjects = commitSubjects(base, head);
const bump = chooseBump(files, subjects);
const packageJson = JSON.parse(readFileSync(packagePath, "utf8")) as { version?: string };
if (bump === "none") {
console.log("[version-bump] no product changes detected; version unchanged");
return;
}
if (typeof packageJson.version !== "string") {
throw new Error("package.json is missing a version");
}
const nextVersion = increment(packageJson.version, bump);
console.log(`[version-bump] ${bump}: ${packageJson.version} -> ${nextVersion}`);
console.log(`[version-bump] range: ${base}..${head}`);
console.log(`[version-bump] files: ${files.join(", ")}`);
if (hasFlag("--dry-run")) return;
packageJson.version = nextVersion;
writeFileSync(packagePath, `${JSON.stringify(packageJson, null, 2)}\n`, "utf8");
console.log(`[version-bump] updated ${path.relative(projectRoot, packagePath)}`);
}
main();