mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Add release prepare and preflight scripts
This commit is contained in:
parent
39c48f5a62
commit
ddf155f917
4 changed files with 159 additions and 4 deletions
12
README.md
12
README.md
|
|
@ -113,17 +113,21 @@ The generation script uses `codex app-server generate-ts --experimental` because
|
|||
|
||||
GitHub Releases attach only `main.js`, `manifest.json`, and `styles.css` as Obsidian install assets. `LICENSE` and `NOTICE` are kept in the repository and source archives for license distribution.
|
||||
|
||||
Create a release by bumping `package.json`, `package-lock.json`, `manifest.json`, and `versions.json`, adding hand-written notes at `.github/release-notes/X.Y.Z.md`, then checking the release metadata, lockfile, and build before pushing the matching tag:
|
||||
Create a release by preparing the next version, editing the generated release notes, committing the release changes, then running the preflight before pushing the matching tag:
|
||||
|
||||
```sh
|
||||
npm run release:check
|
||||
npm ci --dry-run
|
||||
npm run check
|
||||
npm run release:prepare -- X.Y.Z
|
||||
# Edit .github/release-notes/X.Y.Z.md.
|
||||
git status --short
|
||||
git add package.json package-lock.json manifest.json versions.json .github/release-notes/X.Y.Z.md
|
||||
git commit -m "Bump version to X.Y.Z"
|
||||
npm run release:preflight
|
||||
git tag X.Y.Z
|
||||
git push origin main X.Y.Z
|
||||
```
|
||||
|
||||
`release:prepare` updates the version files and creates a `## Changes` release notes template. `release:preflight` verifies the local Git state, release metadata, lockfile, and full build once after the release commit is on `main`.
|
||||
|
||||
The release workflow runs `npm ci`, `npm run release:check`, `npm run check`, attaches the install assets, and generates GitHub artifact attestations for them. The release notes file is required and must contain a single `## Changes` section. If a tag-triggered release fails before creating the GitHub Release, fix the commit, move the local tag with `git tag -f X.Y.Z`, then update the remote tag with `git push --force origin X.Y.Z`.
|
||||
|
||||
## License
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@
|
|||
"generate:app-server-types": "codex app-server generate-ts --experimental --out src/generated/app-server && node scripts/normalize-generated-types.mjs",
|
||||
"lint": "eslint src tests --max-warnings=0",
|
||||
"release:check": "node scripts/check-release.mjs",
|
||||
"release:preflight": "node scripts/preflight-release.mjs",
|
||||
"release:prepare": "node scripts/prepare-release.mjs",
|
||||
"test": "vitest run",
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit",
|
||||
"check": "npm run typecheck && npm run test && npm run lint && npm run format:check && npm run build:prod"
|
||||
|
|
|
|||
58
scripts/preflight-release.mjs
Normal file
58
scripts/preflight-release.mjs
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import { spawnSync } from "node:child_process";
|
||||
|
||||
function fail(message) {
|
||||
console.error(`release preflight failed: ${message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function run(command, args, options = {}) {
|
||||
const result = spawnSync(command, args, {
|
||||
encoding: "utf8",
|
||||
stdio: options.capture ? "pipe" : "inherit",
|
||||
shell: false,
|
||||
});
|
||||
if (result.error) fail(`${command} ${args.join(" ")}: ${result.error.message}`);
|
||||
if (result.status !== 0) {
|
||||
if (options.capture) {
|
||||
const output = `${result.stdout || ""}${result.stderr || ""}`.trim();
|
||||
if (output) console.error(output);
|
||||
}
|
||||
fail(`${command} ${args.join(" ")} exited with ${result.status}`);
|
||||
}
|
||||
return options.capture ? result.stdout.trim() : "";
|
||||
}
|
||||
|
||||
function maybeRun(command, args) {
|
||||
const result = spawnSync(command, args, {
|
||||
encoding: "utf8",
|
||||
stdio: "pipe",
|
||||
shell: false,
|
||||
});
|
||||
return result.status === 0 ? result.stdout.trim() : null;
|
||||
}
|
||||
|
||||
const branch = run("git", ["branch", "--show-current"], { capture: true });
|
||||
if (branch !== "main") fail(`release must be prepared from main, got ${branch || "(detached HEAD)"}`);
|
||||
|
||||
const status = run("git", ["status", "--short"], { capture: true });
|
||||
if (status) fail(`working tree must be clean before release preflight\n${status}`);
|
||||
|
||||
const upstream = maybeRun("git", ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"]);
|
||||
if (!upstream) fail("main must have an upstream tracking branch");
|
||||
|
||||
const upstreamAncestor = spawnSync("git", ["merge-base", "--is-ancestor", upstream, "HEAD"], {
|
||||
encoding: "utf8",
|
||||
stdio: "pipe",
|
||||
shell: false,
|
||||
});
|
||||
if (upstreamAncestor.status !== 0) fail(`HEAD must contain ${upstream}; pull or rebase before tagging`);
|
||||
|
||||
const packageVersion = run("node", ["-p", "require('./package.json').version"], { capture: true });
|
||||
const existingTag = run("git", ["tag", "--list", packageVersion], { capture: true });
|
||||
if (existingTag) fail(`local tag ${packageVersion} already exists`);
|
||||
|
||||
run("npm", ["run", "release:check"]);
|
||||
run("npm", ["ci", "--dry-run"]);
|
||||
run("npm", ["run", "check"]);
|
||||
|
||||
console.log(`release preflight passed for ${packageVersion}`);
|
||||
91
scripts/prepare-release.mjs
Normal file
91
scripts/prepare-release.mjs
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
function fail(message) {
|
||||
console.error(`release prepare failed: ${message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
async function readJson(file) {
|
||||
return JSON.parse(await readFile(file, "utf8"));
|
||||
}
|
||||
|
||||
async function writeJson(file, value) {
|
||||
await writeFile(file, `${JSON.stringify(value, null, 2)}\n`);
|
||||
}
|
||||
|
||||
function parseVersion(version) {
|
||||
const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version);
|
||||
if (!match) return null;
|
||||
return {
|
||||
major: Number(match[1]),
|
||||
minor: Number(match[2]),
|
||||
patch: Number(match[3]),
|
||||
};
|
||||
}
|
||||
|
||||
function compareVersions(a, b) {
|
||||
return a.major - b.major || a.minor - b.minor || a.patch - b.patch;
|
||||
}
|
||||
|
||||
function isExpectedNextVersion(previous, current) {
|
||||
if (current.major === previous.major && current.minor === previous.minor) {
|
||||
return current.patch === previous.patch + 1;
|
||||
}
|
||||
if (current.major === previous.major && current.minor === previous.minor + 1) {
|
||||
return current.patch === 0;
|
||||
}
|
||||
if (current.major === previous.major + 1) {
|
||||
return current.minor === 0 && current.patch === 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const releaseVersion = process.argv[2];
|
||||
if (!releaseVersion) fail("usage: npm run release:prepare -- X.Y.Z");
|
||||
|
||||
const currentVersion = parseVersion(releaseVersion);
|
||||
if (!currentVersion) fail(`release version must be X.Y.Z, got ${releaseVersion}`);
|
||||
|
||||
const packageJson = await readJson("package.json");
|
||||
const packageLockJson = await readJson("package-lock.json");
|
||||
const manifestJson = await readJson("manifest.json");
|
||||
const versionsJson = await readJson("versions.json");
|
||||
|
||||
const versionKeys = Object.keys(versionsJson);
|
||||
const previousVersionKey = versionKeys.at(-1);
|
||||
const previousVersion = parseVersion(previousVersionKey);
|
||||
if (!previousVersion) fail(`versions.json contains invalid latest version ${previousVersionKey}`);
|
||||
if (compareVersions(previousVersion, currentVersion) >= 0) {
|
||||
fail(`release version ${releaseVersion} must be newer than ${previousVersionKey}`);
|
||||
}
|
||||
if (!isExpectedNextVersion(previousVersion, currentVersion)) {
|
||||
fail(`release version ${releaseVersion} is not the expected next version after ${previousVersionKey}`);
|
||||
}
|
||||
if (versionsJson[releaseVersion] !== undefined) fail(`versions.json already contains ${releaseVersion}`);
|
||||
|
||||
packageJson.version = releaseVersion;
|
||||
packageLockJson.version = releaseVersion;
|
||||
if (!packageLockJson.packages?.[""]) fail('package-lock.json is missing packages[""]');
|
||||
packageLockJson.packages[""].version = releaseVersion;
|
||||
manifestJson.version = releaseVersion;
|
||||
versionsJson[releaseVersion] = manifestJson.minAppVersion;
|
||||
|
||||
await writeJson("package.json", packageJson);
|
||||
await writeJson("package-lock.json", packageLockJson);
|
||||
await writeJson("manifest.json", manifestJson);
|
||||
await writeJson("versions.json", versionsJson);
|
||||
|
||||
const notesDir = path.join(".github", "release-notes");
|
||||
const notesPath = path.join(notesDir, `${releaseVersion}.md`);
|
||||
await mkdir(notesDir, { recursive: true });
|
||||
try {
|
||||
await readFile(notesPath, "utf8");
|
||||
fail(`${notesPath} already exists`);
|
||||
} catch (error) {
|
||||
if (error.code !== "ENOENT") throw error;
|
||||
}
|
||||
await writeFile(notesPath, "## Changes\n\n- \n");
|
||||
|
||||
console.log(`prepared release ${releaseVersion}`);
|
||||
console.log(`edit ${notesPath}, then run npm run release:preflight after committing`);
|
||||
Loading…
Reference in a new issue