@bensandee/tooling 0.52.3 → 0.53.1
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 +20 -0
- package/dist/bin.mjs +90 -12
- package/package.json +1 -1
- package/tooling.schema.json +10 -0
package/README.md
CHANGED
|
@@ -153,6 +153,26 @@ To override defaults, add a `docker` entry to `.tooling.json`:
|
|
|
153
153
|
|
|
154
154
|
The `context` field defaults to `"."` (project root) when omitted. Versions for tagging are read from each package's own `package.json`.
|
|
155
155
|
|
|
156
|
+
#### Per-package build args
|
|
157
|
+
|
|
158
|
+
Each `docker` entry may declare `buildArgs` — a map of `ARG_NAME` to value that is passed to that package's `docker build` as `--build-arg ARG_NAME=<value>`. Args flow to the configured package only, so they don't trigger "unused build arg" warnings on sibling images. They apply to both `docker:build` and `docker:publish`, so CI-published images match local builds.
|
|
159
|
+
|
|
160
|
+
```json
|
|
161
|
+
{
|
|
162
|
+
"docker": {
|
|
163
|
+
"frontend": {
|
|
164
|
+
"dockerfile": "packages/frontend/docker/Dockerfile",
|
|
165
|
+
"context": ".",
|
|
166
|
+
"buildArgs": {
|
|
167
|
+
"VITE_BASE_PATH": "${BASE_PATH:-/}"
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Values support `${VAR}` and `${VAR:-default}` expansion against `process.env`. Empty strings are treated as unset (POSIX `:-` semantics). Unset variables with no default expand to `""` and log a warning. When the CLI also receives `--build-arg` via `--` pass-through, the CLI value comes after the configured value and wins on duplicate `ARG_NAME`.
|
|
175
|
+
|
|
156
176
|
#### `docker:build`
|
|
157
177
|
|
|
158
178
|
Builds all discovered packages, or a single package with `--package`:
|
package/dist/bin.mjs
CHANGED
|
@@ -624,7 +624,8 @@ const ToolingConfigSchema = z.strictObject({
|
|
|
624
624
|
detectPackageTypes: z.boolean().optional().meta({ description: "Auto-detect project types for monorepo packages" }),
|
|
625
625
|
docker: z.record(z.string(), z.object({
|
|
626
626
|
dockerfile: z.string().meta({ description: "Path to Dockerfile relative to package" }),
|
|
627
|
-
context: z.string().default(".").meta({ description: "Docker build context relative to package" })
|
|
627
|
+
context: z.string().default(".").meta({ description: "Docker build context relative to package" }),
|
|
628
|
+
buildArgs: z.record(z.string(), z.string()).optional().meta({ description: "Build args passed to `docker build` for this package. Values support ${VAR} and ${VAR:-default} expansion against process.env." })
|
|
628
629
|
})).optional().meta({ description: "Docker package overrides (package name → config)" }),
|
|
629
630
|
dockerCheck: z.union([z.literal(false), DockerCheckConfigSchema]).optional().meta({ description: "Docker health check configuration or false to disable" }),
|
|
630
631
|
registryHost: z.string().optional().meta({ description: "Docker registry hostname (e.g. ghcr.io, code.example.com)" }),
|
|
@@ -715,7 +716,8 @@ function mergeWithSavedConfig(detected, saved) {
|
|
|
715
716
|
//#region src/release/docker.ts
|
|
716
717
|
const ToolingDockerMapSchema = z.record(z.string(), z.object({
|
|
717
718
|
dockerfile: z.string(),
|
|
718
|
-
context: z.string().default(".")
|
|
719
|
+
context: z.string().default("."),
|
|
720
|
+
buildArgs: z.record(z.string(), z.string()).optional()
|
|
719
721
|
}));
|
|
720
722
|
const ToolingConfigDockerSchema = z.object({ docker: ToolingDockerMapSchema.optional() });
|
|
721
723
|
const PackageInfoSchema = z.object({
|
|
@@ -877,16 +879,53 @@ function readRepoName(executor, cwd) {
|
|
|
877
879
|
if (!repoName) throw new FatalError("Root package.json must have a name field");
|
|
878
880
|
return repoName;
|
|
879
881
|
}
|
|
882
|
+
const INTERPOLATE_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g;
|
|
883
|
+
/**
|
|
884
|
+
* Expand `${VAR}` and `${VAR:-default}` in `value` against `env`. Unset (or empty)
|
|
885
|
+
* variables with no default produce `""` and invoke `onMissing(name)` once per occurrence.
|
|
886
|
+
* Literal `$` not followed by `{…}` passes through unchanged. POSIX `:-` semantics:
|
|
887
|
+
* empty string is treated the same as unset.
|
|
888
|
+
*/
|
|
889
|
+
function interpolate(value, env, onMissing) {
|
|
890
|
+
return value.replace(INTERPOLATE_RE, (_, name, defaultValue) => {
|
|
891
|
+
const v = env[name];
|
|
892
|
+
if (v !== void 0 && v !== "") return v;
|
|
893
|
+
if (defaultValue !== void 0) return defaultValue;
|
|
894
|
+
onMissing?.(name);
|
|
895
|
+
return "";
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
/**
|
|
899
|
+
* Translate a package's configured buildArgs map into `--build-arg K=V` flags,
|
|
900
|
+
* expanding env-var references in values. Warns once per unset variable per build.
|
|
901
|
+
*/
|
|
902
|
+
function resolveBuildArgs(pkg, env) {
|
|
903
|
+
const entries = Object.entries(pkg.docker.buildArgs ?? {});
|
|
904
|
+
if (entries.length === 0) return [];
|
|
905
|
+
const warned = /* @__PURE__ */ new Set();
|
|
906
|
+
const flags = [];
|
|
907
|
+
for (const [k, rawValue] of entries) {
|
|
908
|
+
const resolved = interpolate(rawValue, env, (missing) => {
|
|
909
|
+
if (warned.has(missing)) return;
|
|
910
|
+
warned.add(missing);
|
|
911
|
+
log.warn(`docker build arg "${k}" for ${pkg.dir}: env var ${missing} is not set; using empty string`);
|
|
912
|
+
});
|
|
913
|
+
flags.push("--build-arg", `${k}=${resolved}`);
|
|
914
|
+
}
|
|
915
|
+
return flags;
|
|
916
|
+
}
|
|
880
917
|
/** Build a single docker image from its config. Paths are resolved relative to cwd. */
|
|
881
918
|
function buildImage(executor, pkg, cwd, extraArgs) {
|
|
882
919
|
const dockerfilePath = path.resolve(cwd, pkg.docker.dockerfile);
|
|
883
920
|
const contextPath = path.resolve(cwd, pkg.docker.context);
|
|
921
|
+
const configuredArgs = resolveBuildArgs(pkg, process.env);
|
|
884
922
|
executor.execInherit("docker", [
|
|
885
923
|
"build",
|
|
886
924
|
"-f",
|
|
887
925
|
dockerfilePath,
|
|
888
926
|
"-t",
|
|
889
927
|
`${pkg.imageName}:latest`,
|
|
928
|
+
...configuredArgs,
|
|
890
929
|
...extraArgs,
|
|
891
930
|
contextPath
|
|
892
931
|
]);
|
|
@@ -1149,6 +1188,45 @@ function preserveDevBinaryPath(generated, existing) {
|
|
|
1149
1188
|
if (!match) return generated;
|
|
1150
1189
|
return generated.replaceAll("pnpm exec bst", match[0]).replaceAll("pnpm dlx @bensandee/tooling", match[0]);
|
|
1151
1190
|
}
|
|
1191
|
+
function escapeRegex(value) {
|
|
1192
|
+
return value.replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1193
|
+
}
|
|
1194
|
+
/** Match a `uses: <name>@<ref>` line, capturing indentation, action name, ref, and trailing comment. */
|
|
1195
|
+
const USES_LINE_PATTERN = /^(?<indent>\s*(?:-\s+)?uses:\s*)(?<name>[^\s@#]+)@(?<ref>\S+)(?<comment>[ \t]+#[^\n]*)?$/gm;
|
|
1196
|
+
/**
|
|
1197
|
+
* Extract a map of action-name → full verbatim tail (`<ref>` + optional inline comment)
|
|
1198
|
+
* from an existing workflow's `uses:` lines. Local actions (starting with `./` or
|
|
1199
|
+
* `docker://`) are skipped. First occurrence wins.
|
|
1200
|
+
*/
|
|
1201
|
+
function extractPinnedActions(existing) {
|
|
1202
|
+
const map = /* @__PURE__ */ new Map();
|
|
1203
|
+
for (const match of existing.matchAll(USES_LINE_PATTERN)) {
|
|
1204
|
+
const { name, ref, comment } = match.groups ?? {};
|
|
1205
|
+
if (!name || !ref) continue;
|
|
1206
|
+
if (name.startsWith("./") || name.startsWith("docker://")) continue;
|
|
1207
|
+
if (!map.has(name)) map.set(name, `${ref}${comment ?? ""}`);
|
|
1208
|
+
}
|
|
1209
|
+
return map;
|
|
1210
|
+
}
|
|
1211
|
+
/**
|
|
1212
|
+
* Preserve existing Renovate-managed action pins when rewriting a workflow.
|
|
1213
|
+
*
|
|
1214
|
+
* For each `uses: <name>@<ref>` in the existing file, if the generated file references the
|
|
1215
|
+
* same action name, substitute the existing ref (including any inline comment like `# v6`)
|
|
1216
|
+
* into the generated output. This keeps SHA pins and their version annotations stable when
|
|
1217
|
+
* unrelated workflow changes force a rewrite.
|
|
1218
|
+
*/
|
|
1219
|
+
function preservePinnedActions(generated, existing) {
|
|
1220
|
+
if (!existing) return generated;
|
|
1221
|
+
const pins = extractPinnedActions(existing);
|
|
1222
|
+
if (pins.size === 0) return generated;
|
|
1223
|
+
let result = generated;
|
|
1224
|
+
for (const [name, tail] of pins) {
|
|
1225
|
+
const pattern = new RegExp(String.raw`^(\s*(?:-\s+)?uses:\s*)${escapeRegex(name)}@\S+(?:[ \t]+#[^\n]*)?$`, "gm");
|
|
1226
|
+
result = result.replace(pattern, `$1${name}@${tail}`);
|
|
1227
|
+
}
|
|
1228
|
+
return result;
|
|
1229
|
+
}
|
|
1152
1230
|
/** Build a complete workflow YAML string from structured options. Single source of truth for workflow files. */
|
|
1153
1231
|
function buildWorkflowYaml(options) {
|
|
1154
1232
|
const doc = { name: options.name };
|
|
@@ -1311,8 +1389,8 @@ async function generateDeployCi(ctx) {
|
|
|
1311
1389
|
description: "Publish workflow already up to date"
|
|
1312
1390
|
};
|
|
1313
1391
|
}
|
|
1314
|
-
const
|
|
1315
|
-
const final = existing ? insertCustomBlocks(
|
|
1392
|
+
const withPinnedActions = preservePinnedActions(preserveDevBinaryPath(content, existing), existing);
|
|
1393
|
+
const final = existing ? insertCustomBlocks(withPinnedActions, extractCustomBlocks(existing)) : withPinnedActions;
|
|
1316
1394
|
ctx.write(workflowPath, final);
|
|
1317
1395
|
return {
|
|
1318
1396
|
filePath: workflowPath,
|
|
@@ -1596,7 +1674,7 @@ async function generatePackageJson(ctx) {
|
|
|
1596
1674
|
const devDeps = { ...ROOT_DEV_DEPS };
|
|
1597
1675
|
if (!isMonorepo) Object.assign(devDeps, PER_PACKAGE_DEV_DEPS);
|
|
1598
1676
|
devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.10.2";
|
|
1599
|
-
devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.
|
|
1677
|
+
devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.53.1";
|
|
1600
1678
|
if (ctx.config.useEslintPlugin) devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "0.12.0";
|
|
1601
1679
|
if (ctx.config.formatter === "oxfmt") devDeps["oxfmt"] = {
|
|
1602
1680
|
"@changesets/cli": "2.30.0",
|
|
@@ -2388,8 +2466,8 @@ async function generateReleaseCi(ctx) {
|
|
|
2388
2466
|
description: "Release workflow already up to date"
|
|
2389
2467
|
};
|
|
2390
2468
|
}
|
|
2391
|
-
const
|
|
2392
|
-
const final = existing ? insertCustomBlocks(
|
|
2469
|
+
const withPinnedActions = preservePinnedActions(preserveDevBinaryPath(content, existing), existing);
|
|
2470
|
+
const final = existing ? insertCustomBlocks(withPinnedActions, extractCustomBlocks(existing)) : withPinnedActions;
|
|
2393
2471
|
ctx.write(workflowPath, final);
|
|
2394
2472
|
return {
|
|
2395
2473
|
filePath: workflowPath,
|
|
@@ -2518,8 +2596,8 @@ async function generateCi(ctx) {
|
|
|
2518
2596
|
description: "CI workflow already up to date"
|
|
2519
2597
|
};
|
|
2520
2598
|
}
|
|
2521
|
-
const
|
|
2522
|
-
const final = existing ? insertCustomBlocks(
|
|
2599
|
+
const withPinnedActions = preservePinnedActions(preserveDevBinaryPath(content, existing), existing);
|
|
2600
|
+
const final = existing ? insertCustomBlocks(withPinnedActions, extractCustomBlocks(existing)) : withPinnedActions;
|
|
2523
2601
|
ctx.write(filePath, final);
|
|
2524
2602
|
return {
|
|
2525
2603
|
filePath,
|
|
@@ -3372,7 +3450,7 @@ async function runGenerators(ctx) {
|
|
|
3372
3450
|
function generateMigratePrompt(results, config, detected) {
|
|
3373
3451
|
const sections = [];
|
|
3374
3452
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
3375
|
-
sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.
|
|
3453
|
+
sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.53.1 repo:sync\` on ${timestamp}_\n`);
|
|
3376
3454
|
const changed = results.filter((r) => r.action === "created" || r.action === "updated" || r.action === "archived");
|
|
3377
3455
|
if (changed.length > 0) {
|
|
3378
3456
|
const items = changed.map((r) => {
|
|
@@ -6675,7 +6753,7 @@ async function sendWebhookWithLogging(url, secret, payload) {
|
|
|
6675
6753
|
const main = defineCommand({
|
|
6676
6754
|
meta: {
|
|
6677
6755
|
name: "bst",
|
|
6678
|
-
version: "0.
|
|
6756
|
+
version: "0.53.1",
|
|
6679
6757
|
description: "Bootstrap and maintain standardized TypeScript project tooling"
|
|
6680
6758
|
},
|
|
6681
6759
|
subCommands: {
|
|
@@ -6743,7 +6821,7 @@ const main = defineCommand({
|
|
|
6743
6821
|
})
|
|
6744
6822
|
}
|
|
6745
6823
|
});
|
|
6746
|
-
console.log(`@bensandee/tooling v0.
|
|
6824
|
+
console.log(`@bensandee/tooling v0.53.1`);
|
|
6747
6825
|
async function run() {
|
|
6748
6826
|
await runMain(main);
|
|
6749
6827
|
process.exit(process.exitCode ?? 0);
|
package/package.json
CHANGED
package/tooling.schema.json
CHANGED
|
@@ -74,6 +74,16 @@
|
|
|
74
74
|
"default": ".",
|
|
75
75
|
"description": "Docker build context relative to package",
|
|
76
76
|
"type": "string"
|
|
77
|
+
},
|
|
78
|
+
"buildArgs": {
|
|
79
|
+
"description": "Build args passed to `docker build` for this package. Values support ${VAR} and ${VAR:-default} expansion against process.env.",
|
|
80
|
+
"type": "object",
|
|
81
|
+
"propertyNames": {
|
|
82
|
+
"type": "string"
|
|
83
|
+
},
|
|
84
|
+
"additionalProperties": {
|
|
85
|
+
"type": "string"
|
|
86
|
+
}
|
|
77
87
|
}
|
|
78
88
|
},
|
|
79
89
|
"required": ["dockerfile", "context"],
|