@bensandee/tooling 0.86.0 → 0.87.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 +3 -3
- package/dist/bin.mjs +970 -842
- package/package.json +3 -3
- package/tooling.schema.json +16 -1
package/dist/bin.mjs
CHANGED
|
@@ -269,7 +269,7 @@ function loadEnvFileForBin(rawArgs, bin, io = processIo) {
|
|
|
269
269
|
*/
|
|
270
270
|
const envFileLoad = loadEnvFileForBin(process.argv.slice(2), {
|
|
271
271
|
name: "bst",
|
|
272
|
-
version: "0.
|
|
272
|
+
version: "0.87.0"
|
|
273
273
|
});
|
|
274
274
|
//#endregion
|
|
275
275
|
//#region ../cli-kit/src/logger.ts
|
|
@@ -1132,7 +1132,7 @@ const AsciiConfigSchema = z.strictObject({
|
|
|
1132
1132
|
allow: z.array(z.string()).optional().meta({ description: "Codepoints (as U+XXXX) to drop from the effective deny-list" }),
|
|
1133
1133
|
treatAsProse: z.array(z.string()).optional().meta({ description: "Path globs (matched the same way as exclude) whose files --fix classifies as prose wholesale regardless of extension, flattening rather than flagging. The opt-back-in for an extension with no dedicated lexical-context classifier where blind flattening is known safe. No built-in defaults." })
|
|
1134
1134
|
});
|
|
1135
|
-
const LintConfigSchema = z.strictObject({ strictTypeAware: z.boolean().optional().meta({ description: "Make
|
|
1135
|
+
const LintConfigSchema = z.strictObject({ strictTypeAware: z.boolean().optional().meta({ description: "Make a missing oxlint-tsgolint binary a hard failure for `bst lint:types` (the type-aware oxlint gate) rather than a notice (default false): arming it is what turns a platform gap into a build break, deliberately, because the repo that armed it is the one that asked for it. Findings block in every repo regardless of this setting." }) });
|
|
1136
1136
|
const ChecksConfigSchema = z.strictObject({
|
|
1137
1137
|
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." }),
|
|
1138
1138
|
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." }),
|
|
@@ -2310,6 +2310,18 @@ const LintSweepConfigSchema = z.strictObject({
|
|
|
2310
2310
|
minWarnings: z.number().int().nonnegative().optional().meta({ description: "How many oxlint warnings the default branch must carry before the sweep opens a pull request (default 5). Below the floor the warn marker on checks:run is the whole response." }),
|
|
2311
2311
|
cooldownDays: z.number().int().nonnegative().optional().meta({ description: "Days after a lint-sweep pull request merged or closed before the sweep may open another (default 7): a merged sweep whose declined warnings survive must not re-file them the next morning." })
|
|
2312
2312
|
});
|
|
2313
|
+
/**
|
|
2314
|
+
* `.tooling.json#agent.egress`: the extra egress hosts this repo's agent containers may reach.
|
|
2315
|
+
*
|
|
2316
|
+
* Only the browse lane (GET and HEAD) is repo-declarable. The passthrough allows every method,
|
|
2317
|
+
* so it stays agent-owned -- see the non-goals of the per-repo egress spec.
|
|
2318
|
+
*
|
|
2319
|
+
* The list is validated as a list of non-empty strings here, not against the pattern grammar:
|
|
2320
|
+
* that grammar lives in the agent package and the dependency edge runs agent -> tooling, never
|
|
2321
|
+
* back. The agent checks every pattern when it reads the key and refuses the run with the bad
|
|
2322
|
+
* pattern named, so `repo:sync` deliberately accepts a malformed pattern.
|
|
2323
|
+
*/
|
|
2324
|
+
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." }) });
|
|
2313
2325
|
const AgentConfigSchema = z.strictObject({
|
|
2314
2326
|
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)." }),
|
|
2315
2327
|
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." }),
|
|
@@ -2322,6 +2334,7 @@ const AgentConfigSchema = z.strictObject({
|
|
|
2322
2334
|
"low"
|
|
2323
2335
|
])).optional().meta({ description: "Map this repo's own issue-label names onto the agent's four priority levels, e.g. {\"P1\": \"critical\", \"P4\": \"low\"}. The map UNIONS with the built-in priority/<level> names rather than replacing them, and the agent does not create aliased labels -- the repo owns names the agent did not choose. Priority affects claim order only: it never changes a run's model, effort, or any budget." }),
|
|
2324
2336
|
triageAnalysis: z.boolean().optional().meta({ description: "Whether the agent may route its own agent-note/human-triage-needed issues through the agent/analyze stage (default: off). Unlike agent.backlog, which defaults on, this defaults off: a backlog run batches every pending finding into one container, while triage routing spends one container per issue -- arming it is the reversible direction, not disarming it." }),
|
|
2337
|
+
egress: AgentEgressConfigSchema.optional().meta({ description: "Extra egress hosts this repo's agent containers may reach. `browseHosts` joins the agent's browse lane (GET and HEAD only) on top of the hosts every run needs; the passthrough lane stays agent-owned. Each entry is a host pattern -- an exact host or a *.host wildcard, never a port. Read from the repo's DEFAULT BRANCH, never off a pull request's own tree, so a run can never widen its own egress: the declaration takes effect on the first container started after a human merges it. An entry whose pattern the agent cannot parse refuses the run rather than being dropped. Changing this block holds the pull request from automerge." }),
|
|
2325
2338
|
findingHolds: FindingHoldsConfigSchema.optional().meta({ description: "Which of a pull request's linked open findings withhold its unattended merge. `rules` is ordered and the first `when` that matches decides; no match falls through to `default`; an absent block is { rules: [], default: true }, so every open linked finding holds by default. Read off the repo's DEFAULT BRANCH, never off the pull request's own tree -- a pull request cannot edit the rule deciding whether it is held. In a `mentions` entry a /.../flags pattern is a regular expression, a literal:<text> pattern is the substring <text> (how to name a path such as /src/utils), and anything else is a case-insensitive substring." })
|
|
2326
2339
|
});
|
|
2327
2340
|
/**
|
|
@@ -2345,7 +2358,8 @@ const AgentConfigSchema = z.strictObject({
|
|
|
2345
2358
|
const TolerantAgentConfigSchema = z.object({
|
|
2346
2359
|
...AgentConfigSchema.shape,
|
|
2347
2360
|
lintSweep: tolerantBlock(LintSweepConfigSchema).optional(),
|
|
2348
|
-
findingHolds: z.unknown().optional()
|
|
2361
|
+
findingHolds: z.unknown().optional(),
|
|
2362
|
+
egress: z.unknown().optional()
|
|
2349
2363
|
});
|
|
2350
2364
|
const BranchPruneConfigSchema = z.strictObject({
|
|
2351
2365
|
mergedDays: z.number().int().nonnegative().optional().meta({ description: "Days after a work branch's PR merged before bst branch:prune deletes it (default 7). Zero is valid on purpose (an explicit 'delete everything merged already'), matching the --merged-days CLI flag's contract." }),
|
|
@@ -3061,7 +3075,7 @@ function managedFileHeader(comment, lines) {
|
|
|
3061
3075
|
return lines.map((line) => line === "" ? comment : `${comment} ${line}`).join("\n") + "\n";
|
|
3062
3076
|
}
|
|
3063
3077
|
/**
|
|
3064
|
-
* Header for generated CI workflow files (`ci.yml`, `release.yml
|
|
3078
|
+
* Header for generated CI workflow files (`ci.yml`, `release.yml`).
|
|
3065
3079
|
*
|
|
3066
3080
|
* The literal `@bensandee/tooling:ignore` directive is kept mid-line on purpose so
|
|
3067
3081
|
* `isToolingIgnored()` (which looks for the directive at the start of a comment) does
|
|
@@ -3242,21 +3256,33 @@ function parseContainerPortSegment(segment) {
|
|
|
3242
3256
|
const parsed = Number.parseInt(segment.trim(), 10);
|
|
3243
3257
|
return Number.isNaN(parsed) ? void 0 : parsed;
|
|
3244
3258
|
}
|
|
3259
|
+
/**
|
|
3260
|
+
* Split a port mapping on the `:` separators outside `${...}` substitutions, whose
|
|
3261
|
+
* defaults may contain `:` themselves (`${PORT:-8080}`, `${BIND_IP:-0.0.0.0}`).
|
|
3262
|
+
* Returns `undefined` for an unclosed substitution.
|
|
3263
|
+
*/
|
|
3264
|
+
function splitPortSegments(mapping) {
|
|
3265
|
+
const segments = [];
|
|
3266
|
+
let depth = 0;
|
|
3267
|
+
let start = 0;
|
|
3268
|
+
for (let i = 0; i < mapping.length; i++) if (mapping.startsWith("${", i)) {
|
|
3269
|
+
depth++;
|
|
3270
|
+
i++;
|
|
3271
|
+
} else if (mapping[i] === "}" && depth > 0) depth--;
|
|
3272
|
+
else if (mapping[i] === ":" && depth === 0) {
|
|
3273
|
+
segments.push(mapping.slice(start, i));
|
|
3274
|
+
start = i + 1;
|
|
3275
|
+
}
|
|
3276
|
+
if (depth !== 0) return void 0;
|
|
3277
|
+
segments.push(mapping.slice(start));
|
|
3278
|
+
return segments;
|
|
3279
|
+
}
|
|
3245
3280
|
/** Parse a single port mapping string and extract both the host and container ports. */
|
|
3246
3281
|
function parsePortString(port) {
|
|
3247
3282
|
const withoutProtocol = port.split("/")[0];
|
|
3248
3283
|
if (!withoutProtocol) return void 0;
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
if (close === -1) return void 0;
|
|
3252
|
-
const hostPort = parseHostPortSegment(withoutProtocol.slice(0, close + 1));
|
|
3253
|
-
const rest = withoutProtocol.slice(close + 1);
|
|
3254
|
-
return {
|
|
3255
|
-
hostPort,
|
|
3256
|
-
containerPort: rest.startsWith(":") ? parseContainerPortSegment(rest.slice(1)) : void 0
|
|
3257
|
-
};
|
|
3258
|
-
}
|
|
3259
|
-
const parts = withoutProtocol.split(":");
|
|
3284
|
+
const parts = splitPortSegments(withoutProtocol);
|
|
3285
|
+
if (!parts) return void 0;
|
|
3260
3286
|
if (parts.length === 2) return {
|
|
3261
3287
|
hostPort: parts[0] ? parseHostPortSegment(parts[0]) : void 0,
|
|
3262
3288
|
containerPort: parts[1] ? parseContainerPortSegment(parts[1]) : void 0
|
|
@@ -4065,6 +4091,77 @@ function tracedRunners(logger) {
|
|
|
4065
4091
|
function fileErrorCode(error) {
|
|
4066
4092
|
if (error instanceof Error && "code" in error && typeof error.code === "string") return error.code;
|
|
4067
4093
|
}
|
|
4094
|
+
function listWorkspacePackages(cwd) {
|
|
4095
|
+
const packagesDir = path.join(cwd, "packages");
|
|
4096
|
+
const packages = [];
|
|
4097
|
+
try {
|
|
4098
|
+
for (const entry of readdirSync(packagesDir)) {
|
|
4099
|
+
const pkgPath = path.join(packagesDir, entry, "package.json");
|
|
4100
|
+
try {
|
|
4101
|
+
const pkg = parsePackageJson(readFileSync(pkgPath, "utf-8"));
|
|
4102
|
+
if (pkg?.name && pkg.version) packages.push({
|
|
4103
|
+
name: pkg.name,
|
|
4104
|
+
version: pkg.version,
|
|
4105
|
+
dir: entry
|
|
4106
|
+
});
|
|
4107
|
+
} catch (_error) {}
|
|
4108
|
+
}
|
|
4109
|
+
} catch (_error) {}
|
|
4110
|
+
return packages;
|
|
4111
|
+
}
|
|
4112
|
+
function readRegularFile(filePath, maxBytes) {
|
|
4113
|
+
let fd;
|
|
4114
|
+
try {
|
|
4115
|
+
fd = openSync(filePath, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
4116
|
+
} catch (error) {
|
|
4117
|
+
const code = fileErrorCode(error);
|
|
4118
|
+
if (code === "ENOENT") return { kind: "absent" };
|
|
4119
|
+
if (code === "ELOOP") return {
|
|
4120
|
+
kind: "refused",
|
|
4121
|
+
reason: "symlink"
|
|
4122
|
+
};
|
|
4123
|
+
return {
|
|
4124
|
+
kind: "refused",
|
|
4125
|
+
reason: "unreadable"
|
|
4126
|
+
};
|
|
4127
|
+
}
|
|
4128
|
+
try {
|
|
4129
|
+
const stat = fstatSync(fd);
|
|
4130
|
+
if (!stat.isFile()) return {
|
|
4131
|
+
kind: "refused",
|
|
4132
|
+
reason: "not-regular"
|
|
4133
|
+
};
|
|
4134
|
+
if (stat.size > maxBytes) return {
|
|
4135
|
+
kind: "refused",
|
|
4136
|
+
reason: "too-large"
|
|
4137
|
+
};
|
|
4138
|
+
const buffer = Buffer.alloc(maxBytes + 1);
|
|
4139
|
+
let bytesRead;
|
|
4140
|
+
try {
|
|
4141
|
+
bytesRead = readSync(fd, buffer, 0, buffer.length, 0);
|
|
4142
|
+
} catch {
|
|
4143
|
+
return {
|
|
4144
|
+
kind: "refused",
|
|
4145
|
+
reason: "unreadable"
|
|
4146
|
+
};
|
|
4147
|
+
}
|
|
4148
|
+
if (bytesRead > maxBytes) return {
|
|
4149
|
+
kind: "refused",
|
|
4150
|
+
reason: "too-large"
|
|
4151
|
+
};
|
|
4152
|
+
return {
|
|
4153
|
+
kind: "ok",
|
|
4154
|
+
content: buffer.toString("utf8", 0, bytesRead)
|
|
4155
|
+
};
|
|
4156
|
+
} catch {
|
|
4157
|
+
return {
|
|
4158
|
+
kind: "refused",
|
|
4159
|
+
reason: "unreadable"
|
|
4160
|
+
};
|
|
4161
|
+
} finally {
|
|
4162
|
+
closeSync(fd);
|
|
4163
|
+
}
|
|
4164
|
+
}
|
|
4068
4165
|
/**
|
|
4069
4166
|
* Create an executor that runs real commands, fetches, and reads the filesystem. Subprocess and
|
|
4070
4167
|
* network calls are traced through `logger` (default: the {@link rootLogger}); see
|
|
@@ -4081,24 +4178,7 @@ function createRealExecutor$1(logger = rootLogger) {
|
|
|
4081
4178
|
return [];
|
|
4082
4179
|
}
|
|
4083
4180
|
},
|
|
4084
|
-
listWorkspacePackages
|
|
4085
|
-
const packagesDir = path.join(cwd, "packages");
|
|
4086
|
-
const packages = [];
|
|
4087
|
-
try {
|
|
4088
|
-
for (const entry of readdirSync(packagesDir)) {
|
|
4089
|
-
const pkgPath = path.join(packagesDir, entry, "package.json");
|
|
4090
|
-
try {
|
|
4091
|
-
const pkg = parsePackageJson(readFileSync(pkgPath, "utf-8"));
|
|
4092
|
-
if (pkg?.name && pkg.version) packages.push({
|
|
4093
|
-
name: pkg.name,
|
|
4094
|
-
version: pkg.version,
|
|
4095
|
-
dir: entry
|
|
4096
|
-
});
|
|
4097
|
-
} catch (_error) {}
|
|
4098
|
-
}
|
|
4099
|
-
} catch (_error) {}
|
|
4100
|
-
return packages;
|
|
4101
|
-
},
|
|
4181
|
+
listWorkspacePackages,
|
|
4102
4182
|
listPackageDirs(cwd) {
|
|
4103
4183
|
const packagesDir = path.join(cwd, "packages");
|
|
4104
4184
|
try {
|
|
@@ -4128,59 +4208,7 @@ function createRealExecutor$1(logger = rootLogger) {
|
|
|
4128
4208
|
return null;
|
|
4129
4209
|
}
|
|
4130
4210
|
},
|
|
4131
|
-
readRegularFile
|
|
4132
|
-
let fd;
|
|
4133
|
-
try {
|
|
4134
|
-
fd = openSync(filePath, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
4135
|
-
} catch (error) {
|
|
4136
|
-
const code = fileErrorCode(error);
|
|
4137
|
-
if (code === "ENOENT") return { kind: "absent" };
|
|
4138
|
-
if (code === "ELOOP") return {
|
|
4139
|
-
kind: "refused",
|
|
4140
|
-
reason: "symlink"
|
|
4141
|
-
};
|
|
4142
|
-
return {
|
|
4143
|
-
kind: "refused",
|
|
4144
|
-
reason: "unreadable"
|
|
4145
|
-
};
|
|
4146
|
-
}
|
|
4147
|
-
try {
|
|
4148
|
-
const stat = fstatSync(fd);
|
|
4149
|
-
if (!stat.isFile()) return {
|
|
4150
|
-
kind: "refused",
|
|
4151
|
-
reason: "not-regular"
|
|
4152
|
-
};
|
|
4153
|
-
if (stat.size > maxBytes) return {
|
|
4154
|
-
kind: "refused",
|
|
4155
|
-
reason: "too-large"
|
|
4156
|
-
};
|
|
4157
|
-
const buffer = Buffer.alloc(maxBytes + 1);
|
|
4158
|
-
let bytesRead;
|
|
4159
|
-
try {
|
|
4160
|
-
bytesRead = readSync(fd, buffer, 0, buffer.length, 0);
|
|
4161
|
-
} catch {
|
|
4162
|
-
return {
|
|
4163
|
-
kind: "refused",
|
|
4164
|
-
reason: "unreadable"
|
|
4165
|
-
};
|
|
4166
|
-
}
|
|
4167
|
-
if (bytesRead > maxBytes) return {
|
|
4168
|
-
kind: "refused",
|
|
4169
|
-
reason: "too-large"
|
|
4170
|
-
};
|
|
4171
|
-
return {
|
|
4172
|
-
kind: "ok",
|
|
4173
|
-
content: buffer.toString("utf8", 0, bytesRead)
|
|
4174
|
-
};
|
|
4175
|
-
} catch {
|
|
4176
|
-
return {
|
|
4177
|
-
kind: "refused",
|
|
4178
|
-
reason: "unreadable"
|
|
4179
|
-
};
|
|
4180
|
-
} finally {
|
|
4181
|
-
closeSync(fd);
|
|
4182
|
-
}
|
|
4183
|
-
},
|
|
4211
|
+
readRegularFile,
|
|
4184
4212
|
writeFile(filePath, content) {
|
|
4185
4213
|
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
4186
4214
|
writeFileSync(filePath, content);
|
|
@@ -5524,6 +5552,24 @@ function readPublishedImageVersion(executor, config, ref) {
|
|
|
5524
5552
|
debug(config, `Unexpected manifest shape for ${ref}; no version label read`);
|
|
5525
5553
|
}
|
|
5526
5554
|
/**
|
|
5555
|
+
* Name every check-only image this run is about to publish or has just left out -- one line
|
|
5556
|
+
* per image with no explicit `publish` setting, since an explicit value either way is the
|
|
5557
|
+
* repo having answered the question already. Under `warn` the image still goes out and the
|
|
5558
|
+
* line is the notice that a later tooling release stops doing that; under `enforce` it did
|
|
5559
|
+
* not go out and the line says so. A dry run takes both lines verbatim: it is the run a
|
|
5560
|
+
* release rehearses, so a rehearsed phase has to read like the real one.
|
|
5561
|
+
*/
|
|
5562
|
+
function reportCheckOnlyImages(packages, phase) {
|
|
5563
|
+
for (const pkg of packages) {
|
|
5564
|
+
if (pkg.checkOnlyIn === void 0 || pkg.docker.publish !== void 0) continue;
|
|
5565
|
+
if (phase === "warn") {
|
|
5566
|
+
log$1.warn(`Image "${pkg.imageName}" is declared only in ${pkg.checkOnlyIn}, the check overlay, so a later tooling release stops publishing it by default. Keep it: set publish to true for "${pkg.imageName}" under .tooling.json#docker. Settle it: set publish to false there, or move the service into a base compose file.`);
|
|
5567
|
+
continue;
|
|
5568
|
+
}
|
|
5569
|
+
log$1.info(`Skipped "${pkg.imageName}": it is declared only in ${pkg.checkOnlyIn}, the check overlay, so it was not pushed. Set publish to true for "${pkg.imageName}" under .tooling.json#docker to publish it anyway.`);
|
|
5570
|
+
}
|
|
5571
|
+
}
|
|
5572
|
+
/**
|
|
5527
5573
|
* Run the full Docker publish pipeline:
|
|
5528
5574
|
* 1. Build all images via runDockerBuild
|
|
5529
5575
|
* 2. Login to registry
|
|
@@ -5543,24 +5589,6 @@ function readPublishedImageVersion(executor, config, ref) {
|
|
|
5543
5589
|
* overlapping runs that both read a ref before either writes can still finish in the wrong order --
|
|
5544
5590
|
* seconds wide, where an unconditional push was open for a whole run.
|
|
5545
5591
|
*/
|
|
5546
|
-
/**
|
|
5547
|
-
* Name every check-only image this run is about to publish or has just left out -- one line
|
|
5548
|
-
* per image with no explicit `publish` setting, since an explicit value either way is the
|
|
5549
|
-
* repo having answered the question already. Under `warn` the image still goes out and the
|
|
5550
|
-
* line is the notice that a later tooling release stops doing that; under `enforce` it did
|
|
5551
|
-
* not go out and the line says so. A dry run takes both lines verbatim: it is the run a
|
|
5552
|
-
* release rehearses, so a rehearsed phase has to read like the real one.
|
|
5553
|
-
*/
|
|
5554
|
-
function reportCheckOnlyImages(packages, phase) {
|
|
5555
|
-
for (const pkg of packages) {
|
|
5556
|
-
if (pkg.checkOnlyIn === void 0 || pkg.docker.publish !== void 0) continue;
|
|
5557
|
-
if (phase === "warn") {
|
|
5558
|
-
log$1.warn(`Image "${pkg.imageName}" is declared only in ${pkg.checkOnlyIn}, the check overlay, so a later tooling release stops publishing it by default. Keep it: set publish to true for "${pkg.imageName}" under .tooling.json#docker. Settle it: set publish to false there, or move the service into a base compose file.`);
|
|
5559
|
-
continue;
|
|
5560
|
-
}
|
|
5561
|
-
log$1.info(`Skipped "${pkg.imageName}": it is declared only in ${pkg.checkOnlyIn}, the check overlay, so it was not pushed. Set publish to true for "${pkg.imageName}" under .tooling.json#docker to publish it anyway.`);
|
|
5562
|
-
}
|
|
5563
|
-
}
|
|
5564
5592
|
function runDockerPublish(executor, config) {
|
|
5565
5593
|
const checkOnlyPhase = config.checkOnlyPhase ?? "warn";
|
|
5566
5594
|
const { packages } = runDockerBuild(executor, {
|
|
@@ -5964,433 +5992,92 @@ function buildWorkflowYaml(options) {
|
|
|
5964
5992
|
return ensureSchemaComment(WORKFLOW_MANAGED_HEADER + "\n" + readable, options.ci);
|
|
5965
5993
|
}
|
|
5966
5994
|
//#endregion
|
|
5967
|
-
//#region src/
|
|
5968
|
-
const
|
|
5995
|
+
//#region src/generators/publish-ci.ts
|
|
5996
|
+
const RegistryHostSchema = z.object({ registryHost: z.string().optional() });
|
|
5969
5997
|
/**
|
|
5970
|
-
*
|
|
5971
|
-
*
|
|
5972
|
-
*
|
|
5998
|
+
* Resolve the docker-registry host at gen time so the workflow generator can
|
|
5999
|
+
* pick registry-specific secret names. Honors `.tooling.json#registryHost`
|
|
6000
|
+
* first, then git remote `origin` (the operational host, not a mirror-pointed
|
|
6001
|
+
* package.json), then derives from `package.json#repository` using the same
|
|
6002
|
+
* parser as the runtime (`parseGitUrl` handles https, git+https, and ssh shapes).
|
|
6003
|
+
* Returns null if neither source yields a host -- in that case the generator
|
|
6004
|
+
* falls back to neutral `REGISTRY_USERNAME` / `REGISTRY_PASSWORD` secrets and
|
|
6005
|
+
* lets the runtime resolve the host from `DOCKER_REGISTRY_HOST` (still emitted
|
|
6006
|
+
* into the step).
|
|
5973
6007
|
*/
|
|
5974
|
-
function
|
|
5975
|
-
|
|
5976
|
-
try {
|
|
5977
|
-
const
|
|
5978
|
-
|
|
5979
|
-
} catch {
|
|
5980
|
-
|
|
6008
|
+
function resolveGenTimeHost(ctx) {
|
|
6009
|
+
const toolingRaw = ctx.read(".tooling.json");
|
|
6010
|
+
if (toolingRaw) try {
|
|
6011
|
+
const parsed = RegistryHostSchema.safeParse(JSON.parse(toolingRaw));
|
|
6012
|
+
if (parsed.success && parsed.data.registryHost && parsed.data.registryHost.length > 0) return parsed.data.registryHost;
|
|
6013
|
+
} catch (error) {
|
|
6014
|
+
log$1.warn(`Failed to parse .tooling.json: ${error instanceof Error ? error.message : error}`);
|
|
5981
6015
|
}
|
|
6016
|
+
const originUrl = detectGitRemoteUrl(ctx.targetDir);
|
|
6017
|
+
if (originUrl !== null) {
|
|
6018
|
+
const parsedOrigin = parseGitUrl(originUrl);
|
|
6019
|
+
if (parsedOrigin) return registryHostFromHostname(parsedOrigin.hostname);
|
|
6020
|
+
}
|
|
6021
|
+
const raw = ctx.read("package.json");
|
|
6022
|
+
if (!raw) return null;
|
|
6023
|
+
const repo = parsePackageJson(raw)?.repository;
|
|
6024
|
+
let urlStr = null;
|
|
6025
|
+
if (typeof repo === "string") urlStr = repo;
|
|
6026
|
+
else if (repo && typeof repo === "object") urlStr = repo.url;
|
|
6027
|
+
if (typeof urlStr !== "string") return null;
|
|
6028
|
+
const parsed = parseGitUrl(urlStr);
|
|
6029
|
+
if (!parsed) return null;
|
|
6030
|
+
return registryHostFromHostname(parsed.hostname);
|
|
5982
6031
|
}
|
|
5983
|
-
|
|
5984
|
-
|
|
5985
|
-
|
|
6032
|
+
const PostReleaseHookSchema = z.object({
|
|
6033
|
+
postReleaseHook: z.string().optional(),
|
|
6034
|
+
postReleaseHookSecrets: z.array(z.string()).optional()
|
|
6035
|
+
});
|
|
6036
|
+
const ReleaseAssetsSchema = z.object({ releaseAssets: z.array(ReleaseAssetSchema).optional() });
|
|
6037
|
+
const VersionStampConfigSchema = z.object({ versionStamp: z.array(VersionStampSchema).optional() });
|
|
6038
|
+
const DeploySchema = z.object({ deploy: DeployConfigSchema.optional() });
|
|
5986
6039
|
/**
|
|
5987
|
-
*
|
|
5988
|
-
*
|
|
5989
|
-
* npm and pnpm use one `registry` key for reads and writes alike, so a project
|
|
5990
|
-
* `.npmrc` aimed at an install cache silently repoints `pnpm publish` too. A
|
|
5991
|
-
* package that never declares its destination inherits one, and an inherited
|
|
5992
|
-
* destination can be changed by a file that never mentions publishing.
|
|
6040
|
+
* Read the `deploy` block from .tooling.json, if configured.
|
|
5993
6041
|
*
|
|
5994
|
-
*
|
|
5995
|
-
*
|
|
5996
|
-
* exists to prevent.
|
|
6042
|
+
* The full file is already validated against ToolingConfigSchema at sync time,
|
|
6043
|
+
* so an invalid `deploy` block fails earlier -- undefined here means "not set".
|
|
5997
6044
|
*/
|
|
5998
|
-
function
|
|
5999
|
-
const
|
|
6000
|
-
|
|
6001
|
-
|
|
6002
|
-
|
|
6003
|
-
|
|
6004
|
-
|
|
6005
|
-
|
|
6006
|
-
|
|
6007
|
-
|
|
6008
|
-
kind: "missing",
|
|
6009
|
-
package: pkg.name,
|
|
6010
|
-
file,
|
|
6011
|
-
message: `${pkg.name} is published from this repo but declares no publishConfig.registry, so its release destination is inherited -- a project .npmrc can repoint it silently. In ${file}, ${FIX}.`
|
|
6012
|
-
});
|
|
6013
|
-
continue;
|
|
6014
|
-
}
|
|
6015
|
-
if (!isUsableRegistry(registry)) issues.push({
|
|
6016
|
-
kind: "malformed",
|
|
6017
|
-
package: pkg.name,
|
|
6018
|
-
file,
|
|
6019
|
-
message: `${pkg.name} declares publishConfig.registry as ${JSON.stringify(registry)}, which is not an absolute http(s) URL and cannot be published to. In ${file}, ${FIX}.`
|
|
6020
|
-
});
|
|
6045
|
+
function getDeployConfig(ctx) {
|
|
6046
|
+
const raw = ctx.read(".tooling.json");
|
|
6047
|
+
if (!raw) return void 0;
|
|
6048
|
+
try {
|
|
6049
|
+
const result = DeploySchema.safeParse(JSON.parse(raw));
|
|
6050
|
+
if (!result.success) return void 0;
|
|
6051
|
+
return result.data.deploy;
|
|
6052
|
+
} catch (error) {
|
|
6053
|
+
log$1.warn(`Failed to parse .tooling.json: ${error instanceof Error ? error.message : error}`);
|
|
6054
|
+
return;
|
|
6021
6055
|
}
|
|
6022
|
-
return issues;
|
|
6023
6056
|
}
|
|
6024
|
-
/**
|
|
6025
|
-
|
|
6026
|
-
|
|
6027
|
-
|
|
6028
|
-
|
|
6029
|
-
|
|
6030
|
-
|
|
6031
|
-
|
|
6032
|
-
|
|
6033
|
-
|
|
6034
|
-
|
|
6035
|
-
|
|
6036
|
-
|
|
6037
|
-
|
|
6038
|
-
|
|
6039
|
-
|
|
6040
|
-
|
|
6041
|
-
|
|
6042
|
-
|
|
6043
|
-
|
|
6044
|
-
|
|
6045
|
-
|
|
6046
|
-
|
|
6047
|
-
|
|
6048
|
-
* The mise version and release-asset digest the fleet's CI and the agent runner base image agree
|
|
6049
|
-
* to run. This is the one place that agreement is recorded; `packages/agent/test/runner-base-image-dockerfile.test.ts`
|
|
6050
|
-
* (task 8 of the mise-toolchain-provisioning plan) reads both constants back through
|
|
6051
|
-
* `readRepoFile` and asserts the base image's own `ARG MISE_VERSION` and `mise.sha256` manifest
|
|
6052
|
-
* agree with them, so the two packages cannot drift without a failing test on either side.
|
|
6053
|
-
*
|
|
6054
|
-
* Each constant sits under its own `// renovate:` marker comment, in the three-line grammar
|
|
6055
|
-
* `packages/agent/docker/agent-goose-image/goose.sha256` uses for its own digest line, so the same
|
|
6056
|
-
* shape of custom regex manager in the root `renovate.json` can rewrite both this version and this
|
|
6057
|
-
* digest (`customManagers` entries scoped to this file).
|
|
6058
|
-
*/
|
|
6059
|
-
const MISE_VERSION = "2026.10.2";
|
|
6060
|
-
const MISE_LINUX_X64_SHA256 = "8f5f6660336f572830e33cd9b378d3131e529a0d4c4f0c553776be90a1ba302a";
|
|
6061
|
-
/** Where generated Forgejo workflows fetch the shared mise setup action: a composite action in
|
|
6062
|
-
* `bensandee/common`, on the forge every fleet repo lives on. GitHub cannot reach it; the
|
|
6063
|
-
* generators keep inline steps there. */
|
|
6064
|
-
const SETUP_MISE_ACTION_URL = "https://code.orangebikelabs.com/bensandee/common/actions/setup-mise";
|
|
6065
|
-
/** The tag in `bensandee/common` that marks the current release of the action. Generated `uses:`
|
|
6066
|
-
* lines carry it as their comment, and Renovate moves the SHA below to wherever it points. */
|
|
6067
|
-
const SETUP_MISE_ACTION_TAG = "setup-mise@latest";
|
|
6068
|
-
const SETUP_MISE_ACTION_SHA = "13f60e0f19ad57b5616f94c9bc18bf214bbe91fe";
|
|
6069
|
-
//#endregion
|
|
6070
|
-
//#region src/generators/ci-utils.ts
|
|
6071
|
-
/** Build a GitHub Actions expression like `${{ expr }}` without triggering no-template-curly-in-string. */
|
|
6072
|
-
function actionsExpr(expr) {
|
|
6073
|
-
return `\${{ ${expr} }}`;
|
|
6074
|
-
}
|
|
6075
|
-
const MAIN_BRANCH_REF = "refs/heads/main";
|
|
6076
|
-
/** `if:` condition that gates a step to push events on the default branch. */
|
|
6077
|
-
const IS_MAIN_BRANCH = `github.ref == '${MAIN_BRANCH_REF}'`;
|
|
6078
|
-
/**
|
|
6079
|
-
* Negated form for concurrency `cancel-in-progress` and similar uses.
|
|
6080
|
-
*
|
|
6081
|
-
* Main is exempt from cancellation on purpose, and the exemption stays: a main run publishes and
|
|
6082
|
-
* pushes tags, and cancelling one mid-flight can leave a partial publish -- some packages on the
|
|
6083
|
-
* registry, no matching tags, and no run left alive to finish. A superseded feature-branch run
|
|
6084
|
-
* wastes minutes; a half-published release costs a manual unwind.
|
|
6085
|
-
*
|
|
6086
|
-
* So several main runs can be in flight at once, each versioning from its own triggering commit.
|
|
6087
|
-
* Overlap safety rests on one invariant: **main advances solely via commits that trigger a CI
|
|
6088
|
-
* run.** The `[skip ci]` coverage, health, and egress records go to `bst/*` orphan branches and
|
|
6089
|
-
* never to main; a `[skip ci]` commit pushed straight to main would leave a superseded run skipping
|
|
6090
|
-
* and its changesets stranded until the next push, which is why the supersession guard skips rather
|
|
6091
|
-
* than fails. Three guards carry the overlap itself: supersession plus a `--force-with-lease`
|
|
6092
|
-
* version push (`release/publish/version.ts`), the `headBumpedVersion` publish guard plus reading
|
|
6093
|
-
* remote tags and releases before acting (`commands/release-changesets.ts`,
|
|
6094
|
-
* `release/publish/publish.ts`), and floating git and Docker tags that only move forward
|
|
6095
|
-
* (`release/publish/floating-tags.ts`, `release/docker.ts`). Such a run may also version pending
|
|
6096
|
-
* changesets after it publishes -- the stale version-PR merge -- under the same supersession guard
|
|
6097
|
-
* and push lease as any other version run. A later commit publishes a bump only when its tag is
|
|
6098
|
-
* missing and every main run since the package's last release tag has settled
|
|
6099
|
-
* (`release/publish/unpublished-bump.ts`), so no run that could still publish it is in flight; a
|
|
6100
|
-
* manual re-run of one of those runs is the overlap left. On Forgejo the concurrency block is
|
|
6101
|
-
* only a PR-run cancellation mechanism -- it holds a main run in its group and, in Forgejo 16,
|
|
6102
|
-
* intermittently fails to release it, which is why main runs get a group of their own there. GitHub's shape stays shared
|
|
6103
|
-
* because its queue works: one run, one pending, a newer arrival replacing the pending, so
|
|
6104
|
-
* `changesets/action@v1` never needs a guard of its own. Any new side effect in a main run must be
|
|
6105
|
-
* idempotent under overlap or be listed here with the guard that makes it so.
|
|
6106
|
-
*/
|
|
6107
|
-
const IS_NOT_MAIN_BRANCH = `github.ref != '${MAIN_BRANCH_REF}'`;
|
|
6108
|
-
/**
|
|
6109
|
-
* Job-level `env:` block that evaluates the default-branch check once, so steps
|
|
6110
|
-
* can gate on `env.IS_MAIN` instead of repeating the `github.ref` expression.
|
|
6111
|
-
*/
|
|
6112
|
-
const IS_MAIN_JOB_ENV = { IS_MAIN: actionsExpr(IS_MAIN_BRANCH) };
|
|
6113
|
-
/** `if:` condition referencing the job-level `IS_MAIN` env var (see {@link IS_MAIN_JOB_ENV}). */
|
|
6114
|
-
const IS_MAIN_ENV_GUARD = "env.IS_MAIN == 'true'";
|
|
6115
|
-
/**
|
|
6116
|
-
* Job-level default for the npm auth token, declared as the empty string.
|
|
6117
|
-
*
|
|
6118
|
-
* This repo's own `Configure npm auth` step (see {@link npmAuthWiring}) appends
|
|
6119
|
-
* `//<registry>/:_authToken=${NODE_AUTH_TOKEN}` lines to `~/.npmrc` for each
|
|
6120
|
-
* declared `publishConfig.registry`. Only the publishing step defines that
|
|
6121
|
-
* variable, so every earlier pnpm invocation -- install, and each nested pnpm
|
|
6122
|
-
* inside `ci:check` -- hits an undefined var and prints
|
|
6123
|
-
* `[WARN] Failed to replace env in config: ${NODE_AUTH_TOKEN}`. Declaring it
|
|
6124
|
-
* empty at job level makes the interpolation succeed and the warning vanish; an
|
|
6125
|
-
* empty token is not sent as credentials, so anonymous registry reads are
|
|
6126
|
-
* unaffected.
|
|
6127
|
-
*
|
|
6128
|
-
* Deliberately empty rather than the real secret: the publishing step overrides
|
|
6129
|
-
* it with `secrets.NPM_TOKEN` via step-level `env`, which keeps the token out of
|
|
6130
|
-
* `pnpm install` (and any dependency's postinstall script) the way it is today.
|
|
6131
|
-
* The empty default also has to survive steps *after* the publish step
|
|
6132
|
-
* (`release:assets`, `docker:publish`, deploy dispatch): those shell out to pnpm
|
|
6133
|
-
* again without the secret in scope, so without this default they would warn too.
|
|
6134
|
-
*
|
|
6135
|
-
* Emit this in lockstep with the `Configure npm auth` step -- both come from the
|
|
6136
|
-
* same {@link npmAuthWiring} call, so a job that gets one gets the other.
|
|
6137
|
-
*/
|
|
6138
|
-
const NPM_AUTH_JOB_ENV = { NODE_AUTH_TOKEN: "" };
|
|
6139
|
-
/**
|
|
6140
|
-
* npm's own registry-key rule for an `.npmrc` auth line: strip the scheme, keep
|
|
6141
|
-
* host/port/path, and end with exactly one slash. `actions/setup-node` derives the
|
|
6142
|
-
* same key from a registry URL; this mirrors that so a hand-written line matches
|
|
6143
|
-
* what npm looks up at publish time -- npm-registry-fetch walks the request path
|
|
6144
|
-
* upward through ancestor keys until one matches.
|
|
6145
|
-
*/
|
|
6146
|
-
function npmrcAuthKey(registryUrl) {
|
|
6147
|
-
return `${registryUrl.replace(/^https?:/i, "").replace(/\/+$/, "")}/`;
|
|
6148
|
-
}
|
|
6149
|
-
const NODE_AUTH_TOKEN_VAR = ["$", "{NODE_AUTH_TOKEN}"].join("");
|
|
6150
|
-
/**
|
|
6151
|
-
* Build every half of a job's npm auth wiring together, so one value decides all
|
|
6152
|
-
* three: the `Configure npm auth` step, the job-level `NODE_AUTH_TOKEN` default,
|
|
6153
|
-
* and the step-level `NODE_AUTH_TOKEN` the publish step should carry. A job that
|
|
6154
|
-
* gets none of the declared registries gets none of the three -- including a
|
|
6155
|
-
* `publishes: true` job whose publishable set is empty (every package private),
|
|
6156
|
-
* since there is then no npmrc line for a real secret to pair with.
|
|
6157
|
-
*/
|
|
6158
|
-
function npmAuthWiring(ctx, publishes) {
|
|
6159
|
-
const registries = publishes ? declaredPublishRegistries(ctx.targetDir, ctx.config.structure, ctx.packageJson) : [];
|
|
6160
|
-
if (registries.length === 0) return {
|
|
6161
|
-
steps: [],
|
|
6162
|
-
jobEnv: {},
|
|
6163
|
-
tokenEnv: {}
|
|
6164
|
-
};
|
|
6165
|
-
return {
|
|
6166
|
-
steps: [{ step: {
|
|
6167
|
-
name: "Configure npm auth",
|
|
6168
|
-
run: registries.map((registry) => `printf '\\n%s\\n' '${npmrcAuthKey(registry)}:_authToken=${NODE_AUTH_TOKEN_VAR}' >> ~/.npmrc`).join("\n")
|
|
6169
|
-
} }],
|
|
6170
|
-
jobEnv: NPM_AUTH_JOB_ENV,
|
|
6171
|
-
tokenEnv: { NODE_AUTH_TOKEN: actionsExpr("secrets.NPM_TOKEN") }
|
|
6172
|
-
};
|
|
6173
|
-
}
|
|
6174
|
-
const MISE_RELEASE_URL = `https://github.com/jdx/mise/releases/download/v${MISE_VERSION}/mise-v${MISE_VERSION}-linux-x64`;
|
|
6175
|
-
/**
|
|
6176
|
-
* The node script `inlineMiseSteps` pipes `mise env --json` into, run under the mise it just
|
|
6177
|
-
* installed. Reads the env map off stdin (mise exec passes it straight through) and exports every
|
|
6178
|
-
* key except `PATH` into `$GITHUB_ENV` in heredoc form -- the form that survives any value,
|
|
6179
|
-
* including one containing `$` or quotes -- then appends mise's shim directory and the
|
|
6180
|
-
* `~/.local/bin` install location to `$GITHUB_PATH` so every later step sees node, pnpm, and any
|
|
6181
|
-
* other declared tool on PATH with no prefix.
|
|
6182
|
-
*/
|
|
6183
|
-
const MISE_ENV_EXPORT_SCRIPT = [
|
|
6184
|
-
"const fs = require(\"fs\");",
|
|
6185
|
-
"const env = JSON.parse(fs.readFileSync(0, \"utf8\"));",
|
|
6186
|
-
"let out = \"\";",
|
|
6187
|
-
"for (const [key, value] of Object.entries(env)) {",
|
|
6188
|
-
" if (key === \"PATH\") continue;",
|
|
6189
|
-
" out += key + \"<<BST_MISE_ENV_EOF\\n\" + value + \"\\nBST_MISE_ENV_EOF\\n\";",
|
|
6190
|
-
"}",
|
|
6191
|
-
"fs.appendFileSync(process.env.GITHUB_ENV, out);",
|
|
6192
|
-
"fs.appendFileSync(",
|
|
6193
|
-
" process.env.GITHUB_PATH,",
|
|
6194
|
-
" process.env.HOME + \"/.local/share/mise/shims\\n\" + process.env.HOME + \"/.local/bin\\n\",",
|
|
6195
|
-
");"
|
|
6196
|
-
].join("\n");
|
|
6197
|
-
/** A fresh object per call: one shared object in two jobs would serialize as a YAML alias. */
|
|
6198
|
-
function pnpmStoreCacheStep() {
|
|
6199
|
-
return { step: {
|
|
6200
|
-
name: "Cache pnpm store",
|
|
6201
|
-
uses: "actions/cache@v4",
|
|
6202
|
-
with: {
|
|
6203
|
-
path: "~/.local/share/pnpm/store",
|
|
6204
|
-
key: `pnpm-${actionsExpr("runner.os")}-${actionsExpr("hashFiles('pnpm-lock.yaml')")}`
|
|
6205
|
-
}
|
|
6206
|
-
} };
|
|
6207
|
-
}
|
|
6208
|
-
/** Actions whose ref the generator pins itself, so `preservePinnedActions` must not keep a
|
|
6209
|
-
* repo's older ref for them. */
|
|
6210
|
-
const GENERATOR_OWNED_ACTIONS = [{
|
|
6211
|
-
name: SETUP_MISE_ACTION_URL,
|
|
6212
|
-
comment: SETUP_MISE_ACTION_TAG
|
|
6213
|
-
}];
|
|
6214
|
-
/**
|
|
6215
|
-
* The steps every workflow generator uses in place of `pnpm/action-setup` + `actions/setup-node`:
|
|
6216
|
-
* install the pinned `mise`, `mise install` the repo's `mise.toml` toolchain, export its env into
|
|
6217
|
-
* the rest of the job, and cache the pnpm store. `MISE_VERSION`/`MISE_LINUX_X64_SHA256`
|
|
6218
|
-
* (`mise-pin.ts`) are the same pin the agent runner's base image installs, checked by
|
|
6219
|
-
* `runner-base-image-dockerfile.test.ts`.
|
|
6220
|
-
*
|
|
6221
|
-
* On Forgejo the mise half is the shared composite action in `bensandee/common`, pinned by commit.
|
|
6222
|
-
* GitHub resolves actions only from github.com, so it keeps the same steps inline.
|
|
6223
|
-
*/
|
|
6224
|
-
function miseSetupSteps(ci) {
|
|
6225
|
-
if (ci !== "forgejo") return inlineMiseSteps();
|
|
6226
|
-
return [{ step: {
|
|
6227
|
-
name: "Set up mise",
|
|
6228
|
-
uses: `${SETUP_MISE_ACTION_URL}@${SETUP_MISE_ACTION_SHA}`,
|
|
6229
|
-
with: {
|
|
6230
|
-
version: MISE_VERSION,
|
|
6231
|
-
sha256: MISE_LINUX_X64_SHA256
|
|
6232
|
-
}
|
|
6233
|
-
} }, pnpmStoreCacheStep()];
|
|
6234
|
-
}
|
|
6235
|
-
/**
|
|
6236
|
-
* The inline form of the shared action, for GitHub. Uses `mise env --json` piped into the
|
|
6237
|
-
* just-installed mise's own `node -e`, not `mise env --shell bash`: the JSON form is
|
|
6238
|
-
* shell-independent and the interpreter reading it is guaranteed to exist (mise just installed
|
|
6239
|
-
* it), so this needs no assumption about which shell the runner's `run:` steps use.
|
|
6240
|
-
*/
|
|
6241
|
-
function inlineMiseSteps() {
|
|
6242
|
-
const installScript = [
|
|
6243
|
-
"curl -fsSL -o \"$RUNNER_TEMP/mise\" " + MISE_RELEASE_URL,
|
|
6244
|
-
`echo "${MISE_LINUX_X64_SHA256} $RUNNER_TEMP/mise" | sha256sum -c`,
|
|
6245
|
-
"mkdir -p \"$HOME/.local/bin\" && install -m 0755 \"$RUNNER_TEMP/mise\" \"$HOME/.local/bin/mise\"",
|
|
6246
|
-
"export MISE_TRUSTED_CONFIG_PATHS=\"$GITHUB_WORKSPACE\"",
|
|
6247
|
-
"export MISE_YES=1",
|
|
6248
|
-
"\"$HOME/.local/bin/mise\" install",
|
|
6249
|
-
`"$HOME/.local/bin/mise" env --json | "$HOME/.local/bin/mise" exec -- node -e '${MISE_ENV_EXPORT_SCRIPT}'`
|
|
6250
|
-
].join("\n");
|
|
6251
|
-
return [
|
|
6252
|
-
{ step: {
|
|
6253
|
-
name: "Cache mise tools",
|
|
6254
|
-
uses: "actions/cache@v4",
|
|
6255
|
-
with: {
|
|
6256
|
-
path: "~/.local/share/mise",
|
|
6257
|
-
key: `mise-${actionsExpr("runner.os")}-${actionsExpr("hashFiles('mise.toml')")}`
|
|
6258
|
-
}
|
|
6259
|
-
} },
|
|
6260
|
-
pnpmStoreCacheStep(),
|
|
6261
|
-
{ step: {
|
|
6262
|
-
name: "Install mise",
|
|
6263
|
-
env: { GITHUB_TOKEN: "" },
|
|
6264
|
-
run: installScript
|
|
6265
|
-
} }
|
|
6266
|
-
];
|
|
6267
|
-
}
|
|
6268
|
-
/**
|
|
6269
|
-
* Build the docker publish env block.
|
|
6270
|
-
*
|
|
6271
|
-
* The LHS env-var names (`DOCKER_USERNAME` / `DOCKER_PASSWORD`) match what
|
|
6272
|
-
* publish-docker.ts reads at runtime. The RHS secret references are
|
|
6273
|
-
* registry-specific (see {@link dockerSecretNames}) so downstream repos can
|
|
6274
|
-
* stop relying on the user/org-level `DOCKER_USERNAME` / `DOCKER_PASSWORD`
|
|
6275
|
-
* secrets -- which the Forgejo act fork auto-injects into every `container:`
|
|
6276
|
-
* image pull regardless of the target registry.
|
|
6277
|
-
*
|
|
6278
|
-
* When `host` is `null` (couldn't be derived at gen time), the secret references
|
|
6279
|
-
* fall back to neutral `REGISTRY_USERNAME` / `REGISTRY_PASSWORD` and the
|
|
6280
|
-
* workflow keeps `DOCKER_REGISTRY_HOST` / `DOCKER_REGISTRY_NAMESPACE` so the CLI
|
|
6281
|
-
* resolves the registry at runtime.
|
|
6282
|
-
*/
|
|
6283
|
-
function dockerPublishEnv(host) {
|
|
6284
|
-
const { username, password } = dockerSecretNames(host);
|
|
6285
|
-
return {
|
|
6286
|
-
...host === null ? {
|
|
6287
|
-
DOCKER_REGISTRY_HOST: actionsExpr("vars.DOCKER_REGISTRY_HOST"),
|
|
6288
|
-
DOCKER_REGISTRY_NAMESPACE: actionsExpr("vars.DOCKER_REGISTRY_NAMESPACE")
|
|
6289
|
-
} : {},
|
|
6290
|
-
DOCKER_USERNAME: actionsExpr(`secrets.${username}`),
|
|
6291
|
-
DOCKER_PASSWORD: actionsExpr(`secrets.${password}`),
|
|
6292
|
-
RELEASE_DEBUG: actionsExpr("vars.RELEASE_DEBUG || 'false'")
|
|
6293
|
-
};
|
|
6294
|
-
}
|
|
6295
|
-
//#endregion
|
|
6296
|
-
//#region src/generators/publish-ci.ts
|
|
6297
|
-
const RegistryHostSchema = z.object({ registryHost: z.string().optional() });
|
|
6298
|
-
/**
|
|
6299
|
-
* Resolve the docker-registry host at gen time so the workflow generator can
|
|
6300
|
-
* pick registry-specific secret names. Honors `.tooling.json#registryHost`
|
|
6301
|
-
* first, then git remote `origin` (the operational host, not a mirror-pointed
|
|
6302
|
-
* package.json), then derives from `package.json#repository` using the same
|
|
6303
|
-
* parser as the runtime (`parseGitUrl` handles https, git+https, and ssh shapes).
|
|
6304
|
-
* Returns null if neither source yields a host -- in that case the generator
|
|
6305
|
-
* falls back to neutral `REGISTRY_USERNAME` / `REGISTRY_PASSWORD` secrets and
|
|
6306
|
-
* lets the runtime resolve the host from `DOCKER_REGISTRY_HOST` (still emitted
|
|
6307
|
-
* into the step).
|
|
6308
|
-
*/
|
|
6309
|
-
function resolveGenTimeHost(ctx) {
|
|
6310
|
-
const toolingRaw = ctx.read(".tooling.json");
|
|
6311
|
-
if (toolingRaw) try {
|
|
6312
|
-
const parsed = RegistryHostSchema.safeParse(JSON.parse(toolingRaw));
|
|
6313
|
-
if (parsed.success && parsed.data.registryHost && parsed.data.registryHost.length > 0) return parsed.data.registryHost;
|
|
6314
|
-
} catch (error) {
|
|
6315
|
-
log$1.warn(`Failed to parse .tooling.json: ${error instanceof Error ? error.message : error}`);
|
|
6316
|
-
}
|
|
6317
|
-
const originUrl = detectGitRemoteUrl(ctx.targetDir);
|
|
6318
|
-
if (originUrl !== null) {
|
|
6319
|
-
const parsedOrigin = parseGitUrl(originUrl);
|
|
6320
|
-
if (parsedOrigin) return registryHostFromHostname(parsedOrigin.hostname);
|
|
6321
|
-
}
|
|
6322
|
-
const raw = ctx.read("package.json");
|
|
6323
|
-
if (!raw) return null;
|
|
6324
|
-
const repo = parsePackageJson(raw)?.repository;
|
|
6325
|
-
let urlStr = null;
|
|
6326
|
-
if (typeof repo === "string") urlStr = repo;
|
|
6327
|
-
else if (repo && typeof repo === "object") urlStr = repo.url;
|
|
6328
|
-
if (typeof urlStr !== "string") return null;
|
|
6329
|
-
const parsed = parseGitUrl(urlStr);
|
|
6330
|
-
if (!parsed) return null;
|
|
6331
|
-
return registryHostFromHostname(parsed.hostname);
|
|
6332
|
-
}
|
|
6333
|
-
function publishSteps(ci, host) {
|
|
6334
|
-
return [
|
|
6335
|
-
{ step: { uses: "actions/checkout@v6" } },
|
|
6336
|
-
...miseSetupSteps(ci),
|
|
6337
|
-
{ step: { run: "pnpm install --frozen-lockfile" } },
|
|
6338
|
-
{ step: {
|
|
6339
|
-
name: "Publish Docker images",
|
|
6340
|
-
env: dockerPublishEnv(host),
|
|
6341
|
-
run: "pnpm exec bst docker:publish"
|
|
6342
|
-
} }
|
|
6343
|
-
];
|
|
6344
|
-
}
|
|
6345
|
-
const PostReleaseHookSchema = z.object({
|
|
6346
|
-
postReleaseHook: z.string().optional(),
|
|
6347
|
-
postReleaseHookSecrets: z.array(z.string()).optional()
|
|
6348
|
-
});
|
|
6349
|
-
const ReleaseAssetsSchema = z.object({ releaseAssets: z.array(ReleaseAssetSchema).optional() });
|
|
6350
|
-
const VersionStampConfigSchema = z.object({ versionStamp: z.array(VersionStampSchema).optional() });
|
|
6351
|
-
const DeploySchema = z.object({ deploy: DeployConfigSchema.optional() });
|
|
6352
|
-
/**
|
|
6353
|
-
* Read the `deploy` block from .tooling.json, if configured.
|
|
6354
|
-
*
|
|
6355
|
-
* The full file is already validated against ToolingConfigSchema at sync time,
|
|
6356
|
-
* so an invalid `deploy` block fails earlier -- undefined here means "not set".
|
|
6357
|
-
*/
|
|
6358
|
-
function getDeployConfig(ctx) {
|
|
6359
|
-
const raw = ctx.read(".tooling.json");
|
|
6360
|
-
if (!raw) return void 0;
|
|
6361
|
-
try {
|
|
6362
|
-
const result = DeploySchema.safeParse(JSON.parse(raw));
|
|
6363
|
-
if (!result.success) return void 0;
|
|
6364
|
-
return result.data.deploy;
|
|
6365
|
-
} catch (error) {
|
|
6366
|
-
log$1.warn(`Failed to parse .tooling.json: ${error instanceof Error ? error.message : error}`);
|
|
6367
|
-
return;
|
|
6368
|
-
}
|
|
6369
|
-
}
|
|
6370
|
-
/** Read the postReleaseHook command override from .tooling.json, if configured. */
|
|
6371
|
-
function getPostReleaseHookCommand(ctx) {
|
|
6372
|
-
const raw = ctx.read(".tooling.json");
|
|
6373
|
-
if (!raw) return void 0;
|
|
6374
|
-
try {
|
|
6375
|
-
const result = PostReleaseHookSchema.safeParse(JSON.parse(raw));
|
|
6376
|
-
if (!result.success) return void 0;
|
|
6377
|
-
return result.data.postReleaseHook;
|
|
6378
|
-
} catch (error) {
|
|
6379
|
-
log$1.warn(`Failed to parse .tooling.json: ${error instanceof Error ? error.message : error}`);
|
|
6380
|
-
return;
|
|
6381
|
-
}
|
|
6382
|
-
}
|
|
6383
|
-
/** Read the postReleaseHookSecrets array from .tooling.json, if configured. */
|
|
6384
|
-
function getPostReleaseHookSecrets(ctx) {
|
|
6385
|
-
const raw = ctx.read(".tooling.json");
|
|
6386
|
-
if (!raw) return void 0;
|
|
6387
|
-
try {
|
|
6388
|
-
const result = PostReleaseHookSchema.safeParse(JSON.parse(raw));
|
|
6389
|
-
if (!result.success) return void 0;
|
|
6390
|
-
return result.data.postReleaseHookSecrets;
|
|
6391
|
-
} catch (error) {
|
|
6392
|
-
log$1.warn(`Failed to parse .tooling.json: ${error instanceof Error ? error.message : error}`);
|
|
6393
|
-
return;
|
|
6057
|
+
/** Read the postReleaseHook command override from .tooling.json, if configured. */
|
|
6058
|
+
function getPostReleaseHookCommand(ctx) {
|
|
6059
|
+
const raw = ctx.read(".tooling.json");
|
|
6060
|
+
if (!raw) return void 0;
|
|
6061
|
+
try {
|
|
6062
|
+
const result = PostReleaseHookSchema.safeParse(JSON.parse(raw));
|
|
6063
|
+
if (!result.success) return void 0;
|
|
6064
|
+
return result.data.postReleaseHook;
|
|
6065
|
+
} catch (error) {
|
|
6066
|
+
log$1.warn(`Failed to parse .tooling.json: ${error instanceof Error ? error.message : error}`);
|
|
6067
|
+
return;
|
|
6068
|
+
}
|
|
6069
|
+
}
|
|
6070
|
+
/** Read the postReleaseHookSecrets array from .tooling.json, if configured. */
|
|
6071
|
+
function getPostReleaseHookSecrets(ctx) {
|
|
6072
|
+
const raw = ctx.read(".tooling.json");
|
|
6073
|
+
if (!raw) return void 0;
|
|
6074
|
+
try {
|
|
6075
|
+
const result = PostReleaseHookSchema.safeParse(JSON.parse(raw));
|
|
6076
|
+
if (!result.success) return void 0;
|
|
6077
|
+
return result.data.postReleaseHookSecrets;
|
|
6078
|
+
} catch (error) {
|
|
6079
|
+
log$1.warn(`Failed to parse .tooling.json: ${error instanceof Error ? error.message : error}`);
|
|
6080
|
+
return;
|
|
6394
6081
|
}
|
|
6395
6082
|
}
|
|
6396
6083
|
/**
|
|
@@ -6462,61 +6149,37 @@ function hasVersionStamp(ctx) {
|
|
|
6462
6149
|
}
|
|
6463
6150
|
/** Where `ctx.remove()` puts what it archives. Named here only to describe it to the user. */
|
|
6464
6151
|
const ARCHIVE_DIR$2 = ".tooling-archived";
|
|
6152
|
+
/**
|
|
6153
|
+
* Archive the manual `publish.yml` this generator used to write; it writes nothing. Docker
|
|
6154
|
+
* images publish from the release workflow, and the manual workflow was a thin wrapper around
|
|
6155
|
+
* `bst docker:publish` that nobody ran -- run that command instead. Only a file carrying the
|
|
6156
|
+
* managed header is removed: a hand-written `publish.yml`, or one opted out with the ignore
|
|
6157
|
+
* comment, belongs to the repo.
|
|
6158
|
+
*/
|
|
6465
6159
|
async function generateDeployCi(ctx) {
|
|
6466
6160
|
const filePath = "deploy-ci";
|
|
6467
|
-
if (
|
|
6161
|
+
if (ctx.config.ci === "none") return {
|
|
6468
6162
|
filePath,
|
|
6469
6163
|
action: "skipped",
|
|
6470
|
-
description: "
|
|
6164
|
+
description: "Publish workflow not applicable"
|
|
6471
6165
|
};
|
|
6472
|
-
const
|
|
6473
|
-
const
|
|
6474
|
-
if (
|
|
6475
|
-
|
|
6476
|
-
|
|
6477
|
-
|
|
6478
|
-
|
|
6479
|
-
|
|
6480
|
-
|
|
6481
|
-
|
|
6482
|
-
|
|
6483
|
-
|
|
6484
|
-
|
|
6485
|
-
action: "skipped",
|
|
6486
|
-
description: "Publish workflow not generated for simple release strategy"
|
|
6487
|
-
};
|
|
6488
|
-
}
|
|
6489
|
-
const host = resolveGenTimeHost(ctx);
|
|
6490
|
-
const steps = publishSteps(ctx.config.ci, host);
|
|
6491
|
-
const content = buildWorkflowYaml({
|
|
6492
|
-
ci: ctx.config.ci,
|
|
6493
|
-
name: "Publish",
|
|
6494
|
-
on: { workflow_dispatch: null },
|
|
6495
|
-
jobName: "publish",
|
|
6496
|
-
steps
|
|
6497
|
-
});
|
|
6498
|
-
const alreadyExists = ctx.exists(workflowPath);
|
|
6499
|
-
const existing = alreadyExists ? ctx.read(workflowPath) : void 0;
|
|
6500
|
-
const withDevPath = applyWorkspaceToolingBuild(content, ctx.packageJson);
|
|
6501
|
-
if (existing) {
|
|
6502
|
-
if (isToolingIgnored(existing)) return {
|
|
6503
|
-
filePath: workflowPath,
|
|
6504
|
-
action: "skipped",
|
|
6505
|
-
description: "Publish workflow has ignore comment"
|
|
6506
|
-
};
|
|
6507
|
-
if (existing.includes("Managed by @bensandee/tooling --") && normalizeWorkflow(existing) === normalizeWorkflow(withDevPath)) return {
|
|
6508
|
-
filePath: workflowPath,
|
|
6509
|
-
action: "skipped",
|
|
6510
|
-
description: "Publish workflow already up to date"
|
|
6511
|
-
};
|
|
6512
|
-
}
|
|
6513
|
-
const withPinnedActions = preservePinnedActions(withDevPath, existing, GENERATOR_OWNED_ACTIONS);
|
|
6514
|
-
const final = existing ? insertCustomBlocks(withPinnedActions, extractCustomBlocks(existing)) : withPinnedActions;
|
|
6515
|
-
ctx.write(workflowPath, final);
|
|
6166
|
+
const workflowPath = ctx.config.ci === "github" ? ".github/workflows/publish.yml" : ".forgejo/workflows/publish.yml";
|
|
6167
|
+
const existing = ctx.read(workflowPath);
|
|
6168
|
+
if (existing === void 0) return {
|
|
6169
|
+
filePath,
|
|
6170
|
+
action: "skipped",
|
|
6171
|
+
description: "No publish workflow to retire"
|
|
6172
|
+
};
|
|
6173
|
+
if (isToolingIgnored(existing) || !existing.includes("Managed by @bensandee/tooling --")) return {
|
|
6174
|
+
filePath: workflowPath,
|
|
6175
|
+
action: "skipped",
|
|
6176
|
+
description: "Publish workflow is not managed by tooling"
|
|
6177
|
+
};
|
|
6178
|
+
ctx.remove(workflowPath);
|
|
6516
6179
|
return {
|
|
6517
6180
|
filePath: workflowPath,
|
|
6518
|
-
action:
|
|
6519
|
-
description: `
|
|
6181
|
+
action: "removed",
|
|
6182
|
+
description: `Moved to ${ARCHIVE_DIR$2}/${workflowPath} (run \`bst docker:publish\` to publish by hand)`
|
|
6520
6183
|
};
|
|
6521
6184
|
}
|
|
6522
6185
|
//#endregion
|
|
@@ -6911,60 +6574,60 @@ function perPackageDevDeps(config) {
|
|
|
6911
6574
|
deps["typescript"] = {
|
|
6912
6575
|
"@changesets/cli": "3.0.3",
|
|
6913
6576
|
"@release-it/bumper": "8.0.1",
|
|
6914
|
-
"@types/node": "24.
|
|
6915
|
-
"@vitest/coverage-v8": "5.0.
|
|
6577
|
+
"@types/node": "24.19.1",
|
|
6578
|
+
"@vitest/coverage-v8": "5.0.3",
|
|
6916
6579
|
"commit-and-tag-version": "13.2.1",
|
|
6917
|
-
"knip": "6.
|
|
6918
|
-
"lefthook": "2.1.
|
|
6580
|
+
"knip": "6.39.0",
|
|
6581
|
+
"lefthook": "2.1.16",
|
|
6919
6582
|
"modern-web-guidance": "0.0.191",
|
|
6920
6583
|
"oxfmt": "0.71.0",
|
|
6921
|
-
"oxlint": "1.
|
|
6584
|
+
"oxlint": "1.86.0",
|
|
6922
6585
|
"oxlint-tsgolint": "7.0.2003",
|
|
6923
6586
|
"prettier": "3.9.9",
|
|
6924
6587
|
"release-it": "21.1.0",
|
|
6925
6588
|
"tsdown": "0.23.0",
|
|
6926
6589
|
"typescript": "6.0.3",
|
|
6927
|
-
"vitest": "5.0.
|
|
6928
|
-
"pnpm": "11.
|
|
6590
|
+
"vitest": "5.0.3",
|
|
6591
|
+
"pnpm": "11.28.3"
|
|
6929
6592
|
}["typescript"];
|
|
6930
6593
|
deps["@types/node"] = {
|
|
6931
6594
|
"@changesets/cli": "3.0.3",
|
|
6932
6595
|
"@release-it/bumper": "8.0.1",
|
|
6933
|
-
"@types/node": "24.
|
|
6934
|
-
"@vitest/coverage-v8": "5.0.
|
|
6596
|
+
"@types/node": "24.19.1",
|
|
6597
|
+
"@vitest/coverage-v8": "5.0.3",
|
|
6935
6598
|
"commit-and-tag-version": "13.2.1",
|
|
6936
|
-
"knip": "6.
|
|
6937
|
-
"lefthook": "2.1.
|
|
6599
|
+
"knip": "6.39.0",
|
|
6600
|
+
"lefthook": "2.1.16",
|
|
6938
6601
|
"modern-web-guidance": "0.0.191",
|
|
6939
6602
|
"oxfmt": "0.71.0",
|
|
6940
|
-
"oxlint": "1.
|
|
6603
|
+
"oxlint": "1.86.0",
|
|
6941
6604
|
"oxlint-tsgolint": "7.0.2003",
|
|
6942
6605
|
"prettier": "3.9.9",
|
|
6943
6606
|
"release-it": "21.1.0",
|
|
6944
6607
|
"tsdown": "0.23.0",
|
|
6945
6608
|
"typescript": "6.0.3",
|
|
6946
|
-
"vitest": "5.0.
|
|
6947
|
-
"pnpm": "11.
|
|
6609
|
+
"vitest": "5.0.3",
|
|
6610
|
+
"pnpm": "11.28.3"
|
|
6948
6611
|
}["@types/node"];
|
|
6949
6612
|
}
|
|
6950
6613
|
if (config.builder === "tsdown") deps["tsdown"] = {
|
|
6951
6614
|
"@changesets/cli": "3.0.3",
|
|
6952
6615
|
"@release-it/bumper": "8.0.1",
|
|
6953
|
-
"@types/node": "24.
|
|
6954
|
-
"@vitest/coverage-v8": "5.0.
|
|
6616
|
+
"@types/node": "24.19.1",
|
|
6617
|
+
"@vitest/coverage-v8": "5.0.3",
|
|
6955
6618
|
"commit-and-tag-version": "13.2.1",
|
|
6956
|
-
"knip": "6.
|
|
6957
|
-
"lefthook": "2.1.
|
|
6619
|
+
"knip": "6.39.0",
|
|
6620
|
+
"lefthook": "2.1.16",
|
|
6958
6621
|
"modern-web-guidance": "0.0.191",
|
|
6959
6622
|
"oxfmt": "0.71.0",
|
|
6960
|
-
"oxlint": "1.
|
|
6623
|
+
"oxlint": "1.86.0",
|
|
6961
6624
|
"oxlint-tsgolint": "7.0.2003",
|
|
6962
6625
|
"prettier": "3.9.9",
|
|
6963
6626
|
"release-it": "21.1.0",
|
|
6964
6627
|
"tsdown": "0.23.0",
|
|
6965
6628
|
"typescript": "6.0.3",
|
|
6966
|
-
"vitest": "5.0.
|
|
6967
|
-
"pnpm": "11.
|
|
6629
|
+
"vitest": "5.0.3",
|
|
6630
|
+
"pnpm": "11.28.3"
|
|
6968
6631
|
}["tsdown"];
|
|
6969
6632
|
return deps;
|
|
6970
6633
|
}
|
|
@@ -6981,40 +6644,40 @@ function vitestDevDeps() {
|
|
|
6981
6644
|
vitest: {
|
|
6982
6645
|
"@changesets/cli": "3.0.3",
|
|
6983
6646
|
"@release-it/bumper": "8.0.1",
|
|
6984
|
-
"@types/node": "24.
|
|
6985
|
-
"@vitest/coverage-v8": "5.0.
|
|
6647
|
+
"@types/node": "24.19.1",
|
|
6648
|
+
"@vitest/coverage-v8": "5.0.3",
|
|
6986
6649
|
"commit-and-tag-version": "13.2.1",
|
|
6987
|
-
"knip": "6.
|
|
6988
|
-
"lefthook": "2.1.
|
|
6650
|
+
"knip": "6.39.0",
|
|
6651
|
+
"lefthook": "2.1.16",
|
|
6989
6652
|
"modern-web-guidance": "0.0.191",
|
|
6990
6653
|
"oxfmt": "0.71.0",
|
|
6991
|
-
"oxlint": "1.
|
|
6654
|
+
"oxlint": "1.86.0",
|
|
6992
6655
|
"oxlint-tsgolint": "7.0.2003",
|
|
6993
6656
|
"prettier": "3.9.9",
|
|
6994
6657
|
"release-it": "21.1.0",
|
|
6995
6658
|
"tsdown": "0.23.0",
|
|
6996
6659
|
"typescript": "6.0.3",
|
|
6997
|
-
"vitest": "5.0.
|
|
6998
|
-
"pnpm": "11.
|
|
6660
|
+
"vitest": "5.0.3",
|
|
6661
|
+
"pnpm": "11.28.3"
|
|
6999
6662
|
}["vitest"],
|
|
7000
6663
|
"@vitest/coverage-v8": {
|
|
7001
6664
|
"@changesets/cli": "3.0.3",
|
|
7002
6665
|
"@release-it/bumper": "8.0.1",
|
|
7003
|
-
"@types/node": "24.
|
|
7004
|
-
"@vitest/coverage-v8": "5.0.
|
|
6666
|
+
"@types/node": "24.19.1",
|
|
6667
|
+
"@vitest/coverage-v8": "5.0.3",
|
|
7005
6668
|
"commit-and-tag-version": "13.2.1",
|
|
7006
|
-
"knip": "6.
|
|
7007
|
-
"lefthook": "2.1.
|
|
6669
|
+
"knip": "6.39.0",
|
|
6670
|
+
"lefthook": "2.1.16",
|
|
7008
6671
|
"modern-web-guidance": "0.0.191",
|
|
7009
6672
|
"oxfmt": "0.71.0",
|
|
7010
|
-
"oxlint": "1.
|
|
6673
|
+
"oxlint": "1.86.0",
|
|
7011
6674
|
"oxlint-tsgolint": "7.0.2003",
|
|
7012
6675
|
"prettier": "3.9.9",
|
|
7013
6676
|
"release-it": "21.1.0",
|
|
7014
6677
|
"tsdown": "0.23.0",
|
|
7015
6678
|
"typescript": "6.0.3",
|
|
7016
|
-
"vitest": "5.0.
|
|
7017
|
-
"pnpm": "11.
|
|
6679
|
+
"vitest": "5.0.3",
|
|
6680
|
+
"pnpm": "11.28.3"
|
|
7018
6681
|
}["@vitest/coverage-v8"]
|
|
7019
6682
|
};
|
|
7020
6683
|
}
|
|
@@ -7028,81 +6691,81 @@ function rootDevDeps(config) {
|
|
|
7028
6691
|
const deps = { lefthook: {
|
|
7029
6692
|
"@changesets/cli": "3.0.3",
|
|
7030
6693
|
"@release-it/bumper": "8.0.1",
|
|
7031
|
-
"@types/node": "24.
|
|
7032
|
-
"@vitest/coverage-v8": "5.0.
|
|
6694
|
+
"@types/node": "24.19.1",
|
|
6695
|
+
"@vitest/coverage-v8": "5.0.3",
|
|
7033
6696
|
"commit-and-tag-version": "13.2.1",
|
|
7034
|
-
"knip": "6.
|
|
7035
|
-
"lefthook": "2.1.
|
|
6697
|
+
"knip": "6.39.0",
|
|
6698
|
+
"lefthook": "2.1.16",
|
|
7036
6699
|
"modern-web-guidance": "0.0.191",
|
|
7037
6700
|
"oxfmt": "0.71.0",
|
|
7038
|
-
"oxlint": "1.
|
|
6701
|
+
"oxlint": "1.86.0",
|
|
7039
6702
|
"oxlint-tsgolint": "7.0.2003",
|
|
7040
6703
|
"prettier": "3.9.9",
|
|
7041
6704
|
"release-it": "21.1.0",
|
|
7042
6705
|
"tsdown": "0.23.0",
|
|
7043
6706
|
"typescript": "6.0.3",
|
|
7044
|
-
"vitest": "5.0.
|
|
7045
|
-
"pnpm": "11.
|
|
6707
|
+
"vitest": "5.0.3",
|
|
6708
|
+
"pnpm": "11.28.3"
|
|
7046
6709
|
}["lefthook"] };
|
|
7047
6710
|
if (config.stack !== "none") {
|
|
7048
6711
|
deps["knip"] = {
|
|
7049
6712
|
"@changesets/cli": "3.0.3",
|
|
7050
6713
|
"@release-it/bumper": "8.0.1",
|
|
7051
|
-
"@types/node": "24.
|
|
7052
|
-
"@vitest/coverage-v8": "5.0.
|
|
6714
|
+
"@types/node": "24.19.1",
|
|
6715
|
+
"@vitest/coverage-v8": "5.0.3",
|
|
7053
6716
|
"commit-and-tag-version": "13.2.1",
|
|
7054
|
-
"knip": "6.
|
|
7055
|
-
"lefthook": "2.1.
|
|
6717
|
+
"knip": "6.39.0",
|
|
6718
|
+
"lefthook": "2.1.16",
|
|
7056
6719
|
"modern-web-guidance": "0.0.191",
|
|
7057
6720
|
"oxfmt": "0.71.0",
|
|
7058
|
-
"oxlint": "1.
|
|
6721
|
+
"oxlint": "1.86.0",
|
|
7059
6722
|
"oxlint-tsgolint": "7.0.2003",
|
|
7060
6723
|
"prettier": "3.9.9",
|
|
7061
6724
|
"release-it": "21.1.0",
|
|
7062
6725
|
"tsdown": "0.23.0",
|
|
7063
6726
|
"typescript": "6.0.3",
|
|
7064
|
-
"vitest": "5.0.
|
|
7065
|
-
"pnpm": "11.
|
|
6727
|
+
"vitest": "5.0.3",
|
|
6728
|
+
"pnpm": "11.28.3"
|
|
7066
6729
|
}["knip"];
|
|
7067
6730
|
deps["oxlint"] = {
|
|
7068
6731
|
"@changesets/cli": "3.0.3",
|
|
7069
6732
|
"@release-it/bumper": "8.0.1",
|
|
7070
|
-
"@types/node": "24.
|
|
7071
|
-
"@vitest/coverage-v8": "5.0.
|
|
6733
|
+
"@types/node": "24.19.1",
|
|
6734
|
+
"@vitest/coverage-v8": "5.0.3",
|
|
7072
6735
|
"commit-and-tag-version": "13.2.1",
|
|
7073
|
-
"knip": "6.
|
|
7074
|
-
"lefthook": "2.1.
|
|
6736
|
+
"knip": "6.39.0",
|
|
6737
|
+
"lefthook": "2.1.16",
|
|
7075
6738
|
"modern-web-guidance": "0.0.191",
|
|
7076
6739
|
"oxfmt": "0.71.0",
|
|
7077
|
-
"oxlint": "1.
|
|
6740
|
+
"oxlint": "1.86.0",
|
|
7078
6741
|
"oxlint-tsgolint": "7.0.2003",
|
|
7079
6742
|
"prettier": "3.9.9",
|
|
7080
6743
|
"release-it": "21.1.0",
|
|
7081
6744
|
"tsdown": "0.23.0",
|
|
7082
6745
|
"typescript": "6.0.3",
|
|
7083
|
-
"vitest": "5.0.
|
|
7084
|
-
"pnpm": "11.
|
|
6746
|
+
"vitest": "5.0.3",
|
|
6747
|
+
"pnpm": "11.28.3"
|
|
7085
6748
|
}["oxlint"];
|
|
7086
6749
|
Object.assign(deps, vitestDevDeps());
|
|
7087
6750
|
}
|
|
7088
6751
|
if (config.stack === "typescript") deps["oxlint-tsgolint"] = {
|
|
7089
6752
|
"@changesets/cli": "3.0.3",
|
|
7090
6753
|
"@release-it/bumper": "8.0.1",
|
|
7091
|
-
"@types/node": "24.
|
|
7092
|
-
"@vitest/coverage-v8": "5.0.
|
|
6754
|
+
"@types/node": "24.19.1",
|
|
6755
|
+
"@vitest/coverage-v8": "5.0.3",
|
|
7093
6756
|
"commit-and-tag-version": "13.2.1",
|
|
7094
|
-
"knip": "6.
|
|
7095
|
-
"lefthook": "2.1.
|
|
6757
|
+
"knip": "6.39.0",
|
|
6758
|
+
"lefthook": "2.1.16",
|
|
7096
6759
|
"modern-web-guidance": "0.0.191",
|
|
7097
6760
|
"oxfmt": "0.71.0",
|
|
7098
|
-
"oxlint": "1.
|
|
6761
|
+
"oxlint": "1.86.0",
|
|
7099
6762
|
"oxlint-tsgolint": "7.0.2003",
|
|
7100
6763
|
"prettier": "3.9.9",
|
|
7101
6764
|
"release-it": "21.1.0",
|
|
7102
6765
|
"tsdown": "0.23.0",
|
|
7103
6766
|
"typescript": "6.0.3",
|
|
7104
|
-
"vitest": "5.0.
|
|
7105
|
-
"pnpm": "11.
|
|
6767
|
+
"vitest": "5.0.3",
|
|
6768
|
+
"pnpm": "11.28.3"
|
|
7106
6769
|
}["oxlint-tsgolint"];
|
|
7107
6770
|
return deps;
|
|
7108
6771
|
}
|
|
@@ -7133,81 +6796,81 @@ function addReleaseDeps(deps, config) {
|
|
|
7133
6796
|
deps["release-it"] = {
|
|
7134
6797
|
"@changesets/cli": "3.0.3",
|
|
7135
6798
|
"@release-it/bumper": "8.0.1",
|
|
7136
|
-
"@types/node": "24.
|
|
7137
|
-
"@vitest/coverage-v8": "5.0.
|
|
6799
|
+
"@types/node": "24.19.1",
|
|
6800
|
+
"@vitest/coverage-v8": "5.0.3",
|
|
7138
6801
|
"commit-and-tag-version": "13.2.1",
|
|
7139
|
-
"knip": "6.
|
|
7140
|
-
"lefthook": "2.1.
|
|
6802
|
+
"knip": "6.39.0",
|
|
6803
|
+
"lefthook": "2.1.16",
|
|
7141
6804
|
"modern-web-guidance": "0.0.191",
|
|
7142
6805
|
"oxfmt": "0.71.0",
|
|
7143
|
-
"oxlint": "1.
|
|
6806
|
+
"oxlint": "1.86.0",
|
|
7144
6807
|
"oxlint-tsgolint": "7.0.2003",
|
|
7145
6808
|
"prettier": "3.9.9",
|
|
7146
6809
|
"release-it": "21.1.0",
|
|
7147
6810
|
"tsdown": "0.23.0",
|
|
7148
6811
|
"typescript": "6.0.3",
|
|
7149
|
-
"vitest": "5.0.
|
|
7150
|
-
"pnpm": "11.
|
|
6812
|
+
"vitest": "5.0.3",
|
|
6813
|
+
"pnpm": "11.28.3"
|
|
7151
6814
|
}["release-it"];
|
|
7152
6815
|
if (config.structure === "monorepo") deps["@release-it/bumper"] = {
|
|
7153
6816
|
"@changesets/cli": "3.0.3",
|
|
7154
6817
|
"@release-it/bumper": "8.0.1",
|
|
7155
|
-
"@types/node": "24.
|
|
7156
|
-
"@vitest/coverage-v8": "5.0.
|
|
6818
|
+
"@types/node": "24.19.1",
|
|
6819
|
+
"@vitest/coverage-v8": "5.0.3",
|
|
7157
6820
|
"commit-and-tag-version": "13.2.1",
|
|
7158
|
-
"knip": "6.
|
|
7159
|
-
"lefthook": "2.1.
|
|
6821
|
+
"knip": "6.39.0",
|
|
6822
|
+
"lefthook": "2.1.16",
|
|
7160
6823
|
"modern-web-guidance": "0.0.191",
|
|
7161
6824
|
"oxfmt": "0.71.0",
|
|
7162
|
-
"oxlint": "1.
|
|
6825
|
+
"oxlint": "1.86.0",
|
|
7163
6826
|
"oxlint-tsgolint": "7.0.2003",
|
|
7164
6827
|
"prettier": "3.9.9",
|
|
7165
6828
|
"release-it": "21.1.0",
|
|
7166
6829
|
"tsdown": "0.23.0",
|
|
7167
6830
|
"typescript": "6.0.3",
|
|
7168
|
-
"vitest": "5.0.
|
|
7169
|
-
"pnpm": "11.
|
|
6831
|
+
"vitest": "5.0.3",
|
|
6832
|
+
"pnpm": "11.28.3"
|
|
7170
6833
|
}["@release-it/bumper"];
|
|
7171
6834
|
break;
|
|
7172
6835
|
case "simple":
|
|
7173
6836
|
deps["commit-and-tag-version"] = {
|
|
7174
6837
|
"@changesets/cli": "3.0.3",
|
|
7175
6838
|
"@release-it/bumper": "8.0.1",
|
|
7176
|
-
"@types/node": "24.
|
|
7177
|
-
"@vitest/coverage-v8": "5.0.
|
|
6839
|
+
"@types/node": "24.19.1",
|
|
6840
|
+
"@vitest/coverage-v8": "5.0.3",
|
|
7178
6841
|
"commit-and-tag-version": "13.2.1",
|
|
7179
|
-
"knip": "6.
|
|
7180
|
-
"lefthook": "2.1.
|
|
6842
|
+
"knip": "6.39.0",
|
|
6843
|
+
"lefthook": "2.1.16",
|
|
7181
6844
|
"modern-web-guidance": "0.0.191",
|
|
7182
6845
|
"oxfmt": "0.71.0",
|
|
7183
|
-
"oxlint": "1.
|
|
6846
|
+
"oxlint": "1.86.0",
|
|
7184
6847
|
"oxlint-tsgolint": "7.0.2003",
|
|
7185
6848
|
"prettier": "3.9.9",
|
|
7186
6849
|
"release-it": "21.1.0",
|
|
7187
6850
|
"tsdown": "0.23.0",
|
|
7188
6851
|
"typescript": "6.0.3",
|
|
7189
|
-
"vitest": "5.0.
|
|
7190
|
-
"pnpm": "11.
|
|
6852
|
+
"vitest": "5.0.3",
|
|
6853
|
+
"pnpm": "11.28.3"
|
|
7191
6854
|
}["commit-and-tag-version"];
|
|
7192
6855
|
break;
|
|
7193
6856
|
case "changesets": deps["@changesets/cli"] = {
|
|
7194
6857
|
"@changesets/cli": "3.0.3",
|
|
7195
6858
|
"@release-it/bumper": "8.0.1",
|
|
7196
|
-
"@types/node": "24.
|
|
7197
|
-
"@vitest/coverage-v8": "5.0.
|
|
6859
|
+
"@types/node": "24.19.1",
|
|
6860
|
+
"@vitest/coverage-v8": "5.0.3",
|
|
7198
6861
|
"commit-and-tag-version": "13.2.1",
|
|
7199
|
-
"knip": "6.
|
|
7200
|
-
"lefthook": "2.1.
|
|
6862
|
+
"knip": "6.39.0",
|
|
6863
|
+
"lefthook": "2.1.16",
|
|
7201
6864
|
"modern-web-guidance": "0.0.191",
|
|
7202
6865
|
"oxfmt": "0.71.0",
|
|
7203
|
-
"oxlint": "1.
|
|
6866
|
+
"oxlint": "1.86.0",
|
|
7204
6867
|
"oxlint-tsgolint": "7.0.2003",
|
|
7205
6868
|
"prettier": "3.9.9",
|
|
7206
6869
|
"release-it": "21.1.0",
|
|
7207
6870
|
"tsdown": "0.23.0",
|
|
7208
6871
|
"typescript": "6.0.3",
|
|
7209
|
-
"vitest": "5.0.
|
|
7210
|
-
"pnpm": "11.
|
|
6872
|
+
"vitest": "5.0.3",
|
|
6873
|
+
"pnpm": "11.28.3"
|
|
7211
6874
|
}["@changesets/cli"];
|
|
7212
6875
|
}
|
|
7213
6876
|
}
|
|
@@ -7401,21 +7064,21 @@ function applyMetadataDefaults(pkg, ctx, changes) {
|
|
|
7401
7064
|
const pinnedPnpm = {
|
|
7402
7065
|
"@changesets/cli": "3.0.3",
|
|
7403
7066
|
"@release-it/bumper": "8.0.1",
|
|
7404
|
-
"@types/node": "24.
|
|
7405
|
-
"@vitest/coverage-v8": "5.0.
|
|
7067
|
+
"@types/node": "24.19.1",
|
|
7068
|
+
"@vitest/coverage-v8": "5.0.3",
|
|
7406
7069
|
"commit-and-tag-version": "13.2.1",
|
|
7407
|
-
"knip": "6.
|
|
7408
|
-
"lefthook": "2.1.
|
|
7070
|
+
"knip": "6.39.0",
|
|
7071
|
+
"lefthook": "2.1.16",
|
|
7409
7072
|
"modern-web-guidance": "0.0.191",
|
|
7410
7073
|
"oxfmt": "0.71.0",
|
|
7411
|
-
"oxlint": "1.
|
|
7074
|
+
"oxlint": "1.86.0",
|
|
7412
7075
|
"oxlint-tsgolint": "7.0.2003",
|
|
7413
7076
|
"prettier": "3.9.9",
|
|
7414
7077
|
"release-it": "21.1.0",
|
|
7415
7078
|
"tsdown": "0.23.0",
|
|
7416
7079
|
"typescript": "6.0.3",
|
|
7417
|
-
"vitest": "5.0.
|
|
7418
|
-
"pnpm": "11.
|
|
7080
|
+
"vitest": "5.0.3",
|
|
7081
|
+
"pnpm": "11.28.3"
|
|
7419
7082
|
}["pnpm"];
|
|
7420
7083
|
const desiredPm = pinnedPnpm ? `pnpm@${pinnedPnpm}` : void 0;
|
|
7421
7084
|
if (desiredPm) {
|
|
@@ -7456,45 +7119,45 @@ function buildManagedDevDeps(ctx) {
|
|
|
7456
7119
|
const devDeps = { ...rootDevDeps(ctx.config) };
|
|
7457
7120
|
if (!isMonorepo) Object.assign(devDeps, perPackageDevDeps(ctx.config));
|
|
7458
7121
|
if (ctx.config.stack !== "none" || ctx.config.ci === "forgejo") devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.14.5";
|
|
7459
|
-
if (!isWorkspacePackage(ctx, "@bensandee/tooling")) devDeps["@bensandee/tooling"] = "0.
|
|
7460
|
-
if (ctx.config.useEslintPlugin && ctx.config.stack !== "none") devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "0.
|
|
7122
|
+
if (!isWorkspacePackage(ctx, "@bensandee/tooling")) devDeps["@bensandee/tooling"] = "0.87.0";
|
|
7123
|
+
if (ctx.config.useEslintPlugin && ctx.config.stack !== "none") devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "0.16.0";
|
|
7461
7124
|
if (ctx.config.formatter === "oxfmt") devDeps["oxfmt"] = {
|
|
7462
7125
|
"@changesets/cli": "3.0.3",
|
|
7463
7126
|
"@release-it/bumper": "8.0.1",
|
|
7464
|
-
"@types/node": "24.
|
|
7465
|
-
"@vitest/coverage-v8": "5.0.
|
|
7127
|
+
"@types/node": "24.19.1",
|
|
7128
|
+
"@vitest/coverage-v8": "5.0.3",
|
|
7466
7129
|
"commit-and-tag-version": "13.2.1",
|
|
7467
|
-
"knip": "6.
|
|
7468
|
-
"lefthook": "2.1.
|
|
7130
|
+
"knip": "6.39.0",
|
|
7131
|
+
"lefthook": "2.1.16",
|
|
7469
7132
|
"modern-web-guidance": "0.0.191",
|
|
7470
7133
|
"oxfmt": "0.71.0",
|
|
7471
|
-
"oxlint": "1.
|
|
7134
|
+
"oxlint": "1.86.0",
|
|
7472
7135
|
"oxlint-tsgolint": "7.0.2003",
|
|
7473
7136
|
"prettier": "3.9.9",
|
|
7474
7137
|
"release-it": "21.1.0",
|
|
7475
7138
|
"tsdown": "0.23.0",
|
|
7476
7139
|
"typescript": "6.0.3",
|
|
7477
|
-
"vitest": "5.0.
|
|
7478
|
-
"pnpm": "11.
|
|
7140
|
+
"vitest": "5.0.3",
|
|
7141
|
+
"pnpm": "11.28.3"
|
|
7479
7142
|
}["oxfmt"];
|
|
7480
7143
|
if (ctx.config.formatter === "prettier") devDeps["prettier"] = {
|
|
7481
7144
|
"@changesets/cli": "3.0.3",
|
|
7482
7145
|
"@release-it/bumper": "8.0.1",
|
|
7483
|
-
"@types/node": "24.
|
|
7484
|
-
"@vitest/coverage-v8": "5.0.
|
|
7146
|
+
"@types/node": "24.19.1",
|
|
7147
|
+
"@vitest/coverage-v8": "5.0.3",
|
|
7485
7148
|
"commit-and-tag-version": "13.2.1",
|
|
7486
|
-
"knip": "6.
|
|
7487
|
-
"lefthook": "2.1.
|
|
7149
|
+
"knip": "6.39.0",
|
|
7150
|
+
"lefthook": "2.1.16",
|
|
7488
7151
|
"modern-web-guidance": "0.0.191",
|
|
7489
7152
|
"oxfmt": "0.71.0",
|
|
7490
|
-
"oxlint": "1.
|
|
7153
|
+
"oxlint": "1.86.0",
|
|
7491
7154
|
"oxlint-tsgolint": "7.0.2003",
|
|
7492
7155
|
"prettier": "3.9.9",
|
|
7493
7156
|
"release-it": "21.1.0",
|
|
7494
7157
|
"tsdown": "0.23.0",
|
|
7495
7158
|
"typescript": "6.0.3",
|
|
7496
|
-
"vitest": "5.0.
|
|
7497
|
-
"pnpm": "11.
|
|
7159
|
+
"vitest": "5.0.3",
|
|
7160
|
+
"pnpm": "11.28.3"
|
|
7498
7161
|
}["prettier"];
|
|
7499
7162
|
addReleaseDeps(devDeps, ctx.config);
|
|
7500
7163
|
return devDeps;
|
|
@@ -7550,21 +7213,21 @@ async function generatePackageJson(ctx) {
|
|
|
7550
7213
|
packageManager: `pnpm@${{
|
|
7551
7214
|
"@changesets/cli": "3.0.3",
|
|
7552
7215
|
"@release-it/bumper": "8.0.1",
|
|
7553
|
-
"@types/node": "24.
|
|
7554
|
-
"@vitest/coverage-v8": "5.0.
|
|
7216
|
+
"@types/node": "24.19.1",
|
|
7217
|
+
"@vitest/coverage-v8": "5.0.3",
|
|
7555
7218
|
"commit-and-tag-version": "13.2.1",
|
|
7556
|
-
"knip": "6.
|
|
7557
|
-
"lefthook": "2.1.
|
|
7219
|
+
"knip": "6.39.0",
|
|
7220
|
+
"lefthook": "2.1.16",
|
|
7558
7221
|
"modern-web-guidance": "0.0.191",
|
|
7559
7222
|
"oxfmt": "0.71.0",
|
|
7560
|
-
"oxlint": "1.
|
|
7223
|
+
"oxlint": "1.86.0",
|
|
7561
7224
|
"oxlint-tsgolint": "7.0.2003",
|
|
7562
7225
|
"prettier": "3.9.9",
|
|
7563
7226
|
"release-it": "21.1.0",
|
|
7564
7227
|
"tsdown": "0.23.0",
|
|
7565
7228
|
"typescript": "6.0.3",
|
|
7566
|
-
"vitest": "5.0.
|
|
7567
|
-
"pnpm": "11.
|
|
7229
|
+
"vitest": "5.0.3",
|
|
7230
|
+
"pnpm": "11.28.3"
|
|
7568
7231
|
}["pnpm"]}`
|
|
7569
7232
|
};
|
|
7570
7233
|
ctx.write(filePath, JSON.stringify(pkg, null, 2) + "\n");
|
|
@@ -8382,6 +8045,17 @@ const CONFIG_WITH_LINT_RULES = `import recommended from "@bensandee/config/oxlin
|
|
|
8382
8045
|
import bensandee from "@bensandee/eslint-plugin/recommended";
|
|
8383
8046
|
import { defineConfig } from "oxlint";
|
|
8384
8047
|
|
|
8048
|
+
export default defineConfig({
|
|
8049
|
+
extends: [recommended],
|
|
8050
|
+
jsPlugins: bensandee.jsPlugins ?? [],
|
|
8051
|
+
rules: bensandee.rules ?? {},
|
|
8052
|
+
ignorePatterns: [${IGNORE_PATTERNS_SPREAD}],
|
|
8053
|
+
});
|
|
8054
|
+
`;
|
|
8055
|
+
const LEGACY_CONFIG_WITH_LINT_RULES = `import recommended from "@bensandee/config/oxlint/recommended";
|
|
8056
|
+
import bensandee from "@bensandee/eslint-plugin/recommended";
|
|
8057
|
+
import { defineConfig } from "oxlint";
|
|
8058
|
+
|
|
8385
8059
|
// Report phase: every plugin rule runs and reports, none blocks. The severities are derived
|
|
8386
8060
|
// from the plugin's own rule names rather than listed, so a rule the plugin adds arrives the
|
|
8387
8061
|
// day it publishes -- enumerating them is how the plugin came to be installed and never
|
|
@@ -8414,7 +8088,11 @@ function addIgnorePatterns(content) {
|
|
|
8414
8088
|
return content.slice(0, insertPos) + ` ignorePatterns: [${IGNORE_PATTERNS_SPREAD}],\n` + content.slice(insertPos);
|
|
8415
8089
|
}
|
|
8416
8090
|
const PLUGIN_IMPORT = "import bensandee from \"@bensandee/eslint-plugin/recommended\";\n";
|
|
8417
|
-
const
|
|
8091
|
+
const TIER_SPREAD = " ...bensandee.rules,\n";
|
|
8092
|
+
const LEGACY_REPORT_SPREAD = " ...Object.fromEntries(Object.keys(bensandee.rules ?? {}).map((name) => [name, \"warn\"])),\n";
|
|
8093
|
+
/** The text every report-phase severity map carries, whatever its shape -- in either quote
|
|
8094
|
+
* style, since the merged spread went through the repo's own formatter. */
|
|
8095
|
+
const REPORT_PHASE_MARKER = /\[name, (["'])warn\1\]/;
|
|
8418
8096
|
const RULES_KEY = /^ {2}"?rules"?: \{/m;
|
|
8419
8097
|
const RULES_KEY_INLINE = /^ {2}"?rules"?\s*[,:]/m;
|
|
8420
8098
|
const JS_PLUGINS_KEY = /^ {2}"?jsPlugins"?: \[/m;
|
|
@@ -8461,28 +8139,29 @@ function addBensandeePlugin(content) {
|
|
|
8461
8139
|
const rulesKey = RULES_KEY.exec(rulesRegion);
|
|
8462
8140
|
if (rulesKey) {
|
|
8463
8141
|
const at = insertPos + rulesKey.index + rulesKey[0].length;
|
|
8464
|
-
return withPlugins.slice(0, at) + `\n${
|
|
8142
|
+
return withPlugins.slice(0, at) + `\n${TIER_SPREAD}` + withPlugins.slice(at);
|
|
8465
8143
|
}
|
|
8466
8144
|
if (RULES_KEY_INLINE.test(rulesRegion)) return UNRECOGNIZED_SHAPE;
|
|
8467
|
-
return withPlugins.slice(0, afterJsPlugins) + ` rules: {\n${
|
|
8468
|
-
}
|
|
8469
|
-
/** The exact
|
|
8470
|
-
* scratch -- checked before
|
|
8471
|
-
*
|
|
8472
|
-
* },` shell behind. */
|
|
8473
|
-
const
|
|
8145
|
+
return withPlugins.slice(0, afterJsPlugins) + ` rules: {\n${TIER_SPREAD} },\n` + withPlugins.slice(afterJsPlugins);
|
|
8146
|
+
}
|
|
8147
|
+
/** The exact literals `addBensandeePlugin` splices in when it creates a `rules` key from
|
|
8148
|
+
* scratch, current and legacy -- checked before the merged-in-place spreads below, since each
|
|
8149
|
+
* spread is a substring of its block and would otherwise match first and leave the empty `rules:
|
|
8150
|
+
* { },` shell behind. */
|
|
8151
|
+
const CREATED_RULES_BLOCKS = [` rules: {\n${TIER_SPREAD} },\n`, ` rules: {\n${LEGACY_REPORT_SPREAD} },\n`];
|
|
8152
|
+
const MERGED_RULES_SPREADS = [`\n${TIER_SPREAD}`, `\n${LEGACY_REPORT_SPREAD}`];
|
|
8474
8153
|
const CREATED_JS_PLUGINS_LINE = " jsPlugins: bensandee.jsPlugins ?? [],\n";
|
|
8475
8154
|
const MERGED_JS_PLUGINS_SPREAD = /\.\.\.\(bensandee\.jsPlugins \?\? \[\]\),\s+/;
|
|
8476
8155
|
/**
|
|
8477
8156
|
* The exact inverse of {@link addBensandeePlugin}: strips the import line and whichever of the
|
|
8478
|
-
* two `jsPlugins` and two `rules` shapes it spliced in,
|
|
8479
|
-
* than re-deriving positions. Returns `undefined` when
|
|
8480
|
-
* does not name the plugin -- keeps a re-sync a no-op),
|
|
8481
|
-
* does but none of the expected literals are present --
|
|
8482
|
-
* this tool never injected, or (for the merged
|
|
8483
|
-
* wider than {@link MERGED_JS_PLUGINS_SPREAD}
|
|
8484
|
-
* `addBensandeePlugin` itself follows, rather than
|
|
8485
|
-
* corrupt the file.
|
|
8157
|
+
* two `jsPlugins` and two `rules` shapes it spliced in, the legacy `rules` literals included, by
|
|
8158
|
+
* removing those same literals rather than re-deriving positions. Returns `undefined` when
|
|
8159
|
+
* there is nothing to remove (the content does not name the plugin -- keeps a re-sync a no-op),
|
|
8160
|
+
* and {@link UNRECOGNIZED_SHAPE} when it does but none of the expected literals are present --
|
|
8161
|
+
* a hand-edit since injection, a shape this tool never injected, or (for the merged
|
|
8162
|
+
* `jsPlugins` spread only) a formatter reflow wider than {@link MERGED_JS_PLUGINS_SPREAD}
|
|
8163
|
+
* tolerates -- declined whole, the same rule `addBensandeePlugin` itself follows, rather than
|
|
8164
|
+
* guessing at a partial removal that could corrupt the file.
|
|
8486
8165
|
*/
|
|
8487
8166
|
function removeBensandeePlugin(content) {
|
|
8488
8167
|
if (!content.includes("@bensandee/eslint-plugin")) return void 0;
|
|
@@ -8491,10 +8170,17 @@ function removeBensandeePlugin(content) {
|
|
|
8491
8170
|
if (result.includes(CREATED_JS_PLUGINS_LINE)) result = result.replace(CREATED_JS_PLUGINS_LINE, "");
|
|
8492
8171
|
else if (MERGED_JS_PLUGINS_SPREAD.test(result)) result = result.replace(MERGED_JS_PLUGINS_SPREAD, "");
|
|
8493
8172
|
else return UNRECOGNIZED_SHAPE;
|
|
8494
|
-
|
|
8495
|
-
|
|
8496
|
-
|
|
8497
|
-
|
|
8173
|
+
const rulesLiteral = CREATED_RULES_BLOCKS.find((block) => result.includes(block)) ?? MERGED_RULES_SPREADS.find((spread) => result.includes(spread));
|
|
8174
|
+
if (rulesLiteral === void 0) return UNRECOGNIZED_SHAPE;
|
|
8175
|
+
return result.replace(rulesLiteral, "");
|
|
8176
|
+
}
|
|
8177
|
+
/**
|
|
8178
|
+
* Swap the report-phase spread an earlier release merged in for {@link TIER_SPREAD}. Only the
|
|
8179
|
+
* byte-exact literal is replaced: a map the repo has reflowed or edited is left for the caller
|
|
8180
|
+
* to report, since rewriting a shape this tool did not write could drop the repo's own rules.
|
|
8181
|
+
*/
|
|
8182
|
+
function migrateReportSpread(content) {
|
|
8183
|
+
return content.replace(`\n${LEGACY_REPORT_SPREAD}`, `\n${TIER_SPREAD}`);
|
|
8498
8184
|
}
|
|
8499
8185
|
async function generateOxlint(ctx) {
|
|
8500
8186
|
const filePath = "oxlint.config.ts";
|
|
@@ -8512,7 +8198,7 @@ async function generateOxlint(ctx) {
|
|
|
8512
8198
|
action: "skipped",
|
|
8513
8199
|
description: "Already configured"
|
|
8514
8200
|
};
|
|
8515
|
-
if (
|
|
8201
|
+
if ((ctx.config.useEslintPlugin ? [CONFIG_PRESET_ONLY, LEGACY_CONFIG_WITH_LINT_RULES] : [CONFIG_WITH_LINT_RULES, LEGACY_CONFIG_WITH_LINT_RULES]).some((template) => isGeneratedTemplate(ctx, filePath, template))) {
|
|
8516
8202
|
ctx.write(filePath, content);
|
|
8517
8203
|
return {
|
|
8518
8204
|
filePath,
|
|
@@ -8525,8 +8211,14 @@ async function generateOxlint(ctx) {
|
|
|
8525
8211
|
let pluginResult;
|
|
8526
8212
|
let withPlugin;
|
|
8527
8213
|
if (ctx.config.useEslintPlugin) {
|
|
8528
|
-
|
|
8529
|
-
|
|
8214
|
+
const migrated = migrateReportSpread(withIgnores);
|
|
8215
|
+
pluginResult = addBensandeePlugin(migrated);
|
|
8216
|
+
withPlugin = typeof pluginResult === "string" ? pluginResult : migrated;
|
|
8217
|
+
if (pluginResult === void 0 && REPORT_PHASE_MARKER.test(migrated)) return {
|
|
8218
|
+
filePath,
|
|
8219
|
+
action: "failed",
|
|
8220
|
+
description: "Plugin severities not migrated: unrecognized config shape -- replace the all-\"warn\" rule map with \"...bensandee.rules,\""
|
|
8221
|
+
};
|
|
8530
8222
|
} else {
|
|
8531
8223
|
pluginResult = removeBensandeePlugin(withIgnores);
|
|
8532
8224
|
withPlugin = typeof pluginResult === "string" ? pluginResult : withIgnores;
|
|
@@ -9664,89 +9356,406 @@ async function generateGitignore(ctx) {
|
|
|
9664
9356
|
...OPTIONAL_ENTRIES,
|
|
9665
9357
|
...conditionalEntries(ctx)
|
|
9666
9358
|
];
|
|
9667
|
-
const existing = ctx.read(filePath);
|
|
9668
|
-
if (existing) {
|
|
9669
|
-
const existingNormalized = new Set(existing.split("\n").map(normalizeEntry).filter((line) => line.length > 0));
|
|
9670
|
-
const missing = allEntries.filter((entry) => !existingNormalized.has(normalizeEntry(entry)));
|
|
9671
|
-
if (missing.length === 0) return {
|
|
9672
|
-
filePath,
|
|
9673
|
-
action: "skipped",
|
|
9674
|
-
description: "Already has all standard entries"
|
|
9675
|
-
};
|
|
9676
|
-
const missingRequired = REQUIRED_ENTRIES.filter((entry) => !existingNormalized.has(normalizeEntry(entry)));
|
|
9677
|
-
const updated = existing.trimEnd() + "\n\n# Added by @bensandee/tooling\n" + missing.join("\n") + "\n";
|
|
9678
|
-
ctx.write(filePath, updated);
|
|
9679
|
-
if (missingRequired.length === 0) return {
|
|
9680
|
-
filePath,
|
|
9681
|
-
action: "skipped",
|
|
9682
|
-
description: "Only optional entries missing"
|
|
9683
|
-
};
|
|
9684
|
-
return {
|
|
9685
|
-
filePath,
|
|
9686
|
-
action: "updated",
|
|
9687
|
-
description: `Appended ${String(missing.length)} missing entries`
|
|
9688
|
-
};
|
|
9689
|
-
}
|
|
9690
|
-
ctx.write(filePath, allEntries.join("\n") + "\n");
|
|
9359
|
+
const existing = ctx.read(filePath);
|
|
9360
|
+
if (existing) {
|
|
9361
|
+
const existingNormalized = new Set(existing.split("\n").map(normalizeEntry).filter((line) => line.length > 0));
|
|
9362
|
+
const missing = allEntries.filter((entry) => !existingNormalized.has(normalizeEntry(entry)));
|
|
9363
|
+
if (missing.length === 0) return {
|
|
9364
|
+
filePath,
|
|
9365
|
+
action: "skipped",
|
|
9366
|
+
description: "Already has all standard entries"
|
|
9367
|
+
};
|
|
9368
|
+
const missingRequired = REQUIRED_ENTRIES.filter((entry) => !existingNormalized.has(normalizeEntry(entry)));
|
|
9369
|
+
const updated = existing.trimEnd() + "\n\n# Added by @bensandee/tooling\n" + missing.join("\n") + "\n";
|
|
9370
|
+
ctx.write(filePath, updated);
|
|
9371
|
+
if (missingRequired.length === 0) return {
|
|
9372
|
+
filePath,
|
|
9373
|
+
action: "skipped",
|
|
9374
|
+
description: "Only optional entries missing"
|
|
9375
|
+
};
|
|
9376
|
+
return {
|
|
9377
|
+
filePath,
|
|
9378
|
+
action: "updated",
|
|
9379
|
+
description: `Appended ${String(missing.length)} missing entries`
|
|
9380
|
+
};
|
|
9381
|
+
}
|
|
9382
|
+
ctx.write(filePath, allEntries.join("\n") + "\n");
|
|
9383
|
+
return {
|
|
9384
|
+
filePath,
|
|
9385
|
+
action: "created",
|
|
9386
|
+
description: "Generated .gitignore"
|
|
9387
|
+
};
|
|
9388
|
+
}
|
|
9389
|
+
//#endregion
|
|
9390
|
+
//#region src/generators/gitattributes.ts
|
|
9391
|
+
const MANAGED_HEADER = ["# Managed by @bensandee/tooling: LF working tree on every platform, overriding", "# core.autocrlf/core.eol at checkout. Rules below this line win over it."].join("\n");
|
|
9392
|
+
/** Placed only when prepending above a consumer's existing content -- explains, in the file
|
|
9393
|
+
* itself, why the rule goes first rather than last (the code has no other way to show it). */
|
|
9394
|
+
const PREPEND_REASON = ["# Prepended above your existing rules: .gitattributes is last-match-wins, so a rule", "# placed below would override any binary/-text marks you already have."].join("\n");
|
|
9395
|
+
const RULE_LINE = "* text=auto eol=lf";
|
|
9396
|
+
/**
|
|
9397
|
+
* True when `content` already carries a rule equivalent to `* text=auto eol=lf`: a
|
|
9398
|
+
* non-comment line whose first whitespace-separated token is exactly `*` and whose
|
|
9399
|
+
* remaining tokens include both `text=auto` and `eol=lf`, in either order. Token-based so
|
|
9400
|
+
* reordered or extra-whitespace variants still count, but a narrower pattern (`*.ts ...`)
|
|
9401
|
+
* or a commented-out line does not.
|
|
9402
|
+
*/
|
|
9403
|
+
function hasLfRule(content) {
|
|
9404
|
+
return content.split("\n").some((line) => {
|
|
9405
|
+
const trimmed = line.trim();
|
|
9406
|
+
if (trimmed.startsWith("#")) return false;
|
|
9407
|
+
const tokens = trimmed.split(/\s+/).filter(Boolean);
|
|
9408
|
+
if (tokens[0] !== "*") return false;
|
|
9409
|
+
const rest = new Set(tokens.slice(1));
|
|
9410
|
+
return rest.has("text=auto") && rest.has("eol=lf");
|
|
9411
|
+
});
|
|
9412
|
+
}
|
|
9413
|
+
/**
|
|
9414
|
+
* Scaffold `.gitattributes` with `* text=auto eol=lf`, the rule that makes the working tree
|
|
9415
|
+
* LF regardless of a developer's global `core.autocrlf`/`core.eol`. No managed-file header
|
|
9416
|
+
* from `utils/generated-header.ts` -- this file is merged into like `.gitignore`, not
|
|
9417
|
+
* wholesale-regenerated. The rule is required, not optional: a missing rule is
|
|
9418
|
+
* created/updated and fails `repo:sync --check` like any other drift.
|
|
9419
|
+
*/
|
|
9420
|
+
async function generateGitattributes(ctx) {
|
|
9421
|
+
const filePath = ".gitattributes";
|
|
9422
|
+
const existing = ctx.read(filePath);
|
|
9423
|
+
if (existing === void 0 || existing.trim() === "") {
|
|
9424
|
+
ctx.write(filePath, `${MANAGED_HEADER}\n${RULE_LINE}\n`);
|
|
9425
|
+
return {
|
|
9426
|
+
filePath,
|
|
9427
|
+
action: existing === void 0 ? "created" : "updated",
|
|
9428
|
+
description: "Generated .gitattributes"
|
|
9429
|
+
};
|
|
9430
|
+
}
|
|
9431
|
+
if (hasLfRule(existing)) return {
|
|
9432
|
+
filePath,
|
|
9433
|
+
action: "skipped",
|
|
9434
|
+
description: "Already has the LF rule"
|
|
9435
|
+
};
|
|
9436
|
+
const updated = `${MANAGED_HEADER}\n${PREPEND_REASON}\n${RULE_LINE}\n\n${existing}`;
|
|
9437
|
+
ctx.write(filePath, updated);
|
|
9438
|
+
return {
|
|
9439
|
+
filePath,
|
|
9440
|
+
action: "updated",
|
|
9441
|
+
description: "Prepended the LF rule"
|
|
9442
|
+
};
|
|
9443
|
+
}
|
|
9444
|
+
//#endregion
|
|
9445
|
+
//#region src/release/publish/publish-config-lint.ts
|
|
9446
|
+
const FIX = `add "publishConfig": { "registry": "<your registry URL>" } to its package.json`;
|
|
9447
|
+
/**
|
|
9448
|
+
* Whether a value can serve as a publish destination: an absolute http(s) URL.
|
|
9449
|
+
* Presence alone is not the test -- a relative or empty registry is as silently
|
|
9450
|
+
* wrong as an absent one, and npm resolves neither to the place the author meant.
|
|
9451
|
+
*/
|
|
9452
|
+
function isUsableRegistry(value) {
|
|
9453
|
+
if (typeof value !== "string" || value.trim() === "") return false;
|
|
9454
|
+
try {
|
|
9455
|
+
const url = new URL(value);
|
|
9456
|
+
return url.protocol === "https:" || url.protocol === "http:";
|
|
9457
|
+
} catch {
|
|
9458
|
+
return false;
|
|
9459
|
+
}
|
|
9460
|
+
}
|
|
9461
|
+
function isRecord(value) {
|
|
9462
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9463
|
+
}
|
|
9464
|
+
/**
|
|
9465
|
+
* Require every package this repo would publish to declare where it publishes.
|
|
9466
|
+
*
|
|
9467
|
+
* npm and pnpm use one `registry` key for reads and writes alike, so a project
|
|
9468
|
+
* `.npmrc` aimed at an install cache silently repoints `pnpm publish` too. A
|
|
9469
|
+
* package that never declares its destination inherits one, and an inherited
|
|
9470
|
+
* destination can be changed by a file that never mentions publishing.
|
|
9471
|
+
*
|
|
9472
|
+
* Reports rather than repairs: the correct registry is not derivable here, and
|
|
9473
|
+
* writing a guess would redirect a working release exactly the way this gate
|
|
9474
|
+
* exists to prevent.
|
|
9475
|
+
*/
|
|
9476
|
+
function lintPublishConfig(targetDir, structure) {
|
|
9477
|
+
const issues = [];
|
|
9478
|
+
for (const pkg of getPublishablePackages(targetDir, structure)) {
|
|
9479
|
+
const manifest = readPackageJson(pkg.dir);
|
|
9480
|
+
if (!manifest) continue;
|
|
9481
|
+
const file = path.relative(targetDir, path.join(pkg.dir, "package.json"));
|
|
9482
|
+
const pc = manifest.publishConfig;
|
|
9483
|
+
const registry = isRecord(pc) ? pc["registry"] : void 0;
|
|
9484
|
+
if (registry === void 0) {
|
|
9485
|
+
issues.push({
|
|
9486
|
+
kind: "missing",
|
|
9487
|
+
package: pkg.name,
|
|
9488
|
+
file,
|
|
9489
|
+
message: `${pkg.name} is published from this repo but declares no publishConfig.registry, so its release destination is inherited -- a project .npmrc can repoint it silently. In ${file}, ${FIX}.`
|
|
9490
|
+
});
|
|
9491
|
+
continue;
|
|
9492
|
+
}
|
|
9493
|
+
if (!isUsableRegistry(registry)) issues.push({
|
|
9494
|
+
kind: "malformed",
|
|
9495
|
+
package: pkg.name,
|
|
9496
|
+
file,
|
|
9497
|
+
message: `${pkg.name} declares publishConfig.registry as ${JSON.stringify(registry)}, which is not an absolute http(s) URL and cannot be published to. In ${file}, ${FIX}.`
|
|
9498
|
+
});
|
|
9499
|
+
}
|
|
9500
|
+
return issues;
|
|
9501
|
+
}
|
|
9502
|
+
/**
|
|
9503
|
+
* The registries this repo's publishable packages actually declare, distinct and in
|
|
9504
|
+
* first-seen order.
|
|
9505
|
+
*
|
|
9506
|
+
* Reuses the same "absolute http(s) URL" narrowing as {@link lintPublishConfig} rather
|
|
9507
|
+
* than re-deriving it -- a registry this misses here would fail that gate first, so
|
|
9508
|
+
* `repo:sync` never reaches a generator with an undeclared or malformed registry. A
|
|
9509
|
+
* caller that invokes a generator directly (or a repo whose publishable set is empty,
|
|
9510
|
+
* e.g. every package private) still gets a safe empty result.
|
|
9511
|
+
*/
|
|
9512
|
+
function declaredPublishRegistries(targetDir, structure, rootPackageJson) {
|
|
9513
|
+
const registries = [];
|
|
9514
|
+
for (const pkg of getPublishablePackages(targetDir, structure, rootPackageJson)) {
|
|
9515
|
+
const manifest = structure === "single" ? rootPackageJson ?? readPackageJson(pkg.dir) : readPackageJson(pkg.dir);
|
|
9516
|
+
if (!manifest) continue;
|
|
9517
|
+
const pc = manifest.publishConfig;
|
|
9518
|
+
const registry = isRecord(pc) ? pc["registry"] : void 0;
|
|
9519
|
+
if (typeof registry === "string" && isUsableRegistry(registry) && !registries.includes(registry)) registries.push(registry);
|
|
9520
|
+
}
|
|
9521
|
+
return registries;
|
|
9522
|
+
}
|
|
9523
|
+
//#endregion
|
|
9524
|
+
//#region src/generators/mise-pin.ts
|
|
9525
|
+
/**
|
|
9526
|
+
* The mise version and release-asset digest the fleet's CI and the agent runner base image agree
|
|
9527
|
+
* to run. This is the one place that agreement is recorded; `packages/agent/test/runner-base-image-dockerfile.test.ts`
|
|
9528
|
+
* (task 8 of the mise-toolchain-provisioning plan) reads both constants back through
|
|
9529
|
+
* `readRepoFile` and asserts the base image's own `ARG MISE_VERSION` and `mise.sha256` manifest
|
|
9530
|
+
* agree with them, so the two packages cannot drift without a failing test on either side.
|
|
9531
|
+
*
|
|
9532
|
+
* Each constant sits under its own `// renovate:` marker comment, in the three-line grammar
|
|
9533
|
+
* `packages/agent/docker/agent-goose-image/goose.sha256` uses for its own digest line, so the same
|
|
9534
|
+
* shape of custom regex manager in the root `renovate.json` can rewrite both this version and this
|
|
9535
|
+
* digest (`customManagers` entries scoped to this file).
|
|
9536
|
+
*/
|
|
9537
|
+
const MISE_VERSION = "2026.10.2";
|
|
9538
|
+
const MISE_LINUX_X64_SHA256 = "8f5f6660336f572830e33cd9b378d3131e529a0d4c4f0c553776be90a1ba302a";
|
|
9539
|
+
/** Where generated Forgejo workflows fetch the shared mise setup action: a composite action in
|
|
9540
|
+
* `bensandee/common`, on the forge every fleet repo lives on. GitHub cannot reach it; the
|
|
9541
|
+
* generators keep inline steps there. */
|
|
9542
|
+
const SETUP_MISE_ACTION_URL = "https://code.orangebikelabs.com/bensandee/common/actions/setup-mise";
|
|
9543
|
+
/** The tag in `bensandee/common` that marks the current release of the action. Generated `uses:`
|
|
9544
|
+
* lines carry it as their comment, and Renovate moves the SHA below to wherever it points. */
|
|
9545
|
+
const SETUP_MISE_ACTION_TAG = "setup-mise@latest";
|
|
9546
|
+
const SETUP_MISE_ACTION_SHA = "db99e4e9ee46bfeaca2146e7ae55cc700b4c23bf";
|
|
9547
|
+
//#endregion
|
|
9548
|
+
//#region src/generators/ci-utils.ts
|
|
9549
|
+
/** Build a GitHub Actions expression like `${{ expr }}` without triggering no-template-curly-in-string. */
|
|
9550
|
+
function actionsExpr(expr) {
|
|
9551
|
+
return `\${{ ${expr} }}`;
|
|
9552
|
+
}
|
|
9553
|
+
const MAIN_BRANCH_REF = "refs/heads/main";
|
|
9554
|
+
/** `if:` condition that gates a step to push events on the default branch. */
|
|
9555
|
+
const IS_MAIN_BRANCH = `github.ref == '${MAIN_BRANCH_REF}'`;
|
|
9556
|
+
/**
|
|
9557
|
+
* Negated form for concurrency `cancel-in-progress` and similar uses.
|
|
9558
|
+
*
|
|
9559
|
+
* Main is exempt from cancellation on purpose, and the exemption stays: a main run publishes and
|
|
9560
|
+
* pushes tags, and cancelling one mid-flight can leave a partial publish -- some packages on the
|
|
9561
|
+
* registry, no matching tags, and no run left alive to finish. A superseded feature-branch run
|
|
9562
|
+
* wastes minutes; a half-published release costs a manual unwind.
|
|
9563
|
+
*
|
|
9564
|
+
* So several main runs can be in flight at once, each versioning from its own triggering commit.
|
|
9565
|
+
* Overlap safety rests on one invariant: **main advances solely via commits that trigger a CI
|
|
9566
|
+
* run.** The `[skip ci]` coverage, health, and egress records go to `bst/*` orphan branches and
|
|
9567
|
+
* never to main; a `[skip ci]` commit pushed straight to main would leave a superseded run skipping
|
|
9568
|
+
* and its changesets stranded until the next push, which is why the supersession guard skips rather
|
|
9569
|
+
* than fails. Three guards carry the overlap itself: supersession plus a `--force-with-lease`
|
|
9570
|
+
* version push (`release/publish/version.ts`), the `headBumpedVersion` publish guard plus reading
|
|
9571
|
+
* remote tags and releases before acting (`commands/release-changesets.ts`,
|
|
9572
|
+
* `release/publish/publish.ts`), and floating git and Docker tags that only move forward
|
|
9573
|
+
* (`release/publish/floating-tags.ts`, `release/docker.ts`). Such a run may also version pending
|
|
9574
|
+
* changesets after it publishes -- the stale version-PR merge -- under the same supersession guard
|
|
9575
|
+
* and push lease as any other version run. A later commit publishes a bump only when its tag is
|
|
9576
|
+
* missing and every main run since the package's last release tag has settled
|
|
9577
|
+
* (`release/publish/unpublished-bump.ts`), so no run that could still publish it is in flight; a
|
|
9578
|
+
* manual re-run of one of those runs is the overlap left. On Forgejo the concurrency block is
|
|
9579
|
+
* only a PR-run cancellation mechanism -- it holds a main run in its group and, in Forgejo 16,
|
|
9580
|
+
* intermittently fails to release it, which is why main runs get a group of their own there. GitHub's shape stays shared
|
|
9581
|
+
* because its queue works: one run, one pending, a newer arrival replacing the pending, so
|
|
9582
|
+
* `changesets/action@v1` never needs a guard of its own. Any new side effect in a main run must be
|
|
9583
|
+
* idempotent under overlap or be listed here with the guard that makes it so.
|
|
9584
|
+
*/
|
|
9585
|
+
const IS_NOT_MAIN_BRANCH = `github.ref != '${MAIN_BRANCH_REF}'`;
|
|
9586
|
+
/**
|
|
9587
|
+
* Job-level `env:` block that evaluates the default-branch check once, so steps
|
|
9588
|
+
* can gate on `env.IS_MAIN` instead of repeating the `github.ref` expression.
|
|
9589
|
+
*/
|
|
9590
|
+
const IS_MAIN_JOB_ENV = { IS_MAIN: actionsExpr(IS_MAIN_BRANCH) };
|
|
9591
|
+
/** `if:` condition referencing the job-level `IS_MAIN` env var (see {@link IS_MAIN_JOB_ENV}). */
|
|
9592
|
+
const IS_MAIN_ENV_GUARD = "env.IS_MAIN == 'true'";
|
|
9593
|
+
/**
|
|
9594
|
+
* npm's own registry-key rule for an `.npmrc` auth line: strip the scheme, keep
|
|
9595
|
+
* host/port/path, and end with exactly one slash. `actions/setup-node` derives the
|
|
9596
|
+
* same key from a registry URL; this mirrors that so a hand-written line matches
|
|
9597
|
+
* what npm looks up at publish time -- npm-registry-fetch walks the request path
|
|
9598
|
+
* upward through ancestor keys until one matches.
|
|
9599
|
+
*/
|
|
9600
|
+
function npmrcAuthKey(registryUrl) {
|
|
9601
|
+
return `${registryUrl.replace(/^https?:/i, "").replace(/\/+$/, "")}/`;
|
|
9602
|
+
}
|
|
9603
|
+
const NODE_AUTH_TOKEN_VAR = ["$", "{NODE_AUTH_TOKEN}"].join("");
|
|
9604
|
+
/**
|
|
9605
|
+
* Build both halves of a job's npm auth wiring together, so one value decides
|
|
9606
|
+
* them: the auth lines the publishing step writes, and the step-level
|
|
9607
|
+
* `NODE_AUTH_TOKEN` it carries. A job that gets none of the declared registries
|
|
9608
|
+
* gets neither -- including a `publishes: true` job whose publishable set is
|
|
9609
|
+
* empty (every package private), since there is then no npmrc line for a real
|
|
9610
|
+
* secret to pair with.
|
|
9611
|
+
*
|
|
9612
|
+
* The lines go into a temp userconfig exported only inside the publishing
|
|
9613
|
+
* step's shell, never `~/.npmrc` or `$GITHUB_ENV`: pnpm 11 warns
|
|
9614
|
+
* `Failed to replace env in config` on every call that reads the placeholder
|
|
9615
|
+
* while the token is unset or empty, so a line in a shared file makes install, `ci:check`,
|
|
9616
|
+
* and every step after the publish warn. An empty job-level default does not
|
|
9617
|
+
* silence it, and `${NODE_AUTH_TOKEN:-}` is rejected as an empty fallback and
|
|
9618
|
+
* warns in every case. The `-n` guard keeps a run without the secret quiet; the
|
|
9619
|
+
* single quotes keep the token itself off disk, since npm expands it at read time.
|
|
9620
|
+
*/
|
|
9621
|
+
function npmAuthWiring(ctx, publishes) {
|
|
9622
|
+
const registries = publishes ? declaredPublishRegistries(ctx.targetDir, ctx.config.structure, ctx.packageJson) : [];
|
|
9623
|
+
if (registries.length === 0) return {
|
|
9624
|
+
publishRun: (command) => command,
|
|
9625
|
+
tokenEnv: {}
|
|
9626
|
+
};
|
|
9627
|
+
const setup = [
|
|
9628
|
+
"if [ -n \"$NODE_AUTH_TOKEN\" ]; then",
|
|
9629
|
+
" NPM_CONFIG_USERCONFIG=\"$(mktemp)\"",
|
|
9630
|
+
" export NPM_CONFIG_USERCONFIG",
|
|
9631
|
+
" if [ -f ~/.npmrc ]; then cat ~/.npmrc > \"$NPM_CONFIG_USERCONFIG\"; fi",
|
|
9632
|
+
...registries.map((registry) => ` printf '\\n%s\\n' '${npmrcAuthKey(registry)}:_authToken=${NODE_AUTH_TOKEN_VAR}' >> "$NPM_CONFIG_USERCONFIG"`),
|
|
9633
|
+
"fi"
|
|
9634
|
+
];
|
|
9691
9635
|
return {
|
|
9692
|
-
|
|
9693
|
-
|
|
9694
|
-
description: "Generated .gitignore"
|
|
9636
|
+
publishRun: (command) => [...setup, command].join("\n"),
|
|
9637
|
+
tokenEnv: { NODE_AUTH_TOKEN: actionsExpr("secrets.NPM_TOKEN") }
|
|
9695
9638
|
};
|
|
9696
9639
|
}
|
|
9697
|
-
|
|
9698
|
-
//#region src/generators/gitattributes.ts
|
|
9699
|
-
const MANAGED_HEADER = ["# Managed by @bensandee/tooling: LF working tree on every platform, overriding", "# core.autocrlf/core.eol at checkout. Rules below this line win over it."].join("\n");
|
|
9700
|
-
/** Placed only when prepending above a consumer's existing content -- explains, in the file
|
|
9701
|
-
* itself, why the rule goes first rather than last (the code has no other way to show it). */
|
|
9702
|
-
const PREPEND_REASON = ["# Prepended above your existing rules: .gitattributes is last-match-wins, so a rule", "# placed below would override any binary/-text marks you already have."].join("\n");
|
|
9703
|
-
const RULE_LINE = "* text=auto eol=lf";
|
|
9640
|
+
const MISE_RELEASE_URL = `https://github.com/jdx/mise/releases/download/v${MISE_VERSION}/mise-v${MISE_VERSION}-linux-x64`;
|
|
9704
9641
|
/**
|
|
9705
|
-
*
|
|
9706
|
-
*
|
|
9707
|
-
*
|
|
9708
|
-
*
|
|
9709
|
-
*
|
|
9642
|
+
* The node script `inlineMiseSteps` pipes `mise env --json` into, run under the mise it just
|
|
9643
|
+
* installed. Reads the env map off stdin (mise exec passes it straight through) and exports every
|
|
9644
|
+
* key except `PATH` into `$GITHUB_ENV` in heredoc form -- the form that survives any value,
|
|
9645
|
+
* including one containing `$` or quotes -- then appends mise's shim directory and the
|
|
9646
|
+
* `~/.local/bin` install location to `$GITHUB_PATH` so every later step sees node, pnpm, and any
|
|
9647
|
+
* other declared tool on PATH with no prefix.
|
|
9710
9648
|
*/
|
|
9711
|
-
|
|
9712
|
-
|
|
9713
|
-
|
|
9714
|
-
|
|
9715
|
-
|
|
9716
|
-
|
|
9717
|
-
|
|
9718
|
-
|
|
9719
|
-
|
|
9649
|
+
const MISE_ENV_EXPORT_SCRIPT = [
|
|
9650
|
+
"const fs = require(\"fs\");",
|
|
9651
|
+
"const env = JSON.parse(fs.readFileSync(0, \"utf8\"));",
|
|
9652
|
+
"let out = \"\";",
|
|
9653
|
+
"for (const [key, value] of Object.entries(env)) {",
|
|
9654
|
+
" if (key === \"PATH\") continue;",
|
|
9655
|
+
" out += key + \"<<BST_MISE_ENV_EOF\\n\" + value + \"\\nBST_MISE_ENV_EOF\\n\";",
|
|
9656
|
+
"}",
|
|
9657
|
+
"fs.appendFileSync(process.env.GITHUB_ENV, out);",
|
|
9658
|
+
"fs.appendFileSync(",
|
|
9659
|
+
" process.env.GITHUB_PATH,",
|
|
9660
|
+
" process.env.HOME + \"/.local/share/mise/shims\\n\" + process.env.HOME + \"/.local/bin\\n\",",
|
|
9661
|
+
");"
|
|
9662
|
+
].join("\n");
|
|
9663
|
+
/** A fresh object per call: one shared object in two jobs would serialize as a YAML alias. */
|
|
9664
|
+
function pnpmStoreCacheStep() {
|
|
9665
|
+
return { step: {
|
|
9666
|
+
name: "Cache pnpm store",
|
|
9667
|
+
uses: "actions/cache@v4",
|
|
9668
|
+
with: {
|
|
9669
|
+
path: "~/.local/share/pnpm/store",
|
|
9670
|
+
key: `pnpm-${actionsExpr("runner.os")}-${actionsExpr("hashFiles('pnpm-lock.yaml')")}`
|
|
9671
|
+
}
|
|
9672
|
+
} };
|
|
9720
9673
|
}
|
|
9674
|
+
/** Actions whose ref the generator pins itself, so `preservePinnedActions` must not keep a
|
|
9675
|
+
* repo's older ref for them. */
|
|
9676
|
+
const GENERATOR_OWNED_ACTIONS = [{
|
|
9677
|
+
name: SETUP_MISE_ACTION_URL,
|
|
9678
|
+
comment: SETUP_MISE_ACTION_TAG
|
|
9679
|
+
}];
|
|
9721
9680
|
/**
|
|
9722
|
-
*
|
|
9723
|
-
*
|
|
9724
|
-
*
|
|
9725
|
-
*
|
|
9726
|
-
*
|
|
9681
|
+
* The steps every workflow generator uses in place of `pnpm/action-setup` + `actions/setup-node`:
|
|
9682
|
+
* install the pinned `mise`, `mise install` the repo's `mise.toml` toolchain, export its env into
|
|
9683
|
+
* the rest of the job, and cache the pnpm store. `MISE_VERSION`/`MISE_LINUX_X64_SHA256`
|
|
9684
|
+
* (`mise-pin.ts`) are the same pin the agent runner's base image installs, checked by
|
|
9685
|
+
* `runner-base-image-dockerfile.test.ts`.
|
|
9686
|
+
*
|
|
9687
|
+
* On Forgejo the mise half is the shared composite action in `bensandee/common`, pinned by commit.
|
|
9688
|
+
* GitHub resolves actions only from github.com, so it keeps the same steps inline.
|
|
9727
9689
|
*/
|
|
9728
|
-
|
|
9729
|
-
|
|
9730
|
-
|
|
9731
|
-
|
|
9732
|
-
|
|
9733
|
-
|
|
9734
|
-
|
|
9735
|
-
|
|
9736
|
-
|
|
9737
|
-
|
|
9738
|
-
|
|
9739
|
-
|
|
9740
|
-
|
|
9741
|
-
|
|
9742
|
-
|
|
9743
|
-
|
|
9744
|
-
|
|
9745
|
-
|
|
9690
|
+
function miseSetupSteps(ci) {
|
|
9691
|
+
if (ci !== "forgejo") return inlineMiseSteps();
|
|
9692
|
+
return [{ step: {
|
|
9693
|
+
name: "Set up mise",
|
|
9694
|
+
uses: `${SETUP_MISE_ACTION_URL}@${SETUP_MISE_ACTION_SHA}`,
|
|
9695
|
+
with: {
|
|
9696
|
+
version: MISE_VERSION,
|
|
9697
|
+
sha256: MISE_LINUX_X64_SHA256
|
|
9698
|
+
}
|
|
9699
|
+
} }, pnpmStoreCacheStep()];
|
|
9700
|
+
}
|
|
9701
|
+
/**
|
|
9702
|
+
* The inline form of the shared action, for GitHub. Uses `mise env --json` piped into the
|
|
9703
|
+
* just-installed mise's own `node -e`, not `mise env --shell bash`: the JSON form is
|
|
9704
|
+
* shell-independent and the interpreter reading it is guaranteed to exist (mise just installed
|
|
9705
|
+
* it), so this needs no assumption about which shell the runner's `run:` steps use.
|
|
9706
|
+
*/
|
|
9707
|
+
function inlineMiseSteps() {
|
|
9708
|
+
const installScript = [
|
|
9709
|
+
"curl -fsSL -o \"$RUNNER_TEMP/mise\" " + MISE_RELEASE_URL,
|
|
9710
|
+
`echo "${MISE_LINUX_X64_SHA256} $RUNNER_TEMP/mise" | sha256sum -c`,
|
|
9711
|
+
"mkdir -p \"$HOME/.local/bin\" && install -m 0755 \"$RUNNER_TEMP/mise\" \"$HOME/.local/bin/mise\"",
|
|
9712
|
+
"export MISE_TRUSTED_CONFIG_PATHS=\"$GITHUB_WORKSPACE\"",
|
|
9713
|
+
"export MISE_YES=1",
|
|
9714
|
+
"\"$HOME/.local/bin/mise\" install",
|
|
9715
|
+
`"$HOME/.local/bin/mise" env --json | "$HOME/.local/bin/mise" exec -- node -e '${MISE_ENV_EXPORT_SCRIPT}'`
|
|
9716
|
+
].join("\n");
|
|
9717
|
+
return [
|
|
9718
|
+
{ step: {
|
|
9719
|
+
name: "Cache mise tools",
|
|
9720
|
+
uses: "actions/cache@v4",
|
|
9721
|
+
with: {
|
|
9722
|
+
path: "~/.local/share/mise",
|
|
9723
|
+
key: `mise-${actionsExpr("runner.os")}-${actionsExpr("hashFiles('mise.toml')")}`
|
|
9724
|
+
}
|
|
9725
|
+
} },
|
|
9726
|
+
pnpmStoreCacheStep(),
|
|
9727
|
+
{ step: {
|
|
9728
|
+
name: "Install mise",
|
|
9729
|
+
env: { GITHUB_TOKEN: "" },
|
|
9730
|
+
run: installScript
|
|
9731
|
+
} }
|
|
9732
|
+
];
|
|
9733
|
+
}
|
|
9734
|
+
/**
|
|
9735
|
+
* Build the docker publish env block.
|
|
9736
|
+
*
|
|
9737
|
+
* The LHS env-var names (`DOCKER_USERNAME` / `DOCKER_PASSWORD`) match what
|
|
9738
|
+
* publish-docker.ts reads at runtime. The RHS secret references are
|
|
9739
|
+
* registry-specific (see {@link dockerSecretNames}) so downstream repos can
|
|
9740
|
+
* stop relying on the user/org-level `DOCKER_USERNAME` / `DOCKER_PASSWORD`
|
|
9741
|
+
* secrets -- which the Forgejo act fork auto-injects into every `container:`
|
|
9742
|
+
* image pull regardless of the target registry.
|
|
9743
|
+
*
|
|
9744
|
+
* When `host` is `null` (couldn't be derived at gen time), the secret references
|
|
9745
|
+
* fall back to neutral `REGISTRY_USERNAME` / `REGISTRY_PASSWORD` and the
|
|
9746
|
+
* workflow keeps `DOCKER_REGISTRY_HOST` / `DOCKER_REGISTRY_NAMESPACE` so the CLI
|
|
9747
|
+
* resolves the registry at runtime.
|
|
9748
|
+
*/
|
|
9749
|
+
function dockerPublishEnv(host) {
|
|
9750
|
+
const { username, password } = dockerSecretNames(host);
|
|
9746
9751
|
return {
|
|
9747
|
-
|
|
9748
|
-
|
|
9749
|
-
|
|
9752
|
+
...host === null ? {
|
|
9753
|
+
DOCKER_REGISTRY_HOST: actionsExpr("vars.DOCKER_REGISTRY_HOST"),
|
|
9754
|
+
DOCKER_REGISTRY_NAMESPACE: actionsExpr("vars.DOCKER_REGISTRY_NAMESPACE")
|
|
9755
|
+
} : {},
|
|
9756
|
+
DOCKER_USERNAME: actionsExpr(`secrets.${username}`),
|
|
9757
|
+
DOCKER_PASSWORD: actionsExpr(`secrets.${password}`),
|
|
9758
|
+
RELEASE_DEBUG: actionsExpr("vars.RELEASE_DEBUG || 'false'")
|
|
9750
9759
|
};
|
|
9751
9760
|
}
|
|
9752
9761
|
//#endregion
|
|
@@ -9801,7 +9810,7 @@ function postReleaseHookJob(opts) {
|
|
|
9801
9810
|
needs: opts.needsJob,
|
|
9802
9811
|
if: opts.publishCondition,
|
|
9803
9812
|
"continue-on-error": true,
|
|
9804
|
-
steps: [...commonSteps(opts.ci
|
|
9813
|
+
steps: [...commonSteps(opts.ci), { step: {
|
|
9805
9814
|
name: "Post-release hook",
|
|
9806
9815
|
env: {
|
|
9807
9816
|
...hookSecretEnv(opts.secrets),
|
|
@@ -9840,21 +9849,20 @@ function deployDispatchStep(opts) {
|
|
|
9840
9849
|
} };
|
|
9841
9850
|
}
|
|
9842
9851
|
/** Common setup steps shared by release-it and simple strategies. */
|
|
9843
|
-
function commonSteps(ci
|
|
9852
|
+
function commonSteps(ci) {
|
|
9844
9853
|
return [
|
|
9845
9854
|
{ step: {
|
|
9846
9855
|
uses: "actions/checkout@v6",
|
|
9847
9856
|
with: { "fetch-depth": 0 }
|
|
9848
9857
|
} },
|
|
9849
9858
|
...miseSetupSteps(ci),
|
|
9850
|
-
...authSteps,
|
|
9851
9859
|
{ step: { run: "pnpm install --frozen-lockfile" } }
|
|
9852
9860
|
];
|
|
9853
9861
|
}
|
|
9854
9862
|
function releaseItSteps(ci, npmAuth) {
|
|
9855
9863
|
const tokenEnv = ci === "github" ? { GITHUB_TOKEN: actionsExpr("secrets.RELEASE_TOKEN") } : { RELEASE_TOKEN: actionsExpr("secrets.RELEASE_TOKEN") };
|
|
9856
|
-
return [...commonSteps(ci
|
|
9857
|
-
run: "pnpm release-it --ci",
|
|
9864
|
+
return [...commonSteps(ci), { step: {
|
|
9865
|
+
run: npmAuth.publishRun("pnpm release-it --ci"),
|
|
9858
9866
|
env: {
|
|
9859
9867
|
...tokenEnv,
|
|
9860
9868
|
...npmAuth.tokenEnv,
|
|
@@ -9905,7 +9913,7 @@ function simpleReleaseEnv() {
|
|
|
9905
9913
|
};
|
|
9906
9914
|
}
|
|
9907
9915
|
function simpleReleaseSteps(ctx) {
|
|
9908
|
-
const { ci,
|
|
9916
|
+
const { ci, hasDocker, hasAssets, host } = ctx;
|
|
9909
9917
|
const releaseEnv = connectionEnv(ci);
|
|
9910
9918
|
const pushedIf = "steps.release.outputs.pushed == 'true'";
|
|
9911
9919
|
const releaseStep = { step: {
|
|
@@ -9931,7 +9939,7 @@ function simpleReleaseSteps(ctx) {
|
|
|
9931
9939
|
run: "pnpm exec bst docker:publish"
|
|
9932
9940
|
} };
|
|
9933
9941
|
return [
|
|
9934
|
-
...commonSteps(ci
|
|
9942
|
+
...commonSteps(ci),
|
|
9935
9943
|
releaseStep,
|
|
9936
9944
|
...hasAssets ? [assetsStep] : [],
|
|
9937
9945
|
...hasDocker ? [dockerStep] : []
|
|
@@ -10014,7 +10022,6 @@ async function generateReleaseCi(ctx) {
|
|
|
10014
10022
|
on,
|
|
10015
10023
|
...isGitHub && { permissions: { contents: "write" } },
|
|
10016
10024
|
jobName: "release",
|
|
10017
|
-
...Object.keys(npmAuth.jobEnv).length > 0 && { jobEnv: npmAuth.jobEnv },
|
|
10018
10025
|
...jobOutputs && { jobOutputs },
|
|
10019
10026
|
steps,
|
|
10020
10027
|
...extraJobs && { extraJobs }
|
|
@@ -10087,7 +10094,7 @@ function healthSteps(includeHistory) {
|
|
|
10087
10094
|
} }];
|
|
10088
10095
|
}
|
|
10089
10096
|
/** Build the release step for the check job (changesets strategy). */
|
|
10090
|
-
function changesetsReleaseStep(ci, publishesNpm,
|
|
10097
|
+
function changesetsReleaseStep(ci, publishesNpm, npmAuth) {
|
|
10091
10098
|
if (ci === "github") {
|
|
10092
10099
|
const changesetsEnv = {
|
|
10093
10100
|
GITHUB_TOKEN: actionsExpr("secrets.RELEASE_TOKEN"),
|
|
@@ -10111,10 +10118,10 @@ function changesetsReleaseStep(ci, publishesNpm, tokenEnv) {
|
|
|
10111
10118
|
FORGEJO_SERVER_URL: actionsExpr("github.server_url"),
|
|
10112
10119
|
FORGEJO_REPOSITORY: actionsExpr("github.repository"),
|
|
10113
10120
|
RELEASE_TOKEN: actionsExpr("secrets.RELEASE_TOKEN"),
|
|
10114
|
-
...tokenEnv,
|
|
10121
|
+
...npmAuth.tokenEnv,
|
|
10115
10122
|
RELEASE_DEBUG: actionsExpr("vars.RELEASE_DEBUG || 'false'")
|
|
10116
10123
|
},
|
|
10117
|
-
run: "pnpm exec bst release:changesets"
|
|
10124
|
+
run: npmAuth.publishRun("pnpm exec bst release:changesets")
|
|
10118
10125
|
} };
|
|
10119
10126
|
}
|
|
10120
10127
|
/**
|
|
@@ -10147,7 +10154,7 @@ function ciConcurrency(ci) {
|
|
|
10147
10154
|
"cancel-in-progress": actionsExpr(IS_NOT_MAIN_BRANCH)
|
|
10148
10155
|
};
|
|
10149
10156
|
}
|
|
10150
|
-
function checkSteps(ci,
|
|
10157
|
+
function checkSteps(ci, coverageEnabled, emitDockerSweepStep) {
|
|
10151
10158
|
const checkCommand = coverageEnabled ? "pnpm ci:check" : "pnpm ci:check --no-coverage";
|
|
10152
10159
|
return [
|
|
10153
10160
|
{ step: {
|
|
@@ -10155,7 +10162,6 @@ function checkSteps(ci, authSteps, coverageEnabled, emitDockerSweepStep) {
|
|
|
10155
10162
|
with: { "fetch-depth": 0 }
|
|
10156
10163
|
} },
|
|
10157
10164
|
...miseSetupSteps(ci),
|
|
10158
|
-
...authSteps,
|
|
10159
10165
|
{ step: { run: "pnpm install --frozen-lockfile" } },
|
|
10160
10166
|
...emitDockerSweepStep ? [{ step: {
|
|
10161
10167
|
name: "Sweep leaked Docker resources",
|
|
@@ -10208,10 +10214,10 @@ async function generateCi(ctx) {
|
|
|
10208
10214
|
}
|
|
10209
10215
|
const npmAuth = npmAuthWiring(ctx, isChangesets && publishesNpm && !isGitHub);
|
|
10210
10216
|
const steps = [
|
|
10211
|
-
...checkSteps(ctx.config.ci,
|
|
10217
|
+
...checkSteps(ctx.config.ci, coverage.enabled, hasDockerPackages(ctx)),
|
|
10212
10218
|
...emitCoverageSteps ? coverageHistorySteps() : [],
|
|
10213
10219
|
...healthSteps(emitHealthHistory),
|
|
10214
|
-
...isChangesets ? [changesetsReleaseStep(ctx.config.ci, publishesNpm, npmAuth
|
|
10220
|
+
...isChangesets ? [changesetsReleaseStep(ctx.config.ci, publishesNpm, npmAuth)] : [],
|
|
10215
10221
|
...isChangesets && isGitHub ? [githubChangesetsDeriveTagStep] : [],
|
|
10216
10222
|
...hasAssets ? [{ step: {
|
|
10217
10223
|
name: "Upload Release Assets",
|
|
@@ -10249,10 +10255,7 @@ async function generateCi(ctx) {
|
|
|
10249
10255
|
secrets: hookSecrets
|
|
10250
10256
|
}) };
|
|
10251
10257
|
}
|
|
10252
|
-
const checkJobEnv = {
|
|
10253
|
-
...emitCoverageSteps || emitHealthHistory ? IS_MAIN_JOB_ENV : {},
|
|
10254
|
-
...npmAuth.jobEnv
|
|
10255
|
-
};
|
|
10258
|
+
const checkJobEnv = emitCoverageSteps || emitHealthHistory ? IS_MAIN_JOB_ENV : {};
|
|
10256
10259
|
const checksSlice = parseToolingConfigSlice(ctx.read(".tooling.json"), z.object({ checks: ChecksConfigSchema.optional() }), { block: "checks" });
|
|
10257
10260
|
const skipOnVersionPr = isChangesets && checksSlice?.checks?.runOnVersionPr !== true;
|
|
10258
10261
|
const content = buildWorkflowYaml({
|
|
@@ -12369,21 +12372,21 @@ function webUiGuideBody() {
|
|
|
12369
12372
|
const mwg = `pnpm dlx modern-web-guidance@${{
|
|
12370
12373
|
"@changesets/cli": "3.0.3",
|
|
12371
12374
|
"@release-it/bumper": "8.0.1",
|
|
12372
|
-
"@types/node": "24.
|
|
12373
|
-
"@vitest/coverage-v8": "5.0.
|
|
12375
|
+
"@types/node": "24.19.1",
|
|
12376
|
+
"@vitest/coverage-v8": "5.0.3",
|
|
12374
12377
|
"commit-and-tag-version": "13.2.1",
|
|
12375
|
-
"knip": "6.
|
|
12376
|
-
"lefthook": "2.1.
|
|
12378
|
+
"knip": "6.39.0",
|
|
12379
|
+
"lefthook": "2.1.16",
|
|
12377
12380
|
"modern-web-guidance": "0.0.191",
|
|
12378
12381
|
"oxfmt": "0.71.0",
|
|
12379
|
-
"oxlint": "1.
|
|
12382
|
+
"oxlint": "1.86.0",
|
|
12380
12383
|
"oxlint-tsgolint": "7.0.2003",
|
|
12381
12384
|
"prettier": "3.9.9",
|
|
12382
12385
|
"release-it": "21.1.0",
|
|
12383
12386
|
"tsdown": "0.23.0",
|
|
12384
12387
|
"typescript": "6.0.3",
|
|
12385
|
-
"vitest": "5.0.
|
|
12386
|
-
"pnpm": "11.
|
|
12388
|
+
"vitest": "5.0.3",
|
|
12389
|
+
"pnpm": "11.28.3"
|
|
12387
12390
|
}["modern-web-guidance"]}`;
|
|
12388
12391
|
return [
|
|
12389
12392
|
"# Web UI",
|
|
@@ -12990,7 +12993,7 @@ function reportFailures(failed, command) {
|
|
|
12990
12993
|
function generateMigratePrompt(results, config, detected) {
|
|
12991
12994
|
const sections = [];
|
|
12992
12995
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
12993
|
-
sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.
|
|
12996
|
+
sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.87.0 repo:sync\` on ${timestamp}_\n`);
|
|
12994
12997
|
const changed = results.filter((r) => r.action === "created" || r.action === "updated" || r.action === "archived");
|
|
12995
12998
|
if (changed.length > 0) {
|
|
12996
12999
|
const items = changed.map((r) => {
|
|
@@ -13976,7 +13979,9 @@ function createFileReader() {
|
|
|
13976
13979
|
* (`commands/sync-gates.ts`) -- see that gate's doc comment for why an absent mise warns rather
|
|
13977
13980
|
* than fails -- plus, when `publishDocker` is on, one more per image only the check overlay
|
|
13978
13981
|
* declares (`runCheckOnlyImageGate`, same file): an advisory that a later tooling release stops
|
|
13979
|
-
* publishing it, counted rather than failed for the same reason the other two are
|
|
13982
|
+
* publishing it, counted rather than failed for the same reason the other two are -- plus one
|
|
13983
|
+
* more per workflow file whose custom block installs packages with apt (`runWorkflowAptGate`,
|
|
13984
|
+
* same file), which the agent's containers cannot run.
|
|
13980
13985
|
*/
|
|
13981
13986
|
const CHECKS_MARKER = "::bst-checks::";
|
|
13982
13987
|
const WARN_LINE = new RegExp(`^${CHECKS_MARKER} warn (\\d+)\\s*$`);
|
|
@@ -15548,6 +15553,113 @@ const runChecksCommand = defineCommand({
|
|
|
15548
15553
|
}
|
|
15549
15554
|
});
|
|
15550
15555
|
//#endregion
|
|
15556
|
+
//#region src/utils/workflow-paths.ts
|
|
15557
|
+
/**
|
|
15558
|
+
* The single definition of "workflow dir", shared by `repo:sync --check`'s advisory
|
|
15559
|
+
* classification and the workflow sync sweep's path allowlist -- so the two halves cannot
|
|
15560
|
+
* disagree about what counts as workflow output. Each prefix carries its trailing slash so a
|
|
15561
|
+
* file named e.g. `.github/workflowsX` never matches.
|
|
15562
|
+
*/
|
|
15563
|
+
const WORKFLOW_DIR_PREFIXES = [".forgejo/workflows/", ".github/workflows/"];
|
|
15564
|
+
/** True when `filePath` (repo-relative) lives under one of the workflow directories. */
|
|
15565
|
+
function isWorkflowDirPath(filePath) {
|
|
15566
|
+
return WORKFLOW_DIR_PREFIXES.some((prefix) => filePath.startsWith(prefix));
|
|
15567
|
+
}
|
|
15568
|
+
//#endregion
|
|
15569
|
+
//#region src/commands/workflow-apt-steps.ts
|
|
15570
|
+
/** `apt-get install`, `apt install`, and either with options before the verb (`apt-get -y
|
|
15571
|
+
* install`). */
|
|
15572
|
+
const APT_INSTALL = /\bapt(?:-get)?(?:\s+-\S+)*\s+install\b/g;
|
|
15573
|
+
const COMMAND_END = /&&|\|\||;|\|/;
|
|
15574
|
+
/** Every readable `.yml`/`.yaml` file in the two workflow directories, as repo-relative paths. */
|
|
15575
|
+
function workflowFiles(targetDir) {
|
|
15576
|
+
const files = [];
|
|
15577
|
+
for (const prefix of WORKFLOW_DIR_PREFIXES) {
|
|
15578
|
+
let names;
|
|
15579
|
+
try {
|
|
15580
|
+
names = readdirSync(path.join(targetDir, prefix));
|
|
15581
|
+
} catch {
|
|
15582
|
+
continue;
|
|
15583
|
+
}
|
|
15584
|
+
for (const name of names) {
|
|
15585
|
+
if (!/\.ya?ml$/.test(name)) continue;
|
|
15586
|
+
const relative = `${prefix}${name}`;
|
|
15587
|
+
if (isReadableFile$1(path.join(targetDir, relative))) files.push(relative);
|
|
15588
|
+
}
|
|
15589
|
+
}
|
|
15590
|
+
return files;
|
|
15591
|
+
}
|
|
15592
|
+
/** False for a directory, a dangling symlink, or anything `stat` refuses: an advisory never fails
|
|
15593
|
+
* the run it advises, so a file it cannot look at is skipped, not thrown on. */
|
|
15594
|
+
function isReadableFile$1(file) {
|
|
15595
|
+
try {
|
|
15596
|
+
return statSync(file).isFile();
|
|
15597
|
+
} catch {
|
|
15598
|
+
return false;
|
|
15599
|
+
}
|
|
15600
|
+
}
|
|
15601
|
+
/** A file's text, or null when it cannot be read -- skipped for the same reason. */
|
|
15602
|
+
function readText(file) {
|
|
15603
|
+
try {
|
|
15604
|
+
return readFileSync(file, "utf8");
|
|
15605
|
+
} catch {
|
|
15606
|
+
return null;
|
|
15607
|
+
}
|
|
15608
|
+
}
|
|
15609
|
+
/** A custom block's content lines, with the markers and every other comment line dropped (a
|
|
15610
|
+
* commented-out apt step has been removed) and `\`-continued lines joined. */
|
|
15611
|
+
function logicalLines(blockLines) {
|
|
15612
|
+
const lines = [];
|
|
15613
|
+
let pending = "";
|
|
15614
|
+
for (const line of blockLines) {
|
|
15615
|
+
if (line.trim().startsWith("#")) continue;
|
|
15616
|
+
const trimmed = line.trimEnd();
|
|
15617
|
+
if (trimmed.endsWith("\\")) {
|
|
15618
|
+
pending += `${trimmed.slice(0, -1)} `;
|
|
15619
|
+
continue;
|
|
15620
|
+
}
|
|
15621
|
+
lines.push(pending + line);
|
|
15622
|
+
pending = "";
|
|
15623
|
+
}
|
|
15624
|
+
if (pending !== "") lines.push(pending);
|
|
15625
|
+
return lines;
|
|
15626
|
+
}
|
|
15627
|
+
/** The packages one line installs, or none when it runs no apt install. */
|
|
15628
|
+
function packagesIn(line) {
|
|
15629
|
+
const packages = [];
|
|
15630
|
+
for (const match of line.matchAll(APT_INSTALL)) {
|
|
15631
|
+
const command = line.slice(match.index + match[0].length).split(COMMAND_END)[0] ?? "";
|
|
15632
|
+
for (const token of command.split(/\s+/)) {
|
|
15633
|
+
if (token === "" || token.startsWith("-")) continue;
|
|
15634
|
+
packages.push(token);
|
|
15635
|
+
}
|
|
15636
|
+
}
|
|
15637
|
+
return packages;
|
|
15638
|
+
}
|
|
15639
|
+
/** True when `line` runs an apt install at all, packages or not. */
|
|
15640
|
+
function installsWithApt(line) {
|
|
15641
|
+
return line.search(APT_INSTALL) !== -1;
|
|
15642
|
+
}
|
|
15643
|
+
function findWorkflowAptSteps(targetDir) {
|
|
15644
|
+
const steps = [];
|
|
15645
|
+
for (const file of workflowFiles(targetDir)) {
|
|
15646
|
+
const content = readText(path.join(targetDir, file));
|
|
15647
|
+
if (content === null || isToolingIgnored(content)) continue;
|
|
15648
|
+
let matched = false;
|
|
15649
|
+
const packages = /* @__PURE__ */ new Set();
|
|
15650
|
+
for (const block of extractCustomBlocks(content)) for (const line of logicalLines(block.lines)) {
|
|
15651
|
+
if (!installsWithApt(line)) continue;
|
|
15652
|
+
matched = true;
|
|
15653
|
+
for (const name of packagesIn(line)) packages.add(name);
|
|
15654
|
+
}
|
|
15655
|
+
if (matched) steps.push({
|
|
15656
|
+
file,
|
|
15657
|
+
packages: [...packages]
|
|
15658
|
+
});
|
|
15659
|
+
}
|
|
15660
|
+
return steps.toSorted((a, b) => a.file.localeCompare(b.file));
|
|
15661
|
+
}
|
|
15662
|
+
//#endregion
|
|
15551
15663
|
//#region src/commands/sync-gates.ts
|
|
15552
15664
|
/**
|
|
15553
15665
|
* The pre-generator gates and config diagnostics every `repo:sync` mode runs -- split out of
|
|
@@ -15607,6 +15719,23 @@ function runCheckOnlyImageGate(config, targetDir) {
|
|
|
15607
15719
|
return images;
|
|
15608
15720
|
}
|
|
15609
15721
|
/**
|
|
15722
|
+
* Warn, once per workflow file, when a `@tooling:custom` block installs packages with apt. CI's
|
|
15723
|
+
* job runs as root and can do that; the agent's containers run without root and cannot, so the
|
|
15724
|
+
* package is present in CI and missing in the agent's measurement of the same tree. Advisory like
|
|
15725
|
+
* {@link runCheckOnlyImageGate}, and for the same reason it returns the entries: `repo:sync
|
|
15726
|
+
* --check` folds their count into the `::bst-checks:: warn` marker. It never fails a run and never
|
|
15727
|
+
* edits the block -- not every Debian package has a conda equivalent under the same name, so moving
|
|
15728
|
+
* it is a human's call.
|
|
15729
|
+
*/
|
|
15730
|
+
function runWorkflowAptGate(targetDir) {
|
|
15731
|
+
const steps = findWorkflowAptSteps(targetDir);
|
|
15732
|
+
for (const step of steps) {
|
|
15733
|
+
const packages = step.packages.length > 0 ? step.packages.join(", ") : "packages";
|
|
15734
|
+
log$1.warn(`${step.file} installs ${packages} with apt in a custom block. The agent's containers cannot run that step, so its runs lack what CI has. Declare each tool in mise.toml as conda:<name> (both CI and the agent install from it), then remove the apt step.`);
|
|
15735
|
+
}
|
|
15736
|
+
return steps;
|
|
15737
|
+
}
|
|
15738
|
+
/**
|
|
15610
15739
|
* Require a declared publish destination before any generator or apply step runs.
|
|
15611
15740
|
* Returns the number of offenders (0 = clean). Each is emitted via `log.error` so
|
|
15612
15741
|
* the user sees the full message and the fix.
|
|
@@ -15894,19 +16023,6 @@ function runToolchainGate(targetDir) {
|
|
|
15894
16023
|
};
|
|
15895
16024
|
}
|
|
15896
16025
|
//#endregion
|
|
15897
|
-
//#region src/utils/workflow-paths.ts
|
|
15898
|
-
/**
|
|
15899
|
-
* The single definition of "workflow dir", shared by `repo:sync --check`'s advisory
|
|
15900
|
-
* classification and the workflow sync sweep's path allowlist -- so the two halves cannot
|
|
15901
|
-
* disagree about what counts as workflow output. Each prefix carries its trailing slash so a
|
|
15902
|
-
* file named e.g. `.github/workflowsX` never matches.
|
|
15903
|
-
*/
|
|
15904
|
-
const WORKFLOW_DIR_PREFIXES = [".forgejo/workflows/", ".github/workflows/"];
|
|
15905
|
-
/** True when `filePath` (repo-relative) lives under one of the workflow directories. */
|
|
15906
|
-
function isWorkflowDirPath(filePath) {
|
|
15907
|
-
return WORKFLOW_DIR_PREFIXES.some((prefix) => filePath.startsWith(prefix));
|
|
15908
|
-
}
|
|
15909
|
-
//#endregion
|
|
15910
16026
|
//#region src/commands/repo-sync.ts
|
|
15911
16027
|
const rawLog$1 = (msg) => console.log(msg);
|
|
15912
16028
|
const syncCommand = defineCommand({
|
|
@@ -15946,7 +16062,7 @@ const syncCommand = defineCommand({
|
|
|
15946
16062
|
},
|
|
15947
16063
|
json: {
|
|
15948
16064
|
type: "boolean",
|
|
15949
|
-
description: "Emit one JSON document: status (up-to-date | drift | workflow-drift | failed), actionable, failed, toolchain, checkOnlyImages, warnCount, exitCode. --check only: the write mode prompts and inherits child streams."
|
|
16065
|
+
description: "Emit one JSON document: status (up-to-date | drift | workflow-drift | failed), actionable, failed, toolchain, checkOnlyImages, aptSteps, warnCount, exitCode. --check only: the write mode prompts and inherits child streams."
|
|
15950
16066
|
}
|
|
15951
16067
|
},
|
|
15952
16068
|
async run({ args }) {
|
|
@@ -16023,6 +16139,7 @@ function gateFailure(toolchainWarned = false) {
|
|
|
16023
16139
|
failed: [],
|
|
16024
16140
|
toolchain: { warned: toolchainWarned },
|
|
16025
16141
|
checkOnlyImages: [],
|
|
16142
|
+
aptSteps: [],
|
|
16026
16143
|
warnCount: 0,
|
|
16027
16144
|
exitCode: 1
|
|
16028
16145
|
};
|
|
@@ -16033,8 +16150,8 @@ function gateFailure(toolchainWarned = false) {
|
|
|
16033
16150
|
* withheld -- the same number rides in `warnCount`, and printing both would put a second thing on
|
|
16034
16151
|
* the stream beside the payload.
|
|
16035
16152
|
*/
|
|
16036
|
-
function upToDateReport(toolchain, checkOnlyImages) {
|
|
16037
|
-
const warnCount = (toolchain.warned ? 1 : 0) + checkOnlyImages.length;
|
|
16153
|
+
function upToDateReport(toolchain, checkOnlyImages, aptSteps) {
|
|
16154
|
+
const warnCount = (toolchain.warned ? 1 : 0) + checkOnlyImages.length + aptSteps.length;
|
|
16038
16155
|
if (warnCount > 0) {
|
|
16039
16156
|
if (!isJsonMode()) rawLog$1(formatWarnMarker(warnCount));
|
|
16040
16157
|
} else log$1.success("Repository is up to date.");
|
|
@@ -16044,6 +16161,7 @@ function upToDateReport(toolchain, checkOnlyImages) {
|
|
|
16044
16161
|
failed: [],
|
|
16045
16162
|
toolchain,
|
|
16046
16163
|
checkOnlyImages,
|
|
16164
|
+
aptSteps,
|
|
16047
16165
|
warnCount,
|
|
16048
16166
|
exitCode: 0
|
|
16049
16167
|
};
|
|
@@ -16079,28 +16197,30 @@ async function runCheckReport(targetDir) {
|
|
|
16079
16197
|
toolchain: { warned: toolchain.warned }
|
|
16080
16198
|
};
|
|
16081
16199
|
const checkOnlyImages = runCheckOnlyImageGate(config, targetDir);
|
|
16200
|
+
const aptSteps = runWorkflowAptGate(targetDir);
|
|
16082
16201
|
const { ctx, pendingWrites } = createDryRunContext(config);
|
|
16083
16202
|
logDetectionSummary(ctx);
|
|
16084
16203
|
const results = await runGenerators(ctx);
|
|
16085
16204
|
const failed = failedResults(results);
|
|
16086
16205
|
reportFailures(failed, "repo:sync");
|
|
16087
16206
|
const actionable = actionableResults(results, pendingWrites, targetDir);
|
|
16088
|
-
const
|
|
16207
|
+
const advisoryWarnCount = (toolchain.warned ? 1 : 0) + checkOnlyImages.length + aptSteps.length;
|
|
16089
16208
|
const toolchainState = { warned: toolchain.warned };
|
|
16090
|
-
if (actionable.length === 0 && failed.length === 0) return upToDateReport(toolchainState, checkOnlyImages);
|
|
16209
|
+
if (actionable.length === 0 && failed.length === 0) return upToDateReport(toolchainState, checkOnlyImages, aptSteps);
|
|
16091
16210
|
if (actionable.length === 0) return {
|
|
16092
16211
|
status: "failed",
|
|
16093
16212
|
actionable: [],
|
|
16094
16213
|
failed,
|
|
16095
16214
|
toolchain: toolchainState,
|
|
16096
16215
|
checkOnlyImages: [],
|
|
16216
|
+
aptSteps: [],
|
|
16097
16217
|
warnCount: 0,
|
|
16098
16218
|
exitCode: 1
|
|
16099
16219
|
};
|
|
16100
16220
|
log$1.warn(`${actionable.length} file(s) would be changed by repo:sync`);
|
|
16101
16221
|
for (const r of actionable) logActionableChange(r, targetDir, pendingWrites);
|
|
16102
16222
|
if (failed.length === 0 && actionable.every((r) => isWorkflowDirPath(r.filePath))) {
|
|
16103
|
-
const warnCount = actionable.length +
|
|
16223
|
+
const warnCount = actionable.length + advisoryWarnCount;
|
|
16104
16224
|
if (!isJsonMode()) rawLog$1(formatWarnMarker(warnCount));
|
|
16105
16225
|
return {
|
|
16106
16226
|
status: "workflow-drift",
|
|
@@ -16108,6 +16228,7 @@ async function runCheckReport(targetDir) {
|
|
|
16108
16228
|
failed: [],
|
|
16109
16229
|
toolchain: toolchainState,
|
|
16110
16230
|
checkOnlyImages,
|
|
16231
|
+
aptSteps,
|
|
16111
16232
|
warnCount,
|
|
16112
16233
|
exitCode: 0
|
|
16113
16234
|
};
|
|
@@ -16118,6 +16239,7 @@ async function runCheckReport(targetDir) {
|
|
|
16118
16239
|
failed,
|
|
16119
16240
|
toolchain: toolchainState,
|
|
16120
16241
|
checkOnlyImages: [],
|
|
16242
|
+
aptSteps: [],
|
|
16121
16243
|
warnCount: 0,
|
|
16122
16244
|
exitCode: 1
|
|
16123
16245
|
};
|
|
@@ -16312,6 +16434,9 @@ const PullListItemSchema = z.object({
|
|
|
16312
16434
|
updated_at: z.string().nullish(),
|
|
16313
16435
|
labels: z.array(z.object({ name: z.string() })).nullish()
|
|
16314
16436
|
});
|
|
16437
|
+
function updatedBefore(since) {
|
|
16438
|
+
return (pr) => pr.updatedAt !== null && Date.parse(pr.updatedAt) < since;
|
|
16439
|
+
}
|
|
16315
16440
|
/**
|
|
16316
16441
|
* Every pull request -- open, merged, and closed-unmerged -- by default, via `state=all`; a caller
|
|
16317
16442
|
* may narrow the read with {@link ListPullsOptions.state}. Paged to an empty page or to
|
|
@@ -16339,7 +16464,9 @@ async function listPulls(executor, conn, options = {}) {
|
|
|
16339
16464
|
limit: String(PULLS_PAGE_SIZE),
|
|
16340
16465
|
page: String(page)
|
|
16341
16466
|
});
|
|
16342
|
-
|
|
16467
|
+
const sort = options.updatedSince === void 0 ? options.sort : "recentupdate";
|
|
16468
|
+
if (sort !== void 0) params.set("sort", sort);
|
|
16469
|
+
for (const id of options.labelIds ?? []) params.append("labels", String(id));
|
|
16343
16470
|
const url = `${conn.serverUrl}/api/v1/repos/${conn.repository}/pulls?${params.toString()}`;
|
|
16344
16471
|
const res = await executor.fetch(url, {
|
|
16345
16472
|
headers,
|
|
@@ -16368,6 +16495,7 @@ async function listPulls(executor, conn, options = {}) {
|
|
|
16368
16495
|
pulls.push(...items);
|
|
16369
16496
|
if (items.length === 0) break;
|
|
16370
16497
|
if (options.stopWhen !== void 0 && items.some(options.stopWhen)) break;
|
|
16498
|
+
if (options.updatedSince !== void 0 && items.some(updatedBefore(options.updatedSince))) break;
|
|
16371
16499
|
if (options.limit !== void 0 && pulls.length >= options.limit) break;
|
|
16372
16500
|
if (page === PULLS_MAX_PAGES) {
|
|
16373
16501
|
log.warn(`PR listing on ${conn.repository} hit the ${String(PULLS_MAX_PAGES)}-page cap without an empty page; results may be truncated`);
|
|
@@ -21125,7 +21253,7 @@ function createRealLintTypesExecutor() {
|
|
|
21125
21253
|
}
|
|
21126
21254
|
//#endregion
|
|
21127
21255
|
//#region src/lint-types/phase.ts
|
|
21128
|
-
const LINT_TYPES_PHASE = "
|
|
21256
|
+
const LINT_TYPES_PHASE = "enforce";
|
|
21129
21257
|
//#endregion
|
|
21130
21258
|
//#region src/lint-types/report.ts
|
|
21131
21259
|
/** oxlint spells a rule code `plugin(rule)` (e.g. `typescript(no-base-to-string)`); the escape
|
|
@@ -21146,8 +21274,8 @@ function formatFindingLine(diagnostic) {
|
|
|
21146
21274
|
return `${span?.line !== void 0 && span.column !== void 0 ? `${diagnostic.filename}:${String(span.line)}:${String(span.column)}` : diagnostic.filename} ${formatRuleCode(diagnostic.code)} ${diagnostic.message}`;
|
|
21147
21275
|
}
|
|
21148
21276
|
/**
|
|
21149
|
-
*
|
|
21150
|
-
* worse than naming none.
|
|
21277
|
+
* Prints only when the phase is pinned to "report". Names no date: inventing one the fleet
|
|
21278
|
+
* cannot meet is worse than naming none.
|
|
21151
21279
|
*/
|
|
21152
21280
|
const CONSEQUENCE_LINE = "These will block once a future release of the tooling flips lint:types from the report phase to the enforce phase.";
|
|
21153
21281
|
const ESCAPE_HATCHES = [" - Turn a rule off in this repo's own oxlint.config.ts (e.g. \"typescript/<rule>\": \"off\") -- plain `pnpm lint` ignores type-aware rules entirely, so this never weakens it.", " - Skip the step entirely with .tooling.json#checks.skip: [\"lint:types\"]."];
|
|
@@ -21351,7 +21479,7 @@ function runLintTypes(executor, targetDir, extraArgs = []) {
|
|
|
21351
21479
|
const lintTypesCommand = defineCommand({
|
|
21352
21480
|
meta: {
|
|
21353
21481
|
name: "lint:types",
|
|
21354
|
-
description: "Run oxlint --type-aware (backed by oxlint-tsgolint) and report every type-aware finding, selected by subtracting a plain oxlint run.
|
|
21482
|
+
description: "Run oxlint --type-aware (backed by oxlint-tsgolint) and report every type-aware finding, selected by subtracting a plain oxlint run. Enforce phase: prints findings and exits 1 when there are any. .tooling.json#lint.strictTypeAware makes a missing oxlint-tsgolint binary a failure instead of a notice."
|
|
21355
21483
|
},
|
|
21356
21484
|
args: {
|
|
21357
21485
|
dir: {
|
|
@@ -32831,7 +32959,7 @@ setForgejoLogger(log$1);
|
|
|
32831
32959
|
* `local <sha>[-dirty]` otherwise -- so an artifact built from the working tree
|
|
32832
32960
|
* can't pass itself off as the published release whose version it carries.
|
|
32833
32961
|
*/
|
|
32834
|
-
const displayVersion = "0.
|
|
32962
|
+
const displayVersion = "0.87.0";
|
|
32835
32963
|
/**
|
|
32836
32964
|
* The registered `bst` subcommand map, exported so both the JSON entry path below (resolving a
|
|
32837
32965
|
* command name before citty parses anything) and the coverage test (enumerating every
|