@biffo/cli 0.142.3 → 0.145.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 +255 -25
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -7732,8 +7732,139 @@ siblingCommand.addCommand(siblingCheckIdentityCommand);
|
|
|
7732
7732
|
// src/commands/check.ts
|
|
7733
7733
|
import { Command as Command23 } from "commander";
|
|
7734
7734
|
|
|
7735
|
-
// src/scripts/check-
|
|
7735
|
+
// src/scripts/check-branch-protection.ts
|
|
7736
|
+
import { Octokit as Octokit2 } from "@octokit/rest";
|
|
7736
7737
|
import { execa as execa5 } from "execa";
|
|
7738
|
+
|
|
7739
|
+
// src/lib/branch-protection-audit.ts
|
|
7740
|
+
function auditBranch(branch, protection) {
|
|
7741
|
+
if (protection === null) {
|
|
7742
|
+
return [
|
|
7743
|
+
{
|
|
7744
|
+
branch,
|
|
7745
|
+
kind: "unprotected",
|
|
7746
|
+
detail: "no branch protection at all \u2014 direct pushes, force-pushes and merges with red or absent checks are all permitted"
|
|
7747
|
+
}
|
|
7748
|
+
];
|
|
7749
|
+
}
|
|
7750
|
+
const findings = [];
|
|
7751
|
+
const checks = protection.required_status_checks;
|
|
7752
|
+
if (!checks || (checks.contexts ?? []).length === 0) {
|
|
7753
|
+
findings.push({
|
|
7754
|
+
branch,
|
|
7755
|
+
kind: "no-required-checks",
|
|
7756
|
+
detail: "protection exists but requires no status checks, so a PR is mergeable before any check has run"
|
|
7757
|
+
});
|
|
7758
|
+
} else if (checks.strict !== true) {
|
|
7759
|
+
findings.push({
|
|
7760
|
+
branch,
|
|
7761
|
+
kind: "not-strict",
|
|
7762
|
+
detail: "required checks are not strict, so a branch can merge without being up to date with the base"
|
|
7763
|
+
});
|
|
7764
|
+
}
|
|
7765
|
+
if (protection.allow_force_pushes?.enabled === true) {
|
|
7766
|
+
findings.push({ branch, kind: "force-push-allowed", detail: "force-pushes are permitted" });
|
|
7767
|
+
}
|
|
7768
|
+
if (protection.allow_deletions?.enabled === true) {
|
|
7769
|
+
findings.push({ branch, kind: "deletion-allowed", detail: "branch deletion is permitted" });
|
|
7770
|
+
}
|
|
7771
|
+
return findings;
|
|
7772
|
+
}
|
|
7773
|
+
function formatFindings(findings) {
|
|
7774
|
+
return findings.map((f) => ` ${f.branch}: ${f.detail}`).join("\n");
|
|
7775
|
+
}
|
|
7776
|
+
|
|
7777
|
+
// src/scripts/check-branch-protection.ts
|
|
7778
|
+
var BRANCHES = ["dev", "staging", "main"];
|
|
7779
|
+
function tokenFromEnv() {
|
|
7780
|
+
const token = process.env["GITHUB_TOKEN"] ?? process.env["GH_TOKEN"] ?? "";
|
|
7781
|
+
if (!token) {
|
|
7782
|
+
console.error(
|
|
7783
|
+
"\u2717 branch-protection guard: no GitHub token.\n Set GITHUB_TOKEN or GH_TOKEN. Reading protection needs admin on the repo,\n so a default read-only CI token is not enough \u2014 this guard is meant to be\n run deliberately, not on every PR."
|
|
7784
|
+
);
|
|
7785
|
+
process.exit(2);
|
|
7786
|
+
}
|
|
7787
|
+
return token;
|
|
7788
|
+
}
|
|
7789
|
+
async function resolveRepo(explicit) {
|
|
7790
|
+
if (explicit) {
|
|
7791
|
+
const [owner, repo] = explicit.split("/");
|
|
7792
|
+
if (!owner || !repo) {
|
|
7793
|
+
console.error(`\u2717 branch-protection guard: --repo must be "owner/name", got "${explicit}"`);
|
|
7794
|
+
process.exit(2);
|
|
7795
|
+
}
|
|
7796
|
+
return { owner, repo };
|
|
7797
|
+
}
|
|
7798
|
+
const { stdout } = await execa5("git", ["remote", "get-url", "origin"]);
|
|
7799
|
+
const m = /github\.com[:/]([^/]+)\/(.+?)(?:\.git)?$/.exec(stdout.trim());
|
|
7800
|
+
if (!m?.[1] || !m[2]) {
|
|
7801
|
+
console.error(
|
|
7802
|
+
`\u2717 branch-protection guard: cannot parse a GitHub repo from origin "${stdout.trim()}"`
|
|
7803
|
+
);
|
|
7804
|
+
process.exit(2);
|
|
7805
|
+
}
|
|
7806
|
+
return { owner: m[1], repo: m[2] };
|
|
7807
|
+
}
|
|
7808
|
+
async function runBranchProtectionCheck(explicitRepo) {
|
|
7809
|
+
const { owner, repo } = await resolveRepo(explicitRepo);
|
|
7810
|
+
const octokit = new Octokit2({
|
|
7811
|
+
auth: tokenFromEnv(),
|
|
7812
|
+
log: { debug: () => {
|
|
7813
|
+
}, info: () => {
|
|
7814
|
+
}, warn: () => {
|
|
7815
|
+
}, error: console.error }
|
|
7816
|
+
});
|
|
7817
|
+
const findings = [];
|
|
7818
|
+
const audited = [];
|
|
7819
|
+
for (const branch of BRANCHES) {
|
|
7820
|
+
try {
|
|
7821
|
+
await octokit.repos.getBranch({ owner, repo, branch });
|
|
7822
|
+
} catch (err) {
|
|
7823
|
+
if (err.status === 404) continue;
|
|
7824
|
+
throw err;
|
|
7825
|
+
}
|
|
7826
|
+
audited.push(branch);
|
|
7827
|
+
try {
|
|
7828
|
+
const { data } = await octokit.repos.getBranchProtection({ owner, repo, branch });
|
|
7829
|
+
findings.push(...auditBranch(branch, data));
|
|
7830
|
+
} catch (err) {
|
|
7831
|
+
if (err.status === 404) {
|
|
7832
|
+
findings.push(...auditBranch(branch, null));
|
|
7833
|
+
continue;
|
|
7834
|
+
}
|
|
7835
|
+
if (err.status === 403) {
|
|
7836
|
+
console.error(
|
|
7837
|
+
`\u2717 branch-protection guard: GitHub returned 403 reading ${owner}/${repo}.
|
|
7838
|
+
Either the token lacks admin, or this org/plan cannot protect private repos.
|
|
7839
|
+
The latter is the condition that caused #715 \u2014 protection was skipped at
|
|
7840
|
+
scaffold time and never revisited. It is a finding, not a reason to pass.`
|
|
7841
|
+
);
|
|
7842
|
+
process.exit(1);
|
|
7843
|
+
}
|
|
7844
|
+
throw err;
|
|
7845
|
+
}
|
|
7846
|
+
}
|
|
7847
|
+
if (audited.length === 0) {
|
|
7848
|
+
console.error(
|
|
7849
|
+
`\u2717 branch-protection guard: ${owner}/${repo} has none of ${BRANCHES.join("/")}.
|
|
7850
|
+
A repo with no dev branch has not been migrated (biffo-template#559).`
|
|
7851
|
+
);
|
|
7852
|
+
process.exit(1);
|
|
7853
|
+
}
|
|
7854
|
+
if (findings.length > 0) {
|
|
7855
|
+
console.error(`\u2717 branch-protection guard: ${owner}/${repo}
|
|
7856
|
+
`);
|
|
7857
|
+
console.error(formatFindings(findings));
|
|
7858
|
+
console.error(
|
|
7859
|
+
"\n Protection is applied once at scaffold time and skipped silently on a 403 (#715).\n Fix with the repo settings API, matching the policy \u2014 not the exact required\n checks, which legitimately differ per repo."
|
|
7860
|
+
);
|
|
7861
|
+
process.exit(1);
|
|
7862
|
+
}
|
|
7863
|
+
console.log(`\u2713 branch-protection guard: ${owner}/${repo} (${audited.join(", ")}) OK`);
|
|
7864
|
+
}
|
|
7865
|
+
|
|
7866
|
+
// src/scripts/check-core-ownership.ts
|
|
7867
|
+
import { execa as execa6 } from "execa";
|
|
7737
7868
|
var BOLD = "\x1B[1m";
|
|
7738
7869
|
var DIM = "\x1B[2m";
|
|
7739
7870
|
var RED = "\x1B[31m";
|
|
@@ -7744,7 +7875,7 @@ async function runOwnershipCheck(argv) {
|
|
|
7744
7875
|
const stagedFlag = args.indexOf("--staged");
|
|
7745
7876
|
const staged = stagedFlag !== -1;
|
|
7746
7877
|
const messageFile = staged ? args[stagedFlag + 1] : void 0;
|
|
7747
|
-
const root = (await
|
|
7878
|
+
const root = (await execa6("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
7748
7879
|
if (!isInstanceRepo(root)) {
|
|
7749
7880
|
console.log("\u2713 core ownership guard: skipped \u2014 this is the template, which owns these paths.");
|
|
7750
7881
|
return;
|
|
@@ -7753,11 +7884,11 @@ async function runOwnershipCheck(argv) {
|
|
|
7753
7884
|
let deletedFiles = [];
|
|
7754
7885
|
let commitMessage = "";
|
|
7755
7886
|
if (staged) {
|
|
7756
|
-
const { stdout } = await
|
|
7887
|
+
const { stdout } = await execa6("git", ["diff", "--cached", "--name-status"], { cwd: root });
|
|
7757
7888
|
({ changed: changedFiles, deleted: deletedFiles } = parseNameStatus(stdout));
|
|
7758
7889
|
if (messageFile) {
|
|
7759
|
-
const { readFileSync: readFileSync25, existsSync:
|
|
7760
|
-
if (
|
|
7890
|
+
const { readFileSync: readFileSync25, existsSync: existsSync34 } = await import("fs");
|
|
7891
|
+
if (existsSync34(messageFile)) commitMessage = readFileSync25(messageFile, "utf8");
|
|
7761
7892
|
}
|
|
7762
7893
|
} else {
|
|
7763
7894
|
const base = process.env["GITHUB_BASE_REF"] ?? args[0];
|
|
@@ -7765,18 +7896,18 @@ async function runOwnershipCheck(argv) {
|
|
|
7765
7896
|
console.error("No base ref: set GITHUB_BASE_REF or pass a base branch as the first argument.");
|
|
7766
7897
|
process.exit(2);
|
|
7767
7898
|
}
|
|
7768
|
-
await
|
|
7769
|
-
const { stdout } = await
|
|
7899
|
+
await execa6("git", ["fetch", "--quiet", "origin", base], { cwd: root, reject: false });
|
|
7900
|
+
const { stdout } = await execa6("git", ["diff", "--name-status", `origin/${base}...HEAD`], {
|
|
7770
7901
|
cwd: root
|
|
7771
7902
|
});
|
|
7772
7903
|
({ changed: changedFiles, deleted: deletedFiles } = parseNameStatus(stdout));
|
|
7773
|
-
const { stdout: log2 } = await
|
|
7904
|
+
const { stdout: log2 } = await execa6("git", ["log", "--format=%B", `origin/${base}..HEAD`], {
|
|
7774
7905
|
cwd: root,
|
|
7775
7906
|
reject: false
|
|
7776
7907
|
});
|
|
7777
7908
|
commitMessage = log2;
|
|
7778
7909
|
}
|
|
7779
|
-
const { stdout: gitBranch } = await
|
|
7910
|
+
const { stdout: gitBranch } = await execa6("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
7780
7911
|
cwd: root,
|
|
7781
7912
|
reject: false
|
|
7782
7913
|
});
|
|
@@ -7857,12 +7988,103 @@ ${BOLD}If the divergence is deliberate${OFF}
|
|
|
7857
7988
|
process.exit(1);
|
|
7858
7989
|
}
|
|
7859
7990
|
|
|
7991
|
+
// src/scripts/check-plugin-collisions.ts
|
|
7992
|
+
import { existsSync as existsSync32 } from "fs";
|
|
7993
|
+
import { join as join32 } from "path";
|
|
7994
|
+
import { execa as execa7 } from "execa";
|
|
7995
|
+
|
|
7996
|
+
// src/lib/plugin-collision-guard.ts
|
|
7997
|
+
import { existsSync as existsSync31, readdirSync as readdirSync13, statSync as statSync7 } from "fs";
|
|
7998
|
+
import { join as join31 } from "path";
|
|
7999
|
+
var PYTEST_SPECIAL = /* @__PURE__ */ new Set(["conftest.py"]);
|
|
8000
|
+
var IGNORED_DIRS = /* @__PURE__ */ new Set([".venv", "node_modules", "__pycache__", ".git", "dist", "build"]);
|
|
8001
|
+
function subdirectories(dir) {
|
|
8002
|
+
if (!existsSync31(dir)) return [];
|
|
8003
|
+
return readdirSync13(dir).filter((entry) => {
|
|
8004
|
+
if (IGNORED_DIRS.has(entry) || entry.startsWith(".")) return false;
|
|
8005
|
+
try {
|
|
8006
|
+
return statSync7(join31(dir, entry)).isDirectory();
|
|
8007
|
+
} catch {
|
|
8008
|
+
return false;
|
|
8009
|
+
}
|
|
8010
|
+
});
|
|
8011
|
+
}
|
|
8012
|
+
function regularPackagesOf(pluginDir2) {
|
|
8013
|
+
return subdirectories(pluginDir2).filter((name) => existsSync31(join31(pluginDir2, name, "__init__.py"))).sort();
|
|
8014
|
+
}
|
|
8015
|
+
function bareTestModulesOf(pluginDir2) {
|
|
8016
|
+
const testsDir = join31(pluginDir2, "tests");
|
|
8017
|
+
if (!existsSync31(testsDir)) return [];
|
|
8018
|
+
if (existsSync31(join31(testsDir, "__init__.py"))) return [];
|
|
8019
|
+
return readdirSync13(testsDir).filter((f) => f.endsWith(".py") && !PYTEST_SPECIAL.has(f)).sort();
|
|
8020
|
+
}
|
|
8021
|
+
function findCollisions(servicesDir, pluginDirs) {
|
|
8022
|
+
const plugins = (pluginDirs ?? subdirectories(servicesDir)).filter((name) => !name.startsWith("_")).filter((name) => name !== "api").sort();
|
|
8023
|
+
const collisions = [];
|
|
8024
|
+
const gather = (kind, namesOf) => {
|
|
8025
|
+
const claims = /* @__PURE__ */ new Map();
|
|
8026
|
+
for (const plugin of plugins) {
|
|
8027
|
+
for (const name of namesOf(join31(servicesDir, plugin))) {
|
|
8028
|
+
claims.set(name, [...claims.get(name) ?? [], plugin]);
|
|
8029
|
+
}
|
|
8030
|
+
}
|
|
8031
|
+
for (const [name, owners] of [...claims.entries()].sort()) {
|
|
8032
|
+
if (owners.length > 1) collisions.push({ kind, name, plugins: owners.sort() });
|
|
8033
|
+
}
|
|
8034
|
+
};
|
|
8035
|
+
gather("regular-package", regularPackagesOf);
|
|
8036
|
+
gather("test-module", bareTestModulesOf);
|
|
8037
|
+
return collisions;
|
|
8038
|
+
}
|
|
8039
|
+
function formatCollisions(collisions) {
|
|
8040
|
+
const lines = [];
|
|
8041
|
+
for (const c of collisions) {
|
|
8042
|
+
if (c.kind === "regular-package") {
|
|
8043
|
+
lines.push(
|
|
8044
|
+
` regular package '${c.name}' is defined by: ${c.plugins.join(", ")}`,
|
|
8045
|
+
` Regular packages do not merge across sys.path \u2014 the first found shadows`,
|
|
8046
|
+
` the rest, and the plugin that breaks is whichever loaded second.`,
|
|
8047
|
+
` Fix: load these modules by file path instead of making '${c.name}' a`,
|
|
8048
|
+
` package (drop its __init__.py), or give it a plugin-scoped name.`
|
|
8049
|
+
);
|
|
8050
|
+
} else {
|
|
8051
|
+
lines.push(
|
|
8052
|
+
` test module '${c.name}' is shipped by: ${c.plugins.join(", ")}`,
|
|
8053
|
+
` pytest's prepend import mode imports test modules by bare basename, so`,
|
|
8054
|
+
` only one of these can be collected.`,
|
|
8055
|
+
` Fix: prefix it with the plugin name, e.g. '${c.plugins[0]}_${c.name}'.`
|
|
8056
|
+
);
|
|
8057
|
+
}
|
|
8058
|
+
}
|
|
8059
|
+
return lines.join("\n");
|
|
8060
|
+
}
|
|
8061
|
+
|
|
8062
|
+
// src/scripts/check-plugin-collisions.ts
|
|
8063
|
+
async function runPluginCollisionCheck() {
|
|
8064
|
+
const root = (await execa7("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
8065
|
+
const servicesDir = join32(root, "services");
|
|
8066
|
+
if (!existsSync32(servicesDir)) {
|
|
8067
|
+
console.log("\u2713 plugin collision guard: no services/ directory \u2014 nothing to compare");
|
|
8068
|
+
return;
|
|
8069
|
+
}
|
|
8070
|
+
const collisions = findCollisions(servicesDir);
|
|
8071
|
+
if (collisions.length > 0) {
|
|
8072
|
+
console.error("\u2717 plugin collision guard: two plugins claim the same importable name\n");
|
|
8073
|
+
console.error(formatCollisions(collisions));
|
|
8074
|
+
console.error(
|
|
8075
|
+
"\nThis breaks the plugin that loads *second*, which is usually the one that\nwas already working. See biffo-template#688."
|
|
8076
|
+
);
|
|
8077
|
+
process.exit(1);
|
|
8078
|
+
}
|
|
8079
|
+
console.log("\u2713 plugin collision guard: OK");
|
|
8080
|
+
}
|
|
8081
|
+
|
|
7860
8082
|
// src/scripts/check-plugin-terraform.ts
|
|
7861
|
-
import { execa as
|
|
8083
|
+
import { execa as execa8 } from "execa";
|
|
7862
8084
|
|
|
7863
8085
|
// src/lib/plugin-terraform-guard.ts
|
|
7864
|
-
import { existsSync as
|
|
7865
|
-
import { dirname as dirname9, join as
|
|
8086
|
+
import { existsSync as existsSync33, readFileSync as readFileSync24, readdirSync as readdirSync14 } from "fs";
|
|
8087
|
+
import { dirname as dirname9, join as join33, relative as relative6, sep as sep3 } from "path";
|
|
7866
8088
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", ".worktrees", "dist", ".venv", "__pycache__"]);
|
|
7867
8089
|
var PLUGIN_MANIFEST_FILE2 = "biffo.plugin.json";
|
|
7868
8090
|
function findPluginManifests(root) {
|
|
@@ -7870,16 +8092,16 @@ function findPluginManifests(root) {
|
|
|
7870
8092
|
const walk = (dir) => {
|
|
7871
8093
|
let entries;
|
|
7872
8094
|
try {
|
|
7873
|
-
entries =
|
|
8095
|
+
entries = readdirSync14(dir, { withFileTypes: true });
|
|
7874
8096
|
} catch {
|
|
7875
8097
|
return;
|
|
7876
8098
|
}
|
|
7877
8099
|
for (const entry of entries) {
|
|
7878
8100
|
if (entry.isDirectory()) {
|
|
7879
8101
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
7880
|
-
walk(
|
|
8102
|
+
walk(join33(dir, entry.name));
|
|
7881
8103
|
} else if (entry.isFile() && entry.name === PLUGIN_MANIFEST_FILE2) {
|
|
7882
|
-
found.push(relative6(root,
|
|
8104
|
+
found.push(relative6(root, join33(dir, entry.name)).split(sep3).join("/"));
|
|
7883
8105
|
}
|
|
7884
8106
|
}
|
|
7885
8107
|
};
|
|
@@ -7904,14 +8126,14 @@ function readSubscriptions(absManifestPath) {
|
|
|
7904
8126
|
}
|
|
7905
8127
|
function checkPluginTerraform(root) {
|
|
7906
8128
|
const violations = [];
|
|
7907
|
-
const coreManifest =
|
|
8129
|
+
const coreManifest = existsSync33(join33(root, CORE_MANIFEST_FILE)) ? readCoreManifest(root) : null;
|
|
7908
8130
|
for (const manifest of findPluginManifests(root)) {
|
|
7909
8131
|
if (coreManifest && !isTemplateOwned(manifest, coreManifest)) continue;
|
|
7910
|
-
const absManifest =
|
|
8132
|
+
const absManifest = join33(root, manifest);
|
|
7911
8133
|
const subscriptions = readSubscriptions(absManifest);
|
|
7912
8134
|
if (subscriptions === null) continue;
|
|
7913
8135
|
const pluginDir2 = dirname9(absManifest);
|
|
7914
|
-
if (
|
|
8136
|
+
if (existsSync33(join33(pluginDir2, "terraform"))) continue;
|
|
7915
8137
|
const relPluginDir = relative6(root, pluginDir2).split(sep3).join("/");
|
|
7916
8138
|
violations.push({
|
|
7917
8139
|
manifest,
|
|
@@ -7931,7 +8153,7 @@ function formatViolations(violations) {
|
|
|
7931
8153
|
|
|
7932
8154
|
// src/scripts/check-plugin-terraform.ts
|
|
7933
8155
|
async function runPluginTerraformCheck() {
|
|
7934
|
-
const root = (await
|
|
8156
|
+
const root = (await execa8("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
7935
8157
|
const violations = checkPluginTerraform(root);
|
|
7936
8158
|
if (violations.length > 0) {
|
|
7937
8159
|
console.error("\u2717 plugin Terraform guard: event subscriptions with no infrastructure\n");
|
|
@@ -7942,7 +8164,7 @@ async function runPluginTerraformCheck() {
|
|
|
7942
8164
|
}
|
|
7943
8165
|
|
|
7944
8166
|
// src/scripts/check-release-subject.ts
|
|
7945
|
-
import { execa as
|
|
8167
|
+
import { execa as execa9 } from "execa";
|
|
7946
8168
|
|
|
7947
8169
|
// src/lib/release-version.ts
|
|
7948
8170
|
var MINOR_TYPES = /* @__PURE__ */ new Set(["feat"]);
|
|
@@ -7980,13 +8202,13 @@ async function runReleaseSubjectCheck(argv) {
|
|
|
7980
8202
|
console.error("No base ref: set GITHUB_BASE_REF or pass a base branch as the first argument.");
|
|
7981
8203
|
process.exit(2);
|
|
7982
8204
|
}
|
|
7983
|
-
const root = (await
|
|
7984
|
-
await
|
|
7985
|
-
const { stdout } = await
|
|
8205
|
+
const root = (await execa9("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
8206
|
+
await execa9("git", ["fetch", "--quiet", "origin", base], { cwd: root, reject: false });
|
|
8207
|
+
const { stdout } = await execa9("git", ["diff", "--name-only", `origin/${base}...HEAD`], {
|
|
7986
8208
|
cwd: root
|
|
7987
8209
|
});
|
|
7988
8210
|
const changedFiles = stdout.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
7989
|
-
const subject = process.env["PR_TITLE"]?.trim() || (await
|
|
8211
|
+
const subject = process.env["PR_TITLE"]?.trim() || (await execa9("git", ["log", "-1", "--format=%s"], { cwd: root })).stdout.trim();
|
|
7990
8212
|
const manifest = readCoreManifest(root);
|
|
7991
8213
|
const { unparseable, bump, templateOwnedChanges, skippedAsInstance } = checkReleaseSubject(
|
|
7992
8214
|
changedFiles,
|
|
@@ -8031,7 +8253,7 @@ async function runReleaseSubjectCheck(argv) {
|
|
|
8031
8253
|
|
|
8032
8254
|
// src/commands/check.ts
|
|
8033
8255
|
var checkCommand = new Command23("check").description(
|
|
8034
|
-
"Repo guards (ownership, release subject, plugin terraform)
|
|
8256
|
+
"Repo guards (ownership, release subject, plugin terraform, plugin collisions) run in CI and git hooks, plus out-of-band audits (branch protection)"
|
|
8035
8257
|
);
|
|
8036
8258
|
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 () => {
|
|
8037
8259
|
await runOwnershipCheck(rawArgsAfter("ownership"));
|
|
@@ -8039,9 +8261,17 @@ checkCommand.command("ownership").description("Refuse changes to template-owned
|
|
|
8039
8261
|
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 () => {
|
|
8040
8262
|
await runReleaseSubjectCheck(rawArgsAfter("release-subject"));
|
|
8041
8263
|
});
|
|
8264
|
+
checkCommand.command("plugin-collisions").description("Refuse two vendored plugins claiming the same importable name (#688)").action(async () => {
|
|
8265
|
+
await runPluginCollisionCheck();
|
|
8266
|
+
});
|
|
8042
8267
|
checkCommand.command("plugin-terraform").description("Verify every template-owned plugin declaring infra ships a Terraform module").action(async () => {
|
|
8043
8268
|
await runPluginTerraformCheck();
|
|
8044
8269
|
});
|
|
8270
|
+
checkCommand.command("branch-protection").description(
|
|
8271
|
+
"Verify dev/staging/main are actually protected \u2014 scaffolding skips this on a 403 (#715)"
|
|
8272
|
+
).option("--repo <owner/name>", "Repo to audit; defaults to this checkout's origin remote").action(async (opts) => {
|
|
8273
|
+
await runBranchProtectionCheck(opts.repo);
|
|
8274
|
+
});
|
|
8045
8275
|
function rawArgsAfter(subcommand) {
|
|
8046
8276
|
const at = process.argv.indexOf(subcommand);
|
|
8047
8277
|
return at === -1 ? [] : process.argv.slice(at + 1);
|