@biffo/cli 0.60.4 → 0.61.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +407 -5
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { Command as
|
|
4
|
+
import { Command as Command24 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/commands/core.ts
|
|
7
7
|
import { Command as Command4 } from "commander";
|
|
@@ -30,6 +30,9 @@ var CoreManifestSchema = z.object({
|
|
|
30
30
|
});
|
|
31
31
|
var CORE_VERSION_FILE = "core.version";
|
|
32
32
|
var INSTANCE_CORE_FILE = "biffo.core.json";
|
|
33
|
+
function isInstanceRepo(repoRoot) {
|
|
34
|
+
return existsSync(join(repoRoot, INSTANCE_CORE_FILE));
|
|
35
|
+
}
|
|
33
36
|
function parseCoreVersion(raw) {
|
|
34
37
|
const match = SEMVER.exec(raw.trim());
|
|
35
38
|
if (!match) {
|
|
@@ -156,7 +159,18 @@ var CoreManifestSchema2 = z2.object({
|
|
|
156
159
|
version: z2.literal(1),
|
|
157
160
|
note: z2.string().optional(),
|
|
158
161
|
templateOwned: z2.array(z2.string()).min(1),
|
|
159
|
-
userOwned: z2.array(z2.string()).default([])
|
|
162
|
+
userOwned: z2.array(z2.string()).default([]),
|
|
163
|
+
/**
|
|
164
|
+
* Paths that version the core but are NOT distributed to instances.
|
|
165
|
+
*
|
|
166
|
+
* `cli/` is the case this exists for. The CLI is released from the same
|
|
167
|
+
* `core-v*` tag as everything else — its published version IS the core
|
|
168
|
+
* version (ADR-0006) — but an instance consumes it from npm and must not
|
|
169
|
+
* carry its source. Without this list a CLI-only change would look like "no
|
|
170
|
+
* template-owned change", the release job would cut nothing, and the fix
|
|
171
|
+
* would never reach npm for any instance to install.
|
|
172
|
+
*/
|
|
173
|
+
released: z2.array(z2.string()).default([])
|
|
160
174
|
});
|
|
161
175
|
var HARD_EXCLUDED_DIRS = /* @__PURE__ */ new Set([
|
|
162
176
|
".git",
|
|
@@ -6957,13 +6971,400 @@ var siblingCommand = new Command21("sibling").description(
|
|
|
6957
6971
|
);
|
|
6958
6972
|
siblingCommand.addCommand(siblingCreateCommand);
|
|
6959
6973
|
|
|
6974
|
+
// src/commands/check.ts
|
|
6975
|
+
import { Command as Command22 } from "commander";
|
|
6976
|
+
|
|
6977
|
+
// src/scripts/check-core-ownership.ts
|
|
6978
|
+
import { execa as execa5 } from "execa";
|
|
6979
|
+
|
|
6980
|
+
// src/lib/core-ownership-guard.ts
|
|
6981
|
+
import { existsSync as existsSync28, readFileSync as readFileSync21 } from "fs";
|
|
6982
|
+
import { join as join29 } from "path";
|
|
6983
|
+
import { z as z7 } from "zod";
|
|
6984
|
+
var DIVERGENCE_FILE = "biffo.divergence.json";
|
|
6985
|
+
var DivergenceEntrySchema = z7.object({
|
|
6986
|
+
prefix: z7.string().min(1),
|
|
6987
|
+
reason: z7.string().min(1),
|
|
6988
|
+
upstream: z7.string().min(1)
|
|
6989
|
+
});
|
|
6990
|
+
var DivergenceConfigSchema = z7.object({
|
|
6991
|
+
note: z7.string().optional(),
|
|
6992
|
+
warnOnly: z7.array(DivergenceEntrySchema).default([])
|
|
6993
|
+
});
|
|
6994
|
+
function readDivergenceConfig(repoRoot) {
|
|
6995
|
+
const path = join29(repoRoot, DIVERGENCE_FILE);
|
|
6996
|
+
if (!existsSync28(path)) return { warnOnly: [] };
|
|
6997
|
+
let raw;
|
|
6998
|
+
try {
|
|
6999
|
+
raw = JSON.parse(readFileSync21(path, "utf8"));
|
|
7000
|
+
} catch (err) {
|
|
7001
|
+
throw new Error(`${DIVERGENCE_FILE} is not valid JSON: ${err.message}`);
|
|
7002
|
+
}
|
|
7003
|
+
const parsed = DivergenceConfigSchema.safeParse(raw);
|
|
7004
|
+
if (!parsed.success) {
|
|
7005
|
+
const issues = parsed.error.issues.map((i) => ` ${i.path.join(".") || "(root)"}: ${i.message}`).join("\n");
|
|
7006
|
+
throw new Error(`${DIVERGENCE_FILE} is invalid:
|
|
7007
|
+
${issues}`);
|
|
7008
|
+
}
|
|
7009
|
+
return parsed.data;
|
|
7010
|
+
}
|
|
7011
|
+
function parseDivergenceTrailer(commitMessage) {
|
|
7012
|
+
const body = commitMessage.split("\n").filter((line) => !line.startsWith("#")).join("\n");
|
|
7013
|
+
const match = /^Core-Divergence:[ \t]*(\S.*?)[ \t]*$/m.exec(body);
|
|
7014
|
+
return match?.[1] ?? null;
|
|
7015
|
+
}
|
|
7016
|
+
function resolveBranch(env, gitBranch) {
|
|
7017
|
+
return (env["GITHUB_HEAD_REF"] || env["GITHUB_REF_NAME"] || gitBranch).trim();
|
|
7018
|
+
}
|
|
7019
|
+
function parseNameStatus(stdout) {
|
|
7020
|
+
const changed = [];
|
|
7021
|
+
const deleted = [];
|
|
7022
|
+
for (const line of stdout.split("\n")) {
|
|
7023
|
+
const parts = line.split(" ").filter(Boolean);
|
|
7024
|
+
const status = parts[0];
|
|
7025
|
+
const path = parts[parts.length - 1];
|
|
7026
|
+
if (!status || !path || parts.length < 2) continue;
|
|
7027
|
+
changed.push(path);
|
|
7028
|
+
if (status.startsWith("D")) deleted.push(path);
|
|
7029
|
+
}
|
|
7030
|
+
return { changed, deleted };
|
|
7031
|
+
}
|
|
7032
|
+
function checkCoreOwnership({
|
|
7033
|
+
changedFiles,
|
|
7034
|
+
manifest,
|
|
7035
|
+
isInstance,
|
|
7036
|
+
branch = "",
|
|
7037
|
+
commitMessage = "",
|
|
7038
|
+
warnOnly = []
|
|
7039
|
+
}) {
|
|
7040
|
+
const empty = { blocked: [], warned: [], divergenceReason: null };
|
|
7041
|
+
if (!isInstance) return { skipped: "template", ...empty };
|
|
7042
|
+
if (branch.startsWith(UPGRADE_BRANCH_PREFIX)) return { skipped: "upgrade-branch", ...empty };
|
|
7043
|
+
const templateOwned = changedFiles.filter((f) => isTemplateOwned(f, manifest));
|
|
7044
|
+
const acknowledged = (path) => warnOnly.filter((entry) => path.startsWith(entry.prefix)).reduce(
|
|
7045
|
+
(best, entry) => !best || entry.prefix.length > best.prefix.length ? entry : best,
|
|
7046
|
+
void 0
|
|
7047
|
+
);
|
|
7048
|
+
const warned = [];
|
|
7049
|
+
const offending = [];
|
|
7050
|
+
for (const path of templateOwned) {
|
|
7051
|
+
const entry = acknowledged(path);
|
|
7052
|
+
if (entry) warned.push({ path, entry });
|
|
7053
|
+
else offending.push(path);
|
|
7054
|
+
}
|
|
7055
|
+
const divergenceReason = parseDivergenceTrailer(commitMessage);
|
|
7056
|
+
if (offending.length > 0 && divergenceReason !== null) {
|
|
7057
|
+
return { skipped: "divergence-trailer", blocked: [], warned, divergenceReason };
|
|
7058
|
+
}
|
|
7059
|
+
return { skipped: null, blocked: offending, warned, divergenceReason: null };
|
|
7060
|
+
}
|
|
7061
|
+
|
|
7062
|
+
// src/scripts/check-core-ownership.ts
|
|
7063
|
+
var BOLD = "\x1B[1m";
|
|
7064
|
+
var DIM = "\x1B[2m";
|
|
7065
|
+
var RED = "\x1B[31m";
|
|
7066
|
+
var YELLOW = "\x1B[33m";
|
|
7067
|
+
var OFF = "\x1B[0m";
|
|
7068
|
+
async function runOwnershipCheck(argv) {
|
|
7069
|
+
const args = argv.filter((a) => a !== "--");
|
|
7070
|
+
const stagedFlag = args.indexOf("--staged");
|
|
7071
|
+
const staged = stagedFlag !== -1;
|
|
7072
|
+
const messageFile = staged ? args[stagedFlag + 1] : void 0;
|
|
7073
|
+
const root = (await execa5("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
7074
|
+
if (!isInstanceRepo(root)) {
|
|
7075
|
+
console.log("\u2713 core ownership guard: skipped \u2014 this is the template, which owns these paths.");
|
|
7076
|
+
return;
|
|
7077
|
+
}
|
|
7078
|
+
let changedFiles;
|
|
7079
|
+
let deletedFiles = [];
|
|
7080
|
+
let commitMessage = "";
|
|
7081
|
+
if (staged) {
|
|
7082
|
+
const { stdout } = await execa5("git", ["diff", "--cached", "--name-status"], { cwd: root });
|
|
7083
|
+
({ changed: changedFiles, deleted: deletedFiles } = parseNameStatus(stdout));
|
|
7084
|
+
if (messageFile) {
|
|
7085
|
+
const { readFileSync: readFileSync23, existsSync: existsSync30 } = await import("fs");
|
|
7086
|
+
if (existsSync30(messageFile)) commitMessage = readFileSync23(messageFile, "utf8");
|
|
7087
|
+
}
|
|
7088
|
+
} else {
|
|
7089
|
+
const base = process.env["GITHUB_BASE_REF"] ?? args[0];
|
|
7090
|
+
if (!base) {
|
|
7091
|
+
console.error("No base ref: set GITHUB_BASE_REF or pass a base branch as the first argument.");
|
|
7092
|
+
process.exit(2);
|
|
7093
|
+
}
|
|
7094
|
+
await execa5("git", ["fetch", "--quiet", "origin", base], { cwd: root, reject: false });
|
|
7095
|
+
const { stdout } = await execa5("git", ["diff", "--name-status", `origin/${base}...HEAD`], {
|
|
7096
|
+
cwd: root
|
|
7097
|
+
});
|
|
7098
|
+
({ changed: changedFiles, deleted: deletedFiles } = parseNameStatus(stdout));
|
|
7099
|
+
const { stdout: log2 } = await execa5("git", ["log", "--format=%B", `origin/${base}..HEAD`], {
|
|
7100
|
+
cwd: root,
|
|
7101
|
+
reject: false
|
|
7102
|
+
});
|
|
7103
|
+
commitMessage = log2;
|
|
7104
|
+
}
|
|
7105
|
+
const { stdout: gitBranch } = await execa5("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
7106
|
+
cwd: root,
|
|
7107
|
+
reject: false
|
|
7108
|
+
});
|
|
7109
|
+
const branch = resolveBranch(process.env, gitBranch);
|
|
7110
|
+
const result = checkCoreOwnership({
|
|
7111
|
+
changedFiles,
|
|
7112
|
+
manifest: readCoreManifest(root),
|
|
7113
|
+
isInstance: true,
|
|
7114
|
+
branch,
|
|
7115
|
+
commitMessage,
|
|
7116
|
+
warnOnly: readDivergenceConfig(root).warnOnly
|
|
7117
|
+
});
|
|
7118
|
+
for (const { path, entry } of result.warned) {
|
|
7119
|
+
console.error(
|
|
7120
|
+
`${YELLOW}\u26A0 ${path}${OFF} ${DIM}\u2014 known divergence in ${entry.prefix} (${entry.upstream}); still conflicts at the next core upgrade.${OFF}`
|
|
7121
|
+
);
|
|
7122
|
+
}
|
|
7123
|
+
if (result.skipped === "upgrade-branch") {
|
|
7124
|
+
console.log("\u2713 core ownership guard: skipped \u2014 this is a core-upgrade branch.");
|
|
7125
|
+
return;
|
|
7126
|
+
}
|
|
7127
|
+
if (result.skipped === "divergence-trailer") {
|
|
7128
|
+
console.log(
|
|
7129
|
+
`\u2713 core ownership guard: allowed by Core-Divergence: ${result.divergenceReason ?? ""}`
|
|
7130
|
+
);
|
|
7131
|
+
return;
|
|
7132
|
+
}
|
|
7133
|
+
if (result.blocked.length === 0) {
|
|
7134
|
+
console.log("\u2713 core ownership guard: no template-owned paths changed.");
|
|
7135
|
+
return;
|
|
7136
|
+
}
|
|
7137
|
+
const shown = result.blocked.slice(0, 15);
|
|
7138
|
+
const more = result.blocked.length - shown.length;
|
|
7139
|
+
console.error(`
|
|
7140
|
+
${RED}${BOLD}\u2717 This change edits template-owned paths.${OFF}
|
|
7141
|
+
|
|
7142
|
+
${shown.map((p) => ` ${RED}${p}${OFF}`).join("\n")}${more > 0 ? `
|
|
7143
|
+
${DIM}\u2026and ${more} more${OFF}` : ""}
|
|
7144
|
+
|
|
7145
|
+
${BOLD}Why this is blocked${OFF}
|
|
7146
|
+
core-manifest.json marks these as owned by biffo-template. Changing them here
|
|
7147
|
+
breaks nothing today \u2014 it becomes a merge conflict at the next
|
|
7148
|
+
\`biffo core upgrade\`, long after the reasoning is gone.
|
|
7149
|
+
|
|
7150
|
+
${BOLD}What to do instead${OFF}
|
|
7151
|
+
Make the change in biffo-template, release it, and take it here with
|
|
7152
|
+
\`biffo core upgrade\`. Instance-specific behaviour belongs in a user-owned
|
|
7153
|
+
path \u2014 see core-manifest.json for the split.
|
|
7154
|
+
|
|
7155
|
+
${result.blocked.some((p) => deletedFiles.includes(p)) ? `${BOLD}Some of these are deletions${OFF}
|
|
7156
|
+
Deleting a template-owned file is not a smaller change than editing one \u2014 a
|
|
7157
|
+
core upgrade will not restore it (#395), so the instance loses it silently and
|
|
7158
|
+
for ever. If the file genuinely should not exist, delete it in biffo-template.
|
|
7159
|
+
|
|
7160
|
+
` : ""}${BOLD}If the divergence is deliberate${OFF}
|
|
7161
|
+
Record it in the commit message and it is allowed:
|
|
7162
|
+
|
|
7163
|
+
${DIM}Core-Divergence: <why this instance must differ from the template>${OFF}
|
|
7164
|
+
|
|
7165
|
+
Raise an upstream issue too, so it does not stay divergent by accident. For a
|
|
7166
|
+
boundary you knowingly sit astride, add a warn-only prefix to ${DIVERGENCE_FILE}.
|
|
7167
|
+
`);
|
|
7168
|
+
process.exit(1);
|
|
7169
|
+
}
|
|
7170
|
+
|
|
7171
|
+
// src/scripts/check-plugin-terraform.ts
|
|
7172
|
+
import { execa as execa6 } from "execa";
|
|
7173
|
+
|
|
7174
|
+
// src/lib/plugin-terraform-guard.ts
|
|
7175
|
+
import { existsSync as existsSync29, readFileSync as readFileSync22, readdirSync as readdirSync12 } from "fs";
|
|
7176
|
+
import { dirname as dirname9, join as join30, relative as relative6, sep as sep3 } from "path";
|
|
7177
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", ".worktrees", "dist", ".venv", "__pycache__"]);
|
|
7178
|
+
var PLUGIN_MANIFEST_FILE2 = "biffo.plugin.json";
|
|
7179
|
+
function findPluginManifests(root) {
|
|
7180
|
+
const found = [];
|
|
7181
|
+
const walk = (dir) => {
|
|
7182
|
+
let entries;
|
|
7183
|
+
try {
|
|
7184
|
+
entries = readdirSync12(dir, { withFileTypes: true });
|
|
7185
|
+
} catch {
|
|
7186
|
+
return;
|
|
7187
|
+
}
|
|
7188
|
+
for (const entry of entries) {
|
|
7189
|
+
if (entry.isDirectory()) {
|
|
7190
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
7191
|
+
walk(join30(dir, entry.name));
|
|
7192
|
+
} else if (entry.isFile() && entry.name === PLUGIN_MANIFEST_FILE2) {
|
|
7193
|
+
found.push(relative6(root, join30(dir, entry.name)).split(sep3).join("/"));
|
|
7194
|
+
}
|
|
7195
|
+
}
|
|
7196
|
+
};
|
|
7197
|
+
walk(root);
|
|
7198
|
+
return found.sort();
|
|
7199
|
+
}
|
|
7200
|
+
function readSubscriptions(absManifestPath) {
|
|
7201
|
+
let parsed;
|
|
7202
|
+
try {
|
|
7203
|
+
parsed = JSON.parse(readFileSync22(absManifestPath, "utf8"));
|
|
7204
|
+
} catch {
|
|
7205
|
+
return null;
|
|
7206
|
+
}
|
|
7207
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
7208
|
+
const subs = parsed["event_subscriptions"];
|
|
7209
|
+
if (!Array.isArray(subs) || subs.length === 0) return null;
|
|
7210
|
+
return subs.map((sub, i) => {
|
|
7211
|
+
if (typeof sub !== "object" || sub === null) return `#${i}`;
|
|
7212
|
+
const { source, detail_type: detailType } = sub;
|
|
7213
|
+
return `${String(source ?? "?")}/${String(detailType ?? "?")}`;
|
|
7214
|
+
});
|
|
7215
|
+
}
|
|
7216
|
+
function checkPluginTerraform(root) {
|
|
7217
|
+
const violations = [];
|
|
7218
|
+
const coreManifest = existsSync29(join30(root, CORE_MANIFEST_FILE)) ? readCoreManifest(root) : null;
|
|
7219
|
+
for (const manifest of findPluginManifests(root)) {
|
|
7220
|
+
if (coreManifest && !isTemplateOwned(manifest, coreManifest)) continue;
|
|
7221
|
+
const absManifest = join30(root, manifest);
|
|
7222
|
+
const subscriptions = readSubscriptions(absManifest);
|
|
7223
|
+
if (subscriptions === null) continue;
|
|
7224
|
+
const pluginDir2 = dirname9(absManifest);
|
|
7225
|
+
if (existsSync29(join30(pluginDir2, "terraform"))) continue;
|
|
7226
|
+
const relPluginDir = relative6(root, pluginDir2).split(sep3).join("/");
|
|
7227
|
+
violations.push({
|
|
7228
|
+
manifest,
|
|
7229
|
+
expectedTerraformDir: relPluginDir ? `${relPluginDir}/terraform` : "terraform",
|
|
7230
|
+
subscriptions
|
|
7231
|
+
});
|
|
7232
|
+
}
|
|
7233
|
+
return violations;
|
|
7234
|
+
}
|
|
7235
|
+
function formatViolations(violations) {
|
|
7236
|
+
return violations.map(
|
|
7237
|
+
(v) => ` ${v.manifest} declares ${v.subscriptions.length} event subscription(s) (${v.subscriptions.join(", ")}) but ships no ${v.expectedTerraformDir}/.
|
|
7238
|
+
Without it there is no Lambda and no EventBridge rule, so those events never reach the plugin \u2014 and \`biffo plugin install\` skips the Terraform copy silently.
|
|
7239
|
+
Fix: copy modules/plugins/_template/ to ${v.expectedTerraformDir}/ and set handler/event_subscriptions to match the manifest.`
|
|
7240
|
+
).join("\n\n");
|
|
7241
|
+
}
|
|
7242
|
+
|
|
7243
|
+
// src/scripts/check-plugin-terraform.ts
|
|
7244
|
+
async function runPluginTerraformCheck() {
|
|
7245
|
+
const root = (await execa6("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
7246
|
+
const violations = checkPluginTerraform(root);
|
|
7247
|
+
if (violations.length > 0) {
|
|
7248
|
+
console.error("\u2717 plugin Terraform guard: event subscriptions with no infrastructure\n");
|
|
7249
|
+
console.error(formatViolations(violations));
|
|
7250
|
+
process.exit(1);
|
|
7251
|
+
}
|
|
7252
|
+
console.log("\u2713 plugin Terraform guard: OK");
|
|
7253
|
+
}
|
|
7254
|
+
|
|
7255
|
+
// src/scripts/check-release-subject.ts
|
|
7256
|
+
import { execa as execa7 } from "execa";
|
|
7257
|
+
|
|
7258
|
+
// src/lib/release-version.ts
|
|
7259
|
+
var MINOR_TYPES = /* @__PURE__ */ new Set(["feat"]);
|
|
7260
|
+
var SUBJECT = /^([a-z]+)(?:\([^)]*\))?(!)?:\s+\S/;
|
|
7261
|
+
function parseConventionalSubject(subject) {
|
|
7262
|
+
const match = SUBJECT.exec(subject.trim());
|
|
7263
|
+
if (!match?.[1]) return null;
|
|
7264
|
+
return { type: match[1], breaking: match[2] === "!" };
|
|
7265
|
+
}
|
|
7266
|
+
function bumpKindFor(subjects) {
|
|
7267
|
+
for (const subject of subjects) {
|
|
7268
|
+
const parsed = parseConventionalSubject(subject);
|
|
7269
|
+
if (parsed && (parsed.breaking || MINOR_TYPES.has(parsed.type))) return "minor";
|
|
7270
|
+
}
|
|
7271
|
+
return "patch";
|
|
7272
|
+
}
|
|
7273
|
+
|
|
7274
|
+
// src/lib/release-subject-guard.ts
|
|
7275
|
+
function checkReleaseSubject(changedFiles, subject, manifest, isInstance) {
|
|
7276
|
+
const templateOwnedChanges = changedFiles.filter((f) => isTemplateOwned(f, manifest));
|
|
7277
|
+
const parsed = parseConventionalSubject(subject);
|
|
7278
|
+
const releases = !isInstance && templateOwnedChanges.length > 0;
|
|
7279
|
+
return {
|
|
7280
|
+
templateOwnedChanges,
|
|
7281
|
+
unparseable: releases && parsed === null,
|
|
7282
|
+
bump: parsed === null ? null : bumpKindFor([subject]),
|
|
7283
|
+
skippedAsInstance: isInstance
|
|
7284
|
+
};
|
|
7285
|
+
}
|
|
7286
|
+
|
|
7287
|
+
// src/scripts/check-release-subject.ts
|
|
7288
|
+
async function runReleaseSubjectCheck(argv) {
|
|
7289
|
+
const base = process.env["GITHUB_BASE_REF"] ?? argv[0];
|
|
7290
|
+
if (!base) {
|
|
7291
|
+
console.error("No base ref: set GITHUB_BASE_REF or pass a base branch as the first argument.");
|
|
7292
|
+
process.exit(2);
|
|
7293
|
+
}
|
|
7294
|
+
const root = (await execa7("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
7295
|
+
await execa7("git", ["fetch", "--quiet", "origin", base], { cwd: root, reject: false });
|
|
7296
|
+
const { stdout } = await execa7("git", ["diff", "--name-only", `origin/${base}...HEAD`], {
|
|
7297
|
+
cwd: root
|
|
7298
|
+
});
|
|
7299
|
+
const changedFiles = stdout.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
7300
|
+
const subject = process.env["PR_TITLE"]?.trim() || (await execa7("git", ["log", "-1", "--format=%s"], { cwd: root })).stdout.trim();
|
|
7301
|
+
const manifest = readCoreManifest(root);
|
|
7302
|
+
const { unparseable, bump, templateOwnedChanges, skippedAsInstance } = checkReleaseSubject(
|
|
7303
|
+
changedFiles,
|
|
7304
|
+
subject,
|
|
7305
|
+
manifest,
|
|
7306
|
+
isInstanceRepo(root)
|
|
7307
|
+
);
|
|
7308
|
+
if (skippedAsInstance) {
|
|
7309
|
+
console.log(
|
|
7310
|
+
`\u2713 release subject guard: skipped \u2014 this is an instance (${INSTANCE_CORE_FILE} present), not the template. Instances cut no core-v* release, so the title is not a release input.`
|
|
7311
|
+
);
|
|
7312
|
+
return;
|
|
7313
|
+
}
|
|
7314
|
+
if (templateOwnedChanges.length === 0) {
|
|
7315
|
+
console.log(
|
|
7316
|
+
"\u2713 release subject guard: no template-owned change, so this PR releases nothing and its title is not a release input."
|
|
7317
|
+
);
|
|
7318
|
+
return;
|
|
7319
|
+
}
|
|
7320
|
+
if (unparseable) {
|
|
7321
|
+
console.error(
|
|
7322
|
+
`
|
|
7323
|
+
\u2717 Pull request title is not a Conventional Commits subject.
|
|
7324
|
+
|
|
7325
|
+
Title: ${subject}
|
|
7326
|
+
|
|
7327
|
+
This PR changes ${templateOwnedChanges.length} template-owned path(s), so merging it cuts a core release:
|
|
7328
|
+
` + templateOwnedChanges.slice(0, 10).map((p) => ` - ${p}`).join("\n") + (templateOwnedChanges.length > 10 ? `
|
|
7329
|
+
\u2026 and ${templateOwnedChanges.length - 10} more` : "") + `
|
|
7330
|
+
|
|
7331
|
+
Squash-merge makes this title the commit subject on \`main\`, and the release job derives the version bump from it (ADR-0006, #423). A title it cannot parse silently becomes a patch \u2014 so a feature would ship as one, and instances tracking the minor line would never see it.
|
|
7332
|
+
|
|
7333
|
+
Retitle as \`type(scope): summary\` \u2014 e.g. \`feat(api): add run history endpoint\`. Use \`feat\` for a feature, \`fix\` for a fix, a trailing \`!\` for a breaking change.
|
|
7334
|
+
`
|
|
7335
|
+
);
|
|
7336
|
+
process.exit(1);
|
|
7337
|
+
}
|
|
7338
|
+
console.log(
|
|
7339
|
+
`\u2713 release subject guard: "${subject}" \u2192 ${bump ?? "patch"} release for ${templateOwnedChanges.length} template-owned change(s).`
|
|
7340
|
+
);
|
|
7341
|
+
}
|
|
7342
|
+
|
|
7343
|
+
// src/commands/check.ts
|
|
7344
|
+
var checkCommand = new Command22("check").description(
|
|
7345
|
+
"Repo guards (ownership, release subject, plugin terraform) \u2014 run in CI and git hooks"
|
|
7346
|
+
);
|
|
7347
|
+
checkCommand.command("ownership").description("Refuse changes to template-owned paths in an instance (#370)").argument("[base]", "Base branch to diff against; defaults to $GITHUB_BASE_REF").option("--staged <messageFile>", "Check staged changes instead of a branch diff (commit hook)").allowExcessArguments(true).action(async () => {
|
|
7348
|
+
await runOwnershipCheck(rawArgsAfter("ownership"));
|
|
7349
|
+
});
|
|
7350
|
+
checkCommand.command("release-subject").description("Require a Conventional Commits PR title on template-owned changes (#423)").argument("[base]", "Base branch to diff against; defaults to $GITHUB_BASE_REF").action(async () => {
|
|
7351
|
+
await runReleaseSubjectCheck(rawArgsAfter("release-subject"));
|
|
7352
|
+
});
|
|
7353
|
+
checkCommand.command("plugin-terraform").description("Verify every template-owned plugin declaring infra ships a Terraform module").action(async () => {
|
|
7354
|
+
await runPluginTerraformCheck();
|
|
7355
|
+
});
|
|
7356
|
+
function rawArgsAfter(subcommand) {
|
|
7357
|
+
const at = process.argv.indexOf(subcommand);
|
|
7358
|
+
return at === -1 ? [] : process.argv.slice(at + 1);
|
|
7359
|
+
}
|
|
7360
|
+
|
|
6960
7361
|
// src/commands/teardown.ts
|
|
6961
7362
|
import { execSync as execSync7 } from "child_process";
|
|
6962
7363
|
import { GetCallerIdentityCommand as GetCallerIdentityCommand3, STSClient as STSClient3 } from "@aws-sdk/client-sts";
|
|
6963
7364
|
import chalk20 from "chalk";
|
|
6964
|
-
import { Command as
|
|
7365
|
+
import { Command as Command23 } from "commander";
|
|
6965
7366
|
import inquirer8 from "inquirer";
|
|
6966
|
-
var teardownCommand = new
|
|
7367
|
+
var teardownCommand = new Command23("teardown").description(
|
|
6967
7368
|
"Destroy all infrastructure then remove the repo, IAM role, and state bucket \u2014 single command"
|
|
6968
7369
|
).option("--project <name>", "Project name to tear down (reads session if omitted)").option("--skip-destroy", "Skip terraform destroy (only use if infrastructure is already gone)").option(
|
|
6969
7370
|
"--confirm <name>",
|
|
@@ -7371,7 +7772,7 @@ function resolveGithubToken4() {
|
|
|
7371
7772
|
}
|
|
7372
7773
|
|
|
7373
7774
|
// src/index.ts
|
|
7374
|
-
var program = new
|
|
7775
|
+
var program = new Command24();
|
|
7375
7776
|
function cliVersion() {
|
|
7376
7777
|
try {
|
|
7377
7778
|
return getLatestCoreVersion();
|
|
@@ -7388,6 +7789,7 @@ program.addCommand(pluginCommand);
|
|
|
7388
7789
|
program.addCommand(dataCommand);
|
|
7389
7790
|
program.addCommand(coreCommand);
|
|
7390
7791
|
program.addCommand(siblingCommand);
|
|
7792
|
+
program.addCommand(checkCommand);
|
|
7391
7793
|
registerNonInteractive(program);
|
|
7392
7794
|
program.parseAsync().catch((err) => {
|
|
7393
7795
|
if (err instanceof NonInteractiveError) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@biffo/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.61.0",
|
|
4
4
|
"description": "Biffo project scaffolding CLI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -34,9 +34,9 @@
|
|
|
34
34
|
"lint:fix": "eslint src/ --fix",
|
|
35
35
|
"typecheck": "tsc --noEmit",
|
|
36
36
|
"test": "vitest run",
|
|
37
|
-
"check:release-subject": "tsx src/
|
|
38
|
-
"check:core-ownership": "tsx src/
|
|
39
|
-
"check:plugin-terraform": "tsx src/
|
|
37
|
+
"check:release-subject": "tsx src/index.ts check release-subject",
|
|
38
|
+
"check:core-ownership": "tsx src/index.ts check ownership",
|
|
39
|
+
"check:plugin-terraform": "tsx src/index.ts check plugin-terraform",
|
|
40
40
|
"sync:core-tag": "tsx src/scripts/sync-core-tag.ts",
|
|
41
41
|
"report:publish-failure": "tsx src/scripts/report-publish-failure.ts"
|
|
42
42
|
},
|