mirror of
https://github.com/civitai/civitai.git
synced 2026-09-20 22:08:18 +08:00
088a9d09b1
`release-app.mjs` derives the next version from apps/<app>/package.json on the
CURRENT branch. When that branch is behind the app's released history the tag it
computes does not continue that line, in one of two ways:
* it already exists -> `git tag` aborts, but only after the release commit has
been made, leaving a junk commit on the branch;
* it is a different line (a minor/major bump off a stale base) -> nothing
collides, the tag becomes the HIGHEST for that app, and the Flux ImagePolicy
selects the highest semver rather than the most recent push. That stale build
is then what production runs.
apps/moderator is in exactly this state, measured 2026-08-17: 0.0.1 on main,
0.0.26 live, all 26 releases cut from `moderator-app-pages` — 211 commits and
+38,630 lines that never merged to main. `pnpm release:moderator` from main
collides on the existing 0.0.2 and aborts; `release:moderator:minor` computes
0.1.0, which does NOT exist, and would deploy main's stale copy to production.
One command, no collision.
So this does NOT bump moderator's version to 0.0.26. That is the obvious fix and
it is the wrong one: it removes the collision that is currently the only brake,
while leaving the app itself 211 commits stale. The real remedy is to land the
branch; the guard is what makes the trap loud until someone does.
Checked after `git pull --rebase` so the tag list is current, and before
`npm version` so a refusal leaves the tree exactly as it found it.
Coverage: 19 tests. The version arithmetic is unit-tested, and — because a guard
nothing calls is not a guard — release-app.mjs is also driven end-to-end as a
real process against a throwaway git repo with a local bare remote (offline; no
tags or commits touch this repository).
Mutation-tested, 4 mutants, each the narrowest expression that can be wrong:
invert the behind-comparison -> 5 tests fail
lexicographic highest-tag compare -> 6 tests fail
guard computed but never acted on -> ONLY the 2 behavioural tests fail,
which is what pins reachability
drop the unparseable-tag skip -> exactly 1 test fails
Baseline restored green (19/19) after the battery.
NOTE: scripts/**/*.test.ts runs in the `unit` vitest project, which is
`continue-on-error: true` in lint.yml — so these tests run but cannot currently
fail CI. That is tracked separately (868kp7fdr); it does not make them useless,
it makes them a local and post-fix gate.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
88 lines
3.9 KiB
JavaScript
88 lines
3.9 KiB
JavaScript
#!/usr/bin/env node
|
|
// Release a per-app image from the monorepo by cutting a prefixed git tag that
|
|
// the in-cluster Tekton `tag-webhook` receiver builds + Flux deploys.
|
|
//
|
|
// node scripts/release-app.mjs <appDir> <tagPrefix> <patch|minor|major>
|
|
// e.g. node scripts/release-app.mjs apps/auth auth-app-v patch -> auth-app-v0.1.1
|
|
//
|
|
// WHY a script (not `npm version` inline): `npm version` only creates the git
|
|
// commit + tag when the package it operates on contains the repo `.git`. In this
|
|
// monorepo `.git` is at the ROOT, so `npm --prefix apps/auth version ...` rewrites
|
|
// apps/auth/package.json but SILENTLY skips the commit + tag (exit 0). So we bump
|
|
// with `--no-git-tag-version` and do the commit/tag/push explicitly, here, at the
|
|
// root — and stage ONLY the app's package.json so unrelated working-tree changes
|
|
// are never swept into the release commit.
|
|
import { execSync } from 'node:child_process';
|
|
import { readFileSync, existsSync } from 'node:fs';
|
|
import { releaseSkew, skewMessage } from './lib/release-version.mjs';
|
|
|
|
const [appDir, tagPrefix, bump] = process.argv.slice(2);
|
|
const BUMPS = new Set(['patch', 'minor', 'major']);
|
|
if (!appDir || !tagPrefix || !BUMPS.has(bump)) {
|
|
console.error('usage: node scripts/release-app.mjs <appDir> <tagPrefix> <patch|minor|major>');
|
|
process.exit(1);
|
|
}
|
|
if (!existsSync(`${appDir}/package.json`)) {
|
|
console.error(`no ${appDir}/package.json`);
|
|
process.exit(1);
|
|
}
|
|
|
|
const sh = (cmd) => execSync(cmd, { stdio: 'inherit' });
|
|
const cap = (cmd) => execSync(cmd, { encoding: 'utf8' }).trim();
|
|
|
|
// Releases cut a commit + tag onto the current branch and push it. Require a
|
|
// clean tree (so we never commit unrelated changes) and an up-to-date main.
|
|
if (cap('git status --porcelain')) {
|
|
console.error('working tree is not clean — commit, stash, or discard changes before releasing.');
|
|
process.exit(1);
|
|
}
|
|
const branch = cap('git rev-parse --abbrev-ref HEAD');
|
|
// Default: releases are cut from 'main'. Set RELEASE_ALLOW_BRANCH=1 to cut from
|
|
// the current branch instead (e.g. an app still living on a feature branch that
|
|
// hasn't merged to main yet).
|
|
if (branch !== 'main' && process.env.RELEASE_ALLOW_BRANCH !== '1') {
|
|
console.error(
|
|
`releases must be cut from 'main' (you are on '${branch}'). ` +
|
|
`Checkout main first, or set RELEASE_ALLOW_BRANCH=1 to release from this branch.`
|
|
);
|
|
process.exit(1);
|
|
}
|
|
if (branch !== 'main') {
|
|
console.warn(`⚠ releasing from '${branch}' (RELEASE_ALLOW_BRANCH=1), not 'main'.`);
|
|
}
|
|
|
|
sh('git pull --rebase');
|
|
|
|
// 🔴 Refuse if this branch is BEHIND the app's released history. Checked here —
|
|
// after the pull so the tag list is current, and BEFORE `npm version` so a refusal
|
|
// leaves the tree exactly as it found it. See scripts/lib/release-version.mjs for
|
|
// the two ways a stale base produces a wrong tag; the dangerous one deploys this
|
|
// branch's code to production without colliding with anything.
|
|
{
|
|
const currentVersion = JSON.parse(readFileSync(`${appDir}/package.json`, 'utf8')).version;
|
|
const tags = cap('git tag -l').split('\n').filter(Boolean);
|
|
const skew = releaseSkew({ currentVersion, tags, tagPrefix });
|
|
if (skew.behind) {
|
|
console.error(
|
|
skewMessage({ appDir, tagPrefix, current: skew.current, highest: skew.highest, branch })
|
|
);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
// Bump the sub-package version ONLY (no git side effects from npm).
|
|
sh(`npm --prefix ${appDir} version ${bump} --no-git-tag-version`);
|
|
const version = JSON.parse(readFileSync(`${appDir}/package.json`, 'utf8')).version;
|
|
const tag = `${tagPrefix}${version}`;
|
|
const app = appDir.split('/').pop();
|
|
|
|
// Commit ONLY the app's package.json, tag, and push the commit + tag.
|
|
sh(`git add ${appDir}/package.json`);
|
|
sh(`git commit -m "chore(${app}): release ${tag}"`);
|
|
sh(`git tag -a ${tag} -m ${tag}`);
|
|
sh('git push --follow-tags');
|
|
|
|
console.log(
|
|
`\nReleased ${tag} (pushed to ${branch}). The tag-webhook will build + Flux will deploy.`
|
|
);
|