feat(version): add version bump script for CI deployments
This commit is contained in:
parent
2922df23dd
commit
b99520bc64
3 changed files with 141 additions and 0 deletions
|
|
@ -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 server-tick` | Mark game servers without a recent heartbeat offline |
|
||||
| `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`.
|
||||
|
||||
|
|
@ -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.
|
||||
|
||||
### 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)**.
|
||||
|
||||
## Project structure
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
"generate:importmap": "cross-env NODE_OPTIONS=--no-deprecation payload generate:importmap",
|
||||
"generate:types": "cross-env NODE_OPTIONS=--no-deprecation payload generate:types",
|
||||
"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",
|
||||
"payload": "cross-env NODE_OPTIONS=--no-deprecation payload",
|
||||
"migrate": "bun run src/scripts/run-migrations.ts",
|
||||
|
|
|
|||
135
src/scripts/bumpVersion.ts
Normal file
135
src/scripts/bumpVersion.ts
Normal 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();
|
||||
Loading…
Reference in a new issue