@bensandee/tooling 0.34.0 → 0.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.mjs +187 -453
- package/package.json +1 -1
package/dist/bin.mjs
CHANGED
|
@@ -10,7 +10,7 @@ import JSON5 from "json5";
|
|
|
10
10
|
import { parse } from "jsonc-parser";
|
|
11
11
|
import { z } from "zod";
|
|
12
12
|
import { FatalError, TransientError, UnexpectedError } from "@bensandee/common";
|
|
13
|
-
import { isMap, isScalar,
|
|
13
|
+
import { isMap, isScalar, parse as parse$1, parseDocument, stringify, visit } from "yaml";
|
|
14
14
|
import picomatch from "picomatch";
|
|
15
15
|
import { tmpdir } from "node:os";
|
|
16
16
|
//#region src/types.ts
|
|
@@ -963,10 +963,6 @@ function ensureSchemaComment(content, ci) {
|
|
|
963
963
|
if (content.includes("yaml-language-server")) return content;
|
|
964
964
|
return FORGEJO_SCHEMA_COMMENT + content;
|
|
965
965
|
}
|
|
966
|
-
/** Migrate content from old tooling binary name to new. */
|
|
967
|
-
function migrateToolingBinary(content) {
|
|
968
|
-
return content.replaceAll("pnpm exec tooling ", "pnpm exec bst ");
|
|
969
|
-
}
|
|
970
966
|
/** Check if a YAML file has an opt-out comment in the first 10 lines. */
|
|
971
967
|
function isToolingIgnored(content) {
|
|
972
968
|
return content.split("\n", 10).some((line) => line.includes(IGNORE_PATTERN));
|
|
@@ -1011,14 +1007,42 @@ function mergeLefthookCommands(existing, requiredCommands) {
|
|
|
1011
1007
|
};
|
|
1012
1008
|
}
|
|
1013
1009
|
}
|
|
1014
|
-
/**
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1010
|
+
/**
|
|
1011
|
+
* Normalize a workflow YAML string for comparison purposes.
|
|
1012
|
+
* Parses the YAML DOM to strip action versions from `uses:` values and remove all comments,
|
|
1013
|
+
* then re-serializes so that pinned hashes, older tags, or formatting differences
|
|
1014
|
+
* don't cause unnecessary overwrites.
|
|
1015
|
+
*/
|
|
1016
|
+
function normalizeWorkflow(content) {
|
|
1017
|
+
const doc = parseDocument(content.replaceAll(/node\s+packages\/tooling-cli\/dist\/bin\.mjs/g, "pnpm exec bst"));
|
|
1018
|
+
doc.comment = null;
|
|
1019
|
+
doc.commentBefore = null;
|
|
1020
|
+
visit(doc, {
|
|
1021
|
+
Node(_key, node) {
|
|
1022
|
+
if ("comment" in node) node.comment = null;
|
|
1023
|
+
if ("commentBefore" in node) node.commentBefore = null;
|
|
1024
|
+
},
|
|
1025
|
+
Pair(_key, node) {
|
|
1026
|
+
if (isScalar(node.key) && node.key.value === "uses" && isScalar(node.value) && typeof node.value.value === "string") node.value.value = node.value.value.replace(/@.*$/, "");
|
|
1027
|
+
}
|
|
1028
|
+
});
|
|
1029
|
+
return doc.toString({
|
|
1030
|
+
lineWidth: 0,
|
|
1031
|
+
nullStr: ""
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
const DEV_BINARY_PATTERN = /node\s+packages\/tooling-cli\/dist\/bin\.mjs/;
|
|
1035
|
+
/**
|
|
1036
|
+
* If the existing file uses the dev binary path (`node packages/tooling-cli/dist/bin.mjs`),
|
|
1037
|
+
* substitute it back into the generated content so we don't overwrite it with `pnpm exec bst`.
|
|
1038
|
+
*/
|
|
1039
|
+
function preserveDevBinaryPath(generated, existing) {
|
|
1040
|
+
if (!existing) return generated;
|
|
1041
|
+
const match = DEV_BINARY_PATTERN.exec(existing);
|
|
1042
|
+
if (!match) return generated;
|
|
1043
|
+
return generated.replaceAll("pnpm exec bst", match[0]);
|
|
1020
1044
|
}
|
|
1021
|
-
/** Build a complete workflow YAML string from structured options. Single source of truth for
|
|
1045
|
+
/** Build a complete workflow YAML string from structured options. Single source of truth for workflow files. */
|
|
1022
1046
|
function buildWorkflowYaml(options) {
|
|
1023
1047
|
const doc = { name: options.name };
|
|
1024
1048
|
if (options.enableEmailNotifications) doc["enable-email-notifications"] = true;
|
|
@@ -1032,132 +1056,7 @@ function buildWorkflowYaml(options) {
|
|
|
1032
1056
|
return ensureSchemaComment(stringify(doc, {
|
|
1033
1057
|
lineWidth: 0,
|
|
1034
1058
|
nullStr: ""
|
|
1035
|
-
}), options.ci);
|
|
1036
|
-
}
|
|
1037
|
-
/**
|
|
1038
|
-
* Ensure required steps exist in a workflow job's steps array.
|
|
1039
|
-
* Only adds missing steps at the end — never modifies existing ones.
|
|
1040
|
-
* Returns unchanged content if the file has an opt-out comment or can't be parsed.
|
|
1041
|
-
*/
|
|
1042
|
-
function mergeWorkflowSteps(existing, jobName, requiredSteps) {
|
|
1043
|
-
if (isToolingIgnored(existing)) return {
|
|
1044
|
-
content: existing,
|
|
1045
|
-
changed: false
|
|
1046
|
-
};
|
|
1047
|
-
try {
|
|
1048
|
-
const doc = parseDocument(existing);
|
|
1049
|
-
const steps = doc.getIn([
|
|
1050
|
-
"jobs",
|
|
1051
|
-
jobName,
|
|
1052
|
-
"steps"
|
|
1053
|
-
]);
|
|
1054
|
-
if (!isSeq(steps)) return {
|
|
1055
|
-
content: existing,
|
|
1056
|
-
changed: false
|
|
1057
|
-
};
|
|
1058
|
-
let changed = false;
|
|
1059
|
-
for (const { match, step } of requiredSteps) if (!steps.items.some((item) => {
|
|
1060
|
-
if (!isMap(item)) return false;
|
|
1061
|
-
if (match.run) {
|
|
1062
|
-
const run = item.get("run");
|
|
1063
|
-
return typeof run === "string" && run.includes(match.run);
|
|
1064
|
-
}
|
|
1065
|
-
if (match.uses) {
|
|
1066
|
-
const uses = item.get("uses");
|
|
1067
|
-
return typeof uses === "string" && uses.startsWith(match.uses);
|
|
1068
|
-
}
|
|
1069
|
-
return false;
|
|
1070
|
-
})) {
|
|
1071
|
-
steps.add(doc.createNode(step));
|
|
1072
|
-
changed = true;
|
|
1073
|
-
}
|
|
1074
|
-
return {
|
|
1075
|
-
content: changed ? doc.toString() : existing,
|
|
1076
|
-
changed
|
|
1077
|
-
};
|
|
1078
|
-
} catch {
|
|
1079
|
-
return {
|
|
1080
|
-
content: existing,
|
|
1081
|
-
changed: false
|
|
1082
|
-
};
|
|
1083
|
-
}
|
|
1084
|
-
}
|
|
1085
|
-
/**
|
|
1086
|
-
* Add a job to an existing workflow YAML if it doesn't already exist.
|
|
1087
|
-
* Returns unchanged content if the job already exists, the file has an opt-out comment,
|
|
1088
|
-
* or the document can't be parsed.
|
|
1089
|
-
*/
|
|
1090
|
-
/**
|
|
1091
|
-
* Ensure a `concurrency` block exists at the workflow top level.
|
|
1092
|
-
* Adds it if missing — never modifies an existing one.
|
|
1093
|
-
* Returns unchanged content if the file has an opt-out comment or can't be parsed.
|
|
1094
|
-
*/
|
|
1095
|
-
/**
|
|
1096
|
-
* Ensure `on.push` has `tags-ignore: ["**"]` so tag pushes don't trigger CI.
|
|
1097
|
-
* Only adds the filter when `on.push` exists and `tags-ignore` is absent.
|
|
1098
|
-
*/
|
|
1099
|
-
function ensureWorkflowTagsIgnore(existing) {
|
|
1100
|
-
if (isToolingIgnored(existing)) return {
|
|
1101
|
-
content: existing,
|
|
1102
|
-
changed: false
|
|
1103
|
-
};
|
|
1104
|
-
try {
|
|
1105
|
-
const doc = parseDocument(existing);
|
|
1106
|
-
const on = doc.get("on");
|
|
1107
|
-
if (!isMap(on)) return {
|
|
1108
|
-
content: existing,
|
|
1109
|
-
changed: false
|
|
1110
|
-
};
|
|
1111
|
-
const push = on.get("push");
|
|
1112
|
-
if (!isMap(push)) return {
|
|
1113
|
-
content: existing,
|
|
1114
|
-
changed: false
|
|
1115
|
-
};
|
|
1116
|
-
if (push.has("tags-ignore")) return {
|
|
1117
|
-
content: existing,
|
|
1118
|
-
changed: false
|
|
1119
|
-
};
|
|
1120
|
-
push.set("tags-ignore", ["**"]);
|
|
1121
|
-
return {
|
|
1122
|
-
content: doc.toString(),
|
|
1123
|
-
changed: true
|
|
1124
|
-
};
|
|
1125
|
-
} catch {
|
|
1126
|
-
return {
|
|
1127
|
-
content: existing,
|
|
1128
|
-
changed: false
|
|
1129
|
-
};
|
|
1130
|
-
}
|
|
1131
|
-
}
|
|
1132
|
-
function ensureWorkflowConcurrency(existing, concurrency) {
|
|
1133
|
-
if (isToolingIgnored(existing)) return {
|
|
1134
|
-
content: existing,
|
|
1135
|
-
changed: false
|
|
1136
|
-
};
|
|
1137
|
-
try {
|
|
1138
|
-
const doc = parseDocument(existing);
|
|
1139
|
-
if (doc.has("concurrency")) return {
|
|
1140
|
-
content: existing,
|
|
1141
|
-
changed: false
|
|
1142
|
-
};
|
|
1143
|
-
doc.set("concurrency", concurrency);
|
|
1144
|
-
const contents = doc.contents;
|
|
1145
|
-
if (isMap(contents)) {
|
|
1146
|
-
const items = contents.items;
|
|
1147
|
-
const nameIdx = items.findIndex((p) => isScalar(p.key) && p.key.value === "name");
|
|
1148
|
-
const concPair = items.pop();
|
|
1149
|
-
if (concPair) items.splice(nameIdx + 1, 0, concPair);
|
|
1150
|
-
}
|
|
1151
|
-
return {
|
|
1152
|
-
content: doc.toString(),
|
|
1153
|
-
changed: true
|
|
1154
|
-
};
|
|
1155
|
-
} catch {
|
|
1156
|
-
return {
|
|
1157
|
-
content: existing,
|
|
1158
|
-
changed: false
|
|
1159
|
-
};
|
|
1160
|
-
}
|
|
1059
|
+
}).replace(/^(?=\S)/gm, (match, offset) => offset === 0 ? match : `\n${match}`), options.ci);
|
|
1161
1060
|
}
|
|
1162
1061
|
//#endregion
|
|
1163
1062
|
//#region src/generators/ci-utils.ts
|
|
@@ -1177,38 +1076,24 @@ function computeNodeVersionYaml(ctx) {
|
|
|
1177
1076
|
//#region src/generators/publish-ci.ts
|
|
1178
1077
|
function publishSteps(nodeVersionYaml) {
|
|
1179
1078
|
return [
|
|
1180
|
-
{
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
},
|
|
1199
|
-
{
|
|
1200
|
-
match: { run: "docker:publish" },
|
|
1201
|
-
step: {
|
|
1202
|
-
name: "Publish Docker images",
|
|
1203
|
-
env: {
|
|
1204
|
-
DOCKER_REGISTRY_HOST: actionsExpr("vars.DOCKER_REGISTRY_HOST"),
|
|
1205
|
-
DOCKER_REGISTRY_NAMESPACE: actionsExpr("vars.DOCKER_REGISTRY_NAMESPACE"),
|
|
1206
|
-
DOCKER_USERNAME: actionsExpr("secrets.DOCKER_USERNAME"),
|
|
1207
|
-
DOCKER_PASSWORD: actionsExpr("secrets.DOCKER_PASSWORD")
|
|
1208
|
-
},
|
|
1209
|
-
run: "pnpm exec bst docker:publish"
|
|
1210
|
-
}
|
|
1211
|
-
}
|
|
1079
|
+
{ step: { uses: "actions/checkout@v6" } },
|
|
1080
|
+
{ step: { uses: "pnpm/action-setup@v5" } },
|
|
1081
|
+
{ step: {
|
|
1082
|
+
uses: "actions/setup-node@v6",
|
|
1083
|
+
with: nodeVersionYaml.startsWith("node-version-file") ? { "node-version-file": "package.json" } : { "node-version": "24" }
|
|
1084
|
+
} },
|
|
1085
|
+
{ step: { run: "pnpm install --frozen-lockfile" } },
|
|
1086
|
+
{ step: {
|
|
1087
|
+
name: "Publish Docker images",
|
|
1088
|
+
env: {
|
|
1089
|
+
DOCKER_REGISTRY_HOST: actionsExpr("vars.DOCKER_REGISTRY_HOST"),
|
|
1090
|
+
DOCKER_REGISTRY_NAMESPACE: actionsExpr("vars.DOCKER_REGISTRY_NAMESPACE"),
|
|
1091
|
+
DOCKER_USERNAME: actionsExpr("secrets.DOCKER_USERNAME"),
|
|
1092
|
+
DOCKER_PASSWORD: actionsExpr("secrets.DOCKER_PASSWORD"),
|
|
1093
|
+
RELEASE_DEBUG: actionsExpr("vars.RELEASE_DEBUG || 'false'")
|
|
1094
|
+
},
|
|
1095
|
+
run: "pnpm exec bst docker:publish"
|
|
1096
|
+
} }
|
|
1212
1097
|
];
|
|
1213
1098
|
}
|
|
1214
1099
|
const DockerMapSchema = z.object({ docker: z.record(z.string(), z.unknown()).optional() });
|
|
@@ -1257,59 +1142,24 @@ async function generateDeployCi(ctx) {
|
|
|
1257
1142
|
jobName: "publish",
|
|
1258
1143
|
steps
|
|
1259
1144
|
});
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
action: "updated",
|
|
1270
|
-
description: "Migrated tooling binary name in publish workflow"
|
|
1271
|
-
};
|
|
1272
|
-
}
|
|
1273
|
-
return {
|
|
1274
|
-
filePath: workflowPath,
|
|
1275
|
-
action: "skipped",
|
|
1276
|
-
description: "Publish workflow already up to date"
|
|
1277
|
-
};
|
|
1278
|
-
}
|
|
1279
|
-
const merged = mergeWorkflowSteps(existing, "publish", toRequiredSteps(steps));
|
|
1280
|
-
const withComment = ensureSchemaComment(merged.content, ctx.config.ci);
|
|
1281
|
-
if (!merged.changed) {
|
|
1282
|
-
if (withComment !== raw) {
|
|
1283
|
-
ctx.write(workflowPath, withComment);
|
|
1284
|
-
return {
|
|
1285
|
-
filePath: workflowPath,
|
|
1286
|
-
action: "updated",
|
|
1287
|
-
description: existing !== raw ? "Migrated tooling binary name in publish workflow" : "Added schema comment to publish workflow"
|
|
1288
|
-
};
|
|
1289
|
-
}
|
|
1290
|
-
return {
|
|
1291
|
-
filePath: workflowPath,
|
|
1292
|
-
action: "skipped",
|
|
1293
|
-
description: "Existing publish workflow preserved"
|
|
1294
|
-
};
|
|
1295
|
-
}
|
|
1296
|
-
ctx.write(workflowPath, withComment);
|
|
1297
|
-
return {
|
|
1298
|
-
filePath: workflowPath,
|
|
1299
|
-
action: "updated",
|
|
1300
|
-
description: "Added missing steps to publish workflow"
|
|
1301
|
-
};
|
|
1302
|
-
}
|
|
1303
|
-
return {
|
|
1145
|
+
const alreadyExists = ctx.exists(workflowPath);
|
|
1146
|
+
const existing = alreadyExists ? ctx.read(workflowPath) : void 0;
|
|
1147
|
+
if (existing) {
|
|
1148
|
+
if (isToolingIgnored(existing)) return {
|
|
1149
|
+
filePath: workflowPath,
|
|
1150
|
+
action: "skipped",
|
|
1151
|
+
description: "Publish workflow has ignore comment"
|
|
1152
|
+
};
|
|
1153
|
+
if (normalizeWorkflow(existing) === normalizeWorkflow(content)) return {
|
|
1304
1154
|
filePath: workflowPath,
|
|
1305
1155
|
action: "skipped",
|
|
1306
1156
|
description: "Publish workflow already up to date"
|
|
1307
1157
|
};
|
|
1308
1158
|
}
|
|
1309
|
-
ctx.write(workflowPath, content);
|
|
1159
|
+
ctx.write(workflowPath, preserveDevBinaryPath(content, existing));
|
|
1310
1160
|
return {
|
|
1311
1161
|
filePath: workflowPath,
|
|
1312
|
-
action: "created",
|
|
1162
|
+
action: alreadyExists ? "updated" : "created",
|
|
1313
1163
|
description: `Generated ${isGitHub ? "GitHub" : "Forgejo"} Actions publish workflow`
|
|
1314
1164
|
};
|
|
1315
1165
|
}
|
|
@@ -1577,7 +1427,7 @@ function getAddedDevDepNames(config) {
|
|
|
1577
1427
|
const deps = { ...ROOT_DEV_DEPS };
|
|
1578
1428
|
if (config.structure !== "monorepo") Object.assign(deps, PER_PACKAGE_DEV_DEPS);
|
|
1579
1429
|
deps["@bensandee/config"] = "0.9.1";
|
|
1580
|
-
deps["@bensandee/tooling"] = "0.
|
|
1430
|
+
deps["@bensandee/tooling"] = "0.35.0";
|
|
1581
1431
|
if (config.formatter === "oxfmt") deps["oxfmt"] = {
|
|
1582
1432
|
"@changesets/cli": "2.30.0",
|
|
1583
1433
|
"@release-it/bumper": "7.0.5",
|
|
@@ -1632,7 +1482,7 @@ async function generatePackageJson(ctx) {
|
|
|
1632
1482
|
const devDeps = { ...ROOT_DEV_DEPS };
|
|
1633
1483
|
if (!isMonorepo) Object.assign(devDeps, PER_PACKAGE_DEV_DEPS);
|
|
1634
1484
|
devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.9.1";
|
|
1635
|
-
devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.
|
|
1485
|
+
devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.35.0";
|
|
1636
1486
|
if (ctx.config.useEslintPlugin) devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "0.9.2";
|
|
1637
1487
|
if (ctx.config.formatter === "oxfmt") devDeps["oxfmt"] = {
|
|
1638
1488
|
"@changesets/cli": "2.30.0",
|
|
@@ -1680,10 +1530,6 @@ async function generatePackageJson(ctx) {
|
|
|
1680
1530
|
changes.push("set type: \"module\"");
|
|
1681
1531
|
}
|
|
1682
1532
|
const existingScripts = pkg.scripts ?? {};
|
|
1683
|
-
for (const [key, value] of Object.entries(existingScripts)) if (typeof value === "string" && value.includes("pnpm exec tooling ")) {
|
|
1684
|
-
existingScripts[key] = migrateToolingBinary(value);
|
|
1685
|
-
changes.push(`migrated script: ${key}`);
|
|
1686
|
-
}
|
|
1687
1533
|
for (const [key, value] of Object.entries(allScripts)) if (!(key in existingScripts)) {
|
|
1688
1534
|
existingScripts[key] = value;
|
|
1689
1535
|
changes.push(`added script: ${key}`);
|
|
@@ -2170,41 +2016,58 @@ async function generateGitignore(ctx) {
|
|
|
2170
2016
|
}
|
|
2171
2017
|
//#endregion
|
|
2172
2018
|
//#region src/generators/ci.ts
|
|
2019
|
+
/** Build the release step for the check job (changesets strategy). */
|
|
2020
|
+
function changesetsReleaseStep(ci, publishesNpm) {
|
|
2021
|
+
if (ci === "github") return { step: {
|
|
2022
|
+
uses: "changesets/action@v1",
|
|
2023
|
+
if: "github.ref == 'refs/heads/main'",
|
|
2024
|
+
with: {
|
|
2025
|
+
publish: "pnpm changeset publish",
|
|
2026
|
+
version: "pnpm changeset version"
|
|
2027
|
+
},
|
|
2028
|
+
env: {
|
|
2029
|
+
GITHUB_TOKEN: actionsExpr("github.token"),
|
|
2030
|
+
...publishesNpm && { NPM_TOKEN: actionsExpr("secrets.NPM_TOKEN") }
|
|
2031
|
+
}
|
|
2032
|
+
} };
|
|
2033
|
+
return { step: {
|
|
2034
|
+
name: "Release",
|
|
2035
|
+
if: "github.ref == 'refs/heads/main'",
|
|
2036
|
+
env: {
|
|
2037
|
+
FORGEJO_SERVER_URL: actionsExpr("github.server_url"),
|
|
2038
|
+
FORGEJO_REPOSITORY: actionsExpr("github.repository"),
|
|
2039
|
+
RELEASE_TOKEN: actionsExpr("secrets.RELEASE_TOKEN"),
|
|
2040
|
+
...publishesNpm && { NODE_AUTH_TOKEN: actionsExpr("secrets.NPM_TOKEN") },
|
|
2041
|
+
RELEASE_DEBUG: actionsExpr("vars.RELEASE_DEBUG || 'false'")
|
|
2042
|
+
},
|
|
2043
|
+
run: "pnpm exec bst release:changesets"
|
|
2044
|
+
} };
|
|
2045
|
+
}
|
|
2173
2046
|
const CI_CONCURRENCY = {
|
|
2174
2047
|
group: `ci-${actionsExpr("github.ref")}`,
|
|
2175
2048
|
"cancel-in-progress": actionsExpr("github.ref != 'refs/heads/main'")
|
|
2176
2049
|
};
|
|
2177
|
-
function checkSteps(nodeVersionYaml) {
|
|
2050
|
+
function checkSteps(nodeVersionYaml, publishesNpm) {
|
|
2051
|
+
const isNodeVersionFile = nodeVersionYaml.startsWith("node-version-file");
|
|
2178
2052
|
return [
|
|
2179
|
-
{
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
},
|
|
2183
|
-
{
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
uses: "actions/setup-node@v6",
|
|
2191
|
-
with: {
|
|
2192
|
-
...nodeVersionYaml.startsWith("node-version-file") ? { "node-version-file": "package.json" } : { "node-version": "24" },
|
|
2193
|
-
cache: "pnpm"
|
|
2194
|
-
}
|
|
2195
|
-
}
|
|
2196
|
-
},
|
|
2197
|
-
{
|
|
2198
|
-
match: { run: "pnpm install" },
|
|
2199
|
-
step: { run: "pnpm install --frozen-lockfile" }
|
|
2200
|
-
},
|
|
2201
|
-
{
|
|
2202
|
-
match: { run: "check" },
|
|
2203
|
-
step: {
|
|
2204
|
-
name: "Run all checks",
|
|
2205
|
-
run: "pnpm ci:check"
|
|
2053
|
+
{ step: {
|
|
2054
|
+
uses: "actions/checkout@v6",
|
|
2055
|
+
with: { "fetch-depth": 0 }
|
|
2056
|
+
} },
|
|
2057
|
+
{ step: { uses: "pnpm/action-setup@v5" } },
|
|
2058
|
+
{ step: {
|
|
2059
|
+
uses: "actions/setup-node@v6",
|
|
2060
|
+
with: {
|
|
2061
|
+
...isNodeVersionFile ? { "node-version-file": "package.json" } : { "node-version": "24" },
|
|
2062
|
+
cache: "pnpm",
|
|
2063
|
+
...publishesNpm && { "registry-url": actionsExpr("vars.NPM_REGISTRY_URL || 'https://registry.npmjs.org'") }
|
|
2206
2064
|
}
|
|
2207
|
-
}
|
|
2065
|
+
} },
|
|
2066
|
+
{ step: { run: "pnpm install --frozen-lockfile" } },
|
|
2067
|
+
{ step: {
|
|
2068
|
+
name: "Run all checks",
|
|
2069
|
+
run: "pnpm ci:check"
|
|
2070
|
+
} }
|
|
2208
2071
|
];
|
|
2209
2072
|
}
|
|
2210
2073
|
/** Resolve the CI workflow filename based on release strategy. */
|
|
@@ -2220,7 +2083,9 @@ async function generateCi(ctx) {
|
|
|
2220
2083
|
const isGitHub = ctx.config.ci === "github";
|
|
2221
2084
|
const isForgejo = !isGitHub;
|
|
2222
2085
|
const isChangesets = ctx.config.releaseStrategy === "changesets";
|
|
2223
|
-
const
|
|
2086
|
+
const nodeVersionYaml = computeNodeVersionYaml(ctx);
|
|
2087
|
+
const publishesNpm = ctx.config.publishNpm === true;
|
|
2088
|
+
const steps = [...checkSteps(nodeVersionYaml, isChangesets && publishesNpm), ...isChangesets ? [changesetsReleaseStep(ctx.config.ci, publishesNpm)] : []];
|
|
2224
2089
|
const content = buildWorkflowYaml({
|
|
2225
2090
|
ci: ctx.config.ci,
|
|
2226
2091
|
name: "CI",
|
|
@@ -2237,42 +2102,24 @@ async function generateCi(ctx) {
|
|
|
2237
2102
|
steps
|
|
2238
2103
|
});
|
|
2239
2104
|
const filePath = ciWorkflowPath(ctx.config.ci, ctx.config.releaseStrategy);
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
if (isChangesets) {
|
|
2250
|
-
const withConcurrency = ensureWorkflowConcurrency(result.content, CI_CONCURRENCY);
|
|
2251
|
-
result = {
|
|
2252
|
-
content: withConcurrency.content,
|
|
2253
|
-
changed: result.changed || withConcurrency.changed
|
|
2254
|
-
};
|
|
2255
|
-
}
|
|
2256
|
-
const withComment = ensureSchemaComment(result.content, isGitHub ? "github" : "forgejo");
|
|
2257
|
-
if (result.changed || withComment !== result.content) {
|
|
2258
|
-
ctx.write(filePath, withComment);
|
|
2259
|
-
return {
|
|
2260
|
-
filePath,
|
|
2261
|
-
action: "updated",
|
|
2262
|
-
description: "Added missing steps to CI workflow"
|
|
2263
|
-
};
|
|
2264
|
-
}
|
|
2265
|
-
}
|
|
2266
|
-
return {
|
|
2105
|
+
const alreadyExists = ctx.exists(filePath);
|
|
2106
|
+
const existing = alreadyExists ? ctx.read(filePath) : void 0;
|
|
2107
|
+
if (existing) {
|
|
2108
|
+
if (isToolingIgnored(existing)) return {
|
|
2109
|
+
filePath,
|
|
2110
|
+
action: "skipped",
|
|
2111
|
+
description: "CI workflow has ignore comment"
|
|
2112
|
+
};
|
|
2113
|
+
if (normalizeWorkflow(existing) === normalizeWorkflow(content)) return {
|
|
2267
2114
|
filePath,
|
|
2268
2115
|
action: "skipped",
|
|
2269
2116
|
description: "CI workflow already up to date"
|
|
2270
2117
|
};
|
|
2271
2118
|
}
|
|
2272
|
-
ctx.write(filePath, content);
|
|
2119
|
+
ctx.write(filePath, preserveDevBinaryPath(content, existing));
|
|
2273
2120
|
return {
|
|
2274
2121
|
filePath,
|
|
2275
|
-
action: "created",
|
|
2122
|
+
action: alreadyExists ? "updated" : "created",
|
|
2276
2123
|
description: `Generated ${isGitHub ? "GitHub" : "Forgejo"} Actions CI workflow`
|
|
2277
2124
|
};
|
|
2278
2125
|
}
|
|
@@ -2749,47 +2596,33 @@ async function generateChangesets(ctx) {
|
|
|
2749
2596
|
function commonSteps(nodeVersionYaml, publishesNpm) {
|
|
2750
2597
|
const isNodeVersionFile = nodeVersionYaml.startsWith("node-version-file");
|
|
2751
2598
|
return [
|
|
2752
|
-
{
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
{
|
|
2764
|
-
match: { uses: "actions/setup-node" },
|
|
2765
|
-
step: {
|
|
2766
|
-
uses: "actions/setup-node@v6",
|
|
2767
|
-
with: {
|
|
2768
|
-
...isNodeVersionFile ? { "node-version-file": "package.json" } : { "node-version": "24" },
|
|
2769
|
-
cache: "pnpm",
|
|
2770
|
-
...publishesNpm && { "registry-url": "https://registry.npmjs.org" }
|
|
2771
|
-
}
|
|
2599
|
+
{ step: {
|
|
2600
|
+
uses: "actions/checkout@v6",
|
|
2601
|
+
with: { "fetch-depth": 0 }
|
|
2602
|
+
} },
|
|
2603
|
+
{ step: { uses: "pnpm/action-setup@v5" } },
|
|
2604
|
+
{ step: {
|
|
2605
|
+
uses: "actions/setup-node@v6",
|
|
2606
|
+
with: {
|
|
2607
|
+
...isNodeVersionFile ? { "node-version-file": "package.json" } : { "node-version": "24" },
|
|
2608
|
+
cache: "pnpm",
|
|
2609
|
+
...publishesNpm && { "registry-url": actionsExpr("vars.NPM_REGISTRY_URL || 'https://registry.npmjs.org'") }
|
|
2772
2610
|
}
|
|
2773
|
-
},
|
|
2774
|
-
{
|
|
2775
|
-
match: { run: "pnpm install" },
|
|
2776
|
-
step: { run: "pnpm install --frozen-lockfile" }
|
|
2777
|
-
}
|
|
2611
|
+
} },
|
|
2612
|
+
{ step: { run: "pnpm install --frozen-lockfile" } }
|
|
2778
2613
|
];
|
|
2779
2614
|
}
|
|
2780
2615
|
function releaseItSteps(ci, nodeVersionYaml, publishesNpm) {
|
|
2781
2616
|
const tokenEnv = ci === "github" ? { GITHUB_TOKEN: actionsExpr("github.token") } : { RELEASE_TOKEN: actionsExpr("secrets.RELEASE_TOKEN") };
|
|
2782
2617
|
const npmEnv = publishesNpm ? { NODE_AUTH_TOKEN: actionsExpr("secrets.NPM_TOKEN") } : {};
|
|
2783
|
-
return [...commonSteps(nodeVersionYaml, publishesNpm), {
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
...npmEnv
|
|
2790
|
-
}
|
|
2618
|
+
return [...commonSteps(nodeVersionYaml, publishesNpm), { step: {
|
|
2619
|
+
run: "pnpm release-it --ci",
|
|
2620
|
+
env: {
|
|
2621
|
+
...tokenEnv,
|
|
2622
|
+
...npmEnv,
|
|
2623
|
+
RELEASE_DEBUG: actionsExpr("vars.RELEASE_DEBUG || 'false'")
|
|
2791
2624
|
}
|
|
2792
|
-
}];
|
|
2625
|
+
} }];
|
|
2793
2626
|
}
|
|
2794
2627
|
/** Build the workflow_dispatch trigger with optional inputs for the simple strategy. */
|
|
2795
2628
|
function simpleWorkflowDispatchTrigger() {
|
|
@@ -2827,70 +2660,36 @@ function simpleReleaseCommand() {
|
|
|
2827
2660
|
].join("\n");
|
|
2828
2661
|
}
|
|
2829
2662
|
function simpleReleaseSteps(ci, nodeVersionYaml, publishesNpm, hasDocker) {
|
|
2830
|
-
const releaseStep = {
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
env: ci === "github" ? { GITHUB_TOKEN: actionsExpr("github.token") } : {
|
|
2663
|
+
const releaseStep = { step: {
|
|
2664
|
+
name: "Release",
|
|
2665
|
+
env: {
|
|
2666
|
+
...ci === "github" ? { GITHUB_TOKEN: actionsExpr("github.token") } : {
|
|
2835
2667
|
FORGEJO_SERVER_URL: actionsExpr("github.server_url"),
|
|
2836
2668
|
FORGEJO_REPOSITORY: actionsExpr("github.repository"),
|
|
2837
2669
|
RELEASE_TOKEN: actionsExpr("secrets.RELEASE_TOKEN")
|
|
2838
2670
|
},
|
|
2839
|
-
|
|
2840
|
-
}
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
};
|
|
2671
|
+
RELEASE_DEBUG: actionsExpr("vars.RELEASE_DEBUG || 'false'")
|
|
2672
|
+
},
|
|
2673
|
+
run: simpleReleaseCommand()
|
|
2674
|
+
} };
|
|
2675
|
+
const dockerStep = { step: {
|
|
2676
|
+
name: "Publish Docker images",
|
|
2677
|
+
if: "success()",
|
|
2678
|
+
env: {
|
|
2679
|
+
DOCKER_REGISTRY_HOST: actionsExpr("vars.DOCKER_REGISTRY_HOST"),
|
|
2680
|
+
DOCKER_REGISTRY_NAMESPACE: actionsExpr("vars.DOCKER_REGISTRY_NAMESPACE"),
|
|
2681
|
+
DOCKER_USERNAME: actionsExpr("secrets.DOCKER_USERNAME"),
|
|
2682
|
+
DOCKER_PASSWORD: actionsExpr("secrets.DOCKER_PASSWORD"),
|
|
2683
|
+
RELEASE_DEBUG: actionsExpr("vars.RELEASE_DEBUG || 'false'")
|
|
2684
|
+
},
|
|
2685
|
+
run: "pnpm exec bst docker:publish"
|
|
2686
|
+
} };
|
|
2856
2687
|
return [
|
|
2857
2688
|
...commonSteps(nodeVersionYaml, publishesNpm),
|
|
2858
2689
|
releaseStep,
|
|
2859
2690
|
...hasDocker ? [dockerStep] : []
|
|
2860
2691
|
];
|
|
2861
2692
|
}
|
|
2862
|
-
/** Build the required release step for the check job (changesets). */
|
|
2863
|
-
function changesetsReleaseStep(ci, publishesNpm) {
|
|
2864
|
-
if (ci === "github") return {
|
|
2865
|
-
match: { uses: "changesets/action" },
|
|
2866
|
-
step: {
|
|
2867
|
-
uses: "changesets/action@v1",
|
|
2868
|
-
if: "github.ref == 'refs/heads/main'",
|
|
2869
|
-
with: {
|
|
2870
|
-
publish: "pnpm changeset publish",
|
|
2871
|
-
version: "pnpm changeset version"
|
|
2872
|
-
},
|
|
2873
|
-
env: {
|
|
2874
|
-
GITHUB_TOKEN: actionsExpr("github.token"),
|
|
2875
|
-
...publishesNpm && { NPM_TOKEN: actionsExpr("secrets.NPM_TOKEN") }
|
|
2876
|
-
}
|
|
2877
|
-
}
|
|
2878
|
-
};
|
|
2879
|
-
return {
|
|
2880
|
-
match: { run: "release:changesets" },
|
|
2881
|
-
step: {
|
|
2882
|
-
name: "Release",
|
|
2883
|
-
if: "github.ref == 'refs/heads/main'",
|
|
2884
|
-
env: {
|
|
2885
|
-
FORGEJO_SERVER_URL: actionsExpr("github.server_url"),
|
|
2886
|
-
FORGEJO_REPOSITORY: actionsExpr("github.repository"),
|
|
2887
|
-
RELEASE_TOKEN: actionsExpr("secrets.RELEASE_TOKEN"),
|
|
2888
|
-
...publishesNpm && { NODE_AUTH_TOKEN: actionsExpr("secrets.NPM_TOKEN") }
|
|
2889
|
-
},
|
|
2890
|
-
run: "pnpm exec bst release:changesets"
|
|
2891
|
-
}
|
|
2892
|
-
};
|
|
2893
|
-
}
|
|
2894
2693
|
function buildSteps(strategy, ci, nodeVersionYaml, publishesNpm, hasDocker) {
|
|
2895
2694
|
switch (strategy) {
|
|
2896
2695
|
case "release-it": return releaseItSteps(ci, nodeVersionYaml, publishesNpm);
|
|
@@ -2898,40 +2697,6 @@ function buildSteps(strategy, ci, nodeVersionYaml, publishesNpm, hasDocker) {
|
|
|
2898
2697
|
default: return null;
|
|
2899
2698
|
}
|
|
2900
2699
|
}
|
|
2901
|
-
function generateChangesetsReleaseCi(ctx, publishesNpm) {
|
|
2902
|
-
const ciPath = ciWorkflowPath(ctx.config.ci, ctx.config.releaseStrategy);
|
|
2903
|
-
const raw = ctx.read(ciPath);
|
|
2904
|
-
if (!raw) return {
|
|
2905
|
-
filePath: ciPath,
|
|
2906
|
-
action: "skipped",
|
|
2907
|
-
description: "CI workflow not found — run check generator first"
|
|
2908
|
-
};
|
|
2909
|
-
const existing = migrateToolingBinary(raw);
|
|
2910
|
-
const merged = mergeWorkflowSteps(existing, "check", [changesetsReleaseStep(ctx.config.ci, publishesNpm)]);
|
|
2911
|
-
if (!merged.changed) {
|
|
2912
|
-
if (existing !== raw) {
|
|
2913
|
-
const withComment = ensureSchemaComment(existing, ctx.config.ci);
|
|
2914
|
-
ctx.write(ciPath, withComment);
|
|
2915
|
-
return {
|
|
2916
|
-
filePath: ciPath,
|
|
2917
|
-
action: "updated",
|
|
2918
|
-
description: "Migrated tooling binary name in CI workflow"
|
|
2919
|
-
};
|
|
2920
|
-
}
|
|
2921
|
-
return {
|
|
2922
|
-
filePath: ciPath,
|
|
2923
|
-
action: "skipped",
|
|
2924
|
-
description: "Release step in CI workflow already up to date"
|
|
2925
|
-
};
|
|
2926
|
-
}
|
|
2927
|
-
const withComment = ensureSchemaComment(merged.content, ctx.config.ci);
|
|
2928
|
-
ctx.write(ciPath, withComment);
|
|
2929
|
-
return {
|
|
2930
|
-
filePath: ciPath,
|
|
2931
|
-
action: "updated",
|
|
2932
|
-
description: "Added release step to CI workflow"
|
|
2933
|
-
};
|
|
2934
|
-
}
|
|
2935
2700
|
async function generateReleaseCi(ctx) {
|
|
2936
2701
|
const filePath = "release-ci";
|
|
2937
2702
|
if (ctx.config.releaseStrategy === "none" || ctx.config.ci === "none") return {
|
|
@@ -2940,7 +2705,11 @@ async function generateReleaseCi(ctx) {
|
|
|
2940
2705
|
description: "Release CI workflow not applicable"
|
|
2941
2706
|
};
|
|
2942
2707
|
const publishesNpm = ctx.config.publishNpm === true;
|
|
2943
|
-
if (ctx.config.releaseStrategy === "changesets") return
|
|
2708
|
+
if (ctx.config.releaseStrategy === "changesets") return {
|
|
2709
|
+
filePath,
|
|
2710
|
+
action: "skipped",
|
|
2711
|
+
description: "Release step included in CI workflow"
|
|
2712
|
+
};
|
|
2944
2713
|
const isGitHub = ctx.config.ci === "github";
|
|
2945
2714
|
const workflowPath = isGitHub ? ".github/workflows/release.yml" : ".forgejo/workflows/release.yml";
|
|
2946
2715
|
const nodeVersionYaml = computeNodeVersionYaml(ctx);
|
|
@@ -2960,59 +2729,24 @@ async function generateReleaseCi(ctx) {
|
|
|
2960
2729
|
jobName: "release",
|
|
2961
2730
|
steps
|
|
2962
2731
|
});
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
action: "updated",
|
|
2973
|
-
description: "Migrated tooling binary name in release workflow"
|
|
2974
|
-
};
|
|
2975
|
-
}
|
|
2976
|
-
return {
|
|
2977
|
-
filePath: workflowPath,
|
|
2978
|
-
action: "skipped",
|
|
2979
|
-
description: "Release workflow already up to date"
|
|
2980
|
-
};
|
|
2981
|
-
}
|
|
2982
|
-
const merged = mergeWorkflowSteps(existing, "release", toRequiredSteps(steps));
|
|
2983
|
-
const withComment = ensureSchemaComment(merged.content, ctx.config.ci);
|
|
2984
|
-
if (!merged.changed) {
|
|
2985
|
-
if (withComment !== raw) {
|
|
2986
|
-
ctx.write(workflowPath, withComment);
|
|
2987
|
-
return {
|
|
2988
|
-
filePath: workflowPath,
|
|
2989
|
-
action: "updated",
|
|
2990
|
-
description: existing !== raw ? "Migrated tooling binary name in release workflow" : "Added schema comment to release workflow"
|
|
2991
|
-
};
|
|
2992
|
-
}
|
|
2993
|
-
return {
|
|
2994
|
-
filePath: workflowPath,
|
|
2995
|
-
action: "skipped",
|
|
2996
|
-
description: "Existing release workflow preserved"
|
|
2997
|
-
};
|
|
2998
|
-
}
|
|
2999
|
-
ctx.write(workflowPath, withComment);
|
|
3000
|
-
return {
|
|
3001
|
-
filePath: workflowPath,
|
|
3002
|
-
action: "updated",
|
|
3003
|
-
description: "Added missing steps to release workflow"
|
|
3004
|
-
};
|
|
3005
|
-
}
|
|
3006
|
-
return {
|
|
2732
|
+
const alreadyExists = ctx.exists(workflowPath);
|
|
2733
|
+
const existing = alreadyExists ? ctx.read(workflowPath) : void 0;
|
|
2734
|
+
if (existing) {
|
|
2735
|
+
if (isToolingIgnored(existing)) return {
|
|
2736
|
+
filePath: workflowPath,
|
|
2737
|
+
action: "skipped",
|
|
2738
|
+
description: "Release workflow has ignore comment"
|
|
2739
|
+
};
|
|
2740
|
+
if (normalizeWorkflow(existing) === normalizeWorkflow(content)) return {
|
|
3007
2741
|
filePath: workflowPath,
|
|
3008
2742
|
action: "skipped",
|
|
3009
2743
|
description: "Release workflow already up to date"
|
|
3010
2744
|
};
|
|
3011
2745
|
}
|
|
3012
|
-
ctx.write(workflowPath, content);
|
|
2746
|
+
ctx.write(workflowPath, preserveDevBinaryPath(content, existing));
|
|
3013
2747
|
return {
|
|
3014
2748
|
filePath: workflowPath,
|
|
3015
|
-
action: "created",
|
|
2749
|
+
action: alreadyExists ? "updated" : "created",
|
|
3016
2750
|
description: `Generated ${isGitHub ? "GitHub" : "Forgejo"} Actions release workflow`
|
|
3017
2751
|
};
|
|
3018
2752
|
}
|
|
@@ -3308,7 +3042,7 @@ function generateMigratePrompt(results, config, detected) {
|
|
|
3308
3042
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
3309
3043
|
sections.push("# Migration Prompt");
|
|
3310
3044
|
sections.push("");
|
|
3311
|
-
sections.push(`_Generated by \`@bensandee/tooling@0.
|
|
3045
|
+
sections.push(`_Generated by \`@bensandee/tooling@0.35.0 repo:sync\` on ${timestamp}_`);
|
|
3312
3046
|
sections.push("");
|
|
3313
3047
|
sections.push("The following prompt was generated by `@bensandee/tooling repo:sync`. Paste it into Claude Code or another AI assistant to finish migrating this repository.");
|
|
3314
3048
|
sections.push("");
|
|
@@ -5181,7 +4915,7 @@ const dockerCheckCommand = defineCommand({
|
|
|
5181
4915
|
const main = defineCommand({
|
|
5182
4916
|
meta: {
|
|
5183
4917
|
name: "bst",
|
|
5184
|
-
version: "0.
|
|
4918
|
+
version: "0.35.0",
|
|
5185
4919
|
description: "Bootstrap and maintain standardized TypeScript project tooling"
|
|
5186
4920
|
},
|
|
5187
4921
|
subCommands: {
|
|
@@ -5197,7 +4931,7 @@ const main = defineCommand({
|
|
|
5197
4931
|
"docker:check": dockerCheckCommand
|
|
5198
4932
|
}
|
|
5199
4933
|
});
|
|
5200
|
-
console.log(`@bensandee/tooling v0.
|
|
4934
|
+
console.log(`@bensandee/tooling v0.35.0`);
|
|
5201
4935
|
async function run() {
|
|
5202
4936
|
await runMain(main);
|
|
5203
4937
|
process.exit(process.exitCode ?? 0);
|