@bensandee/tooling 0.90.0 → 0.91.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/README.md +1 -1
- package/dist/bin.mjs +766 -340
- package/package.json +1 -1
- package/tooling.schema.json +5 -4
package/dist/bin.mjs
CHANGED
|
@@ -175,7 +175,7 @@ function loadEnvFileForBin(rawArgs, bin, io = processIo) {
|
|
|
175
175
|
//#region src/env-file-preload.ts
|
|
176
176
|
const envFileLoad = loadEnvFileForBin(process.argv.slice(2), {
|
|
177
177
|
name: "bst",
|
|
178
|
-
version: "0.
|
|
178
|
+
version: "0.91.0"
|
|
179
179
|
});
|
|
180
180
|
//#endregion
|
|
181
181
|
//#region ../cli-kit/src/logger.ts
|
|
@@ -783,7 +783,7 @@ const LintConfigSchema = z.strictObject({ strictTypeAware: z.boolean().optional(
|
|
|
783
783
|
const ChecksConfigSchema = z.strictObject({
|
|
784
784
|
add: z.array(z.string()).optional().meta({ description: "Extra package.json scripts to run as first-class check steps, appended to the built-in list (e.g. ['lint:php']). An added step with no matching script is a hard failure." }),
|
|
785
785
|
skip: z.array(z.string()).optional().meta({ description: "Check steps to skip, unioned with the built-in CI defaults (never replacing them). Supports glob patterns." }),
|
|
786
|
-
runOnVersionPr: z.boolean().optional().meta({ description: "Run the CI check job on the changesets version PR (changeset-release
|
|
786
|
+
runOnVersionPr: z.boolean().optional().meta({ description: "Run the CI check job on the changesets version PR (changeset-release/<defaultBranch>). Default false: the generated check job is skipped there, because that branch is regenerated on every push to the default branch and that branch's own run gates the release. Has no effect unless releaseStrategy is changesets." })
|
|
787
787
|
});
|
|
788
788
|
const CoverageConfigSchema = z.strictObject({
|
|
789
789
|
history: z.enum(["orphan-branch", "none"]).optional().meta({ description: "How coverage history is persisted. 'orphan-branch' (default) appends to a dedicated branch; 'none' disables CI history but keeps the local ratchet." }),
|
|
@@ -835,6 +835,16 @@ const TolerantDockerCheckConfigSchema = z.object({
|
|
|
835
835
|
...DockerCheckConfigSchema.shape,
|
|
836
836
|
healthChecks: z.array(tolerantBlock(DeclarativeHealthCheckSchema)).optional()
|
|
837
837
|
});
|
|
838
|
+
const DEFAULT_BRANCH_NAME_PATTERN = /^(?!.*\/\/)(?!.*\.\.)(?!.*\/$)(?!.*\.lock$)[A-Za-z0-9][A-Za-z0-9._/-]*$/;
|
|
839
|
+
const DefaultBranchNameSchema = z.string().regex(DEFAULT_BRANCH_NAME_PATTERN, "must be a plain branch name: ASCII letters, digits, '.', '_', '-' and '/', starting with a letter or digit, with no '//', '..', trailing '/', or '.lock' suffix");
|
|
840
|
+
const AgentEnabledSliceSchema = z.looseObject({ agent: z.looseObject({ enabled: z.boolean().optional() }).optional() });
|
|
841
|
+
function agentEnabledIn(raw) {
|
|
842
|
+
try {
|
|
843
|
+
return parseToolingConfigSlice(raw, AgentEnabledSliceSchema, { block: "agent" })?.agent?.enabled === true;
|
|
844
|
+
} catch {
|
|
845
|
+
return false;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
838
848
|
//#endregion
|
|
839
849
|
//#region src/utils/stack.ts
|
|
840
850
|
const IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
@@ -1700,7 +1710,7 @@ const LintSweepConfigSchema = z.strictObject({
|
|
|
1700
1710
|
});
|
|
1701
1711
|
const AgentEgressConfigSchema = z.strictObject({ browseHosts: z.array(z.string().min(1)).optional().meta({ description: "Host patterns this repo's agent containers may fetch from, on top of the hosts every run needs: an exact host (`fastdl.mongodb.org`) or a wildcard over its leftmost label (`*.example.com`). No ports. These join the browse lane, which serves GET and HEAD only." }) });
|
|
1702
1712
|
const AgentConfigSchema = z.strictObject({
|
|
1703
|
-
enabled: z.boolean().optional().meta({ description: "The repo's explicit consent to the agent (default false). Only `true`, read from the repo's **default branch**, lets the agent work the repo: claim units, run sweeps, recover orphans, merge, act on a chat proposal or start a new spec. A missing key, `false`, a missing `.tooling.json` or a read that fails all mean not enabled, and the answer is re-read within about a minute. Every other `agent.*` key below has no effect without this one; `repo:sync` warns when the block sets one of them and this key is not `true`. Where this is `true`, `repo:sync` also installs `@bensandee/agent-cli`. Provisioning the agent's labels is not consent, and is never gated by this key." }),
|
|
1713
|
+
enabled: z.boolean().optional().meta({ description: "The repo's explicit consent to the agent (default false). Only `true`, read from the repo's **default branch**, lets the agent work the repo: claim units, run sweeps, recover orphans, merge, act on a chat proposal or start a new spec. A missing key, `false`, a missing `.tooling.json` or a read that fails all mean not enabled, and the answer is re-read within about a minute. Every other `agent.*` key below has no effect without this one; `repo:sync` warns when the block sets one of them and this key is not `true`. Where this is `true`, `repo:sync` also installs `@bensandee/agent-cli`. `bst setup:agent` sets it to true and runs the sync. Provisioning the agent's labels is not consent, and is never gated by this key." }),
|
|
1704
1714
|
autonomy: z.boolean().optional().meta({ description: "Opt into interior autonomy (default false). When a phase's latest rationale reports readiness \"ready\" with an empty openQuestions[] and the review phase is clean, the agent applies the existing plan -> implement -> review -> park-at-mergeable advance itself instead of parking for a human. Never applies to the design/spec gate or the final merge from the mergeable terminal -- those always wait on a human. Also arms the trunk-red detector: when this repo's default branch combined status settles red, the agent files a tracking issue naming the failing checks and labels it agent/spec, starting the fix through the ordinary spec/plan/implement/review pipeline. It also closes that issue, with a comment, if the default branch recovers on its own before a human acts on it. Also arms the world-changed retrigger sweep: a pull request labelled agent-flags/autopilot that is parked at a -stuck or -failed terminal is re-queued when the fact its park recorded stops being true (the default branch went green, or moved past a conflicted base)." }),
|
|
1705
1715
|
preRunRebase: z.boolean().optional().meta({ description: "Merge the current default branch into the work branch before the implement (execute) phase's container starts (default true). A clean merge or an already-up-to-date branch runs the container as is; a conflicting merge is aborted, recorded on the run's status comment, and the run proceeds on the un-synced branch -- it never fails the run. Two consecutive conflicting attempts hand the unit to agent/repair before any container runs; a timeout or a failed merge never counts toward that pair. Set false to skip the attempt entirely for this repo: no fetch, no merge, no abort, and the branch is worked exactly as cloned." }),
|
|
1706
1716
|
backlog: z.boolean().optional().meta({ description: "Whether the agent may self-trigger backlog runs consuming this repo's .agent/findings/ ledger (default: on wherever the ledger exists, which only happens in a repo whose own review loop wrote it). A backlog run opens a pull request that fixes or refutes the pending findings; false disarms it, leaving the ledger to accumulate for a human. A run starts only once at least three findings are pending or one is older than seven days." }),
|
|
@@ -1769,7 +1779,7 @@ const ToolingConfigSchema = z.strictObject({
|
|
|
1769
1779
|
docs: z.union([z.literal(false), DocsConfigSchema]).optional().meta({ description: "Plan/spec lifecycle gate (`bst docs:check`). Overrides only -- dirs are discovered by convention. Set to false to disable. Violations are advisory until a pair is named in `enforced`." }),
|
|
1770
1780
|
deploy: DeployConfigSchema.optional().meta({ description: "Generate a deploy-dispatch step in the release workflow that triggers a workflow_dispatch event in another repo after a successful release. The resolved release token must have rights on `dispatchRepo`." }),
|
|
1771
1781
|
pnpmWorkspace: z.literal(false).optional().meta({ description: "Set to false to opt out of pnpm-workspace.yaml management (skips both generation and the repo:sync --check validation)." }),
|
|
1772
|
-
defaultBranch:
|
|
1782
|
+
defaultBranch: DefaultBranchNameSchema.optional().meta({ description: "Repository default branch name, B. Drives the generated CI `on.push` trigger and every default-branch gate in `ci.yml` and `release.yml`, the release path (`release:changesets`, the version PR, `release:trigger`) and `checks:run`'s coverage gate. Generated output uses 'main' when unset; `checks:run` still falls back to `origin/HEAD`. Must be a plain branch name: ASCII letters, digits, '.', '_', '-' and '/'." }),
|
|
1773
1783
|
unmanagedDevDependencies: UnmanagedDevDependenciesSchema.optional().meta({ description: "devDependency names repo:sync must never add or re-pin. Escape hatch for holding a tooling-managed dep (oxlint, vitest, knip, ...) at a deliberately off-canonical version; without it, repo:sync raises any managed devDep whose version has drifted below the canonical pin." }),
|
|
1774
1784
|
stack: StackSchema.optional().meta({ description: "Whether the repo has TS/JS source. Detected from source evidence; TypeScript is never inferred from absence." }),
|
|
1775
1785
|
builder: z.enum([
|
|
@@ -1786,6 +1796,7 @@ const ToolingConfigSchema = z.strictObject({
|
|
|
1786
1796
|
});
|
|
1787
1797
|
const TolerantToolingConfigSchema = z.object({
|
|
1788
1798
|
...ToolingConfigSchema.shape,
|
|
1799
|
+
defaultBranch: z.string().optional(),
|
|
1789
1800
|
docker: TolerantDockerMapSchema.optional(),
|
|
1790
1801
|
dockerCheck: z.union([z.literal(false), TolerantDockerCheckConfigSchema]).optional(),
|
|
1791
1802
|
coverage: z.union([z.literal(false), tolerantBlock(CoverageConfigSchema)]).optional(),
|
|
@@ -1831,10 +1842,16 @@ function loadToolingConfig(targetDir) {
|
|
|
1831
1842
|
if (!file) return void 0;
|
|
1832
1843
|
return parseConfigFile(file, TolerantToolingConfigSchema);
|
|
1833
1844
|
}
|
|
1845
|
+
function parseToolingConfigStrict(raw, displayPath) {
|
|
1846
|
+
return parseConfigFile({
|
|
1847
|
+
raw,
|
|
1848
|
+
displayPath
|
|
1849
|
+
}, ToolingConfigSchema);
|
|
1850
|
+
}
|
|
1834
1851
|
function loadToolingConfigStrict(targetDir) {
|
|
1835
1852
|
const file = readConfigFile(targetDir);
|
|
1836
1853
|
if (!file) return void 0;
|
|
1837
|
-
return
|
|
1854
|
+
return parseToolingConfigStrict(file.raw, file.displayPath);
|
|
1838
1855
|
}
|
|
1839
1856
|
const OVERRIDE_KEYS = [
|
|
1840
1857
|
"structure",
|
|
@@ -4449,13 +4466,22 @@ const SETUP_MISE_ACTION_SHA = "ebe622ef0d24c7efec4a5f6093ddb18500900e24";
|
|
|
4449
4466
|
function actionsExpr(expr) {
|
|
4450
4467
|
return `\${{ ${expr} }}`;
|
|
4451
4468
|
}
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4469
|
+
function branchNameLiteral(name) {
|
|
4470
|
+
if (DEFAULT_BRANCH_NAME_PATTERN.test(name)) return name;
|
|
4471
|
+
return name.replace(/'/g, "''");
|
|
4472
|
+
}
|
|
4473
|
+
function isDefaultBranch(name) {
|
|
4474
|
+
return `github.ref == 'refs/heads/${branchNameLiteral(name)}'`;
|
|
4475
|
+
}
|
|
4476
|
+
function isNotDefaultBranch(name) {
|
|
4477
|
+
return `github.ref != 'refs/heads/${branchNameLiteral(name)}'`;
|
|
4478
|
+
}
|
|
4479
|
+
function defaultBranchJobEnv(name) {
|
|
4480
|
+
return { IS_MAIN: actionsExpr(isDefaultBranch(name)) };
|
|
4481
|
+
}
|
|
4456
4482
|
const IS_MAIN_ENV_GUARD = "env.IS_MAIN == 'true'";
|
|
4457
|
-
function onlyOnDefaultBranch(condition) {
|
|
4458
|
-
return `${
|
|
4483
|
+
function onlyOnDefaultBranch(name, condition) {
|
|
4484
|
+
return `${isDefaultBranch(name)} && (${condition})`;
|
|
4459
4485
|
}
|
|
4460
4486
|
function npmrcAuthKey(registryUrl) {
|
|
4461
4487
|
return `${registryUrl.replace(/^https?:/i, "").replace(/\/+$/, "")}/`;
|
|
@@ -4860,9 +4886,8 @@ function diagnoseCheckScriptForwardingAt(targetDir) {
|
|
|
4860
4886
|
//#endregion
|
|
4861
4887
|
//#region src/generators/agent-cli-dep.ts
|
|
4862
4888
|
const AGENT_CLI_PACKAGE = "@bensandee/agent-cli";
|
|
4863
|
-
const AgentEnabledSliceSchema = z.looseObject({ agent: z.looseObject({ enabled: z.boolean().optional() }).optional() });
|
|
4864
4889
|
function agentEnabled(ctx) {
|
|
4865
|
-
return
|
|
4890
|
+
return agentEnabledIn(ctx.read(".tooling.json"));
|
|
4866
4891
|
}
|
|
4867
4892
|
function wantsAgentCli(ctx) {
|
|
4868
4893
|
if (!agentEnabled(ctx)) return false;
|
|
@@ -5408,9 +5433,9 @@ function buildManagedDevDeps(ctx) {
|
|
|
5408
5433
|
const devDeps = { ...rootDevDeps(ctx.config) };
|
|
5409
5434
|
if (!isMonorepo) Object.assign(devDeps, perPackageDevDeps(ctx.config));
|
|
5410
5435
|
if (ctx.config.stack !== "none" || ctx.config.ci === "forgejo") devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.15.0";
|
|
5411
|
-
if (!isWorkspacePackage(ctx, "@bensandee/tooling")) devDeps["@bensandee/tooling"] = "0.
|
|
5436
|
+
if (!isWorkspacePackage(ctx, "@bensandee/tooling")) devDeps["@bensandee/tooling"] = "0.91.0";
|
|
5412
5437
|
if (ctx.config.useEslintPlugin && ctx.config.stack !== "none") devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "0.17.0";
|
|
5413
|
-
if (wantsAgentCli(ctx)) devDeps[AGENT_CLI_PACKAGE] = "0.1.
|
|
5438
|
+
if (wantsAgentCli(ctx)) devDeps[AGENT_CLI_PACKAGE] = "0.1.2";
|
|
5414
5439
|
if (ctx.config.formatter === "oxfmt") devDeps["oxfmt"] = {
|
|
5415
5440
|
"@changesets/cli": "3.0.3",
|
|
5416
5441
|
"@release-it/bumper": "8.0.1",
|
|
@@ -7332,6 +7357,59 @@ async function generateGitattributes(ctx) {
|
|
|
7332
7357
|
};
|
|
7333
7358
|
}
|
|
7334
7359
|
//#endregion
|
|
7360
|
+
//#region src/default-branch.ts
|
|
7361
|
+
const DEFAULT_BRANCH = "main";
|
|
7362
|
+
const DefaultBranchSliceSchema = z.object({ defaultBranch: z.union([z.literal(""), DefaultBranchNameSchema]).optional() });
|
|
7363
|
+
function configuredDefaultBranch(raw) {
|
|
7364
|
+
const configured = parseToolingConfigSlice(raw, DefaultBranchSliceSchema, { block: "defaultBranch" })?.defaultBranch;
|
|
7365
|
+
if (configured === void 0 || configured === "") return DEFAULT_BRANCH;
|
|
7366
|
+
return configured;
|
|
7367
|
+
}
|
|
7368
|
+
function readConfiguredDefaultBranch(dir) {
|
|
7369
|
+
const configPath = path.join(dir, ".tooling.json");
|
|
7370
|
+
if (!existsSync(configPath)) return DEFAULT_BRANCH;
|
|
7371
|
+
let raw;
|
|
7372
|
+
try {
|
|
7373
|
+
raw = readFileSync(configPath, "utf-8");
|
|
7374
|
+
} catch (error) {
|
|
7375
|
+
throw new FatalError(`Cannot read ${configPath}: ${describeError$1(error)}`, { cause: error });
|
|
7376
|
+
}
|
|
7377
|
+
return configuredDefaultBranch(raw);
|
|
7378
|
+
}
|
|
7379
|
+
function processCiEventSource() {
|
|
7380
|
+
return {
|
|
7381
|
+
env: process.env,
|
|
7382
|
+
readFile: (absolutePath) => {
|
|
7383
|
+
try {
|
|
7384
|
+
return readFileSync(absolutePath, "utf-8");
|
|
7385
|
+
} catch (_error) {
|
|
7386
|
+
return;
|
|
7387
|
+
}
|
|
7388
|
+
}
|
|
7389
|
+
};
|
|
7390
|
+
}
|
|
7391
|
+
const CiEventSchema = z.object({ repository: z.object({ default_branch: z.string().min(1) }) });
|
|
7392
|
+
function ciDefaultBranch(source) {
|
|
7393
|
+
if (source === void 0 || source.env["GITHUB_ACTIONS"] !== "true") return void 0;
|
|
7394
|
+
const eventPath = source.env["GITHUB_EVENT_PATH"];
|
|
7395
|
+
if (eventPath === void 0 || eventPath === "") return void 0;
|
|
7396
|
+
const raw = source.readFile(eventPath);
|
|
7397
|
+
if (raw === void 0 || raw.trim() === "") return void 0;
|
|
7398
|
+
let json;
|
|
7399
|
+
try {
|
|
7400
|
+
json = JSON.parse(raw);
|
|
7401
|
+
} catch (_error) {
|
|
7402
|
+
return;
|
|
7403
|
+
}
|
|
7404
|
+
const result = CiEventSchema.safeParse(json);
|
|
7405
|
+
return result.success ? result.data.repository.default_branch : void 0;
|
|
7406
|
+
}
|
|
7407
|
+
function refuseForgeDefaultMismatch(configured, source, command) {
|
|
7408
|
+
const reported = ciDefaultBranch(source);
|
|
7409
|
+
if (reported === void 0 || reported === configured) return;
|
|
7410
|
+
throw new FatalError(`${command}: .tooling.json#defaultBranch is "${configured}", but the forge reports "${reported}" as this repository's default branch. Set .tooling.json#defaultBranch to "${reported}" and run bst repo:sync.`);
|
|
7411
|
+
}
|
|
7412
|
+
//#endregion
|
|
7335
7413
|
//#region src/generators/release-ci.ts
|
|
7336
7414
|
const DEFAULT_POST_RELEASE_COMMAND = [
|
|
7337
7415
|
"pnpm exec bst webhook:send",
|
|
@@ -7351,7 +7429,7 @@ function hookSecretEnv(secrets) {
|
|
|
7351
7429
|
};
|
|
7352
7430
|
}
|
|
7353
7431
|
function postReleaseHookStep(opts) {
|
|
7354
|
-
const ifCondition = opts?.condition ?? (opts?.guardMain ?
|
|
7432
|
+
const ifCondition = opts?.condition ?? (opts?.guardMain ? isDefaultBranch(opts.defaultBranch) : void 0);
|
|
7355
7433
|
return { step: {
|
|
7356
7434
|
name: "Post-release hook",
|
|
7357
7435
|
...ifCondition && { if: ifCondition },
|
|
@@ -7362,7 +7440,7 @@ function postReleaseHookStep(opts) {
|
|
|
7362
7440
|
function postReleaseHookJob(opts) {
|
|
7363
7441
|
return {
|
|
7364
7442
|
needs: opts.needsJob,
|
|
7365
|
-
if: onlyOnDefaultBranch(opts.publishCondition),
|
|
7443
|
+
if: onlyOnDefaultBranch(opts.defaultBranch, opts.publishCondition),
|
|
7366
7444
|
"continue-on-error": true,
|
|
7367
7445
|
steps: [...commonSteps(opts.ci), { step: {
|
|
7368
7446
|
name: "Post-release hook",
|
|
@@ -7506,6 +7584,7 @@ function releaseWorkflowPath(ci) {
|
|
|
7506
7584
|
async function generateReleaseCi(ctx) {
|
|
7507
7585
|
const filePath = "release-ci";
|
|
7508
7586
|
const isGitHub = ctx.config.ci === "github";
|
|
7587
|
+
const defaultBranch = configuredDefaultBranch(ctx.read(".tooling.json"));
|
|
7509
7588
|
if (ctx.config.ci === "none" || ctx.config.releaseStrategy === "none") return {
|
|
7510
7589
|
filePath,
|
|
7511
7590
|
action: "skipped",
|
|
@@ -7549,6 +7628,7 @@ async function generateReleaseCi(ctx) {
|
|
|
7549
7628
|
let extraJobs;
|
|
7550
7629
|
let jobOutputs;
|
|
7551
7630
|
if (ctx.config.releaseStrategy === "release-it") steps.push(postReleaseHookStep({
|
|
7631
|
+
defaultBranch,
|
|
7552
7632
|
command: getPostReleaseHookCommand(ctx),
|
|
7553
7633
|
secrets: hookSecrets
|
|
7554
7634
|
}));
|
|
@@ -7558,6 +7638,7 @@ async function generateReleaseCi(ctx) {
|
|
|
7558
7638
|
tag: actionsExpr("steps.release.outputs.tag")
|
|
7559
7639
|
};
|
|
7560
7640
|
extraJobs = { "post-release-hook": postReleaseHookJob({
|
|
7641
|
+
defaultBranch,
|
|
7561
7642
|
ci: ctx.config.ci,
|
|
7562
7643
|
needsJob: "release",
|
|
7563
7644
|
publishCondition: "needs.release.outputs.pushed == 'true'",
|
|
@@ -7573,7 +7654,7 @@ async function generateReleaseCi(ctx) {
|
|
|
7573
7654
|
on,
|
|
7574
7655
|
...isGitHub && { permissions: { contents: "write" } },
|
|
7575
7656
|
jobName: "release",
|
|
7576
|
-
jobIf:
|
|
7657
|
+
jobIf: isDefaultBranch(defaultBranch),
|
|
7577
7658
|
...jobOutputs && { jobOutputs },
|
|
7578
7659
|
steps,
|
|
7579
7660
|
...extraJobs && { extraJobs }
|
|
@@ -7604,7 +7685,9 @@ async function generateReleaseCi(ctx) {
|
|
|
7604
7685
|
}
|
|
7605
7686
|
//#endregion
|
|
7606
7687
|
//#region src/release/version-branch.ts
|
|
7607
|
-
|
|
7688
|
+
function versionPrBranch(defaultBranch) {
|
|
7689
|
+
return `changeset-release/${defaultBranch}`;
|
|
7690
|
+
}
|
|
7608
7691
|
//#endregion
|
|
7609
7692
|
//#region src/generators/ci.ts
|
|
7610
7693
|
function coverageHistorySteps() {
|
|
@@ -7638,7 +7721,7 @@ function healthSteps(includeHistory) {
|
|
|
7638
7721
|
run: "pnpm exec bst health:status --limit 10"
|
|
7639
7722
|
} }];
|
|
7640
7723
|
}
|
|
7641
|
-
function changesetsReleaseStep(ci, publishesNpm, npmAuth) {
|
|
7724
|
+
function changesetsReleaseStep(ci, publishesNpm, npmAuth, defaultBranch) {
|
|
7642
7725
|
if (ci === "github") {
|
|
7643
7726
|
const changesetsEnv = {
|
|
7644
7727
|
GITHUB_TOKEN: actionsExpr("secrets.RELEASE_TOKEN"),
|
|
@@ -7647,7 +7730,7 @@ function changesetsReleaseStep(ci, publishesNpm, npmAuth) {
|
|
|
7647
7730
|
return { step: {
|
|
7648
7731
|
id: "release",
|
|
7649
7732
|
uses: "changesets/action@v1",
|
|
7650
|
-
if:
|
|
7733
|
+
if: isDefaultBranch(defaultBranch),
|
|
7651
7734
|
with: {
|
|
7652
7735
|
publish: "pnpm changeset publish",
|
|
7653
7736
|
version: "pnpm changeset version"
|
|
@@ -7665,19 +7748,19 @@ function changesetsReleaseStep(ci, publishesNpm, npmAuth) {
|
|
|
7665
7748
|
return { step: {
|
|
7666
7749
|
id: "release",
|
|
7667
7750
|
name: "Release",
|
|
7668
|
-
if:
|
|
7751
|
+
if: isDefaultBranch(defaultBranch),
|
|
7669
7752
|
env: releaseEnv,
|
|
7670
7753
|
run: npmAuth.publishRun("pnpm exec bst release:changesets")
|
|
7671
7754
|
} };
|
|
7672
7755
|
}
|
|
7673
|
-
function ciConcurrency(ci) {
|
|
7756
|
+
function ciConcurrency(ci, defaultBranch) {
|
|
7674
7757
|
if (ci === "forgejo") return {
|
|
7675
|
-
group: `ci-${actionsExpr(`${
|
|
7676
|
-
"cancel-in-progress": actionsExpr(
|
|
7758
|
+
group: `ci-${actionsExpr(`${isDefaultBranch(defaultBranch)} && github.sha || github.ref`)}`,
|
|
7759
|
+
"cancel-in-progress": actionsExpr(isNotDefaultBranch(defaultBranch))
|
|
7677
7760
|
};
|
|
7678
7761
|
return {
|
|
7679
7762
|
group: `ci-${actionsExpr("github.ref")}`,
|
|
7680
|
-
"cancel-in-progress": actionsExpr(
|
|
7763
|
+
"cancel-in-progress": actionsExpr(isNotDefaultBranch(defaultBranch))
|
|
7681
7764
|
};
|
|
7682
7765
|
}
|
|
7683
7766
|
function checkSteps(ci, coverageEnabled, emitDockerSweepStep) {
|
|
@@ -7708,6 +7791,7 @@ async function generateCi(ctx) {
|
|
|
7708
7791
|
action: "skipped",
|
|
7709
7792
|
description: "CI workflow not requested"
|
|
7710
7793
|
};
|
|
7794
|
+
const defaultBranch = configuredDefaultBranch(ctx.read(".tooling.json"));
|
|
7711
7795
|
const isGitHub = ctx.config.ci === "github";
|
|
7712
7796
|
const isForgejo = !isGitHub;
|
|
7713
7797
|
const isChangesets = ctx.config.releaseStrategy === "changesets";
|
|
@@ -7722,7 +7806,7 @@ async function generateCi(ctx) {
|
|
|
7722
7806
|
const hasAssets = isChangesets && (hasReleaseAssets(ctx) || hasVersionStamp(ctx));
|
|
7723
7807
|
const host = hasDocker ? resolveGenTimeHost(ctx) : null;
|
|
7724
7808
|
const publishedIf = "steps.release.outputs.published == 'true'";
|
|
7725
|
-
const
|
|
7809
|
+
const publishedOnDefaultIf = onlyOnDefaultBranch(defaultBranch, publishedIf);
|
|
7726
7810
|
const githubChangesetsDeriveTagStep = { step: {
|
|
7727
7811
|
name: "Derive release tag",
|
|
7728
7812
|
id: "derive-tag",
|
|
@@ -7743,17 +7827,17 @@ async function generateCi(ctx) {
|
|
|
7743
7827
|
...checkSteps(ctx.config.ci, coverage.enabled, hasDockerPackages(ctx)),
|
|
7744
7828
|
...emitCoverageSteps ? coverageHistorySteps() : [],
|
|
7745
7829
|
...healthSteps(emitHealthHistory),
|
|
7746
|
-
...isChangesets ? [changesetsReleaseStep(ctx.config.ci, publishesNpm, npmAuth)] : [],
|
|
7830
|
+
...isChangesets ? [changesetsReleaseStep(ctx.config.ci, publishesNpm, npmAuth, defaultBranch)] : [],
|
|
7747
7831
|
...isChangesets && isGitHub ? [githubChangesetsDeriveTagStep] : [],
|
|
7748
7832
|
...hasAssets ? [{ step: {
|
|
7749
7833
|
name: "Upload Release Assets",
|
|
7750
|
-
if:
|
|
7834
|
+
if: publishedOnDefaultIf,
|
|
7751
7835
|
env: releaseEnv,
|
|
7752
7836
|
run: "pnpm exec bst release:assets"
|
|
7753
7837
|
} }] : [],
|
|
7754
7838
|
...hasDocker ? [{ step: {
|
|
7755
7839
|
name: "Publish Docker images",
|
|
7756
|
-
if:
|
|
7840
|
+
if: publishedOnDefaultIf,
|
|
7757
7841
|
env: dockerPublishEnv(host),
|
|
7758
7842
|
run: "pnpm exec bst docker:publish"
|
|
7759
7843
|
} }] : [],
|
|
@@ -7761,7 +7845,7 @@ async function generateCi(ctx) {
|
|
|
7761
7845
|
deploy,
|
|
7762
7846
|
packageName: ctx.config.name,
|
|
7763
7847
|
releaseEnv,
|
|
7764
|
-
condition:
|
|
7848
|
+
condition: publishedOnDefaultIf,
|
|
7765
7849
|
secrets: hookSecrets
|
|
7766
7850
|
})] : []
|
|
7767
7851
|
];
|
|
@@ -7773,6 +7857,7 @@ async function generateCi(ctx) {
|
|
|
7773
7857
|
tag: changesetsTagOutputExpr
|
|
7774
7858
|
};
|
|
7775
7859
|
extraJobs = { "post-release-hook": postReleaseHookJob({
|
|
7860
|
+
defaultBranch,
|
|
7776
7861
|
ci: ctx.config.ci,
|
|
7777
7862
|
needsJob: "check",
|
|
7778
7863
|
publishCondition: "needs.check.outputs.published == 'true'",
|
|
@@ -7781,7 +7866,7 @@ async function generateCi(ctx) {
|
|
|
7781
7866
|
secrets: hookSecrets
|
|
7782
7867
|
}) };
|
|
7783
7868
|
}
|
|
7784
|
-
const checkJobEnv = emitCoverageSteps || emitHealthHistory ?
|
|
7869
|
+
const checkJobEnv = emitCoverageSteps || emitHealthHistory ? defaultBranchJobEnv(defaultBranch) : {};
|
|
7785
7870
|
const checksSlice = parseToolingConfigSlice(ctx.read(".tooling.json"), z.object({ checks: ChecksConfigSchema.optional() }), { block: "checks" });
|
|
7786
7871
|
const skipOnVersionPr = isChangesets && checksSlice?.checks?.runOnVersionPr !== true;
|
|
7787
7872
|
const content = buildWorkflowYaml({
|
|
@@ -7789,16 +7874,16 @@ async function generateCi(ctx) {
|
|
|
7789
7874
|
name: "CI",
|
|
7790
7875
|
on: {
|
|
7791
7876
|
push: {
|
|
7792
|
-
branches: [
|
|
7877
|
+
branches: [defaultBranch],
|
|
7793
7878
|
"tags-ignore": ["**"]
|
|
7794
7879
|
},
|
|
7795
7880
|
pull_request: null
|
|
7796
7881
|
},
|
|
7797
7882
|
...isGitHub && (emitCoverageSteps || emitHealthHistory) && { permissions: { contents: "write" } },
|
|
7798
7883
|
enableEmailNotifications: isForgejo,
|
|
7799
|
-
...isChangesets && { concurrency: ciConcurrency(ctx.config.ci) },
|
|
7884
|
+
...isChangesets && { concurrency: ciConcurrency(ctx.config.ci, defaultBranch) },
|
|
7800
7885
|
jobName: "check",
|
|
7801
|
-
...skipOnVersionPr && { jobIf: `github.head_ref != '
|
|
7886
|
+
...skipOnVersionPr && { jobIf: `github.head_ref != '${versionPrBranch(defaultBranch)}'` },
|
|
7802
7887
|
...Object.keys(checkJobEnv).length > 0 && { jobEnv: checkJobEnv },
|
|
7803
7888
|
...jobOutputs && { jobOutputs },
|
|
7804
7889
|
steps,
|
|
@@ -8495,7 +8580,7 @@ async function generateReleaseIt(ctx) {
|
|
|
8495
8580
|
}
|
|
8496
8581
|
//#endregion
|
|
8497
8582
|
//#region src/generators/changesets.ts
|
|
8498
|
-
function buildConfig$2() {
|
|
8583
|
+
function buildConfig$2(defaultBranch) {
|
|
8499
8584
|
return {
|
|
8500
8585
|
$schema: "https://unpkg.com/@changesets/config@3.1.1/schema.json",
|
|
8501
8586
|
changelog: "@changesets/cli/changelog",
|
|
@@ -8503,7 +8588,7 @@ function buildConfig$2() {
|
|
|
8503
8588
|
fixed: [],
|
|
8504
8589
|
linked: [],
|
|
8505
8590
|
access: "public",
|
|
8506
|
-
baseBranch:
|
|
8591
|
+
baseBranch: defaultBranch,
|
|
8507
8592
|
updateInternalDependencies: "patch",
|
|
8508
8593
|
ignore: []
|
|
8509
8594
|
};
|
|
@@ -8515,7 +8600,7 @@ async function generateChangesets(ctx) {
|
|
|
8515
8600
|
action: "skipped",
|
|
8516
8601
|
description: "Changesets not requested"
|
|
8517
8602
|
};
|
|
8518
|
-
const content = JSON.stringify(buildConfig$2(), null, 2) + "\n";
|
|
8603
|
+
const content = JSON.stringify(buildConfig$2(configuredDefaultBranch(ctx.read(".tooling.json"))), null, 2) + "\n";
|
|
8519
8604
|
const existing = ctx.read(filePath);
|
|
8520
8605
|
if (existing) return {
|
|
8521
8606
|
filePath,
|
|
@@ -9143,6 +9228,7 @@ const AGENT_RULES_MID = [
|
|
|
9143
9228
|
const TS_EXTENSIONLESS_RULE = "- Prefer extensionless imports; if an extension is required, use `.ts` over `.js`.";
|
|
9144
9229
|
const CHANGESETS_RULE = "- A user-facing change owes a changeset in `.changeset/`, written with the change: `patch` for a fix, `minor` for a feature, naming every package it affects. One file per smallest unit of work.";
|
|
9145
9230
|
const WORK_STATUS_RULE = "- Before starting or resuming work, run `bst work:status`.";
|
|
9231
|
+
const WORK_START_RULE = "- Before starting or resuming work, run `bst work:start <n>`.";
|
|
9146
9232
|
const FIND_COMMAND_RULE = "- Find a command with `bst --help`, its flags with `bst <command> --help`, before guessing.";
|
|
9147
9233
|
const FORGEJO_CLI_RULE_SUFFIX = " For Forgejo API work, run `bst-fj --help`.";
|
|
9148
9234
|
const AGENT_CLI_RULE_SUFFIX = " For the agent's runs and transcripts, run `bst-agent --help`.";
|
|
@@ -9163,20 +9249,22 @@ function declaresAgentCli(packageJson) {
|
|
|
9163
9249
|
function findCommandRule(packageJson) {
|
|
9164
9250
|
return FIND_COMMAND_RULE + (declaresForgejoCli(packageJson) ? FORGEJO_CLI_RULE_SUFFIX : "") + (declaresAgentCli(packageJson) ? AGENT_CLI_RULE_SUFFIX : "");
|
|
9165
9251
|
}
|
|
9166
|
-
|
|
9167
|
-
|
|
9168
|
-
|
|
9169
|
-
|
|
9170
|
-
|
|
9171
|
-
|
|
9172
|
-
|
|
9173
|
-
|
|
9174
|
-
|
|
9175
|
-
|
|
9176
|
-
|
|
9177
|
-
|
|
9178
|
-
|
|
9179
|
-
|
|
9252
|
+
function agentRulesTail(agentWorked) {
|
|
9253
|
+
return [
|
|
9254
|
+
"- The bst-hook blocks a short list of dangerous commands (`.claude/hooks/bst-hook-guide.md` lists them); Claude Code's auto mode judges everything else.",
|
|
9255
|
+
"- Machine-reading a `bst` command? Pass `--json` after the command name (`bst <cmd> --json`, not `bst --json <cmd>`); capture `2>&1` and parse it as one JSON document, failures included as `{ok: false, error}`. `--verbose` breaks this.",
|
|
9256
|
+
agentWorked ? WORK_START_RULE : WORK_STATUS_RULE,
|
|
9257
|
+
FIND_COMMAND_RULE,
|
|
9258
|
+
"",
|
|
9259
|
+
"#### Git discipline",
|
|
9260
|
+
"",
|
|
9261
|
+
"**The branch you are on is the mode switch.** The bst-hook refuses any command that would move a protected branch, so on one you leave changes unstaged; on a feature branch you commit, push, and open the PR yourself. Pick the mode, announce it, and only ask when it's genuinely ambiguous.",
|
|
9262
|
+
"",
|
|
9263
|
+
"**Small change** -- a fix, a doc edit, a one-file tweak: work on the default branch and leave it unstaged. Don't commit; surface the commit message as text for the user to run.",
|
|
9264
|
+
"",
|
|
9265
|
+
"**Large effort** -- anything that warranted a written plan or spec (spec, then plan, then code, each approved before the next): start it in a worktree (see the Git guide), commit as you go, finish with `bst pr:create`. Push later commits with `bst pr:update`, which rewrites nothing unless given `--title`/`--body`. The one exception: a headless agent run (a spec, plan, execute, or review container) never starts a worktree -- it works and commits on the branch it was started on."
|
|
9266
|
+
];
|
|
9267
|
+
}
|
|
9180
9268
|
const COMMENTS_GUIDE_BODY = [
|
|
9181
9269
|
"# Comments",
|
|
9182
9270
|
"",
|
|
@@ -9215,7 +9303,11 @@ const GIT_GUIDE_BODY = [
|
|
|
9215
9303
|
"",
|
|
9216
9304
|
"## Claiming work by hand",
|
|
9217
9305
|
"",
|
|
9218
|
-
"If the bst agent drives this repo, claim an issue or pull request before you work it by hand, so the agent does not start a run beside you
|
|
9306
|
+
"If the bst agent drives this repo, claim an issue or pull request before you work it by hand, so the agent does not start a run beside you: run `bst work:start <n>`. It prints the same orientation `bst work:status` does and then puts `agent-flags/hand-authored` on the object. It claims only where the repo's default branch sets `agent.enabled: true`, and does nothing anywhere else. **Exit 1 means do not start**: a run is live on the pair, or the agent holds it -- the output names which. Exit 0 means the pair is yours, whether the command wrote the flag or found it there already.",
|
|
9307
|
+
"",
|
|
9308
|
+
"A pull request you open yourself that closes no claimed issue needs its own `bst work:start <n>` once it exists -- the flag on either half of a pair holds both, but there is no pair until something closes something.",
|
|
9309
|
+
"",
|
|
9310
|
+
"Hand the work back with the control panel's **Release to the agent**, or by removing `agent-flags/hand-authored` from both halves. Removing it does not queue anything: the agent picks the object up on its next cycle only if a label or a comment asks it to.",
|
|
9219
9311
|
"",
|
|
9220
9312
|
"## Issue linking",
|
|
9221
9313
|
"",
|
|
@@ -9596,13 +9688,13 @@ function guideIndex(guides) {
|
|
|
9596
9688
|
for (const guide of guides.filter((g) => g.enabled)) lines.push(`- Before you ${guide.trigger}: \`${GUIDES_DIR}/${guide.name}\`.`);
|
|
9597
9689
|
return lines.join("\n");
|
|
9598
9690
|
}
|
|
9599
|
-
function agentRulesBody(config, guides, packageJson) {
|
|
9691
|
+
function agentRulesBody(config, guides, packageJson, agentWorked) {
|
|
9600
9692
|
const lines = [...AGENT_RULES_HEAD];
|
|
9601
9693
|
if (config.stack === "typescript") lines.push(TS_TYPECAST_RULE);
|
|
9602
9694
|
lines.push(...AGENT_RULES_MID);
|
|
9603
9695
|
if (config.stack === "typescript") lines.push(TS_EXTENSIONLESS_RULE);
|
|
9604
9696
|
if (config.releaseStrategy === "changesets") lines.push(CHANGESETS_RULE);
|
|
9605
|
-
const tail =
|
|
9697
|
+
const tail = agentRulesTail(agentWorked).map((line) => line === FIND_COMMAND_RULE ? findCommandRule(packageJson) : line);
|
|
9606
9698
|
lines.push(...tail, "", guideIndex(guides));
|
|
9607
9699
|
return lines.join("\n");
|
|
9608
9700
|
}
|
|
@@ -9666,7 +9758,7 @@ function syncClaudeMdFile(ctx, guides) {
|
|
|
9666
9758
|
name: "agent-rules",
|
|
9667
9759
|
startMarker: "<!-- @tooling:agent-rules -->",
|
|
9668
9760
|
endMarker: "<!-- @tooling:endagent-rules -->",
|
|
9669
|
-
body: agentRulesBody(ctx.config, guides, ctx.packageJson),
|
|
9761
|
+
body: agentRulesBody(ctx.config, guides, ctx.packageJson, agentEnabled(ctx)),
|
|
9670
9762
|
enabled: true
|
|
9671
9763
|
}, ...RETIRED_BLOCK_NAMES.map((name) => ({
|
|
9672
9764
|
name,
|
|
@@ -9907,7 +9999,7 @@ function reportFailures(failed, command) {
|
|
|
9907
9999
|
function generateMigratePrompt(results, config, detected) {
|
|
9908
10000
|
const sections = [];
|
|
9909
10001
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
9910
|
-
sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.
|
|
10002
|
+
sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.91.0 repo:sync\` on ${timestamp}_\n`);
|
|
9911
10003
|
const changed = results.filter((r) => r.action === "created" || r.action === "updated" || r.action === "archived");
|
|
9912
10004
|
if (changed.length > 0) {
|
|
9913
10005
|
const items = changed.map((r) => {
|
|
@@ -10035,7 +10127,7 @@ function logDetectionSummary(ctx) {
|
|
|
10035
10127
|
log$1.info(`Release assets convention detected in ${label} -- emit build artifacts to ${CONVENTION_OUTPUT_DIR}/ (gitignored).`);
|
|
10036
10128
|
}
|
|
10037
10129
|
}
|
|
10038
|
-
async function
|
|
10130
|
+
async function runInitReport(config, options = {}) {
|
|
10039
10131
|
const detected = detectProject(config.targetDir);
|
|
10040
10132
|
const { ctx, archivedFiles, sawCrlf } = createContext(config, options.confirmOverwrite ?? (async (relativePath) => {
|
|
10041
10133
|
const result = await select({
|
|
@@ -10072,7 +10164,10 @@ async function runInit(config, options = {}) {
|
|
|
10072
10164
|
}
|
|
10073
10165
|
if (!hasChanges && options.noPrompt) {
|
|
10074
10166
|
if (failed.length === 0) log$1.success("Repository is up to date.");
|
|
10075
|
-
return
|
|
10167
|
+
return {
|
|
10168
|
+
results,
|
|
10169
|
+
install: "not-run"
|
|
10170
|
+
};
|
|
10076
10171
|
}
|
|
10077
10172
|
if (results.some((r) => r.action === "archived" && r.filePath.startsWith(".husky/"))) try {
|
|
10078
10173
|
execSync("git config --unset core.hooksPath", {
|
|
@@ -10091,16 +10186,19 @@ async function runInit(config, options = {}) {
|
|
|
10091
10186
|
if (!options.noPrompt) log$1.info(`Migration prompt written to ${promptPath}`);
|
|
10092
10187
|
const packageJsonChanged = results.some((r) => r.filePath === "package.json" && (r.action === "created" || r.action === "updated"));
|
|
10093
10188
|
const hasLockfile = ctx.exists("pnpm-lock.yaml");
|
|
10189
|
+
let install = "not-run";
|
|
10094
10190
|
if (packageJsonChanged && hasLockfile) {
|
|
10095
10191
|
log$1.info("package.json changed -- running pnpm install...");
|
|
10096
10192
|
try {
|
|
10097
10193
|
execSync("pnpm install", {
|
|
10098
10194
|
cwd: config.targetDir,
|
|
10099
|
-
stdio: "inherit",
|
|
10195
|
+
stdio: options.installOutput === "capture" ? "ignore" : "inherit",
|
|
10100
10196
|
timeout: 12e4
|
|
10101
10197
|
});
|
|
10198
|
+
install = "ran";
|
|
10102
10199
|
} catch (_error) {
|
|
10103
10200
|
log$1.warn("pnpm install failed -- run it manually");
|
|
10201
|
+
install = "failed";
|
|
10104
10202
|
}
|
|
10105
10203
|
}
|
|
10106
10204
|
if (hasChanges && ctx.exists("package.json")) try {
|
|
@@ -10115,7 +10213,13 @@ async function runInit(config, options = {}) {
|
|
|
10115
10213
|
"Run: pnpm check",
|
|
10116
10214
|
...!options.noPrompt ? ["Run: claude \"Execute the migration steps in .tooling-migrate.md\""] : []
|
|
10117
10215
|
].map((step, i) => `${i + 1}. ${step}`).join("\n"), "Next steps");
|
|
10118
|
-
return
|
|
10216
|
+
return {
|
|
10217
|
+
results,
|
|
10218
|
+
install
|
|
10219
|
+
};
|
|
10220
|
+
}
|
|
10221
|
+
async function runInit(config, options = {}) {
|
|
10222
|
+
return (await runInitReport(config, options)).results;
|
|
10119
10223
|
}
|
|
10120
10224
|
//#endregion
|
|
10121
10225
|
//#region src/docker-check/compose-lint.ts
|
|
@@ -10854,34 +10958,6 @@ function repoPrefix(executor, cwd) {
|
|
|
10854
10958
|
function ceilingFileNamesAt(prefix) {
|
|
10855
10959
|
return HEALTH_CEILING_FILES.map((name) => `${prefix}${name}`);
|
|
10856
10960
|
}
|
|
10857
|
-
function processCiEventSource() {
|
|
10858
|
-
return {
|
|
10859
|
-
env: process.env,
|
|
10860
|
-
readFile: (absolutePath) => {
|
|
10861
|
-
try {
|
|
10862
|
-
return readFileSync(absolutePath, "utf-8");
|
|
10863
|
-
} catch (_error) {
|
|
10864
|
-
return;
|
|
10865
|
-
}
|
|
10866
|
-
}
|
|
10867
|
-
};
|
|
10868
|
-
}
|
|
10869
|
-
const CiEventSchema = z.object({ repository: z.object({ default_branch: z.string().min(1) }) });
|
|
10870
|
-
function ciDefaultBranch(source) {
|
|
10871
|
-
if (source === void 0 || source.env["GITHUB_ACTIONS"] !== "true") return void 0;
|
|
10872
|
-
const eventPath = source.env["GITHUB_EVENT_PATH"];
|
|
10873
|
-
if (eventPath === void 0 || eventPath === "") return void 0;
|
|
10874
|
-
const raw = source.readFile(eventPath);
|
|
10875
|
-
if (raw === void 0 || raw.trim() === "") return void 0;
|
|
10876
|
-
let json;
|
|
10877
|
-
try {
|
|
10878
|
-
json = JSON.parse(raw);
|
|
10879
|
-
} catch (_error) {
|
|
10880
|
-
return;
|
|
10881
|
-
}
|
|
10882
|
-
const result = CiEventSchema.safeParse(json);
|
|
10883
|
-
return result.success ? result.data.repository.default_branch : void 0;
|
|
10884
|
-
}
|
|
10885
10961
|
const DefaultBranchSchema = z.object({ defaultBranch: z.string().optional() });
|
|
10886
10962
|
function parseDefaultBranchOverride(raw) {
|
|
10887
10963
|
if (raw === void 0 || raw.trim() === "") return void 0;
|
|
@@ -12036,7 +12112,7 @@ function findInertAgentKeys(saved) {
|
|
|
12036
12112
|
function runInertAgentSettingsGate(saved) {
|
|
12037
12113
|
const keys = findInertAgentKeys(saved);
|
|
12038
12114
|
if (keys.length === 0) return [];
|
|
12039
|
-
log$1.warn(`.tooling.json sets ${keys.map((key) => `agent.${key}`).join(", ")}, but agent.enabled is not true, so the agent ignores them.
|
|
12115
|
+
log$1.warn(`.tooling.json sets ${keys.map((key) => `agent.${key}`).join(", ")}, but agent.enabled is not true, so the agent ignores them. Run \`bst setup:agent\` to enable the agent, or remove those keys.`);
|
|
12040
12116
|
return keys;
|
|
12041
12117
|
}
|
|
12042
12118
|
function runPublishConfigGate(config, targetDir) {
|
|
@@ -12084,6 +12160,18 @@ function warnStaleChecksAdd(config) {
|
|
|
12084
12160
|
if (stale.length === 0) return;
|
|
12085
12161
|
log$1.warn(`.tooling.json#checks.add names built-in step(s) checks:run already runs: ${stale.join(", ")}. The entries are deduplicated at run time and do nothing -- remove them.`);
|
|
12086
12162
|
}
|
|
12163
|
+
function warnUnconfiguredDefaultBranch(targetDir, saved, git = defaultGitRunner) {
|
|
12164
|
+
if (saved?.defaultBranch !== void 0) return;
|
|
12165
|
+
const head = git([
|
|
12166
|
+
"symbolic-ref",
|
|
12167
|
+
"--short",
|
|
12168
|
+
"refs/remotes/origin/HEAD"
|
|
12169
|
+
], targetDir);
|
|
12170
|
+
if (head === void 0) return;
|
|
12171
|
+
const name = head.startsWith("origin/") ? head.slice(7) : head;
|
|
12172
|
+
if (name === "" || name === "main") return;
|
|
12173
|
+
log$1.warn(`origin/HEAD names "${name}", but .tooling.json sets no defaultBranch, so the generated CI workflow triggers on "${DEFAULT_BRANCH}" and gates its release steps there. Set .tooling.json#defaultBranch to "${name}" and run bst repo:sync.`);
|
|
12174
|
+
}
|
|
12087
12175
|
function warnCoveredChecksAdd(config, targetDir) {
|
|
12088
12176
|
const covered = coveredChecksAddEntries(config, readPackageJson(targetDir)?.scripts ?? {});
|
|
12089
12177
|
for (const entry of covered) log$1.warn(`.tooling.json#checks.add entry "${entry.name}" is already run by the "${entry.coveredBy}" step's script ("${entry.fragment}"), so every pipeline run executes it twice. Remove the add entry, or drop the inline invocation from the "${entry.coveredBy}" script.`);
|
|
@@ -12203,26 +12291,14 @@ function checkDeclaredToolsActive(targetDir, declared) {
|
|
|
12203
12291
|
}
|
|
12204
12292
|
function runToolchainGate(targetDir) {
|
|
12205
12293
|
const miseTomlPath = path.join(targetDir, "mise.toml");
|
|
12206
|
-
if (!existsSync(miseTomlPath)) return
|
|
12207
|
-
failures: 0,
|
|
12208
|
-
warned: false
|
|
12209
|
-
};
|
|
12294
|
+
if (!existsSync(miseTomlPath)) return 0;
|
|
12210
12295
|
if (miseIsAbsent(targetDir)) {
|
|
12211
|
-
log$1.
|
|
12212
|
-
return
|
|
12213
|
-
failures: 0,
|
|
12214
|
-
warned: true
|
|
12215
|
-
};
|
|
12296
|
+
log$1.error(`mise.toml declares this repo's toolchain, but mise is not on PATH. Install it (${MISE_INSTALL_URL}), then add \`eval "$(mise activate bash)"\` to ~/.bashrc for interactive shells and \`eval "$(mise activate bash --shims)"\` to ~/.profile, so editors, git hooks, and AI sessions get the same tools. If this repo's mise.toml has an [env] block, also add the directory your checkouts live in to \`trusted_config_paths\` in ~/.config/mise/config.toml.`);
|
|
12297
|
+
return 1;
|
|
12216
12298
|
}
|
|
12217
12299
|
const declared = readDeclaredTools(miseTomlPath);
|
|
12218
|
-
if (!declared) return
|
|
12219
|
-
|
|
12220
|
-
warned: false
|
|
12221
|
-
};
|
|
12222
|
-
return {
|
|
12223
|
-
failures: checkDeclaredToolsActive(targetDir, declared),
|
|
12224
|
-
warned: false
|
|
12225
|
-
};
|
|
12300
|
+
if (!declared) return 0;
|
|
12301
|
+
return checkDeclaredToolsActive(targetDir, declared);
|
|
12226
12302
|
}
|
|
12227
12303
|
//#endregion
|
|
12228
12304
|
//#region src/commands/repo-sync.ts
|
|
@@ -12264,7 +12340,7 @@ const syncCommand = defineCommand({
|
|
|
12264
12340
|
},
|
|
12265
12341
|
json: {
|
|
12266
12342
|
type: "boolean",
|
|
12267
|
-
description: "Emit one JSON document: status (up-to-date | drift | workflow-drift | failed), actionable, failed,
|
|
12343
|
+
description: "Emit one JSON document: status (up-to-date | drift | workflow-drift | failed), actionable, failed, checkOnlyImages, aptSteps, inertAgentKeys, warnCount, exitCode. --check only: the write mode prompts and inherits child streams."
|
|
12268
12344
|
}
|
|
12269
12345
|
},
|
|
12270
12346
|
async run({ args }) {
|
|
@@ -12302,40 +12378,59 @@ const syncCommand = defineCommand({
|
|
|
12302
12378
|
})).length > 0) process.exitCode = 1;
|
|
12303
12379
|
}
|
|
12304
12380
|
});
|
|
12305
|
-
async function
|
|
12381
|
+
async function runUpdateReport(targetDir, flags = {}, options = {}) {
|
|
12306
12382
|
const dockerDiscovery = createDockerDiscoveryMemo();
|
|
12307
12383
|
const saved = loadToolingConfigStrict(targetDir);
|
|
12308
12384
|
if (!saved) {
|
|
12309
12385
|
log$1.error("No .tooling.json found. Run `bst repo:sync` first to initialize the project.");
|
|
12310
12386
|
process.exitCode = 1;
|
|
12311
|
-
return
|
|
12387
|
+
return {
|
|
12388
|
+
status: "stopped",
|
|
12389
|
+
results: [],
|
|
12390
|
+
install: "not-run"
|
|
12391
|
+
};
|
|
12312
12392
|
}
|
|
12313
12393
|
warnStaleChecksAdd(saved);
|
|
12394
|
+
warnUnconfiguredDefaultBranch(targetDir, saved);
|
|
12314
12395
|
warnCoveredChecksAdd(saved, targetDir);
|
|
12315
12396
|
runInertAgentSettingsGate(saved);
|
|
12316
12397
|
if (runComposeLintGate(targetDir, saved, dockerDiscovery) > 0) {
|
|
12317
12398
|
process.exitCode = 1;
|
|
12318
|
-
return
|
|
12399
|
+
return {
|
|
12400
|
+
status: "stopped",
|
|
12401
|
+
results: [],
|
|
12402
|
+
install: "not-run"
|
|
12403
|
+
};
|
|
12319
12404
|
}
|
|
12320
12405
|
const config = mergeWithSavedConfig(buildDefaultConfig(targetDir, flags), saved);
|
|
12321
12406
|
runReleaseTargetGate(config, targetDir);
|
|
12322
12407
|
runVersionStampGate(saved, targetDir, config.releaseStrategy);
|
|
12323
12408
|
if (runPublishConfigGate(config, targetDir) > 0) {
|
|
12324
12409
|
process.exitCode = 1;
|
|
12325
|
-
return
|
|
12410
|
+
return {
|
|
12411
|
+
status: "stopped",
|
|
12412
|
+
results: [],
|
|
12413
|
+
install: "not-run"
|
|
12414
|
+
};
|
|
12326
12415
|
}
|
|
12327
|
-
return
|
|
12328
|
-
|
|
12329
|
-
|
|
12330
|
-
|
|
12331
|
-
|
|
12416
|
+
return {
|
|
12417
|
+
status: "ran",
|
|
12418
|
+
...await runInitReport(config, {
|
|
12419
|
+
noPrompt: true,
|
|
12420
|
+
confirmOverwrite: async () => "overwrite",
|
|
12421
|
+
dockerDiscovery,
|
|
12422
|
+
installOutput: options.installOutput
|
|
12423
|
+
})
|
|
12424
|
+
};
|
|
12332
12425
|
}
|
|
12333
|
-
function
|
|
12426
|
+
async function runUpdate(targetDir, flags = {}) {
|
|
12427
|
+
return (await runUpdateReport(targetDir, flags)).results;
|
|
12428
|
+
}
|
|
12429
|
+
function gateFailure() {
|
|
12334
12430
|
return {
|
|
12335
12431
|
status: "failed",
|
|
12336
12432
|
actionable: [],
|
|
12337
12433
|
failed: [],
|
|
12338
|
-
toolchain: { warned: toolchainWarned },
|
|
12339
12434
|
checkOnlyImages: [],
|
|
12340
12435
|
aptSteps: [],
|
|
12341
12436
|
inertAgentKeys: [],
|
|
@@ -12344,10 +12439,9 @@ function gateFailure(toolchainWarned = false) {
|
|
|
12344
12439
|
};
|
|
12345
12440
|
}
|
|
12346
12441
|
function advisoryWarnCount(advisories) {
|
|
12347
|
-
return
|
|
12442
|
+
return advisories.checkOnlyImages.length + advisories.aptSteps.length + (advisories.inertAgentKeys.length > 0 ? 1 : 0);
|
|
12348
12443
|
}
|
|
12349
12444
|
function upToDateReport(advisories) {
|
|
12350
|
-
const toolchain = { warned: advisories.toolchainWarned };
|
|
12351
12445
|
const warnCount = advisoryWarnCount(advisories);
|
|
12352
12446
|
if (warnCount > 0) {
|
|
12353
12447
|
if (!isJsonMode()) rawLog$1(formatWarnMarker(warnCount));
|
|
@@ -12356,7 +12450,6 @@ function upToDateReport(advisories) {
|
|
|
12356
12450
|
status: "up-to-date",
|
|
12357
12451
|
actionable: [],
|
|
12358
12452
|
failed: [],
|
|
12359
|
-
toolchain,
|
|
12360
12453
|
checkOnlyImages: advisories.checkOnlyImages,
|
|
12361
12454
|
aptSteps: advisories.aptSteps,
|
|
12362
12455
|
inertAgentKeys: advisories.inertAgentKeys,
|
|
@@ -12367,6 +12460,7 @@ function upToDateReport(advisories) {
|
|
|
12367
12460
|
async function runCheckReport(targetDir) {
|
|
12368
12461
|
const dockerDiscovery = createDockerDiscoveryMemo();
|
|
12369
12462
|
const saved = loadToolingConfigStrict(targetDir);
|
|
12463
|
+
warnUnconfiguredDefaultBranch(targetDir, saved);
|
|
12370
12464
|
if (saved) {
|
|
12371
12465
|
warnStaleChecksAdd(saved);
|
|
12372
12466
|
warnCoveredChecksAdd(saved, targetDir);
|
|
@@ -12377,13 +12471,8 @@ async function runCheckReport(targetDir) {
|
|
|
12377
12471
|
if (runReleaseTargetGate(config, targetDir)) return gateFailure();
|
|
12378
12472
|
if (runVersionStampGate(saved, targetDir, config.releaseStrategy) > 0) return gateFailure();
|
|
12379
12473
|
if (runPublishConfigGate(config, targetDir) > 0) return gateFailure();
|
|
12380
|
-
|
|
12381
|
-
if (toolchain.failures > 0) return {
|
|
12382
|
-
...gateFailure(),
|
|
12383
|
-
toolchain: { warned: toolchain.warned }
|
|
12384
|
-
};
|
|
12474
|
+
if (runToolchainGate(targetDir) > 0) return gateFailure();
|
|
12385
12475
|
const advisories = {
|
|
12386
|
-
toolchainWarned: toolchain.warned,
|
|
12387
12476
|
checkOnlyImages: runCheckOnlyImageGate(config, targetDir, dockerDiscovery),
|
|
12388
12477
|
aptSteps: runWorkflowAptGate(targetDir),
|
|
12389
12478
|
inertAgentKeys: runInertAgentSettingsGate(saved)
|
|
@@ -12395,13 +12484,11 @@ async function runCheckReport(targetDir) {
|
|
|
12395
12484
|
reportFailures(failed, "repo:sync");
|
|
12396
12485
|
const actionable = actionableResults(results, pendingWrites, targetDir);
|
|
12397
12486
|
const advisoryCount = advisoryWarnCount(advisories);
|
|
12398
|
-
const toolchainState = { warned: advisories.toolchainWarned };
|
|
12399
12487
|
if (actionable.length === 0 && failed.length === 0) return upToDateReport(advisories);
|
|
12400
12488
|
if (actionable.length === 0) return {
|
|
12401
12489
|
status: "failed",
|
|
12402
12490
|
actionable: [],
|
|
12403
12491
|
failed,
|
|
12404
|
-
toolchain: toolchainState,
|
|
12405
12492
|
checkOnlyImages: [],
|
|
12406
12493
|
aptSteps: [],
|
|
12407
12494
|
inertAgentKeys: [],
|
|
@@ -12417,7 +12504,6 @@ async function runCheckReport(targetDir) {
|
|
|
12417
12504
|
status: "workflow-drift",
|
|
12418
12505
|
actionable,
|
|
12419
12506
|
failed: [],
|
|
12420
|
-
toolchain: toolchainState,
|
|
12421
12507
|
checkOnlyImages: advisories.checkOnlyImages,
|
|
12422
12508
|
aptSteps: advisories.aptSteps,
|
|
12423
12509
|
inertAgentKeys: advisories.inertAgentKeys,
|
|
@@ -12429,7 +12515,6 @@ async function runCheckReport(targetDir) {
|
|
|
12429
12515
|
status: "drift",
|
|
12430
12516
|
actionable,
|
|
12431
12517
|
failed,
|
|
12432
|
-
toolchain: toolchainState,
|
|
12433
12518
|
checkOnlyImages: [],
|
|
12434
12519
|
aptSteps: [],
|
|
12435
12520
|
inertAgentKeys: [],
|
|
@@ -12789,8 +12874,9 @@ function childOutput(result) {
|
|
|
12789
12874
|
}
|
|
12790
12875
|
//#endregion
|
|
12791
12876
|
//#region src/release/publish/version.ts
|
|
12792
|
-
|
|
12793
|
-
|
|
12877
|
+
function defaultBranchRef(config) {
|
|
12878
|
+
return `refs/heads/${config.defaultBranch}`;
|
|
12879
|
+
}
|
|
12794
12880
|
function readRemoteRef(executor, config, auth, ref) {
|
|
12795
12881
|
const result = executor.exec("git", [
|
|
12796
12882
|
...auth.args,
|
|
@@ -12802,13 +12888,13 @@ function readRemoteRef(executor, config, auth, ref) {
|
|
|
12802
12888
|
return result.stdout.split("\n").find((entry) => entry.trim().endsWith(`\t${ref}`))?.split(" ")[0]?.trim() ?? "";
|
|
12803
12889
|
}
|
|
12804
12890
|
function isSuperseded(executor, config, auth, baseSha) {
|
|
12805
|
-
const
|
|
12806
|
-
if (
|
|
12807
|
-
debug(config,
|
|
12891
|
+
const remoteDefault = readRemoteRef(executor, config, auth, defaultBranchRef(config));
|
|
12892
|
+
if (remoteDefault === null || remoteDefault === "" || baseSha === "") {
|
|
12893
|
+
debug(config, `Supersession check: ${config.defaultBranch} tip or base commit unreadable -- proceeding`);
|
|
12808
12894
|
return false;
|
|
12809
12895
|
}
|
|
12810
|
-
if (
|
|
12811
|
-
log$1.warn(`Superseded release run: this job versioned ${baseSha}, but
|
|
12896
|
+
if (remoteDefault === baseSha) return false;
|
|
12897
|
+
log$1.warn(`Superseded release run: this job versioned ${baseSha}, but ${config.defaultBranch} has advanced to ${remoteDefault}. A newer run is versioning a superset of these changesets -- skipping to avoid overwriting its version commit on ${versionPrBranch(config.defaultBranch)}.`);
|
|
12812
12898
|
return true;
|
|
12813
12899
|
}
|
|
12814
12900
|
const DATELESS_VERSION_RE = /^## (\d+\.\d+\.\d+(?:-[^\s)]+)?)$/gm;
|
|
@@ -12895,12 +12981,14 @@ function buildPrContent(executor, cwd, packagesBefore) {
|
|
|
12895
12981
|
async function runVersionMode(executor, config) {
|
|
12896
12982
|
log$1.info("Changesets detected -- versioning packages");
|
|
12897
12983
|
const auth = buildGitAuth(config);
|
|
12984
|
+
const branch = versionPrBranch(config.defaultBranch);
|
|
12985
|
+
const branchRef = `refs/heads/${branch}`;
|
|
12898
12986
|
const baseShaResult = executor.exec("git", ["rev-parse", "HEAD"], { cwd: config.cwd });
|
|
12899
12987
|
const baseSha = baseShaResult.exitCode === 0 ? baseShaResult.stdout.trim() : "";
|
|
12900
|
-
const leaseTip = config.dryRun ? null : readRemoteRef(executor, config, auth,
|
|
12901
|
-
if (leaseTip === null && !config.dryRun) throw new FatalError(`Could not read the current tip of ${
|
|
12988
|
+
const leaseTip = config.dryRun ? null : readRemoteRef(executor, config, auth, branchRef);
|
|
12989
|
+
if (leaseTip === null && !config.dryRun) throw new FatalError(`Could not read the current tip of ${branch} from the remote, so this run cannot take a push lease. Refusing to force-push without one -- an unleased push is what allows a concurrent run's version commit to be silently discarded.`);
|
|
12902
12990
|
debug(config, `Base commit: ${baseSha || "(unknown)"}`);
|
|
12903
|
-
debug(config, `Lease tip for ${
|
|
12991
|
+
debug(config, `Lease tip for ${branch}: ${leaseTip === null ? "(dry-run)" : leaseTip || "(absent)"}`);
|
|
12904
12992
|
const packagesBefore = executor.listWorkspacePackages(config.cwd);
|
|
12905
12993
|
debug(config, `Packages before versioning: ${packagesBefore.map((pkg) => `${pkg.name}@${pkg.version}`).join(", ") || "(none)"}`);
|
|
12906
12994
|
const changesetConfigPath = path.join(config.cwd, ".changeset", "config.json");
|
|
@@ -12962,28 +13050,28 @@ async function runVersionMode(executor, config) {
|
|
|
12962
13050
|
pr: "none"
|
|
12963
13051
|
};
|
|
12964
13052
|
if (leaseTip === null) throw new UnexpectedError("Push lease was never observed on a non-dry-run release");
|
|
12965
|
-
debug(config, `Force-pushing version commit to ${
|
|
13053
|
+
debug(config, `Force-pushing version commit to ${branch} (lease: ${leaseTip || "(absent)"})`);
|
|
12966
13054
|
const pushResult = executor.exec("git", [
|
|
12967
13055
|
...auth.args,
|
|
12968
13056
|
"push",
|
|
12969
13057
|
auth.url,
|
|
12970
|
-
`HEAD:${
|
|
12971
|
-
`--force-with-lease=${
|
|
13058
|
+
`HEAD:${branchRef}`,
|
|
13059
|
+
`--force-with-lease=${branchRef}:${leaseTip}`
|
|
12972
13060
|
], { cwd: config.cwd });
|
|
12973
13061
|
debugExec(config, "git push", pushResult);
|
|
12974
|
-
if (pushResult.exitCode !== 0) throw new FatalError(`git push to ${
|
|
13062
|
+
if (pushResult.exitCode !== 0) throw new FatalError(`git push to ${branch} failed (exit code ${String(pushResult.exitCode)}). If the lease was rejected, the branch moved after this run started (expected ${leaseTip || "the branch to be absent"}) -- another run pushed a version commit, and overwriting it would discard that release. This job fails rather than clobber it.\n${pushResult.stderr || pushResult.stdout}`);
|
|
12975
13063
|
const conn = {
|
|
12976
13064
|
serverUrl: config.serverUrl,
|
|
12977
13065
|
repository: config.repository,
|
|
12978
13066
|
token: config.token
|
|
12979
13067
|
};
|
|
12980
|
-
const existingPr = await findOpenPr(executor, conn,
|
|
12981
|
-
debug(config, `Existing open PR for ${
|
|
13068
|
+
const existingPr = await findOpenPr(executor, conn, branch);
|
|
13069
|
+
debug(config, `Existing open PR for ${branch}: ${existingPr === null ? "(none)" : `#${String(existingPr)}`}`);
|
|
12982
13070
|
if (existingPr === null) try {
|
|
12983
13071
|
await createPr(executor, conn, {
|
|
12984
13072
|
title,
|
|
12985
|
-
head:
|
|
12986
|
-
base:
|
|
13073
|
+
head: branch,
|
|
13074
|
+
base: config.defaultBranch,
|
|
12987
13075
|
body
|
|
12988
13076
|
});
|
|
12989
13077
|
log$1.info("Created version PR");
|
|
@@ -12993,7 +13081,7 @@ async function runVersionMode(executor, config) {
|
|
|
12993
13081
|
};
|
|
12994
13082
|
} catch (error) {
|
|
12995
13083
|
debug(config, `createPr failed, retrying as update: ${describeError$1(error)}`);
|
|
12996
|
-
const racePr = await findOpenPr(executor, conn,
|
|
13084
|
+
const racePr = await findOpenPr(executor, conn, branch);
|
|
12997
13085
|
if (racePr === null) throw error;
|
|
12998
13086
|
await updatePr(executor, conn, racePr, {
|
|
12999
13087
|
title,
|
|
@@ -13478,7 +13566,7 @@ const releaseForgejoCommand = defineCommand({
|
|
|
13478
13566
|
const result = await runRelease(buildReleaseConfig({
|
|
13479
13567
|
dryRun,
|
|
13480
13568
|
verbose: args.verbose === true || isEnvVerbose()
|
|
13481
|
-
}), createRealExecutor$1());
|
|
13569
|
+
}), createRealExecutor$1(), processCiEventSource());
|
|
13482
13570
|
out.json(releasePayload(result, dryRun));
|
|
13483
13571
|
if (result.mode === "publish") {
|
|
13484
13572
|
const outputFile = process.env["GITHUB_OUTPUT"];
|
|
@@ -13519,6 +13607,7 @@ function buildReleaseConfig(flags) {
|
|
|
13519
13607
|
return {
|
|
13520
13608
|
...resolved.conn,
|
|
13521
13609
|
cwd: process.cwd(),
|
|
13610
|
+
defaultBranch: readConfiguredDefaultBranch(process.cwd()),
|
|
13522
13611
|
dryRun: flags.dryRun ?? false,
|
|
13523
13612
|
verbose: flags.verbose ?? false
|
|
13524
13613
|
};
|
|
@@ -13532,15 +13621,16 @@ function getCurrentBranch(executor, cwd) {
|
|
|
13532
13621
|
"HEAD"
|
|
13533
13622
|
], { cwd }).stdout.trim();
|
|
13534
13623
|
}
|
|
13535
|
-
async function runRelease(config, executor) {
|
|
13624
|
+
async function runRelease(config, executor, ciEvent) {
|
|
13536
13625
|
debug(config, `Repository: ${config.repository} on ${config.serverUrl}`);
|
|
13537
13626
|
debug(config, `Config: dryRun=${String(config.dryRun)}`);
|
|
13538
13627
|
const branch = getCurrentBranch(executor, config.cwd);
|
|
13539
13628
|
debug(config, `Current branch: ${branch}`);
|
|
13540
|
-
if (branch !==
|
|
13541
|
-
debug(config, `Skipping release
|
|
13629
|
+
if (branch !== config.defaultBranch) {
|
|
13630
|
+
debug(config, `Skipping release: ${branch} is not the configured default branch (${config.defaultBranch})`);
|
|
13542
13631
|
return { mode: "none" };
|
|
13543
13632
|
}
|
|
13633
|
+
refuseForgeDefaultMismatch(config.defaultBranch, ciEvent, "release:changesets");
|
|
13544
13634
|
const identity = await configureGitIdentity(executor, "forgejo", config.cwd, config);
|
|
13545
13635
|
log$1.info(`Git identity: ${identity.name} <${identity.email}>`);
|
|
13546
13636
|
const changesetFiles = executor.listChangesetFiles(config.cwd);
|
|
@@ -13742,7 +13832,7 @@ const releaseTriggerCommand = defineCommand({
|
|
|
13742
13832
|
args: {
|
|
13743
13833
|
ref: {
|
|
13744
13834
|
type: "string",
|
|
13745
|
-
description: "Git ref to trigger on (default: main)",
|
|
13835
|
+
description: "Git ref to trigger on (default: .tooling.json#defaultBranch, else main)",
|
|
13746
13836
|
required: false
|
|
13747
13837
|
},
|
|
13748
13838
|
bump: {
|
|
@@ -13762,7 +13852,7 @@ const releaseTriggerCommand = defineCommand({
|
|
|
13762
13852
|
},
|
|
13763
13853
|
async run({ args }) {
|
|
13764
13854
|
const out = createCliOutput(args.json === true);
|
|
13765
|
-
const ref = args.ref ??
|
|
13855
|
+
const ref = args.ref ?? readConfiguredDefaultBranch(process.cwd());
|
|
13766
13856
|
const inputs = {};
|
|
13767
13857
|
if (args.bump) inputs["bump"] = args.bump;
|
|
13768
13858
|
if (args.prerelease) inputs["prerelease"] = args.prerelease;
|
|
@@ -13779,6 +13869,104 @@ const releaseTriggerCommand = defineCommand({
|
|
|
13779
13869
|
}
|
|
13780
13870
|
});
|
|
13781
13871
|
//#endregion
|
|
13872
|
+
//#region ../forgejo/src/label-provisioning.ts
|
|
13873
|
+
const LabelSchema$1 = z.object({
|
|
13874
|
+
id: z.number(),
|
|
13875
|
+
name: z.string(),
|
|
13876
|
+
color: z.string().optional(),
|
|
13877
|
+
description: z.string().optional(),
|
|
13878
|
+
exclusive: z.boolean().optional()
|
|
13879
|
+
});
|
|
13880
|
+
const LabelListSchema = z.array(LabelSchema$1);
|
|
13881
|
+
const LABELS_PAGE_SIZE = 100;
|
|
13882
|
+
const LABELS_MAX_PAGES = 20;
|
|
13883
|
+
async function fetchAllForgejoLabels(executor, conn) {
|
|
13884
|
+
const labels = [];
|
|
13885
|
+
for (let page = 1; page <= LABELS_MAX_PAGES; page++) {
|
|
13886
|
+
const res = await executor.fetch(`${conn.serverUrl}/api/v1/repos/${conn.repository}/labels?limit=${String(LABELS_PAGE_SIZE)}&page=${String(page)}`, { headers: authHeaders$2(conn) });
|
|
13887
|
+
if (!res.ok) await throwForgejoFailure(res, {
|
|
13888
|
+
verb: "list labels",
|
|
13889
|
+
resource: conn.repository
|
|
13890
|
+
}, repoNotFoundMessage(conn));
|
|
13891
|
+
const parsed = LabelListSchema.safeParse(await res.json());
|
|
13892
|
+
if (!parsed.success) throw new UnexpectedError(`Unexpected label-list response: ${parsed.error.message}`);
|
|
13893
|
+
labels.push(...parsed.data);
|
|
13894
|
+
if (parsed.data.length < LABELS_PAGE_SIZE) break;
|
|
13895
|
+
}
|
|
13896
|
+
return labels;
|
|
13897
|
+
}
|
|
13898
|
+
async function resolveLabelIds(executor, conn, names) {
|
|
13899
|
+
if (names.length === 0) return [];
|
|
13900
|
+
const labels = await fetchAllForgejoLabels(executor, conn);
|
|
13901
|
+
return names.map((name) => {
|
|
13902
|
+
const match = labels.find((label) => label.name === name);
|
|
13903
|
+
if (match === void 0) {
|
|
13904
|
+
const known = labels.map((label) => label.name).join(", ");
|
|
13905
|
+
throw new FatalError(`No label named "${name}" found among the repository's labels in ${conn.repository} (organization-level labels are not searched). Known labels: ${known}`);
|
|
13906
|
+
}
|
|
13907
|
+
return match.id;
|
|
13908
|
+
});
|
|
13909
|
+
}
|
|
13910
|
+
const LabelSpecSchema = z.strictObject({
|
|
13911
|
+
name: z.string().min(1),
|
|
13912
|
+
color: z.string().regex(/^#[0-9a-fA-F]{6}$/),
|
|
13913
|
+
description: z.string(),
|
|
13914
|
+
exclusive: z.boolean().optional()
|
|
13915
|
+
});
|
|
13916
|
+
z.record(z.string(), z.array(LabelSpecSchema));
|
|
13917
|
+
//#endregion
|
|
13918
|
+
//#region ../forgejo/src/issue-labels.ts
|
|
13919
|
+
const TimelineEntrySchema = z.object({
|
|
13920
|
+
id: z.number(),
|
|
13921
|
+
type: z.string(),
|
|
13922
|
+
body: z.string().nullish(),
|
|
13923
|
+
created_at: z.string(),
|
|
13924
|
+
label: z.object({ name: z.string() }).nullish(),
|
|
13925
|
+
user: z.object({ login: z.string() }).nullish()
|
|
13926
|
+
});
|
|
13927
|
+
z.array(TimelineEntrySchema).nullable();
|
|
13928
|
+
async function resolveLabelId(executor, conn, name, labelIds) {
|
|
13929
|
+
const cached = labelIds?.get(name);
|
|
13930
|
+
if (cached !== void 0) return cached;
|
|
13931
|
+
const [id] = await resolveLabelIds(executor, conn, [name]);
|
|
13932
|
+
if (id === void 0) throw new UnexpectedError(`Label id resolution returned nothing for "${name}"`);
|
|
13933
|
+
return id;
|
|
13934
|
+
}
|
|
13935
|
+
async function addForgejoIssueLabel(executor, conn, number, name, labelIds) {
|
|
13936
|
+
const id = await resolveLabelId(executor, conn, name, labelIds);
|
|
13937
|
+
const res = await executor.fetch(`${issuesBase(conn)}/${String(number)}/labels`, {
|
|
13938
|
+
method: "POST",
|
|
13939
|
+
headers: jsonHeaders(conn),
|
|
13940
|
+
body: JSON.stringify({ labels: [id] })
|
|
13941
|
+
}, { idempotent: true });
|
|
13942
|
+
if (!res.ok) await throwForgejoFailure(res, {
|
|
13943
|
+
verb: "add label",
|
|
13944
|
+
resource: `#${String(number)}`
|
|
13945
|
+
}, issueNotFoundMessage(conn, number));
|
|
13946
|
+
}
|
|
13947
|
+
async function removeForgejoIssueLabel(executor, conn, number, name, labelIds) {
|
|
13948
|
+
const id = await resolveLabelId(executor, conn, name, labelIds);
|
|
13949
|
+
const res = await executor.fetch(`${issuesBase(conn)}/${String(number)}/labels/${String(id)}`, {
|
|
13950
|
+
method: "DELETE",
|
|
13951
|
+
headers: authHeaders$2(conn)
|
|
13952
|
+
}, { idempotent: true });
|
|
13953
|
+
if (!res.ok) await throwForgejoFailure(res, {
|
|
13954
|
+
verb: "remove label",
|
|
13955
|
+
resource: `#${String(number)}`
|
|
13956
|
+
}, issueNotFoundMessage(conn, number));
|
|
13957
|
+
}
|
|
13958
|
+
async function replaceForgejoIssueLabels(executor, conn, number, labelIds) {
|
|
13959
|
+
const res = await executor.fetch(`${issuesBase(conn)}/${String(number)}/labels`, {
|
|
13960
|
+
method: "PUT",
|
|
13961
|
+
headers: jsonHeaders(conn),
|
|
13962
|
+
body: JSON.stringify({ labels: labelIds })
|
|
13963
|
+
}, { idempotent: true });
|
|
13964
|
+
if (!res.ok) await throwForgejoFailure(res, {
|
|
13965
|
+
verb: "replace labels",
|
|
13966
|
+
resource: `#${String(number)}`
|
|
13967
|
+
}, issueNotFoundMessage(conn, number));
|
|
13968
|
+
}
|
|
13969
|
+
//#endregion
|
|
13782
13970
|
//#region ../forgejo/src/issue-comments.ts
|
|
13783
13971
|
const CommentResponseSchema = z.object({
|
|
13784
13972
|
id: z.number(),
|
|
@@ -13854,52 +14042,6 @@ async function addForgejoComment(executor, conn, number, body) {
|
|
|
13854
14042
|
return normalizeComment(parseCommentOrThrow(await res.json()));
|
|
13855
14043
|
}
|
|
13856
14044
|
//#endregion
|
|
13857
|
-
//#region ../forgejo/src/label-provisioning.ts
|
|
13858
|
-
const LabelSchema$1 = z.object({
|
|
13859
|
-
id: z.number(),
|
|
13860
|
-
name: z.string(),
|
|
13861
|
-
color: z.string().optional(),
|
|
13862
|
-
description: z.string().optional(),
|
|
13863
|
-
exclusive: z.boolean().optional()
|
|
13864
|
-
});
|
|
13865
|
-
const LabelListSchema = z.array(LabelSchema$1);
|
|
13866
|
-
const LABELS_PAGE_SIZE = 100;
|
|
13867
|
-
const LABELS_MAX_PAGES = 20;
|
|
13868
|
-
async function fetchAllForgejoLabels(executor, conn) {
|
|
13869
|
-
const labels = [];
|
|
13870
|
-
for (let page = 1; page <= LABELS_MAX_PAGES; page++) {
|
|
13871
|
-
const res = await executor.fetch(`${conn.serverUrl}/api/v1/repos/${conn.repository}/labels?limit=${String(LABELS_PAGE_SIZE)}&page=${String(page)}`, { headers: authHeaders$2(conn) });
|
|
13872
|
-
if (!res.ok) await throwForgejoFailure(res, {
|
|
13873
|
-
verb: "list labels",
|
|
13874
|
-
resource: conn.repository
|
|
13875
|
-
}, repoNotFoundMessage(conn));
|
|
13876
|
-
const parsed = LabelListSchema.safeParse(await res.json());
|
|
13877
|
-
if (!parsed.success) throw new UnexpectedError(`Unexpected label-list response: ${parsed.error.message}`);
|
|
13878
|
-
labels.push(...parsed.data);
|
|
13879
|
-
if (parsed.data.length < LABELS_PAGE_SIZE) break;
|
|
13880
|
-
}
|
|
13881
|
-
return labels;
|
|
13882
|
-
}
|
|
13883
|
-
async function resolveLabelIds(executor, conn, names) {
|
|
13884
|
-
if (names.length === 0) return [];
|
|
13885
|
-
const labels = await fetchAllForgejoLabels(executor, conn);
|
|
13886
|
-
return names.map((name) => {
|
|
13887
|
-
const match = labels.find((label) => label.name === name);
|
|
13888
|
-
if (match === void 0) {
|
|
13889
|
-
const known = labels.map((label) => label.name).join(", ");
|
|
13890
|
-
throw new FatalError(`No label named "${name}" found among the repository's labels in ${conn.repository} (organization-level labels are not searched). Known labels: ${known}`);
|
|
13891
|
-
}
|
|
13892
|
-
return match.id;
|
|
13893
|
-
});
|
|
13894
|
-
}
|
|
13895
|
-
const LabelSpecSchema = z.strictObject({
|
|
13896
|
-
name: z.string().min(1),
|
|
13897
|
-
color: z.string().regex(/^#[0-9a-fA-F]{6}$/),
|
|
13898
|
-
description: z.string(),
|
|
13899
|
-
exclusive: z.boolean().optional()
|
|
13900
|
-
});
|
|
13901
|
-
z.record(z.string(), z.array(LabelSpecSchema));
|
|
13902
|
-
//#endregion
|
|
13903
14045
|
//#region ../forgejo/src/issues.ts
|
|
13904
14046
|
const IssueResponseSchema = z.object({
|
|
13905
14047
|
number: z.number(),
|
|
@@ -14345,6 +14487,178 @@ function resolveIssueTransport(executor, cwd, target, injectedToken, signal = nu
|
|
|
14345
14487
|
};
|
|
14346
14488
|
}
|
|
14347
14489
|
//#endregion
|
|
14490
|
+
//#region src/work-status/labels.ts
|
|
14491
|
+
const SCOPE_ORDER = [
|
|
14492
|
+
"agent",
|
|
14493
|
+
"agent-flags",
|
|
14494
|
+
"agent-note",
|
|
14495
|
+
"priority",
|
|
14496
|
+
"other"
|
|
14497
|
+
];
|
|
14498
|
+
function scopeOf(name) {
|
|
14499
|
+
const separator = name.indexOf("/");
|
|
14500
|
+
if (separator < 0) return "other";
|
|
14501
|
+
const prefix = name.slice(0, separator);
|
|
14502
|
+
if (prefix === "agent" || prefix === "agent-flags" || prefix === "agent-note" || prefix === "priority") return prefix;
|
|
14503
|
+
return "other";
|
|
14504
|
+
}
|
|
14505
|
+
function groupLabels(names, descriptions) {
|
|
14506
|
+
const groups = [];
|
|
14507
|
+
for (const scope of SCOPE_ORDER) {
|
|
14508
|
+
const inScope = names.filter((name) => scopeOf(name) === scope).toSorted((a, b) => a.localeCompare(b)).map((name) => ({
|
|
14509
|
+
name,
|
|
14510
|
+
description: descriptions.get(name) ?? null
|
|
14511
|
+
}));
|
|
14512
|
+
if (inScope.length === 0) continue;
|
|
14513
|
+
groups.push({
|
|
14514
|
+
scope,
|
|
14515
|
+
labels: inScope
|
|
14516
|
+
});
|
|
14517
|
+
}
|
|
14518
|
+
return groups;
|
|
14519
|
+
}
|
|
14520
|
+
const RUNNING_LABEL = /^agent\/[a-z-]+-running$/;
|
|
14521
|
+
const HAND_AUTHORED_LABEL = "agent-flags/hand-authored";
|
|
14522
|
+
function liftedFacts(names) {
|
|
14523
|
+
return {
|
|
14524
|
+
runLive: names.filter((name) => RUNNING_LABEL.test(name)).toSorted(),
|
|
14525
|
+
heldByHand: names.includes(HAND_AUTHORED_LABEL)
|
|
14526
|
+
};
|
|
14527
|
+
}
|
|
14528
|
+
function labelNamesOf(section) {
|
|
14529
|
+
const names = [];
|
|
14530
|
+
for (const group of [...section.pr, ...section.issue]) for (const label of group.labels) names.push(label.name);
|
|
14531
|
+
return names;
|
|
14532
|
+
}
|
|
14533
|
+
function ownerOf(names) {
|
|
14534
|
+
const agent = names.filter((name) => name.startsWith("agent/")).toSorted();
|
|
14535
|
+
const running = agent.find((name) => RUNNING_LABEL.test(name));
|
|
14536
|
+
if (running !== void 0) return {
|
|
14537
|
+
owner: "agent",
|
|
14538
|
+
label: running,
|
|
14539
|
+
running: true
|
|
14540
|
+
};
|
|
14541
|
+
if (names.includes("agent-flags/hand-authored")) return { owner: "person" };
|
|
14542
|
+
const held = agent[0];
|
|
14543
|
+
if (held !== void 0) return {
|
|
14544
|
+
owner: "agent",
|
|
14545
|
+
label: held,
|
|
14546
|
+
running: false
|
|
14547
|
+
};
|
|
14548
|
+
return { owner: "unclaimed" };
|
|
14549
|
+
}
|
|
14550
|
+
//#endregion
|
|
14551
|
+
//#region src/work-status/claim.ts
|
|
14552
|
+
const HANDOVER_ROUTES = "Claim it from the control panel's \"Claim for hand work\", or remove the agent label by hand and run bst work:start again.";
|
|
14553
|
+
function decideClaim(report, enabled) {
|
|
14554
|
+
if (!enabled) return {
|
|
14555
|
+
kind: "answer",
|
|
14556
|
+
exitCode: 0,
|
|
14557
|
+
result: {
|
|
14558
|
+
outcome: "not-agent-repo",
|
|
14559
|
+
reason: "Nothing was claimed: this repository's default branch does not set agent.enabled, so the agent does not work it and there is nothing to claim against."
|
|
14560
|
+
}
|
|
14561
|
+
};
|
|
14562
|
+
if (report.owner === null) return {
|
|
14563
|
+
kind: "answer",
|
|
14564
|
+
exitCode: 1,
|
|
14565
|
+
result: {
|
|
14566
|
+
outcome: "failed",
|
|
14567
|
+
reason: "A label read failed, so a run may be live on this object unseen. Nothing was claimed; retry once the forge read works."
|
|
14568
|
+
}
|
|
14569
|
+
};
|
|
14570
|
+
const held = ownershipOf(report);
|
|
14571
|
+
if (held.kind === "running") return {
|
|
14572
|
+
kind: "answer",
|
|
14573
|
+
exitCode: 1,
|
|
14574
|
+
result: {
|
|
14575
|
+
outcome: "refused",
|
|
14576
|
+
label: held.label,
|
|
14577
|
+
reason: `A run is live on this object (${held.label}); a claim does not stop it.`
|
|
14578
|
+
}
|
|
14579
|
+
};
|
|
14580
|
+
if (held.kind === "person") return {
|
|
14581
|
+
kind: "answer",
|
|
14582
|
+
exitCode: 0,
|
|
14583
|
+
result: { outcome: "already-claimed" }
|
|
14584
|
+
};
|
|
14585
|
+
if (held.kind === "agent") return {
|
|
14586
|
+
kind: "answer",
|
|
14587
|
+
exitCode: 1,
|
|
14588
|
+
result: {
|
|
14589
|
+
outcome: "refused",
|
|
14590
|
+
label: held.label,
|
|
14591
|
+
reason: `The agent holds this object (${held.label}). ${HANDOVER_ROUTES}`
|
|
14592
|
+
}
|
|
14593
|
+
};
|
|
14594
|
+
return { kind: "write" };
|
|
14595
|
+
}
|
|
14596
|
+
function ownershipOf(report) {
|
|
14597
|
+
if (report.labels.state === "ok") {
|
|
14598
|
+
const ownership = ownerOf(labelNamesOf(report.labels));
|
|
14599
|
+
if (ownership.owner === "agent") return ownership.running ? {
|
|
14600
|
+
kind: "running",
|
|
14601
|
+
label: ownership.label
|
|
14602
|
+
} : {
|
|
14603
|
+
kind: "agent",
|
|
14604
|
+
label: ownership.label
|
|
14605
|
+
};
|
|
14606
|
+
if (ownership.owner === "person") return { kind: "person" };
|
|
14607
|
+
return { kind: "unclaimed" };
|
|
14608
|
+
}
|
|
14609
|
+
if (report.owner === "agent") {
|
|
14610
|
+
const live = report.runLive?.[0];
|
|
14611
|
+
return live === void 0 ? {
|
|
14612
|
+
kind: "agent",
|
|
14613
|
+
label: "an agent label"
|
|
14614
|
+
} : {
|
|
14615
|
+
kind: "running",
|
|
14616
|
+
label: live
|
|
14617
|
+
};
|
|
14618
|
+
}
|
|
14619
|
+
return report.owner === "person" ? { kind: "person" } : { kind: "unclaimed" };
|
|
14620
|
+
}
|
|
14621
|
+
async function writeClaim(executor, cwd, target, number) {
|
|
14622
|
+
try {
|
|
14623
|
+
const { conn } = requireForgejoTransport(executor, cwd, target, "bst work:start claims work on Forgejo, the forge the agent drives; a GitHub checkout has no agent to hand work to.");
|
|
14624
|
+
const flag = (await fetchAllForgejoLabels(executor, conn)).find((label) => label.name === HAND_AUTHORED_LABEL);
|
|
14625
|
+
if (flag === void 0) return {
|
|
14626
|
+
outcome: "failed",
|
|
14627
|
+
reason: `The forge has no label named ${HAND_AUTHORED_LABEL}. Provision the agent's labels first with bst-agent agent:labels --repo ${conn.repository}, or the control panel's Provision labels.`
|
|
14628
|
+
};
|
|
14629
|
+
await addForgejoIssueLabel(executor, conn, number, HAND_AUTHORED_LABEL, /* @__PURE__ */ new Map([[HAND_AUTHORED_LABEL, flag.id]]));
|
|
14630
|
+
return {
|
|
14631
|
+
outcome: "claimed",
|
|
14632
|
+
label: HAND_AUTHORED_LABEL
|
|
14633
|
+
};
|
|
14634
|
+
} catch (error) {
|
|
14635
|
+
return {
|
|
14636
|
+
outcome: "failed",
|
|
14637
|
+
reason: describeError$1(error)
|
|
14638
|
+
};
|
|
14639
|
+
}
|
|
14640
|
+
}
|
|
14641
|
+
//#endregion
|
|
14642
|
+
//#region src/work-status/consent.ts
|
|
14643
|
+
async function readAgentConsent(git, defaultBranch) {
|
|
14644
|
+
const onBranch = await git.show(`origin/${defaultBranch}`, ".tooling.json");
|
|
14645
|
+
if (onBranch !== null) return {
|
|
14646
|
+
enabled: agentEnabledIn(onBranch),
|
|
14647
|
+
source: "default-branch"
|
|
14648
|
+
};
|
|
14649
|
+
return {
|
|
14650
|
+
enabled: agentEnabledIn(readWorkingTree(git.topLevel)),
|
|
14651
|
+
source: "working-tree"
|
|
14652
|
+
};
|
|
14653
|
+
}
|
|
14654
|
+
function readWorkingTree(topLevel) {
|
|
14655
|
+
try {
|
|
14656
|
+
return readFileSync(path.join(topLevel, ".tooling.json"), "utf8");
|
|
14657
|
+
} catch {
|
|
14658
|
+
return;
|
|
14659
|
+
}
|
|
14660
|
+
}
|
|
14661
|
+
//#endregion
|
|
14348
14662
|
//#region ../forgejo/src/issue-run.ts
|
|
14349
14663
|
var NothingToUpdateError = class extends FatalError {
|
|
14350
14664
|
constructor() {
|
|
@@ -15236,6 +15550,42 @@ async function createWorkGit(executor, cwd) {
|
|
|
15236
15550
|
};
|
|
15237
15551
|
}
|
|
15238
15552
|
//#endregion
|
|
15553
|
+
//#region src/work-status/prepare.ts
|
|
15554
|
+
function tryResolveWorkForge(executor, topLevel, target) {
|
|
15555
|
+
try {
|
|
15556
|
+
return resolveWorkForge(executor, topLevel, target);
|
|
15557
|
+
} catch (error) {
|
|
15558
|
+
return error instanceof Error ? error : new UnexpectedError(String(error));
|
|
15559
|
+
}
|
|
15560
|
+
}
|
|
15561
|
+
function sameRepository(one, other) {
|
|
15562
|
+
return one.toLowerCase() === other.toLowerCase();
|
|
15563
|
+
}
|
|
15564
|
+
function refNamesAnotherRepository(checkoutRepository, refRepository) {
|
|
15565
|
+
return new FatalError(`Usage: this command reads this checkout's repository (${checkoutRepository}); the ref names ${refRepository}`);
|
|
15566
|
+
}
|
|
15567
|
+
async function prepareWorkStatus(refText, cwd) {
|
|
15568
|
+
const ref = refText === "" ? null : parseIssueRef(refText);
|
|
15569
|
+
const executor = createRealExecutor$1();
|
|
15570
|
+
const git = await createWorkGit(executor, cwd);
|
|
15571
|
+
const target = ref?.target;
|
|
15572
|
+
if (target !== void 0) {
|
|
15573
|
+
const checkout = await git.originRepository();
|
|
15574
|
+
if (checkout !== null && !sameRepository(target.repository, checkout)) throw refNamesAnotherRepository(checkout, target.repository);
|
|
15575
|
+
}
|
|
15576
|
+
let forge = tryResolveWorkForge(executor, git.topLevel);
|
|
15577
|
+
if (target !== void 0 && !(forge instanceof Error)) {
|
|
15578
|
+
if (!sameRepository(target.repository, forge.repository)) throw refNamesAnotherRepository(forge.repository, target.repository);
|
|
15579
|
+
forge = tryResolveWorkForge(executor, git.topLevel, target);
|
|
15580
|
+
}
|
|
15581
|
+
return {
|
|
15582
|
+
ref,
|
|
15583
|
+
executor,
|
|
15584
|
+
git,
|
|
15585
|
+
forge
|
|
15586
|
+
};
|
|
15587
|
+
}
|
|
15588
|
+
//#endregion
|
|
15239
15589
|
//#region src/docs-check/discover.ts
|
|
15240
15590
|
const DOCS_ROOT = "docs";
|
|
15241
15591
|
const RETIRED_DIR = "retired";
|
|
@@ -17498,45 +17848,6 @@ function feedbackSince(comments, headCommitDate, cap, agentLogin) {
|
|
|
17498
17848
|
};
|
|
17499
17849
|
}
|
|
17500
17850
|
//#endregion
|
|
17501
|
-
//#region src/work-status/labels.ts
|
|
17502
|
-
const SCOPE_ORDER = [
|
|
17503
|
-
"agent",
|
|
17504
|
-
"agent-flags",
|
|
17505
|
-
"agent-note",
|
|
17506
|
-
"priority",
|
|
17507
|
-
"other"
|
|
17508
|
-
];
|
|
17509
|
-
function scopeOf(name) {
|
|
17510
|
-
const separator = name.indexOf("/");
|
|
17511
|
-
if (separator < 0) return "other";
|
|
17512
|
-
const prefix = name.slice(0, separator);
|
|
17513
|
-
if (prefix === "agent" || prefix === "agent-flags" || prefix === "agent-note" || prefix === "priority") return prefix;
|
|
17514
|
-
return "other";
|
|
17515
|
-
}
|
|
17516
|
-
function groupLabels(names, descriptions) {
|
|
17517
|
-
const groups = [];
|
|
17518
|
-
for (const scope of SCOPE_ORDER) {
|
|
17519
|
-
const inScope = names.filter((name) => scopeOf(name) === scope).toSorted((a, b) => a.localeCompare(b)).map((name) => ({
|
|
17520
|
-
name,
|
|
17521
|
-
description: descriptions.get(name) ?? null
|
|
17522
|
-
}));
|
|
17523
|
-
if (inScope.length === 0) continue;
|
|
17524
|
-
groups.push({
|
|
17525
|
-
scope,
|
|
17526
|
-
labels: inScope
|
|
17527
|
-
});
|
|
17528
|
-
}
|
|
17529
|
-
return groups;
|
|
17530
|
-
}
|
|
17531
|
-
const RUNNING_LABEL = /^agent\/[a-z-]+-running$/;
|
|
17532
|
-
const HAND_AUTHORED_LABEL = "agent-flags/hand-authored";
|
|
17533
|
-
function liftedFacts(names) {
|
|
17534
|
-
return {
|
|
17535
|
-
runLive: names.filter((name) => RUNNING_LABEL.test(name)).toSorted(),
|
|
17536
|
-
heldByHand: names.includes(HAND_AUTHORED_LABEL)
|
|
17537
|
-
};
|
|
17538
|
-
}
|
|
17539
|
-
//#endregion
|
|
17540
17851
|
//#region src/work-status/report.ts
|
|
17541
17852
|
const COMMIT_CAP = 20;
|
|
17542
17853
|
const FEEDBACK_CAP = 20;
|
|
@@ -17772,11 +18083,14 @@ function objectSection({ resolved, forgeDown }) {
|
|
|
17772
18083
|
return ok(facts);
|
|
17773
18084
|
}
|
|
17774
18085
|
function liftedFactsOf(resolved, forgeDown) {
|
|
17775
|
-
const
|
|
18086
|
+
const names = labelNames([resolved.pr, resolved.issue]);
|
|
18087
|
+
const read = liftedFacts(names);
|
|
18088
|
+
const ownership = ownerOf(names);
|
|
17776
18089
|
const complete = forgeDown === null && resolved.prFailed === null && resolved.issueFailed === null;
|
|
17777
18090
|
return {
|
|
17778
18091
|
runLive: complete || read.runLive.length > 0 ? read.runLive : null,
|
|
17779
|
-
heldByHand: complete || read.heldByHand ? read.heldByHand : null
|
|
18092
|
+
heldByHand: complete || read.heldByHand ? read.heldByHand : null,
|
|
18093
|
+
owner: complete || ownership.owner !== "unclaimed" ? ownership.owner : null
|
|
17780
18094
|
};
|
|
17781
18095
|
}
|
|
17782
18096
|
async function docsSection({ git, docsExtraDirs, resolved }) {
|
|
@@ -17892,6 +18206,7 @@ async function buildWorkStatus(deps, ref) {
|
|
|
17892
18206
|
},
|
|
17893
18207
|
runLive: lifted.runLive,
|
|
17894
18208
|
heldByHand: lifted.heldByHand,
|
|
18209
|
+
owner: lifted.owner,
|
|
17895
18210
|
object: await section(() => objectSection(inputs)),
|
|
17896
18211
|
docs: await section(() => docsSection(inputs)),
|
|
17897
18212
|
labels: await section(() => labelsSection(inputs)),
|
|
@@ -17933,13 +18248,22 @@ function heading({ kind, branch, head }) {
|
|
|
17933
18248
|
if (head !== null) named.push(`head ${short(head)}`);
|
|
17934
18249
|
return `work:status ${named.join(", ")}`;
|
|
17935
18250
|
}
|
|
17936
|
-
function
|
|
18251
|
+
function claimableNumber(report) {
|
|
18252
|
+
if (report.object.state !== "ok") return null;
|
|
18253
|
+
if (report.target.kind === "issue") return report.object.issue?.number ?? null;
|
|
18254
|
+
return report.object.pr?.number ?? report.object.issue?.number ?? null;
|
|
18255
|
+
}
|
|
18256
|
+
function renderWorkStatus(report, options) {
|
|
17937
18257
|
const quiet = nothingInFlight(report);
|
|
17938
18258
|
if (quiet !== null) return [heading(report.target), quiet].join("\n");
|
|
17939
18259
|
const lines = [heading(report.target)];
|
|
17940
18260
|
for (const label of report.runLive ?? []) lines.push(`RUN LIVE: ${label} -- a run is working this object; do not start another.`);
|
|
17941
18261
|
if (report.runLive === null || report.heldByHand === null) lines.push("UNKNOWN: whether a run is live or a human holds this -- not every label could be read.");
|
|
17942
18262
|
if (report.heldByHand === true) lines.push("HELD BY HAND: agent-flags/hand-authored -- a human claimed this one; do not drive it.");
|
|
18263
|
+
if (options?.agentEnabled === true && report.owner === "unclaimed") {
|
|
18264
|
+
const number = claimableNumber(report);
|
|
18265
|
+
if (number !== null) lines.push(`UNCLAIMED: no agent label and no hand claim -- run \`bst work:start ${String(number)}\` before you work this.`);
|
|
18266
|
+
}
|
|
17943
18267
|
lines.push(...block("object", report.object, objectRows));
|
|
17944
18268
|
lines.push(...block("docs", report.docs, (value) => {
|
|
17945
18269
|
const rows = [` base ${value.base}`];
|
|
@@ -17988,20 +18312,77 @@ function renderWorkStatus(report) {
|
|
|
17988
18312
|
return lines.join("\n");
|
|
17989
18313
|
}
|
|
17990
18314
|
//#endregion
|
|
17991
|
-
//#region src/commands/work-
|
|
17992
|
-
function
|
|
17993
|
-
|
|
17994
|
-
|
|
17995
|
-
|
|
17996
|
-
|
|
17997
|
-
}
|
|
18315
|
+
//#region src/commands/work-start.ts
|
|
18316
|
+
function claimLine(result) {
|
|
18317
|
+
if (result.outcome === "claimed") return `claim: claimed -- ${result.label ?? "agent-flags/hand-authored"} is on this object.`;
|
|
18318
|
+
if (result.outcome === "already-claimed") return "claim: already-claimed -- the flag is already on it; nothing was written.";
|
|
18319
|
+
if (result.outcome === "not-agent-repo") return `claim: not-agent-repo -- ${result.reason ?? "nothing was claimed."}`;
|
|
18320
|
+
return `claim: ${result.outcome} -- ${result.reason ?? "nothing was claimed."}`;
|
|
17998
18321
|
}
|
|
17999
|
-
function
|
|
18000
|
-
return
|
|
18001
|
-
}
|
|
18002
|
-
function refNamesAnotherRepository(checkoutRepository, refRepository) {
|
|
18003
|
-
return new FatalError(`Usage: bst work:status reads this checkout's repository (${checkoutRepository}); the ref names ${refRepository}`);
|
|
18322
|
+
function claimFailed(result) {
|
|
18323
|
+
return result.outcome === "refused" || result.outcome === "failed";
|
|
18004
18324
|
}
|
|
18325
|
+
const workStartCommand = defineCommand({
|
|
18326
|
+
meta: {
|
|
18327
|
+
name: "work:start",
|
|
18328
|
+
description: "Claim an issue or pull request for hand work: put agent-flags/hand-authored on it so the agent leaves it alone"
|
|
18329
|
+
},
|
|
18330
|
+
args: {
|
|
18331
|
+
ref: {
|
|
18332
|
+
type: "positional",
|
|
18333
|
+
required: true,
|
|
18334
|
+
description: "Issue or pull request number or URL to claim"
|
|
18335
|
+
},
|
|
18336
|
+
json: {
|
|
18337
|
+
type: "boolean",
|
|
18338
|
+
description: "Emit one JSON document: the whole work:status report plus a claim member {outcome, label?, reason?}. outcome is claimed (the flag was written), already-claimed (it was already on the object, nothing written), not-agent-repo (the default branch does not set agent.enabled, nothing written), refused (a live run or an agent label holds it; exit 1), or failed (a read or write could not be done; exit 1). refused and failed carry ok: false and an error member of kind runtime, and the report survives beside them."
|
|
18339
|
+
},
|
|
18340
|
+
verbose: {
|
|
18341
|
+
type: "boolean",
|
|
18342
|
+
description: "Emit detailed debug logging"
|
|
18343
|
+
}
|
|
18344
|
+
},
|
|
18345
|
+
async run({ args }) {
|
|
18346
|
+
const { ref, executor, git, forge } = await prepareWorkStatus(typeof args.ref === "string" ? args.ref.trim() : "", process.cwd());
|
|
18347
|
+
const number = ref?.number;
|
|
18348
|
+
if (number === void 0) throw new FatalError("Usage: bst work:start needs an issue or pull request number or URL");
|
|
18349
|
+
const docs = resolveDocsConfig(git.topLevel);
|
|
18350
|
+
const defaultBranch = resolveDefaultBranch({ cwd: git.topLevel });
|
|
18351
|
+
const report = await buildWorkStatus({
|
|
18352
|
+
git,
|
|
18353
|
+
forge,
|
|
18354
|
+
docsExtraDirs: docs.extraDirs,
|
|
18355
|
+
defaultBranch
|
|
18356
|
+
}, ref);
|
|
18357
|
+
const consent = await readAgentConsent(git, defaultBranch);
|
|
18358
|
+
const decision = decideClaim(report, consent.enabled);
|
|
18359
|
+
let result;
|
|
18360
|
+
let exitCode;
|
|
18361
|
+
if (decision.kind === "write") {
|
|
18362
|
+
result = await writeClaim(executor, git.topLevel, ref?.target, number);
|
|
18363
|
+
exitCode = claimFailed(result) ? 1 : 0;
|
|
18364
|
+
} else {
|
|
18365
|
+
result = decision.result;
|
|
18366
|
+
exitCode = decision.exitCode;
|
|
18367
|
+
}
|
|
18368
|
+
const out = createCliOutput(args.json === true);
|
|
18369
|
+
const failed = claimFailed(result);
|
|
18370
|
+
out.json({
|
|
18371
|
+
ok: !failed,
|
|
18372
|
+
...report,
|
|
18373
|
+
claim: result,
|
|
18374
|
+
...failed ? { error: {
|
|
18375
|
+
kind: "runtime",
|
|
18376
|
+
message: result.reason ?? result.outcome
|
|
18377
|
+
} } : {}
|
|
18378
|
+
});
|
|
18379
|
+
out.text(renderWorkStatus(report, { agentEnabled: consent.enabled }));
|
|
18380
|
+
out.text(claimLine(result));
|
|
18381
|
+
if (exitCode !== 0) process.exitCode = exitCode;
|
|
18382
|
+
}
|
|
18383
|
+
});
|
|
18384
|
+
//#endregion
|
|
18385
|
+
//#region src/commands/work-status.ts
|
|
18005
18386
|
const workStatusCommand = defineCommand({
|
|
18006
18387
|
meta: {
|
|
18007
18388
|
name: "work:status",
|
|
@@ -18015,7 +18396,7 @@ const workStatusCommand = defineCommand({
|
|
|
18015
18396
|
},
|
|
18016
18397
|
json: {
|
|
18017
18398
|
type: "boolean",
|
|
18018
|
-
description: "Emit one JSON document: target, runLive, heldByHand (each null when not every label could be read), and six sections (object, docs, labels, feedback, findings, commits), each {state: ok} with its fields, {state: absent, reason} or {state: unavailable, reason}. The output is advice, not a gate: the feedback cutoff is the newest agent run or review comment on the pull request and is approximate, and review comments on diff lines are not in the thread this reads."
|
|
18399
|
+
description: "Emit one JSON document: target, runLive, heldByHand, owner (agent, person or unclaimed; each null when not every label could be read), and six sections (object, docs, labels, feedback, findings, commits), each {state: ok} with its fields, {state: absent, reason} or {state: unavailable, reason}. The output is advice, not a gate: the feedback cutoff is the newest agent run or review comment on the pull request and is approximate, and review comments on diff lines are not in the thread this reads."
|
|
18019
18400
|
},
|
|
18020
18401
|
verbose: {
|
|
18021
18402
|
type: "boolean",
|
|
@@ -18023,20 +18404,7 @@ const workStatusCommand = defineCommand({
|
|
|
18023
18404
|
}
|
|
18024
18405
|
},
|
|
18025
18406
|
async run({ args }) {
|
|
18026
|
-
const
|
|
18027
|
-
const ref = refText === "" ? null : parseIssueRef(refText);
|
|
18028
|
-
const executor = createRealExecutor$1();
|
|
18029
|
-
const git = await createWorkGit(executor, process.cwd());
|
|
18030
|
-
const target = ref?.target;
|
|
18031
|
-
if (target !== void 0) {
|
|
18032
|
-
const checkout = await git.originRepository();
|
|
18033
|
-
if (checkout !== null && !sameRepository(target.repository, checkout)) throw refNamesAnotherRepository(checkout, target.repository);
|
|
18034
|
-
}
|
|
18035
|
-
let forge = tryResolveWorkForge(executor, git.topLevel);
|
|
18036
|
-
if (target !== void 0 && !(forge instanceof Error)) {
|
|
18037
|
-
if (!sameRepository(target.repository, forge.repository)) throw refNamesAnotherRepository(forge.repository, target.repository);
|
|
18038
|
-
forge = tryResolveWorkForge(executor, git.topLevel, target);
|
|
18039
|
-
}
|
|
18407
|
+
const { ref, git, forge } = await prepareWorkStatus(typeof args.ref === "string" ? args.ref.trim() : "", process.cwd());
|
|
18040
18408
|
const docs = resolveDocsConfig(git.topLevel);
|
|
18041
18409
|
const defaultBranch = resolveDefaultBranch({ cwd: git.topLevel });
|
|
18042
18410
|
const report = await buildWorkStatus({
|
|
@@ -18045,12 +18413,13 @@ const workStatusCommand = defineCommand({
|
|
|
18045
18413
|
docsExtraDirs: docs.extraDirs,
|
|
18046
18414
|
defaultBranch
|
|
18047
18415
|
}, ref);
|
|
18416
|
+
const consent = await readAgentConsent(git, defaultBranch);
|
|
18048
18417
|
const out = createCliOutput(args.json === true);
|
|
18049
18418
|
out.json({
|
|
18050
18419
|
ok: true,
|
|
18051
18420
|
...report
|
|
18052
18421
|
});
|
|
18053
|
-
out.text(renderWorkStatus(report));
|
|
18422
|
+
out.text(renderWorkStatus(report, { agentEnabled: consent.enabled }));
|
|
18054
18423
|
}
|
|
18055
18424
|
});
|
|
18056
18425
|
//#endregion
|
|
@@ -18306,8 +18675,9 @@ const PASSED_VERDICT = {
|
|
|
18306
18675
|
reason: null
|
|
18307
18676
|
};
|
|
18308
18677
|
async function mergeVersionPrForgejo(executor, conn, options) {
|
|
18309
|
-
const
|
|
18310
|
-
|
|
18678
|
+
const branch = versionPrBranch(options.defaultBranch);
|
|
18679
|
+
const prNumber = await findOpenPr(executor, conn, branch);
|
|
18680
|
+
if (prNumber === null) throw new FatalError(`No open PR found for branch ${branch}`);
|
|
18311
18681
|
const sleep$1 = options.sleep ?? sleep;
|
|
18312
18682
|
const recheckDelayMs = envMsOverride(process.env["BST_TEST_VERSION_MERGE_RECHECK_MS"], RECHECK_DELAY_MS);
|
|
18313
18683
|
const confirmation = await confirmMergeable(executor, conn, prNumber, {
|
|
@@ -18328,12 +18698,12 @@ async function mergeVersionPrForgejo(executor, conn, options) {
|
|
|
18328
18698
|
headSha,
|
|
18329
18699
|
mergeBase: decidedFrom.mergeBase,
|
|
18330
18700
|
changedFiles: decidedFrom.changedFiles,
|
|
18331
|
-
branch
|
|
18701
|
+
branch,
|
|
18332
18702
|
rechecked,
|
|
18333
18703
|
verdict: { ...PASSED_VERDICT }
|
|
18334
18704
|
};
|
|
18335
18705
|
if (options.dryRun) {
|
|
18336
|
-
log$1.info(`[dry-run] Would merge PR #${String(prNumber)} at ${headSha} and delete branch ${
|
|
18706
|
+
log$1.info(`[dry-run] Would merge PR #${String(prNumber)} at ${headSha} and delete branch ${branch}`);
|
|
18337
18707
|
return {
|
|
18338
18708
|
...outcome,
|
|
18339
18709
|
status: "would-merge"
|
|
@@ -18344,7 +18714,7 @@ async function mergeVersionPrForgejo(executor, conn, options) {
|
|
|
18344
18714
|
deleteBranch: true,
|
|
18345
18715
|
expectedHeadSha: headSha
|
|
18346
18716
|
});
|
|
18347
|
-
log$1.info(`Merged PR #${String(prNumber)} and deleted branch ${
|
|
18717
|
+
log$1.info(`Merged PR #${String(prNumber)} and deleted branch ${branch}`);
|
|
18348
18718
|
return {
|
|
18349
18719
|
...outcome,
|
|
18350
18720
|
status: "merged"
|
|
@@ -18359,16 +18729,17 @@ const GitHubVersionPrSchema = z.object({
|
|
|
18359
18729
|
changedFiles: z.number()
|
|
18360
18730
|
});
|
|
18361
18731
|
function mergeVersionPrGitHub(executor, options) {
|
|
18732
|
+
const branch = versionPrBranch(options.defaultBranch);
|
|
18362
18733
|
const view = executor.exec("gh", [
|
|
18363
18734
|
"pr",
|
|
18364
18735
|
"view",
|
|
18365
|
-
|
|
18736
|
+
branch,
|
|
18366
18737
|
"--json",
|
|
18367
18738
|
"number,state,isDraft,mergeable,headRefOid,changedFiles"
|
|
18368
18739
|
], { cwd: process.cwd() });
|
|
18369
|
-
if (view.exitCode !== 0) throw new FatalError(`Failed to read PR for branch ${
|
|
18740
|
+
if (view.exitCode !== 0) throw new FatalError(`Failed to read PR for branch ${branch}: ${view.stderr || view.stdout || "unknown error"}`);
|
|
18370
18741
|
const parsed = GitHubVersionPrSchema.safeParse(JSON.parse(view.stdout));
|
|
18371
|
-
if (!parsed.success) throw new FatalError(`Unexpected 'gh pr view' response for branch ${
|
|
18742
|
+
if (!parsed.success) throw new FatalError(`Unexpected 'gh pr view' response for branch ${branch}: ${parsed.error.message}`);
|
|
18372
18743
|
const data = parsed.data;
|
|
18373
18744
|
const emptiness = data.changedFiles === 0 ? "empty" : "non-empty";
|
|
18374
18745
|
const mergeable = data.mergeable === "CONFLICTING" ? "conflicted" : "mergeable";
|
|
@@ -18387,12 +18758,12 @@ function mergeVersionPrGitHub(executor, options) {
|
|
|
18387
18758
|
headSha,
|
|
18388
18759
|
mergeBase: null,
|
|
18389
18760
|
changedFiles: data.changedFiles,
|
|
18390
|
-
branch
|
|
18761
|
+
branch,
|
|
18391
18762
|
rechecked: false,
|
|
18392
18763
|
verdict: { ...PASSED_VERDICT }
|
|
18393
18764
|
};
|
|
18394
18765
|
if (options.dryRun) {
|
|
18395
|
-
log$1.info(`[dry-run] Would merge PR #${String(data.number)} at ${headSha} and delete branch ${
|
|
18766
|
+
log$1.info(`[dry-run] Would merge PR #${String(data.number)} at ${headSha} and delete branch ${branch}`);
|
|
18396
18767
|
return {
|
|
18397
18768
|
...outcome,
|
|
18398
18769
|
status: "would-merge"
|
|
@@ -18401,14 +18772,14 @@ function mergeVersionPrGitHub(executor, options) {
|
|
|
18401
18772
|
const result = executor.exec("gh", [
|
|
18402
18773
|
"pr",
|
|
18403
18774
|
"merge",
|
|
18404
|
-
|
|
18775
|
+
branch,
|
|
18405
18776
|
"--merge",
|
|
18406
18777
|
"--delete-branch",
|
|
18407
18778
|
"--match-head-commit",
|
|
18408
18779
|
headSha
|
|
18409
18780
|
], { cwd: process.cwd() });
|
|
18410
18781
|
if (result.exitCode !== 0) throw new FatalError(`Failed to merge PR: ${result.stderr || result.stdout || "unknown error"}`);
|
|
18411
|
-
log$1.info(`Merged changesets PR and deleted branch ${
|
|
18782
|
+
log$1.info(`Merged changesets PR and deleted branch ${branch}`);
|
|
18412
18783
|
return {
|
|
18413
18784
|
...outcome,
|
|
18414
18785
|
status: "merged"
|
|
@@ -18436,7 +18807,14 @@ const releaseMergeCommand = defineCommand({
|
|
|
18436
18807
|
const dryRun = args["dry-run"] === true;
|
|
18437
18808
|
const resolved = resolveConnection(process.cwd());
|
|
18438
18809
|
const executor = createRealExecutor$1();
|
|
18439
|
-
const
|
|
18810
|
+
const defaultBranch = readConfiguredDefaultBranch(process.cwd());
|
|
18811
|
+
const result = resolved.type === "forgejo" ? await mergeVersionPrForgejo(executor, resolved.conn, {
|
|
18812
|
+
dryRun,
|
|
18813
|
+
defaultBranch
|
|
18814
|
+
}) : mergeVersionPrGitHub(executor, {
|
|
18815
|
+
dryRun,
|
|
18816
|
+
defaultBranch
|
|
18817
|
+
});
|
|
18440
18818
|
out.json(result);
|
|
18441
18819
|
}
|
|
18442
18820
|
});
|
|
@@ -20896,6 +21274,102 @@ async function runHooksCheck(config) {
|
|
|
20896
21274
|
};
|
|
20897
21275
|
}
|
|
20898
21276
|
//#endregion
|
|
21277
|
+
//#region src/commands/setup-agent.ts
|
|
21278
|
+
const RawToolingConfigSchema = z.record(z.string(), z.unknown());
|
|
21279
|
+
function previousState(saved) {
|
|
21280
|
+
const enabled = saved.agent?.enabled;
|
|
21281
|
+
if (enabled === void 0) return "absent";
|
|
21282
|
+
if (enabled === true) return "true";
|
|
21283
|
+
return "false";
|
|
21284
|
+
}
|
|
21285
|
+
function withAgentEnabled(raw) {
|
|
21286
|
+
const agent = RawToolingConfigSchema.safeParse(raw["agent"]);
|
|
21287
|
+
const block = agent.success ? { ...agent.data } : {};
|
|
21288
|
+
block["enabled"] = true;
|
|
21289
|
+
return {
|
|
21290
|
+
...raw,
|
|
21291
|
+
agent: block
|
|
21292
|
+
};
|
|
21293
|
+
}
|
|
21294
|
+
const CHANGE_ACTIONS = /* @__PURE__ */ new Set([
|
|
21295
|
+
"created",
|
|
21296
|
+
"updated",
|
|
21297
|
+
"removed",
|
|
21298
|
+
"archived"
|
|
21299
|
+
]);
|
|
21300
|
+
async function runSetupAgent(targetDir, options) {
|
|
21301
|
+
const saved = loadToolingConfigStrict(targetDir);
|
|
21302
|
+
if (saved === void 0) throw new FatalError(`No .tooling.json in ${targetDir}. Run bst repo:sync first to create one, so the repo's own conventions are chosen before the agent is enabled.`);
|
|
21303
|
+
const previous = previousState(saved);
|
|
21304
|
+
let consent = "already-enabled";
|
|
21305
|
+
if (previous !== "true") {
|
|
21306
|
+
const displayPath = path.join(targetDir, ".tooling.json");
|
|
21307
|
+
let parsedFile;
|
|
21308
|
+
try {
|
|
21309
|
+
parsedFile = JSON.parse(readFileSync(displayPath, "utf8"));
|
|
21310
|
+
} catch (error) {
|
|
21311
|
+
throw new FatalError(`Could not read ${displayPath} as JSON: ${describeError$1(error)}`);
|
|
21312
|
+
}
|
|
21313
|
+
const rawFile = RawToolingConfigSchema.safeParse(parsedFile);
|
|
21314
|
+
if (!rawFile.success) throw new FatalError(`Could not read ${displayPath} as a config object: ${rawFile.error.message}`);
|
|
21315
|
+
const edited = withAgentEnabled(rawFile.data);
|
|
21316
|
+
const content = `${JSON.stringify(edited, null, 2)}\n`;
|
|
21317
|
+
parseToolingConfigStrict(content, displayPath);
|
|
21318
|
+
const context = createContext(mergeWithSavedConfig(buildDefaultConfig(targetDir, {}), saved), async () => "overwrite");
|
|
21319
|
+
context.ctx.write(".tooling.json", context.ctx.format(".tooling.json", content));
|
|
21320
|
+
consent = "written";
|
|
21321
|
+
}
|
|
21322
|
+
if (consent === "written") log$1.success("Set agent.enabled to true in .tooling.json");
|
|
21323
|
+
else log$1.info("agent.enabled is already true");
|
|
21324
|
+
const report = await runUpdateReport(targetDir, {}, { installOutput: options.installOutput });
|
|
21325
|
+
const failed = failedResults(report.results);
|
|
21326
|
+
const changed = report.results.filter((result) => CHANGE_ACTIONS.has(result.action)).map((result) => ({
|
|
21327
|
+
filePath: result.filePath,
|
|
21328
|
+
action: result.action
|
|
21329
|
+
}));
|
|
21330
|
+
let status;
|
|
21331
|
+
if (report.status === "stopped" || failed.length > 0) status = "failed";
|
|
21332
|
+
else if (changed.length > 0) status = "changed";
|
|
21333
|
+
else status = "up-to-date";
|
|
21334
|
+
if (status === "failed") log$1.error("The sync failed. Fix what it reported, then run bst setup:agent again.");
|
|
21335
|
+
else log$1.info("Commit these changes and land them on the default branch; the agent reads consent there within about a minute.");
|
|
21336
|
+
return {
|
|
21337
|
+
dir: targetDir,
|
|
21338
|
+
consent,
|
|
21339
|
+
previous,
|
|
21340
|
+
sync: {
|
|
21341
|
+
status,
|
|
21342
|
+
changed,
|
|
21343
|
+
failed,
|
|
21344
|
+
install: report.install
|
|
21345
|
+
},
|
|
21346
|
+
exitCode: status === "failed" ? 1 : 0
|
|
21347
|
+
};
|
|
21348
|
+
}
|
|
21349
|
+
const setupAgentCommand = defineCommand({
|
|
21350
|
+
meta: {
|
|
21351
|
+
name: "setup:agent",
|
|
21352
|
+
description: "Enable the agent for this repository: set agent.enabled to true in .tooling.json, then run the same sync as repo:sync, which installs @bensandee/agent-cli and generates the files the agent reads. Commits nothing -- land the change on the default branch yourself."
|
|
21353
|
+
},
|
|
21354
|
+
args: {
|
|
21355
|
+
dir: {
|
|
21356
|
+
type: "positional",
|
|
21357
|
+
valueHint: "dir",
|
|
21358
|
+
description: "Directory to enable (default: current directory)",
|
|
21359
|
+
required: false
|
|
21360
|
+
},
|
|
21361
|
+
json: {
|
|
21362
|
+
type: "boolean",
|
|
21363
|
+
description: "Output the report as JSON: dir, consent, previous, sync { status, changed, failed, install }, exitCode"
|
|
21364
|
+
}
|
|
21365
|
+
},
|
|
21366
|
+
async run({ args }) {
|
|
21367
|
+
const report = await runSetupAgent(path.resolve(args.dir ?? "."), { installOutput: args.json === true || isJsonMode() ? "capture" : "inherit" });
|
|
21368
|
+
createCliOutput(args.json === true).json(report);
|
|
21369
|
+
process.exitCode = report.exitCode;
|
|
21370
|
+
}
|
|
21371
|
+
});
|
|
21372
|
+
//#endregion
|
|
20899
21373
|
//#region src/commands/setup-secrets.ts
|
|
20900
21374
|
function isCancelled(value) {
|
|
20901
21375
|
return clack.isCancel(value);
|
|
@@ -21840,6 +22314,7 @@ const coverageRecordCommand = defineCommand({
|
|
|
21840
22314
|
});
|
|
21841
22315
|
return;
|
|
21842
22316
|
}
|
|
22317
|
+
refuseForgeDefaultMismatch(readConfiguredDefaultBranch(targetDir), processCiEventSource(), "coverage:record");
|
|
21843
22318
|
const summaryPath = path.join(targetDir, COVERAGE_SUMMARY_FILE);
|
|
21844
22319
|
if (!existsSync(summaryPath)) throw new FatalError(`${COVERAGE_SUMMARY_FILE} not found. Run \`pnpm test:coverage\` before \`coverage:record\`.`);
|
|
21845
22320
|
const summary = parseV8Summary(readFileSync(summaryPath, "utf-8"));
|
|
@@ -23312,6 +23787,7 @@ const healthRecordCommand = defineCommand({
|
|
|
23312
23787
|
});
|
|
23313
23788
|
return;
|
|
23314
23789
|
}
|
|
23790
|
+
refuseForgeDefaultMismatch(readConfiguredDefaultBranch(targetDir), processCiEventSource(), "health:record");
|
|
23315
23791
|
const executor = createRealHealthExecutor(targetDir);
|
|
23316
23792
|
const headResult = executor.exec("git", ["rev-parse", "HEAD"], { cwd: targetDir });
|
|
23317
23793
|
if (headResult.exitCode !== 0) throw new FatalError(`Failed to resolve HEAD sha: ${headResult.stderr.trim()}`);
|
|
@@ -25663,58 +26139,6 @@ const issueDownloadCommand = defineCommand({
|
|
|
25663
26139
|
}
|
|
25664
26140
|
});
|
|
25665
26141
|
//#endregion
|
|
25666
|
-
//#region ../forgejo/src/issue-labels.ts
|
|
25667
|
-
const TimelineEntrySchema = z.object({
|
|
25668
|
-
id: z.number(),
|
|
25669
|
-
type: z.string(),
|
|
25670
|
-
body: z.string().nullish(),
|
|
25671
|
-
created_at: z.string(),
|
|
25672
|
-
label: z.object({ name: z.string() }).nullish(),
|
|
25673
|
-
user: z.object({ login: z.string() }).nullish()
|
|
25674
|
-
});
|
|
25675
|
-
z.array(TimelineEntrySchema).nullable();
|
|
25676
|
-
async function resolveLabelId(executor, conn, name, labelIds) {
|
|
25677
|
-
const cached = labelIds?.get(name);
|
|
25678
|
-
if (cached !== void 0) return cached;
|
|
25679
|
-
const [id] = await resolveLabelIds(executor, conn, [name]);
|
|
25680
|
-
if (id === void 0) throw new UnexpectedError(`Label id resolution returned nothing for "${name}"`);
|
|
25681
|
-
return id;
|
|
25682
|
-
}
|
|
25683
|
-
async function addForgejoIssueLabel(executor, conn, number, name, labelIds) {
|
|
25684
|
-
const id = await resolveLabelId(executor, conn, name, labelIds);
|
|
25685
|
-
const res = await executor.fetch(`${issuesBase(conn)}/${String(number)}/labels`, {
|
|
25686
|
-
method: "POST",
|
|
25687
|
-
headers: jsonHeaders(conn),
|
|
25688
|
-
body: JSON.stringify({ labels: [id] })
|
|
25689
|
-
}, { idempotent: true });
|
|
25690
|
-
if (!res.ok) await throwForgejoFailure(res, {
|
|
25691
|
-
verb: "add label",
|
|
25692
|
-
resource: `#${String(number)}`
|
|
25693
|
-
}, issueNotFoundMessage(conn, number));
|
|
25694
|
-
}
|
|
25695
|
-
async function removeForgejoIssueLabel(executor, conn, number, name, labelIds) {
|
|
25696
|
-
const id = await resolveLabelId(executor, conn, name, labelIds);
|
|
25697
|
-
const res = await executor.fetch(`${issuesBase(conn)}/${String(number)}/labels/${String(id)}`, {
|
|
25698
|
-
method: "DELETE",
|
|
25699
|
-
headers: authHeaders$2(conn)
|
|
25700
|
-
}, { idempotent: true });
|
|
25701
|
-
if (!res.ok) await throwForgejoFailure(res, {
|
|
25702
|
-
verb: "remove label",
|
|
25703
|
-
resource: `#${String(number)}`
|
|
25704
|
-
}, issueNotFoundMessage(conn, number));
|
|
25705
|
-
}
|
|
25706
|
-
async function replaceForgejoIssueLabels(executor, conn, number, labelIds) {
|
|
25707
|
-
const res = await executor.fetch(`${issuesBase(conn)}/${String(number)}/labels`, {
|
|
25708
|
-
method: "PUT",
|
|
25709
|
-
headers: jsonHeaders(conn),
|
|
25710
|
-
body: JSON.stringify({ labels: labelIds })
|
|
25711
|
-
}, { idempotent: true });
|
|
25712
|
-
if (!res.ok) await throwForgejoFailure(res, {
|
|
25713
|
-
verb: "replace labels",
|
|
25714
|
-
resource: `#${String(number)}`
|
|
25715
|
-
}, issueNotFoundMessage(conn, number));
|
|
25716
|
-
}
|
|
25717
|
-
//#endregion
|
|
25718
26142
|
//#region ../forgejo/src/issue-run-labels.ts
|
|
25719
26143
|
async function labelIssue(resolve, request) {
|
|
25720
26144
|
const ref = parseIssueRef(request.ref);
|
|
@@ -26920,7 +27344,7 @@ async function runCli(config) {
|
|
|
26920
27344
|
//#region src/cli-main.ts
|
|
26921
27345
|
setLogLevel(resolveLogLevel({}));
|
|
26922
27346
|
setForgejoLogger(log$1);
|
|
26923
|
-
const displayVersion = "0.
|
|
27347
|
+
const displayVersion = "0.91.0";
|
|
26924
27348
|
const bstSubCommands = {
|
|
26925
27349
|
"repo:sync": syncCommand,
|
|
26926
27350
|
"checks:run": runChecksCommand,
|
|
@@ -26943,6 +27367,7 @@ const bstSubCommands = {
|
|
|
26943
27367
|
"hooks:audit": hooksAuditCommand,
|
|
26944
27368
|
"hooks:sync": hooksSyncCommand,
|
|
26945
27369
|
"setup:secrets": setupSecretsCommand,
|
|
27370
|
+
"setup:agent": setupAgentCommand,
|
|
26946
27371
|
"webhook:send": webhookSendCommand,
|
|
26947
27372
|
"coverage:summary": coverageSummaryCommand,
|
|
26948
27373
|
"coverage:check": coverageCheckCommand,
|
|
@@ -26976,7 +27401,8 @@ const bstSubCommands = {
|
|
|
26976
27401
|
"plan:brief": planBriefCommand,
|
|
26977
27402
|
"plan:review-package": planReviewPackageCommand,
|
|
26978
27403
|
"plan:status": planStatusCommand,
|
|
26979
|
-
"work:status": workStatusCommand
|
|
27404
|
+
"work:status": workStatusCommand,
|
|
27405
|
+
"work:start": workStartCommand
|
|
26980
27406
|
};
|
|
26981
27407
|
async function runBstCli(extraSubCommands = {}) {
|
|
26982
27408
|
await runCli({
|