@bensandee/tooling 0.33.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/README.md +21 -10
- package/dist/bin.mjs +389 -633
- package/dist/{check-B2AAPCBO.mjs → check-Ceom_OgJ.mjs} +43 -1
- package/dist/docker-check/index.d.mts +2 -0
- package/dist/docker-check/index.mjs +1 -1
- package/package.json +1 -1
package/dist/bin.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { l as createRealExecutor$1, t as runDockerCheck, u as isExecSyncError } from "./check-
|
|
2
|
+
import { d as debug, f as debugExec, h as note, l as createRealExecutor$1, m as log, p as isEnvVerbose, t as runDockerCheck, u as isExecSyncError } from "./check-Ceom_OgJ.mjs";
|
|
3
3
|
import { defineCommand, runMain } from "citty";
|
|
4
|
-
import * as
|
|
4
|
+
import * as p from "@clack/prompts";
|
|
5
5
|
import { isCancel, select } from "@clack/prompts";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
@@ -10,25 +10,9 @@ 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
|
-
//#region src/utils/log.ts
|
|
17
|
-
const out = (msg) => console.log(msg);
|
|
18
|
-
const isCI = Boolean(process.env["CI"]);
|
|
19
|
-
const log$2 = isCI ? {
|
|
20
|
-
info: out,
|
|
21
|
-
warn: (msg) => out(`[warn] ${msg}`),
|
|
22
|
-
error: (msg) => out(`[error] ${msg}`),
|
|
23
|
-
success: (msg) => out(`✓ ${msg}`)
|
|
24
|
-
} : clack.log;
|
|
25
|
-
function note(body, title) {
|
|
26
|
-
if (isCI) {
|
|
27
|
-
if (title) out(`--- ${title} ---`);
|
|
28
|
-
out(body);
|
|
29
|
-
} else clack.note(body, title);
|
|
30
|
-
}
|
|
31
|
-
//#endregion
|
|
32
16
|
//#region src/types.ts
|
|
33
17
|
const LEGACY_TOOLS = [
|
|
34
18
|
"eslint",
|
|
@@ -312,7 +296,7 @@ function getMonorepoPackages(targetDir) {
|
|
|
312
296
|
//#endregion
|
|
313
297
|
//#region src/prompts/init-prompts.ts
|
|
314
298
|
function isCancelled(value) {
|
|
315
|
-
return
|
|
299
|
+
return p.isCancel(value);
|
|
316
300
|
}
|
|
317
301
|
function detectProjectInfo(targetDir) {
|
|
318
302
|
const existingPkg = readPackageJson(targetDir);
|
|
@@ -323,7 +307,7 @@ function detectProjectInfo(targetDir) {
|
|
|
323
307
|
};
|
|
324
308
|
}
|
|
325
309
|
async function runInitPrompts(targetDir, saved) {
|
|
326
|
-
|
|
310
|
+
p.intro("@bensandee/tooling repo:sync");
|
|
327
311
|
const { detected, defaults, name } = detectProjectInfo(targetDir);
|
|
328
312
|
const isFirstInit = !saved;
|
|
329
313
|
const structure = saved?.structure ?? defaults.structure;
|
|
@@ -336,7 +320,7 @@ async function runInitPrompts(targetDir, saved) {
|
|
|
336
320
|
const projectType = saved?.projectType ?? defaults.projectType;
|
|
337
321
|
const detectPackageTypes = saved?.detectPackageTypes ?? defaults.detectPackageTypes;
|
|
338
322
|
if (detected.legacyConfigs.some((l) => l.tool === "prettier") && isFirstInit) {
|
|
339
|
-
const formatterAnswer = await
|
|
323
|
+
const formatterAnswer = await p.select({
|
|
340
324
|
message: "Existing Prettier config found. Keep Prettier or migrate to oxfmt?",
|
|
341
325
|
initialValue: "prettier",
|
|
342
326
|
options: [{
|
|
@@ -349,14 +333,14 @@ async function runInitPrompts(targetDir, saved) {
|
|
|
349
333
|
}]
|
|
350
334
|
});
|
|
351
335
|
if (isCancelled(formatterAnswer)) {
|
|
352
|
-
|
|
336
|
+
p.cancel("Cancelled.");
|
|
353
337
|
process.exit(0);
|
|
354
338
|
}
|
|
355
339
|
formatter = formatterAnswer;
|
|
356
340
|
}
|
|
357
341
|
const detectedCi = detectCiPlatform(targetDir);
|
|
358
342
|
if (isFirstInit && detectedCi === "none") {
|
|
359
|
-
const ciAnswer = await
|
|
343
|
+
const ciAnswer = await p.select({
|
|
360
344
|
message: "CI workflow",
|
|
361
345
|
initialValue: "forgejo",
|
|
362
346
|
options: [
|
|
@@ -375,14 +359,14 @@ async function runInitPrompts(targetDir, saved) {
|
|
|
375
359
|
]
|
|
376
360
|
});
|
|
377
361
|
if (isCancelled(ciAnswer)) {
|
|
378
|
-
|
|
362
|
+
p.cancel("Cancelled.");
|
|
379
363
|
process.exit(0);
|
|
380
364
|
}
|
|
381
365
|
ci = ciAnswer;
|
|
382
366
|
}
|
|
383
367
|
const hasExistingRelease = detected.hasReleaseItConfig || detected.hasSimpleReleaseConfig || detected.hasChangesetsConfig;
|
|
384
368
|
if (isFirstInit && !hasExistingRelease) {
|
|
385
|
-
const releaseAnswer = await
|
|
369
|
+
const releaseAnswer = await p.select({
|
|
386
370
|
message: "Release management",
|
|
387
371
|
initialValue: defaults.releaseStrategy,
|
|
388
372
|
options: [
|
|
@@ -408,7 +392,7 @@ async function runInitPrompts(targetDir, saved) {
|
|
|
408
392
|
]
|
|
409
393
|
});
|
|
410
394
|
if (isCancelled(releaseAnswer)) {
|
|
411
|
-
|
|
395
|
+
p.cancel("Cancelled.");
|
|
412
396
|
process.exit(0);
|
|
413
397
|
}
|
|
414
398
|
releaseStrategy = releaseAnswer;
|
|
@@ -416,12 +400,12 @@ async function runInitPrompts(targetDir, saved) {
|
|
|
416
400
|
let publishNpm = saved?.publishNpm ?? false;
|
|
417
401
|
if (isFirstInit && releaseStrategy !== "none") {
|
|
418
402
|
if (getPublishablePackages(targetDir, structure).length > 0) {
|
|
419
|
-
const answer = await
|
|
403
|
+
const answer = await p.confirm({
|
|
420
404
|
message: "Publish packages to npm?",
|
|
421
405
|
initialValue: false
|
|
422
406
|
});
|
|
423
407
|
if (isCancelled(answer)) {
|
|
424
|
-
|
|
408
|
+
p.cancel("Cancelled.");
|
|
425
409
|
process.exit(0);
|
|
426
410
|
}
|
|
427
411
|
publishNpm = answer;
|
|
@@ -430,18 +414,18 @@ async function runInitPrompts(targetDir, saved) {
|
|
|
430
414
|
let publishDocker = saved?.publishDocker ?? false;
|
|
431
415
|
if (isFirstInit) {
|
|
432
416
|
if (existsSync(path.join(targetDir, "Dockerfile")) || existsSync(path.join(targetDir, "docker/Dockerfile"))) {
|
|
433
|
-
const answer = await
|
|
417
|
+
const answer = await p.confirm({
|
|
434
418
|
message: "Publish Docker images to a registry?",
|
|
435
419
|
initialValue: false
|
|
436
420
|
});
|
|
437
421
|
if (isCancelled(answer)) {
|
|
438
|
-
|
|
422
|
+
p.cancel("Cancelled.");
|
|
439
423
|
process.exit(0);
|
|
440
424
|
}
|
|
441
425
|
publishDocker = answer;
|
|
442
426
|
}
|
|
443
427
|
}
|
|
444
|
-
|
|
428
|
+
p.outro("Configuration complete!");
|
|
445
429
|
return {
|
|
446
430
|
name,
|
|
447
431
|
structure,
|
|
@@ -859,9 +843,6 @@ function generateTags(version) {
|
|
|
859
843
|
function imageRef(namespace, imageName, tag) {
|
|
860
844
|
return `${namespace}/${imageName}:${tag}`;
|
|
861
845
|
}
|
|
862
|
-
function log$1(message) {
|
|
863
|
-
console.log(message);
|
|
864
|
-
}
|
|
865
846
|
/** Read the repo name from root package.json. */
|
|
866
847
|
function readRepoName(executor, cwd) {
|
|
867
848
|
const rootPkgRaw = executor.readFile(path.join(cwd, "package.json"));
|
|
@@ -894,22 +875,24 @@ function runDockerBuild(executor, config) {
|
|
|
894
875
|
const repoName = readRepoName(executor, config.cwd);
|
|
895
876
|
if (config.packageDir) {
|
|
896
877
|
const pkg = readSinglePackageDocker(executor, config.cwd, config.packageDir, repoName);
|
|
897
|
-
log
|
|
878
|
+
log.info(`Building image for ${pkg.dir} (${pkg.imageName}:latest)...`);
|
|
879
|
+
debug(config, `Dockerfile: ${pkg.docker.dockerfile}, context: ${pkg.docker.context}`);
|
|
898
880
|
buildImage(executor, pkg, config.cwd, config.extraArgs);
|
|
899
|
-
log
|
|
881
|
+
log.info(`Built ${pkg.imageName}:latest`);
|
|
900
882
|
return { packages: [pkg] };
|
|
901
883
|
}
|
|
902
884
|
const packages = detectDockerPackages(executor, config.cwd, repoName);
|
|
903
885
|
if (packages.length === 0) {
|
|
904
|
-
log
|
|
886
|
+
log.info("No packages with docker config found");
|
|
905
887
|
return { packages: [] };
|
|
906
888
|
}
|
|
907
|
-
log
|
|
889
|
+
log.info(`Found ${String(packages.length)} Docker package(s): ${packages.map((p) => p.dir).join(", ")}`);
|
|
908
890
|
for (const pkg of packages) {
|
|
909
|
-
log
|
|
891
|
+
log.info(`Building image for ${pkg.dir} (${pkg.imageName}:latest)...`);
|
|
892
|
+
debug(config, `Dockerfile: ${pkg.docker.dockerfile}, context: ${pkg.docker.context}`);
|
|
910
893
|
buildImage(executor, pkg, config.cwd, config.extraArgs);
|
|
911
894
|
}
|
|
912
|
-
log
|
|
895
|
+
log.info(`Built ${String(packages.length)} image(s)`);
|
|
913
896
|
return { packages };
|
|
914
897
|
}
|
|
915
898
|
/**
|
|
@@ -924,7 +907,8 @@ function runDockerPublish(executor, config) {
|
|
|
924
907
|
const { packages } = runDockerBuild(executor, {
|
|
925
908
|
cwd: config.cwd,
|
|
926
909
|
packageDir: void 0,
|
|
927
|
-
extraArgs: []
|
|
910
|
+
extraArgs: [],
|
|
911
|
+
verbose: config.verbose
|
|
928
912
|
});
|
|
929
913
|
if (packages.length === 0) return {
|
|
930
914
|
packages: [],
|
|
@@ -932,35 +916,38 @@ function runDockerPublish(executor, config) {
|
|
|
932
916
|
};
|
|
933
917
|
for (const pkg of packages) if (!pkg.version) throw new FatalError(`Package ${pkg.dir} has docker config but no version in package.json`);
|
|
934
918
|
if (!config.dryRun) {
|
|
935
|
-
log
|
|
919
|
+
log.info(`Logging in to ${config.registryHost}...`);
|
|
936
920
|
const loginResult = executor.exec(`echo "${config.password}" | docker login ${config.registryHost} -u ${config.username} --password-stdin`);
|
|
921
|
+
debugExec(config, "docker login", loginResult);
|
|
937
922
|
if (loginResult.exitCode !== 0) throw new FatalError(`Docker login failed: ${loginResult.stderr}`);
|
|
938
|
-
} else log
|
|
923
|
+
} else log.info("[dry-run] Skipping docker login");
|
|
939
924
|
const allTags = [];
|
|
940
925
|
try {
|
|
941
926
|
for (const pkg of packages) {
|
|
942
927
|
const tags = generateTags(pkg.version ?? "");
|
|
943
|
-
log
|
|
928
|
+
log.info(`${pkg.dir} v${pkg.version} → tags: ${tags.join(", ")}`);
|
|
944
929
|
for (const tag of tags) {
|
|
945
930
|
const ref = imageRef(config.registryNamespace, pkg.imageName, tag);
|
|
946
931
|
allTags.push(ref);
|
|
947
|
-
log
|
|
932
|
+
log.info(`Tagging ${pkg.imageName} → ${ref}`);
|
|
948
933
|
const tagResult = executor.exec(`docker tag ${pkg.imageName} ${ref}`);
|
|
934
|
+
debugExec(config, `docker tag ${pkg.imageName} ${ref}`, tagResult);
|
|
949
935
|
if (tagResult.exitCode !== 0) throw new FatalError(`docker tag failed: ${tagResult.stderr}`);
|
|
950
936
|
if (!config.dryRun) {
|
|
951
|
-
log
|
|
937
|
+
log.info(`Pushing ${ref}...`);
|
|
952
938
|
const pushResult = executor.exec(`docker push ${ref}`);
|
|
939
|
+
debugExec(config, `docker push ${ref}`, pushResult);
|
|
953
940
|
if (pushResult.exitCode !== 0) throw new FatalError(`docker push failed: ${pushResult.stderr}`);
|
|
954
|
-
} else log
|
|
941
|
+
} else log.info(`[dry-run] Skipping push for ${ref}`);
|
|
955
942
|
}
|
|
956
943
|
}
|
|
957
944
|
} finally {
|
|
958
945
|
if (!config.dryRun) {
|
|
959
|
-
log
|
|
946
|
+
log.info(`Logging out from ${config.registryHost}...`);
|
|
960
947
|
executor.exec(`docker logout ${config.registryHost}`);
|
|
961
948
|
}
|
|
962
949
|
}
|
|
963
|
-
log
|
|
950
|
+
log.info(`Published ${String(allTags.length)} image tag(s)`);
|
|
964
951
|
return {
|
|
965
952
|
packages,
|
|
966
953
|
tags: allTags
|
|
@@ -976,10 +963,6 @@ function ensureSchemaComment(content, ci) {
|
|
|
976
963
|
if (content.includes("yaml-language-server")) return content;
|
|
977
964
|
return FORGEJO_SCHEMA_COMMENT + content;
|
|
978
965
|
}
|
|
979
|
-
/** Migrate content from old tooling binary name to new. */
|
|
980
|
-
function migrateToolingBinary(content) {
|
|
981
|
-
return content.replaceAll("pnpm exec tooling ", "pnpm exec bst ");
|
|
982
|
-
}
|
|
983
966
|
/** Check if a YAML file has an opt-out comment in the first 10 lines. */
|
|
984
967
|
function isToolingIgnored(content) {
|
|
985
968
|
return content.split("\n", 10).some((line) => line.includes(IGNORE_PATTERN));
|
|
@@ -1024,14 +1007,42 @@ function mergeLefthookCommands(existing, requiredCommands) {
|
|
|
1024
1007
|
};
|
|
1025
1008
|
}
|
|
1026
1009
|
}
|
|
1027
|
-
/**
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
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
1033
|
}
|
|
1034
|
-
|
|
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]);
|
|
1044
|
+
}
|
|
1045
|
+
/** Build a complete workflow YAML string from structured options. Single source of truth for workflow files. */
|
|
1035
1046
|
function buildWorkflowYaml(options) {
|
|
1036
1047
|
const doc = { name: options.name };
|
|
1037
1048
|
if (options.enableEmailNotifications) doc["enable-email-notifications"] = true;
|
|
@@ -1045,132 +1056,7 @@ function buildWorkflowYaml(options) {
|
|
|
1045
1056
|
return ensureSchemaComment(stringify(doc, {
|
|
1046
1057
|
lineWidth: 0,
|
|
1047
1058
|
nullStr: ""
|
|
1048
|
-
}), options.ci);
|
|
1049
|
-
}
|
|
1050
|
-
/**
|
|
1051
|
-
* Ensure required steps exist in a workflow job's steps array.
|
|
1052
|
-
* Only adds missing steps at the end — never modifies existing ones.
|
|
1053
|
-
* Returns unchanged content if the file has an opt-out comment or can't be parsed.
|
|
1054
|
-
*/
|
|
1055
|
-
function mergeWorkflowSteps(existing, jobName, requiredSteps) {
|
|
1056
|
-
if (isToolingIgnored(existing)) return {
|
|
1057
|
-
content: existing,
|
|
1058
|
-
changed: false
|
|
1059
|
-
};
|
|
1060
|
-
try {
|
|
1061
|
-
const doc = parseDocument(existing);
|
|
1062
|
-
const steps = doc.getIn([
|
|
1063
|
-
"jobs",
|
|
1064
|
-
jobName,
|
|
1065
|
-
"steps"
|
|
1066
|
-
]);
|
|
1067
|
-
if (!isSeq(steps)) return {
|
|
1068
|
-
content: existing,
|
|
1069
|
-
changed: false
|
|
1070
|
-
};
|
|
1071
|
-
let changed = false;
|
|
1072
|
-
for (const { match, step } of requiredSteps) if (!steps.items.some((item) => {
|
|
1073
|
-
if (!isMap(item)) return false;
|
|
1074
|
-
if (match.run) {
|
|
1075
|
-
const run = item.get("run");
|
|
1076
|
-
return typeof run === "string" && run.includes(match.run);
|
|
1077
|
-
}
|
|
1078
|
-
if (match.uses) {
|
|
1079
|
-
const uses = item.get("uses");
|
|
1080
|
-
return typeof uses === "string" && uses.startsWith(match.uses);
|
|
1081
|
-
}
|
|
1082
|
-
return false;
|
|
1083
|
-
})) {
|
|
1084
|
-
steps.add(doc.createNode(step));
|
|
1085
|
-
changed = true;
|
|
1086
|
-
}
|
|
1087
|
-
return {
|
|
1088
|
-
content: changed ? doc.toString() : existing,
|
|
1089
|
-
changed
|
|
1090
|
-
};
|
|
1091
|
-
} catch {
|
|
1092
|
-
return {
|
|
1093
|
-
content: existing,
|
|
1094
|
-
changed: false
|
|
1095
|
-
};
|
|
1096
|
-
}
|
|
1097
|
-
}
|
|
1098
|
-
/**
|
|
1099
|
-
* Add a job to an existing workflow YAML if it doesn't already exist.
|
|
1100
|
-
* Returns unchanged content if the job already exists, the file has an opt-out comment,
|
|
1101
|
-
* or the document can't be parsed.
|
|
1102
|
-
*/
|
|
1103
|
-
/**
|
|
1104
|
-
* Ensure a `concurrency` block exists at the workflow top level.
|
|
1105
|
-
* Adds it if missing — never modifies an existing one.
|
|
1106
|
-
* Returns unchanged content if the file has an opt-out comment or can't be parsed.
|
|
1107
|
-
*/
|
|
1108
|
-
/**
|
|
1109
|
-
* Ensure `on.push` has `tags-ignore: ["**"]` so tag pushes don't trigger CI.
|
|
1110
|
-
* Only adds the filter when `on.push` exists and `tags-ignore` is absent.
|
|
1111
|
-
*/
|
|
1112
|
-
function ensureWorkflowTagsIgnore(existing) {
|
|
1113
|
-
if (isToolingIgnored(existing)) return {
|
|
1114
|
-
content: existing,
|
|
1115
|
-
changed: false
|
|
1116
|
-
};
|
|
1117
|
-
try {
|
|
1118
|
-
const doc = parseDocument(existing);
|
|
1119
|
-
const on = doc.get("on");
|
|
1120
|
-
if (!isMap(on)) return {
|
|
1121
|
-
content: existing,
|
|
1122
|
-
changed: false
|
|
1123
|
-
};
|
|
1124
|
-
const push = on.get("push");
|
|
1125
|
-
if (!isMap(push)) return {
|
|
1126
|
-
content: existing,
|
|
1127
|
-
changed: false
|
|
1128
|
-
};
|
|
1129
|
-
if (push.has("tags-ignore")) return {
|
|
1130
|
-
content: existing,
|
|
1131
|
-
changed: false
|
|
1132
|
-
};
|
|
1133
|
-
push.set("tags-ignore", ["**"]);
|
|
1134
|
-
return {
|
|
1135
|
-
content: doc.toString(),
|
|
1136
|
-
changed: true
|
|
1137
|
-
};
|
|
1138
|
-
} catch {
|
|
1139
|
-
return {
|
|
1140
|
-
content: existing,
|
|
1141
|
-
changed: false
|
|
1142
|
-
};
|
|
1143
|
-
}
|
|
1144
|
-
}
|
|
1145
|
-
function ensureWorkflowConcurrency(existing, concurrency) {
|
|
1146
|
-
if (isToolingIgnored(existing)) return {
|
|
1147
|
-
content: existing,
|
|
1148
|
-
changed: false
|
|
1149
|
-
};
|
|
1150
|
-
try {
|
|
1151
|
-
const doc = parseDocument(existing);
|
|
1152
|
-
if (doc.has("concurrency")) return {
|
|
1153
|
-
content: existing,
|
|
1154
|
-
changed: false
|
|
1155
|
-
};
|
|
1156
|
-
doc.set("concurrency", concurrency);
|
|
1157
|
-
const contents = doc.contents;
|
|
1158
|
-
if (isMap(contents)) {
|
|
1159
|
-
const items = contents.items;
|
|
1160
|
-
const nameIdx = items.findIndex((p) => isScalar(p.key) && p.key.value === "name");
|
|
1161
|
-
const concPair = items.pop();
|
|
1162
|
-
if (concPair) items.splice(nameIdx + 1, 0, concPair);
|
|
1163
|
-
}
|
|
1164
|
-
return {
|
|
1165
|
-
content: doc.toString(),
|
|
1166
|
-
changed: true
|
|
1167
|
-
};
|
|
1168
|
-
} catch {
|
|
1169
|
-
return {
|
|
1170
|
-
content: existing,
|
|
1171
|
-
changed: false
|
|
1172
|
-
};
|
|
1173
|
-
}
|
|
1059
|
+
}).replace(/^(?=\S)/gm, (match, offset) => offset === 0 ? match : `\n${match}`), options.ci);
|
|
1174
1060
|
}
|
|
1175
1061
|
//#endregion
|
|
1176
1062
|
//#region src/generators/ci-utils.ts
|
|
@@ -1190,38 +1076,24 @@ function computeNodeVersionYaml(ctx) {
|
|
|
1190
1076
|
//#region src/generators/publish-ci.ts
|
|
1191
1077
|
function publishSteps(nodeVersionYaml) {
|
|
1192
1078
|
return [
|
|
1193
|
-
{
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
},
|
|
1212
|
-
{
|
|
1213
|
-
match: { run: "docker:publish" },
|
|
1214
|
-
step: {
|
|
1215
|
-
name: "Publish Docker images",
|
|
1216
|
-
env: {
|
|
1217
|
-
DOCKER_REGISTRY_HOST: actionsExpr("vars.DOCKER_REGISTRY_HOST"),
|
|
1218
|
-
DOCKER_REGISTRY_NAMESPACE: actionsExpr("vars.DOCKER_REGISTRY_NAMESPACE"),
|
|
1219
|
-
DOCKER_USERNAME: actionsExpr("secrets.DOCKER_USERNAME"),
|
|
1220
|
-
DOCKER_PASSWORD: actionsExpr("secrets.DOCKER_PASSWORD")
|
|
1221
|
-
},
|
|
1222
|
-
run: "pnpm exec bst docker:publish"
|
|
1223
|
-
}
|
|
1224
|
-
}
|
|
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
|
+
} }
|
|
1225
1097
|
];
|
|
1226
1098
|
}
|
|
1227
1099
|
const DockerMapSchema = z.object({ docker: z.record(z.string(), z.unknown()).optional() });
|
|
@@ -1270,59 +1142,24 @@ async function generateDeployCi(ctx) {
|
|
|
1270
1142
|
jobName: "publish",
|
|
1271
1143
|
steps
|
|
1272
1144
|
});
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
action: "updated",
|
|
1283
|
-
description: "Migrated tooling binary name in publish workflow"
|
|
1284
|
-
};
|
|
1285
|
-
}
|
|
1286
|
-
return {
|
|
1287
|
-
filePath: workflowPath,
|
|
1288
|
-
action: "skipped",
|
|
1289
|
-
description: "Publish workflow already up to date"
|
|
1290
|
-
};
|
|
1291
|
-
}
|
|
1292
|
-
const merged = mergeWorkflowSteps(existing, "publish", toRequiredSteps(steps));
|
|
1293
|
-
const withComment = ensureSchemaComment(merged.content, ctx.config.ci);
|
|
1294
|
-
if (!merged.changed) {
|
|
1295
|
-
if (withComment !== raw) {
|
|
1296
|
-
ctx.write(workflowPath, withComment);
|
|
1297
|
-
return {
|
|
1298
|
-
filePath: workflowPath,
|
|
1299
|
-
action: "updated",
|
|
1300
|
-
description: existing !== raw ? "Migrated tooling binary name in publish workflow" : "Added schema comment to publish workflow"
|
|
1301
|
-
};
|
|
1302
|
-
}
|
|
1303
|
-
return {
|
|
1304
|
-
filePath: workflowPath,
|
|
1305
|
-
action: "skipped",
|
|
1306
|
-
description: "Existing publish workflow preserved"
|
|
1307
|
-
};
|
|
1308
|
-
}
|
|
1309
|
-
ctx.write(workflowPath, withComment);
|
|
1310
|
-
return {
|
|
1311
|
-
filePath: workflowPath,
|
|
1312
|
-
action: "updated",
|
|
1313
|
-
description: "Added missing steps to publish workflow"
|
|
1314
|
-
};
|
|
1315
|
-
}
|
|
1316
|
-
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 {
|
|
1317
1154
|
filePath: workflowPath,
|
|
1318
1155
|
action: "skipped",
|
|
1319
1156
|
description: "Publish workflow already up to date"
|
|
1320
1157
|
};
|
|
1321
1158
|
}
|
|
1322
|
-
ctx.write(workflowPath, content);
|
|
1159
|
+
ctx.write(workflowPath, preserveDevBinaryPath(content, existing));
|
|
1323
1160
|
return {
|
|
1324
1161
|
filePath: workflowPath,
|
|
1325
|
-
action: "created",
|
|
1162
|
+
action: alreadyExists ? "updated" : "created",
|
|
1326
1163
|
description: `Generated ${isGitHub ? "GitHub" : "Forgejo"} Actions publish workflow`
|
|
1327
1164
|
};
|
|
1328
1165
|
}
|
|
@@ -1590,7 +1427,7 @@ function getAddedDevDepNames(config) {
|
|
|
1590
1427
|
const deps = { ...ROOT_DEV_DEPS };
|
|
1591
1428
|
if (config.structure !== "monorepo") Object.assign(deps, PER_PACKAGE_DEV_DEPS);
|
|
1592
1429
|
deps["@bensandee/config"] = "0.9.1";
|
|
1593
|
-
deps["@bensandee/tooling"] = "0.
|
|
1430
|
+
deps["@bensandee/tooling"] = "0.35.0";
|
|
1594
1431
|
if (config.formatter === "oxfmt") deps["oxfmt"] = {
|
|
1595
1432
|
"@changesets/cli": "2.30.0",
|
|
1596
1433
|
"@release-it/bumper": "7.0.5",
|
|
@@ -1645,7 +1482,7 @@ async function generatePackageJson(ctx) {
|
|
|
1645
1482
|
const devDeps = { ...ROOT_DEV_DEPS };
|
|
1646
1483
|
if (!isMonorepo) Object.assign(devDeps, PER_PACKAGE_DEV_DEPS);
|
|
1647
1484
|
devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.9.1";
|
|
1648
|
-
devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.
|
|
1485
|
+
devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.35.0";
|
|
1649
1486
|
if (ctx.config.useEslintPlugin) devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "0.9.2";
|
|
1650
1487
|
if (ctx.config.formatter === "oxfmt") devDeps["oxfmt"] = {
|
|
1651
1488
|
"@changesets/cli": "2.30.0",
|
|
@@ -1693,10 +1530,6 @@ async function generatePackageJson(ctx) {
|
|
|
1693
1530
|
changes.push("set type: \"module\"");
|
|
1694
1531
|
}
|
|
1695
1532
|
const existingScripts = pkg.scripts ?? {};
|
|
1696
|
-
for (const [key, value] of Object.entries(existingScripts)) if (typeof value === "string" && value.includes("pnpm exec tooling ")) {
|
|
1697
|
-
existingScripts[key] = migrateToolingBinary(value);
|
|
1698
|
-
changes.push(`migrated script: ${key}`);
|
|
1699
|
-
}
|
|
1700
1533
|
for (const [key, value] of Object.entries(allScripts)) if (!(key in existingScripts)) {
|
|
1701
1534
|
existingScripts[key] = value;
|
|
1702
1535
|
changes.push(`added script: ${key}`);
|
|
@@ -2183,41 +2016,58 @@ async function generateGitignore(ctx) {
|
|
|
2183
2016
|
}
|
|
2184
2017
|
//#endregion
|
|
2185
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
|
+
}
|
|
2186
2046
|
const CI_CONCURRENCY = {
|
|
2187
2047
|
group: `ci-${actionsExpr("github.ref")}`,
|
|
2188
2048
|
"cancel-in-progress": actionsExpr("github.ref != 'refs/heads/main'")
|
|
2189
2049
|
};
|
|
2190
|
-
function checkSteps(nodeVersionYaml) {
|
|
2050
|
+
function checkSteps(nodeVersionYaml, publishesNpm) {
|
|
2051
|
+
const isNodeVersionFile = nodeVersionYaml.startsWith("node-version-file");
|
|
2191
2052
|
return [
|
|
2192
|
-
{
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
},
|
|
2196
|
-
{
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
uses: "actions/setup-node@v6",
|
|
2204
|
-
with: {
|
|
2205
|
-
...nodeVersionYaml.startsWith("node-version-file") ? { "node-version-file": "package.json" } : { "node-version": "24" },
|
|
2206
|
-
cache: "pnpm"
|
|
2207
|
-
}
|
|
2208
|
-
}
|
|
2209
|
-
},
|
|
2210
|
-
{
|
|
2211
|
-
match: { run: "pnpm install" },
|
|
2212
|
-
step: { run: "pnpm install --frozen-lockfile" }
|
|
2213
|
-
},
|
|
2214
|
-
{
|
|
2215
|
-
match: { run: "check" },
|
|
2216
|
-
step: {
|
|
2217
|
-
name: "Run all checks",
|
|
2218
|
-
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'") }
|
|
2219
2064
|
}
|
|
2220
|
-
}
|
|
2065
|
+
} },
|
|
2066
|
+
{ step: { run: "pnpm install --frozen-lockfile" } },
|
|
2067
|
+
{ step: {
|
|
2068
|
+
name: "Run all checks",
|
|
2069
|
+
run: "pnpm ci:check"
|
|
2070
|
+
} }
|
|
2221
2071
|
];
|
|
2222
2072
|
}
|
|
2223
2073
|
/** Resolve the CI workflow filename based on release strategy. */
|
|
@@ -2233,7 +2083,9 @@ async function generateCi(ctx) {
|
|
|
2233
2083
|
const isGitHub = ctx.config.ci === "github";
|
|
2234
2084
|
const isForgejo = !isGitHub;
|
|
2235
2085
|
const isChangesets = ctx.config.releaseStrategy === "changesets";
|
|
2236
|
-
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)] : []];
|
|
2237
2089
|
const content = buildWorkflowYaml({
|
|
2238
2090
|
ci: ctx.config.ci,
|
|
2239
2091
|
name: "CI",
|
|
@@ -2250,42 +2102,24 @@ async function generateCi(ctx) {
|
|
|
2250
2102
|
steps
|
|
2251
2103
|
});
|
|
2252
2104
|
const filePath = ciWorkflowPath(ctx.config.ci, ctx.config.releaseStrategy);
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
if (isChangesets) {
|
|
2263
|
-
const withConcurrency = ensureWorkflowConcurrency(result.content, CI_CONCURRENCY);
|
|
2264
|
-
result = {
|
|
2265
|
-
content: withConcurrency.content,
|
|
2266
|
-
changed: result.changed || withConcurrency.changed
|
|
2267
|
-
};
|
|
2268
|
-
}
|
|
2269
|
-
const withComment = ensureSchemaComment(result.content, isGitHub ? "github" : "forgejo");
|
|
2270
|
-
if (result.changed || withComment !== result.content) {
|
|
2271
|
-
ctx.write(filePath, withComment);
|
|
2272
|
-
return {
|
|
2273
|
-
filePath,
|
|
2274
|
-
action: "updated",
|
|
2275
|
-
description: "Added missing steps to CI workflow"
|
|
2276
|
-
};
|
|
2277
|
-
}
|
|
2278
|
-
}
|
|
2279
|
-
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 {
|
|
2280
2114
|
filePath,
|
|
2281
2115
|
action: "skipped",
|
|
2282
2116
|
description: "CI workflow already up to date"
|
|
2283
2117
|
};
|
|
2284
2118
|
}
|
|
2285
|
-
ctx.write(filePath, content);
|
|
2119
|
+
ctx.write(filePath, preserveDevBinaryPath(content, existing));
|
|
2286
2120
|
return {
|
|
2287
2121
|
filePath,
|
|
2288
|
-
action: "created",
|
|
2122
|
+
action: alreadyExists ? "updated" : "created",
|
|
2289
2123
|
description: `Generated ${isGitHub ? "GitHub" : "Forgejo"} Actions CI workflow`
|
|
2290
2124
|
};
|
|
2291
2125
|
}
|
|
@@ -2762,47 +2596,33 @@ async function generateChangesets(ctx) {
|
|
|
2762
2596
|
function commonSteps(nodeVersionYaml, publishesNpm) {
|
|
2763
2597
|
const isNodeVersionFile = nodeVersionYaml.startsWith("node-version-file");
|
|
2764
2598
|
return [
|
|
2765
|
-
{
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
{
|
|
2777
|
-
match: { uses: "actions/setup-node" },
|
|
2778
|
-
step: {
|
|
2779
|
-
uses: "actions/setup-node@v6",
|
|
2780
|
-
with: {
|
|
2781
|
-
...isNodeVersionFile ? { "node-version-file": "package.json" } : { "node-version": "24" },
|
|
2782
|
-
cache: "pnpm",
|
|
2783
|
-
...publishesNpm && { "registry-url": "https://registry.npmjs.org" }
|
|
2784
|
-
}
|
|
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'") }
|
|
2785
2610
|
}
|
|
2786
|
-
},
|
|
2787
|
-
{
|
|
2788
|
-
match: { run: "pnpm install" },
|
|
2789
|
-
step: { run: "pnpm install --frozen-lockfile" }
|
|
2790
|
-
}
|
|
2611
|
+
} },
|
|
2612
|
+
{ step: { run: "pnpm install --frozen-lockfile" } }
|
|
2791
2613
|
];
|
|
2792
2614
|
}
|
|
2793
2615
|
function releaseItSteps(ci, nodeVersionYaml, publishesNpm) {
|
|
2794
2616
|
const tokenEnv = ci === "github" ? { GITHUB_TOKEN: actionsExpr("github.token") } : { RELEASE_TOKEN: actionsExpr("secrets.RELEASE_TOKEN") };
|
|
2795
2617
|
const npmEnv = publishesNpm ? { NODE_AUTH_TOKEN: actionsExpr("secrets.NPM_TOKEN") } : {};
|
|
2796
|
-
return [...commonSteps(nodeVersionYaml, publishesNpm), {
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
...npmEnv
|
|
2803
|
-
}
|
|
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'")
|
|
2804
2624
|
}
|
|
2805
|
-
}];
|
|
2625
|
+
} }];
|
|
2806
2626
|
}
|
|
2807
2627
|
/** Build the workflow_dispatch trigger with optional inputs for the simple strategy. */
|
|
2808
2628
|
function simpleWorkflowDispatchTrigger() {
|
|
@@ -2840,70 +2660,36 @@ function simpleReleaseCommand() {
|
|
|
2840
2660
|
].join("\n");
|
|
2841
2661
|
}
|
|
2842
2662
|
function simpleReleaseSteps(ci, nodeVersionYaml, publishesNpm, hasDocker) {
|
|
2843
|
-
const releaseStep = {
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
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") } : {
|
|
2848
2667
|
FORGEJO_SERVER_URL: actionsExpr("github.server_url"),
|
|
2849
2668
|
FORGEJO_REPOSITORY: actionsExpr("github.repository"),
|
|
2850
2669
|
RELEASE_TOKEN: actionsExpr("secrets.RELEASE_TOKEN")
|
|
2851
2670
|
},
|
|
2852
|
-
|
|
2853
|
-
}
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
};
|
|
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
|
+
} };
|
|
2869
2687
|
return [
|
|
2870
2688
|
...commonSteps(nodeVersionYaml, publishesNpm),
|
|
2871
2689
|
releaseStep,
|
|
2872
2690
|
...hasDocker ? [dockerStep] : []
|
|
2873
2691
|
];
|
|
2874
2692
|
}
|
|
2875
|
-
/** Build the required release step for the check job (changesets). */
|
|
2876
|
-
function changesetsReleaseStep(ci, publishesNpm) {
|
|
2877
|
-
if (ci === "github") return {
|
|
2878
|
-
match: { uses: "changesets/action" },
|
|
2879
|
-
step: {
|
|
2880
|
-
uses: "changesets/action@v1",
|
|
2881
|
-
if: "github.ref == 'refs/heads/main'",
|
|
2882
|
-
with: {
|
|
2883
|
-
publish: "pnpm changeset publish",
|
|
2884
|
-
version: "pnpm changeset version"
|
|
2885
|
-
},
|
|
2886
|
-
env: {
|
|
2887
|
-
GITHUB_TOKEN: actionsExpr("github.token"),
|
|
2888
|
-
...publishesNpm && { NPM_TOKEN: actionsExpr("secrets.NPM_TOKEN") }
|
|
2889
|
-
}
|
|
2890
|
-
}
|
|
2891
|
-
};
|
|
2892
|
-
return {
|
|
2893
|
-
match: { run: "release:changesets" },
|
|
2894
|
-
step: {
|
|
2895
|
-
name: "Release",
|
|
2896
|
-
if: "github.ref == 'refs/heads/main'",
|
|
2897
|
-
env: {
|
|
2898
|
-
FORGEJO_SERVER_URL: actionsExpr("github.server_url"),
|
|
2899
|
-
FORGEJO_REPOSITORY: actionsExpr("github.repository"),
|
|
2900
|
-
RELEASE_TOKEN: actionsExpr("secrets.RELEASE_TOKEN"),
|
|
2901
|
-
...publishesNpm && { NODE_AUTH_TOKEN: actionsExpr("secrets.NPM_TOKEN") }
|
|
2902
|
-
},
|
|
2903
|
-
run: "pnpm exec bst release:changesets"
|
|
2904
|
-
}
|
|
2905
|
-
};
|
|
2906
|
-
}
|
|
2907
2693
|
function buildSteps(strategy, ci, nodeVersionYaml, publishesNpm, hasDocker) {
|
|
2908
2694
|
switch (strategy) {
|
|
2909
2695
|
case "release-it": return releaseItSteps(ci, nodeVersionYaml, publishesNpm);
|
|
@@ -2911,40 +2697,6 @@ function buildSteps(strategy, ci, nodeVersionYaml, publishesNpm, hasDocker) {
|
|
|
2911
2697
|
default: return null;
|
|
2912
2698
|
}
|
|
2913
2699
|
}
|
|
2914
|
-
function generateChangesetsReleaseCi(ctx, publishesNpm) {
|
|
2915
|
-
const ciPath = ciWorkflowPath(ctx.config.ci, ctx.config.releaseStrategy);
|
|
2916
|
-
const raw = ctx.read(ciPath);
|
|
2917
|
-
if (!raw) return {
|
|
2918
|
-
filePath: ciPath,
|
|
2919
|
-
action: "skipped",
|
|
2920
|
-
description: "CI workflow not found — run check generator first"
|
|
2921
|
-
};
|
|
2922
|
-
const existing = migrateToolingBinary(raw);
|
|
2923
|
-
const merged = mergeWorkflowSteps(existing, "check", [changesetsReleaseStep(ctx.config.ci, publishesNpm)]);
|
|
2924
|
-
if (!merged.changed) {
|
|
2925
|
-
if (existing !== raw) {
|
|
2926
|
-
const withComment = ensureSchemaComment(existing, ctx.config.ci);
|
|
2927
|
-
ctx.write(ciPath, withComment);
|
|
2928
|
-
return {
|
|
2929
|
-
filePath: ciPath,
|
|
2930
|
-
action: "updated",
|
|
2931
|
-
description: "Migrated tooling binary name in CI workflow"
|
|
2932
|
-
};
|
|
2933
|
-
}
|
|
2934
|
-
return {
|
|
2935
|
-
filePath: ciPath,
|
|
2936
|
-
action: "skipped",
|
|
2937
|
-
description: "Release step in CI workflow already up to date"
|
|
2938
|
-
};
|
|
2939
|
-
}
|
|
2940
|
-
const withComment = ensureSchemaComment(merged.content, ctx.config.ci);
|
|
2941
|
-
ctx.write(ciPath, withComment);
|
|
2942
|
-
return {
|
|
2943
|
-
filePath: ciPath,
|
|
2944
|
-
action: "updated",
|
|
2945
|
-
description: "Added release step to CI workflow"
|
|
2946
|
-
};
|
|
2947
|
-
}
|
|
2948
2700
|
async function generateReleaseCi(ctx) {
|
|
2949
2701
|
const filePath = "release-ci";
|
|
2950
2702
|
if (ctx.config.releaseStrategy === "none" || ctx.config.ci === "none") return {
|
|
@@ -2953,7 +2705,11 @@ async function generateReleaseCi(ctx) {
|
|
|
2953
2705
|
description: "Release CI workflow not applicable"
|
|
2954
2706
|
};
|
|
2955
2707
|
const publishesNpm = ctx.config.publishNpm === true;
|
|
2956
|
-
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
|
+
};
|
|
2957
2713
|
const isGitHub = ctx.config.ci === "github";
|
|
2958
2714
|
const workflowPath = isGitHub ? ".github/workflows/release.yml" : ".forgejo/workflows/release.yml";
|
|
2959
2715
|
const nodeVersionYaml = computeNodeVersionYaml(ctx);
|
|
@@ -2973,59 +2729,24 @@ async function generateReleaseCi(ctx) {
|
|
|
2973
2729
|
jobName: "release",
|
|
2974
2730
|
steps
|
|
2975
2731
|
});
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
action: "updated",
|
|
2986
|
-
description: "Migrated tooling binary name in release workflow"
|
|
2987
|
-
};
|
|
2988
|
-
}
|
|
2989
|
-
return {
|
|
2990
|
-
filePath: workflowPath,
|
|
2991
|
-
action: "skipped",
|
|
2992
|
-
description: "Release workflow already up to date"
|
|
2993
|
-
};
|
|
2994
|
-
}
|
|
2995
|
-
const merged = mergeWorkflowSteps(existing, "release", toRequiredSteps(steps));
|
|
2996
|
-
const withComment = ensureSchemaComment(merged.content, ctx.config.ci);
|
|
2997
|
-
if (!merged.changed) {
|
|
2998
|
-
if (withComment !== raw) {
|
|
2999
|
-
ctx.write(workflowPath, withComment);
|
|
3000
|
-
return {
|
|
3001
|
-
filePath: workflowPath,
|
|
3002
|
-
action: "updated",
|
|
3003
|
-
description: existing !== raw ? "Migrated tooling binary name in release workflow" : "Added schema comment to release workflow"
|
|
3004
|
-
};
|
|
3005
|
-
}
|
|
3006
|
-
return {
|
|
3007
|
-
filePath: workflowPath,
|
|
3008
|
-
action: "skipped",
|
|
3009
|
-
description: "Existing release workflow preserved"
|
|
3010
|
-
};
|
|
3011
|
-
}
|
|
3012
|
-
ctx.write(workflowPath, withComment);
|
|
3013
|
-
return {
|
|
3014
|
-
filePath: workflowPath,
|
|
3015
|
-
action: "updated",
|
|
3016
|
-
description: "Added missing steps to release workflow"
|
|
3017
|
-
};
|
|
3018
|
-
}
|
|
3019
|
-
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 {
|
|
3020
2741
|
filePath: workflowPath,
|
|
3021
2742
|
action: "skipped",
|
|
3022
2743
|
description: "Release workflow already up to date"
|
|
3023
2744
|
};
|
|
3024
2745
|
}
|
|
3025
|
-
ctx.write(workflowPath, content);
|
|
2746
|
+
ctx.write(workflowPath, preserveDevBinaryPath(content, existing));
|
|
3026
2747
|
return {
|
|
3027
2748
|
filePath: workflowPath,
|
|
3028
|
-
action: "created",
|
|
2749
|
+
action: alreadyExists ? "updated" : "created",
|
|
3029
2750
|
description: `Generated ${isGitHub ? "GitHub" : "Forgejo"} Actions release workflow`
|
|
3030
2751
|
};
|
|
3031
2752
|
}
|
|
@@ -3321,7 +3042,7 @@ function generateMigratePrompt(results, config, detected) {
|
|
|
3321
3042
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
3322
3043
|
sections.push("# Migration Prompt");
|
|
3323
3044
|
sections.push("");
|
|
3324
|
-
sections.push(`_Generated by \`@bensandee/tooling@0.
|
|
3045
|
+
sections.push(`_Generated by \`@bensandee/tooling@0.35.0 repo:sync\` on ${timestamp}_`);
|
|
3325
3046
|
sections.push("");
|
|
3326
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.");
|
|
3327
3048
|
sections.push("");
|
|
@@ -3505,11 +3226,11 @@ function contextAsDockerReader(ctx) {
|
|
|
3505
3226
|
function logDetectionSummary(ctx) {
|
|
3506
3227
|
if (ctx.config.publishDocker) {
|
|
3507
3228
|
const dockerPackages = detectDockerPackages(contextAsDockerReader(ctx), ctx.targetDir, ctx.config.name);
|
|
3508
|
-
if (dockerPackages.length > 0) log
|
|
3229
|
+
if (dockerPackages.length > 0) log.info(`Docker images: ${dockerPackages.map((pkg) => pkg.imageName).join(", ")}`);
|
|
3509
3230
|
}
|
|
3510
3231
|
if (ctx.config.publishNpm) {
|
|
3511
3232
|
const publishable = getPublishablePackages(ctx.targetDir, ctx.config.structure, ctx.packageJson);
|
|
3512
|
-
if (publishable.length > 0) log
|
|
3233
|
+
if (publishable.length > 0) log.info(`npm packages: ${publishable.map((pkg) => pkg.name).join(", ")}`);
|
|
3513
3234
|
}
|
|
3514
3235
|
}
|
|
3515
3236
|
async function runInit(config, options = {}) {
|
|
@@ -3543,7 +3264,7 @@ async function runInit(config, options = {}) {
|
|
|
3543
3264
|
const promptPath = ".tooling-migrate.md";
|
|
3544
3265
|
ctx.write(promptPath, prompt);
|
|
3545
3266
|
if (!hasChanges && options.noPrompt) {
|
|
3546
|
-
log
|
|
3267
|
+
log.success("Repository is up to date.");
|
|
3547
3268
|
return results;
|
|
3548
3269
|
}
|
|
3549
3270
|
if (results.some((r) => r.action === "archived" && r.filePath.startsWith(".husky/"))) try {
|
|
@@ -3558,13 +3279,13 @@ async function runInit(config, options = {}) {
|
|
|
3558
3279
|
if (updated.length > 0) summaryLines.push(`Updated: ${updated.map((r) => r.filePath).join(", ")}`);
|
|
3559
3280
|
note(summaryLines.join("\n"), "Summary");
|
|
3560
3281
|
if (!options.noPrompt) {
|
|
3561
|
-
log
|
|
3562
|
-
log
|
|
3282
|
+
log.info(`Migration prompt written to ${promptPath}`);
|
|
3283
|
+
log.info("In Claude Code, run: \"Execute the steps in .tooling-migrate.md\"");
|
|
3563
3284
|
}
|
|
3564
3285
|
const bensandeeDeps = getAddedDevDepNames(config).filter((name) => name.startsWith("@bensandee/"));
|
|
3565
3286
|
const hasLockfile = ctx.exists("pnpm-lock.yaml");
|
|
3566
3287
|
if (bensandeeDeps.length > 0 && hasLockfile) {
|
|
3567
|
-
log
|
|
3288
|
+
log.info("Updating @bensandee/* packages...");
|
|
3568
3289
|
try {
|
|
3569
3290
|
execSync(`pnpm update --latest ${bensandeeDeps.join(" ")}`, {
|
|
3570
3291
|
cwd: config.targetDir,
|
|
@@ -3572,7 +3293,7 @@ async function runInit(config, options = {}) {
|
|
|
3572
3293
|
timeout: 6e4
|
|
3573
3294
|
});
|
|
3574
3295
|
} catch (_error) {
|
|
3575
|
-
log
|
|
3296
|
+
log.warn("Could not update @bensandee/* packages — run pnpm install manually");
|
|
3576
3297
|
}
|
|
3577
3298
|
}
|
|
3578
3299
|
if (hasChanges && ctx.exists("package.json")) try {
|
|
@@ -3664,22 +3385,22 @@ async function runCheck(targetDir) {
|
|
|
3664
3385
|
return true;
|
|
3665
3386
|
});
|
|
3666
3387
|
if (actionable.length === 0) {
|
|
3667
|
-
log
|
|
3388
|
+
log.success("Repository is up to date.");
|
|
3668
3389
|
return 0;
|
|
3669
3390
|
}
|
|
3670
|
-
log
|
|
3391
|
+
log.warn(`${actionable.length} file(s) would be changed by repo:sync`);
|
|
3671
3392
|
for (const r of actionable) {
|
|
3672
|
-
log
|
|
3393
|
+
log.info(` ${r.action}: ${r.filePath} — ${r.description}`);
|
|
3673
3394
|
const newContent = pendingWrites.get(r.filePath);
|
|
3674
3395
|
if (!newContent) continue;
|
|
3675
3396
|
const existingPath = path.join(targetDir, r.filePath);
|
|
3676
3397
|
const existing = existsSync(existingPath) ? readFileSync(existingPath, "utf-8") : void 0;
|
|
3677
3398
|
if (!existing) {
|
|
3678
3399
|
const lineCount = newContent.split("\n").length - 1;
|
|
3679
|
-
log
|
|
3400
|
+
log.info(` + ${lineCount} new lines`);
|
|
3680
3401
|
} else {
|
|
3681
3402
|
const diff = lineDiff(existing, newContent);
|
|
3682
|
-
for (const line of diff) log
|
|
3403
|
+
for (const line of diff) log.info(` ${line}`);
|
|
3683
3404
|
}
|
|
3684
3405
|
}
|
|
3685
3406
|
return 1;
|
|
@@ -3827,6 +3548,17 @@ function reconcileTags(expectedTags, remoteTags, stdoutTags) {
|
|
|
3827
3548
|
}
|
|
3828
3549
|
//#endregion
|
|
3829
3550
|
//#region src/release/forgejo.ts
|
|
3551
|
+
const RETRY_ATTEMPTS = 3;
|
|
3552
|
+
const RETRY_BASE_DELAY_MS = 1e3;
|
|
3553
|
+
/** Safely read response body text for inclusion in error messages. */
|
|
3554
|
+
async function responseBodyText(res) {
|
|
3555
|
+
try {
|
|
3556
|
+
const text = await res.text();
|
|
3557
|
+
return text.length > 500 ? text.slice(0, 500) + "…" : text;
|
|
3558
|
+
} catch {
|
|
3559
|
+
return "(could not read response body)";
|
|
3560
|
+
}
|
|
3561
|
+
}
|
|
3830
3562
|
const PullRequestSchema = z.array(z.object({
|
|
3831
3563
|
number: z.number(),
|
|
3832
3564
|
head: z.object({ ref: z.string() })
|
|
@@ -3840,7 +3572,10 @@ const PullRequestSchema = z.array(z.object({
|
|
|
3840
3572
|
async function findOpenPr(executor, conn, head) {
|
|
3841
3573
|
const url = `${conn.serverUrl}/api/v1/repos/${conn.repository}/pulls?state=open`;
|
|
3842
3574
|
const res = await executor.fetch(url, { headers: { Authorization: `token ${conn.token}` } });
|
|
3843
|
-
if (!res.ok)
|
|
3575
|
+
if (!res.ok) {
|
|
3576
|
+
const body = await responseBodyText(res);
|
|
3577
|
+
throw new TransientError(`Failed to list PRs: ${res.status} ${res.statusText}\n${body}`);
|
|
3578
|
+
}
|
|
3844
3579
|
const parsed = PullRequestSchema.safeParse(await res.json());
|
|
3845
3580
|
if (!parsed.success) throw new UnexpectedError(`Unexpected PR list response: ${parsed.error.message}`);
|
|
3846
3581
|
return parsed.data.find((pr) => pr.head.ref === head)?.number ?? null;
|
|
@@ -3862,7 +3597,10 @@ async function createPr(executor, conn, options) {
|
|
|
3862
3597
|
},
|
|
3863
3598
|
body: JSON.stringify(payload)
|
|
3864
3599
|
});
|
|
3865
|
-
if (!res.ok)
|
|
3600
|
+
if (!res.ok) {
|
|
3601
|
+
const body = await responseBodyText(res);
|
|
3602
|
+
throw new TransientError(`Failed to create PR: ${res.status} ${res.statusText}\n${body}`);
|
|
3603
|
+
}
|
|
3866
3604
|
}
|
|
3867
3605
|
/** Update an existing pull request's title and body. */
|
|
3868
3606
|
async function updatePr(executor, conn, prNumber, options) {
|
|
@@ -3878,7 +3616,10 @@ async function updatePr(executor, conn, prNumber, options) {
|
|
|
3878
3616
|
body: options.body
|
|
3879
3617
|
})
|
|
3880
3618
|
});
|
|
3881
|
-
if (!res.ok)
|
|
3619
|
+
if (!res.ok) {
|
|
3620
|
+
const body = await responseBodyText(res);
|
|
3621
|
+
throw new TransientError(`Failed to update PR #${String(prNumber)}: ${res.status} ${res.statusText}\n${body}`);
|
|
3622
|
+
}
|
|
3882
3623
|
}
|
|
3883
3624
|
/** Merge a pull request by number. */
|
|
3884
3625
|
async function mergePr(executor, conn, prNumber, options) {
|
|
@@ -3894,7 +3635,10 @@ async function mergePr(executor, conn, prNumber, options) {
|
|
|
3894
3635
|
delete_branch_after_merge: options?.deleteBranch ?? true
|
|
3895
3636
|
})
|
|
3896
3637
|
});
|
|
3897
|
-
if (!res.ok)
|
|
3638
|
+
if (!res.ok) {
|
|
3639
|
+
const body = await responseBodyText(res);
|
|
3640
|
+
throw new TransientError(`Failed to merge PR #${String(prNumber)}: ${res.status} ${res.statusText}\n${body}`);
|
|
3641
|
+
}
|
|
3898
3642
|
}
|
|
3899
3643
|
/** Check whether a Forgejo release already exists for a given tag. */
|
|
3900
3644
|
async function findRelease(executor, conn, tag) {
|
|
@@ -3903,7 +3647,8 @@ async function findRelease(executor, conn, tag) {
|
|
|
3903
3647
|
const res = await executor.fetch(url, { headers: { Authorization: `token ${conn.token}` } });
|
|
3904
3648
|
if (res.status === 200) return true;
|
|
3905
3649
|
if (res.status === 404) return false;
|
|
3906
|
-
|
|
3650
|
+
const body = await responseBodyText(res);
|
|
3651
|
+
throw new TransientError(`Failed to check release for ${tag}: ${res.status} ${res.statusText}\n${body}`);
|
|
3907
3652
|
}
|
|
3908
3653
|
/** Create a Forgejo release for a given tag. */
|
|
3909
3654
|
async function createRelease(executor, conn, tag) {
|
|
@@ -3920,21 +3665,35 @@ async function createRelease(executor, conn, tag) {
|
|
|
3920
3665
|
body: `Published ${tag}`
|
|
3921
3666
|
})
|
|
3922
3667
|
});
|
|
3923
|
-
if (!res.ok)
|
|
3668
|
+
if (!res.ok) {
|
|
3669
|
+
const body = await responseBodyText(res);
|
|
3670
|
+
throw new TransientError(`Failed to create release for ${tag}: ${res.status} ${res.statusText}\n${body}`);
|
|
3671
|
+
}
|
|
3924
3672
|
}
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3673
|
+
/**
|
|
3674
|
+
* Ensure a Forgejo release exists for a tag, creating it if necessary.
|
|
3675
|
+
*
|
|
3676
|
+
* Handles two edge cases:
|
|
3677
|
+
* - The release already exists before we try (skips creation)
|
|
3678
|
+
* - Forgejo auto-creates a release when a tag is pushed, causing a 500 race
|
|
3679
|
+
* condition (detected by re-checking after failure)
|
|
3680
|
+
*
|
|
3681
|
+
* Retries on transient errors with exponential backoff.
|
|
3682
|
+
* Returns "created" | "exists" | "race" indicating what happened.
|
|
3683
|
+
*/
|
|
3684
|
+
async function ensureRelease(executor, conn, tag) {
|
|
3685
|
+
if (await findRelease(executor, conn, tag)) return "exists";
|
|
3686
|
+
for (let attempt = 1; attempt <= RETRY_ATTEMPTS; attempt++) try {
|
|
3687
|
+
await createRelease(executor, conn, tag);
|
|
3688
|
+
return "created";
|
|
3689
|
+
} catch (error) {
|
|
3690
|
+
if (await findRelease(executor, conn, tag)) return "race";
|
|
3691
|
+
if (attempt >= RETRY_ATTEMPTS) throw error;
|
|
3692
|
+
log.warn(`Release creation attempt ${String(attempt)}/${String(RETRY_ATTEMPTS)} failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
3693
|
+
const delay = RETRY_BASE_DELAY_MS * 2 ** (attempt - 1);
|
|
3694
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
3695
|
+
}
|
|
3696
|
+
throw new TransientError(`Failed to create release for ${tag} after ${String(RETRY_ATTEMPTS)} attempts`);
|
|
3938
3697
|
}
|
|
3939
3698
|
//#endregion
|
|
3940
3699
|
//#region src/release/version.ts
|
|
@@ -4006,7 +3765,7 @@ function buildPrContent(executor, cwd, packagesBefore) {
|
|
|
4006
3765
|
}
|
|
4007
3766
|
/** Mode 1: version packages and create/update a PR. */
|
|
4008
3767
|
async function runVersionMode(executor, config) {
|
|
4009
|
-
log
|
|
3768
|
+
log.info("Changesets detected — versioning packages");
|
|
4010
3769
|
const packagesBefore = executor.listWorkspacePackages(config.cwd);
|
|
4011
3770
|
debug(config, `Packages before versioning: ${packagesBefore.map((pkg) => `${pkg.name}@${pkg.version}`).join(", ") || "(none)"}`);
|
|
4012
3771
|
const changesetConfigPath = path.join(config.cwd, ".changeset", "config.json");
|
|
@@ -4032,19 +3791,19 @@ async function runVersionMode(executor, config) {
|
|
|
4032
3791
|
const addResult = executor.exec("git add -A", { cwd: config.cwd });
|
|
4033
3792
|
if (addResult.exitCode !== 0) throw new FatalError(`git add failed: ${addResult.stderr || addResult.stdout}`);
|
|
4034
3793
|
const remainingChangesets = executor.listChangesetFiles(config.cwd);
|
|
4035
|
-
if (remainingChangesets.length > 0) log
|
|
3794
|
+
if (remainingChangesets.length > 0) log.warn(`Changeset files still present after versioning: ${remainingChangesets.join(", ")}`);
|
|
4036
3795
|
debug(config, `Changeset files after versioning: ${remainingChangesets.length > 0 ? remainingChangesets.join(", ") : "(none — all consumed)"}`);
|
|
4037
3796
|
const commitResult = executor.exec("git commit -m \"chore: version packages\"", { cwd: config.cwd });
|
|
4038
3797
|
debugExec(config, "git commit", commitResult);
|
|
4039
3798
|
if (commitResult.exitCode !== 0) {
|
|
4040
|
-
log
|
|
3799
|
+
log.info("Nothing to commit after versioning");
|
|
4041
3800
|
return {
|
|
4042
3801
|
mode: "version",
|
|
4043
3802
|
pr: "none"
|
|
4044
3803
|
};
|
|
4045
3804
|
}
|
|
4046
3805
|
if (config.dryRun) {
|
|
4047
|
-
log
|
|
3806
|
+
log.info("[dry-run] Would push and create/update PR");
|
|
4048
3807
|
return {
|
|
4049
3808
|
mode: "version",
|
|
4050
3809
|
pr: "none"
|
|
@@ -4067,7 +3826,7 @@ async function runVersionMode(executor, config) {
|
|
|
4067
3826
|
base: "main",
|
|
4068
3827
|
body
|
|
4069
3828
|
});
|
|
4070
|
-
log
|
|
3829
|
+
log.info("Created version PR");
|
|
4071
3830
|
return {
|
|
4072
3831
|
mode: "version",
|
|
4073
3832
|
pr: "created"
|
|
@@ -4077,7 +3836,7 @@ async function runVersionMode(executor, config) {
|
|
|
4077
3836
|
title,
|
|
4078
3837
|
body
|
|
4079
3838
|
});
|
|
4080
|
-
log
|
|
3839
|
+
log.info(`Updated version PR #${String(existingPr)}`);
|
|
4081
3840
|
return {
|
|
4082
3841
|
mode: "version",
|
|
4083
3842
|
pr: "updated"
|
|
@@ -4085,24 +3844,9 @@ async function runVersionMode(executor, config) {
|
|
|
4085
3844
|
}
|
|
4086
3845
|
//#endregion
|
|
4087
3846
|
//#region src/release/publish.ts
|
|
4088
|
-
const RETRY_ATTEMPTS = 3;
|
|
4089
|
-
const RETRY_BASE_DELAY_MS = 1e3;
|
|
4090
|
-
async function retryAsync(fn) {
|
|
4091
|
-
let lastError;
|
|
4092
|
-
for (let attempt = 0; attempt <= RETRY_ATTEMPTS; attempt++) try {
|
|
4093
|
-
return await fn();
|
|
4094
|
-
} catch (error) {
|
|
4095
|
-
lastError = error;
|
|
4096
|
-
if (attempt < RETRY_ATTEMPTS) {
|
|
4097
|
-
const delay = RETRY_BASE_DELAY_MS * 2 ** attempt;
|
|
4098
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
4099
|
-
}
|
|
4100
|
-
}
|
|
4101
|
-
throw lastError;
|
|
4102
|
-
}
|
|
4103
3847
|
/** Mode 2: publish to npm, push tags, and create Forgejo releases. */
|
|
4104
3848
|
async function runPublishMode(executor, config) {
|
|
4105
|
-
log
|
|
3849
|
+
log.info("No changesets — publishing packages");
|
|
4106
3850
|
const publishResult = executor.exec("pnpm changeset publish", { cwd: config.cwd });
|
|
4107
3851
|
debugExec(config, "pnpm changeset publish", publishResult);
|
|
4108
3852
|
if (publishResult.exitCode !== 0) throw new FatalError(`pnpm changeset publish failed (exit code ${String(publishResult.exitCode)}):\n${publishResult.stderr}`);
|
|
@@ -4117,11 +3861,11 @@ async function runPublishMode(executor, config) {
|
|
|
4117
3861
|
debug(config, `Reconciled tags to push: ${tagsToPush.length > 0 ? tagsToPush.join(", ") : "(none)"}`);
|
|
4118
3862
|
if (config.dryRun) {
|
|
4119
3863
|
if (tagsToPush.length === 0) {
|
|
4120
|
-
log
|
|
3864
|
+
log.info("No packages were published");
|
|
4121
3865
|
return { mode: "none" };
|
|
4122
3866
|
}
|
|
4123
|
-
log
|
|
4124
|
-
log
|
|
3867
|
+
log.info(`Tags to process: ${tagsToPush.join(", ")}`);
|
|
3868
|
+
log.info("[dry-run] Would push tags and create releases");
|
|
4125
3869
|
return {
|
|
4126
3870
|
mode: "publish",
|
|
4127
3871
|
tags: tagsToPush
|
|
@@ -4137,10 +3881,10 @@ async function runPublishMode(executor, config) {
|
|
|
4137
3881
|
for (const tag of remoteExpectedTags) if (!await findRelease(executor, conn, tag)) tagsWithMissingReleases.push(tag);
|
|
4138
3882
|
const allTags = [...tagsToPush, ...tagsWithMissingReleases];
|
|
4139
3883
|
if (allTags.length === 0) {
|
|
4140
|
-
log
|
|
3884
|
+
log.info("No packages were published");
|
|
4141
3885
|
return { mode: "none" };
|
|
4142
3886
|
}
|
|
4143
|
-
log
|
|
3887
|
+
log.info(`Tags to process: ${allTags.join(", ")}`);
|
|
4144
3888
|
const errors = [];
|
|
4145
3889
|
for (const tag of allTags) try {
|
|
4146
3890
|
if (!remoteSet.has(tag)) {
|
|
@@ -4151,24 +3895,14 @@ async function runPublishMode(executor, config) {
|
|
|
4151
3895
|
const pushTagResult = executor.exec(`git push origin refs/tags/${tag}`, { cwd: config.cwd });
|
|
4152
3896
|
if (pushTagResult.exitCode !== 0) throw new FatalError(`Failed to push tag ${tag}: ${pushTagResult.stderr || pushTagResult.stdout}`);
|
|
4153
3897
|
}
|
|
4154
|
-
if (await
|
|
4155
|
-
else {
|
|
4156
|
-
await retryAsync(async () => {
|
|
4157
|
-
try {
|
|
4158
|
-
await createRelease(executor, conn, tag);
|
|
4159
|
-
} catch (error) {
|
|
4160
|
-
if (await findRelease(executor, conn, tag)) return;
|
|
4161
|
-
throw error;
|
|
4162
|
-
}
|
|
4163
|
-
});
|
|
4164
|
-
log$2.info(`Created release for ${tag}`);
|
|
4165
|
-
}
|
|
3898
|
+
if (await ensureRelease(executor, conn, tag) === "exists") log.warn(`Release for ${tag} already exists — skipping`);
|
|
3899
|
+
else log.info(`Created release for ${tag}`);
|
|
4166
3900
|
} catch (error) {
|
|
4167
3901
|
errors.push({
|
|
4168
3902
|
tag,
|
|
4169
3903
|
error
|
|
4170
3904
|
});
|
|
4171
|
-
log
|
|
3905
|
+
log.warn(`Failed to process ${tag}: ${error instanceof Error ? error.message : String(error)}`);
|
|
4172
3906
|
}
|
|
4173
3907
|
if (errors.length > 0) throw new TransientError(`Failed to create releases for: ${errors.map((e) => e.tag).join(", ")}`);
|
|
4174
3908
|
return {
|
|
@@ -4262,6 +3996,14 @@ function configureGitAuth(executor, conn, cwd) {
|
|
|
4262
3996
|
const authUrl = `https://x-access-token:${conn.token}@${host}/${conn.repository}`;
|
|
4263
3997
|
executor.exec(`git remote set-url origin ${authUrl}`, { cwd });
|
|
4264
3998
|
}
|
|
3999
|
+
/** Configure git user.name and user.email for CI bot commits. */
|
|
4000
|
+
function configureGitIdentity(executor, platform, cwd) {
|
|
4001
|
+
const isGitHub = platform === "github";
|
|
4002
|
+
const name = isGitHub ? "github-actions[bot]" : "forgejo-actions[bot]";
|
|
4003
|
+
const email = isGitHub ? "github-actions[bot]@users.noreply.github.com" : "forgejo-actions[bot]@noreply.localhost";
|
|
4004
|
+
executor.exec(`git config user.name "${name}"`, { cwd });
|
|
4005
|
+
executor.exec(`git config user.email "${email}"`, { cwd });
|
|
4006
|
+
}
|
|
4265
4007
|
//#endregion
|
|
4266
4008
|
//#region src/commands/release-changesets.ts
|
|
4267
4009
|
const releaseForgejoCommand = defineCommand({
|
|
@@ -4276,13 +4018,13 @@ const releaseForgejoCommand = defineCommand({
|
|
|
4276
4018
|
},
|
|
4277
4019
|
verbose: {
|
|
4278
4020
|
type: "boolean",
|
|
4279
|
-
description: "Enable detailed debug logging (also enabled by
|
|
4021
|
+
description: "Enable detailed debug logging (also enabled by TOOLING_DEBUG env var)"
|
|
4280
4022
|
}
|
|
4281
4023
|
},
|
|
4282
4024
|
async run({ args }) {
|
|
4283
4025
|
if ((await runRelease(buildReleaseConfig({
|
|
4284
4026
|
dryRun: args["dry-run"] === true,
|
|
4285
|
-
verbose: args.verbose === true ||
|
|
4027
|
+
verbose: args.verbose === true || isEnvVerbose()
|
|
4286
4028
|
}), createRealExecutor())).mode === "none") process.exitCode = 0;
|
|
4287
4029
|
}
|
|
4288
4030
|
});
|
|
@@ -4310,8 +4052,7 @@ async function runRelease(config, executor) {
|
|
|
4310
4052
|
debug(config, `Skipping release on non-main branch: ${branch}`);
|
|
4311
4053
|
return { mode: "none" };
|
|
4312
4054
|
}
|
|
4313
|
-
executor
|
|
4314
|
-
executor.exec("git config user.email \"forgejo-actions[bot]@noreply.localhost\"", { cwd: config.cwd });
|
|
4055
|
+
configureGitIdentity(executor, "forgejo", config.cwd);
|
|
4315
4056
|
configureGitAuth(executor, config, config.cwd);
|
|
4316
4057
|
const changesetFiles = executor.listChangesetFiles(config.cwd);
|
|
4317
4058
|
debug(config, `Changeset files found: ${changesetFiles.length > 0 ? changesetFiles.join(", ") : "(none)"}`);
|
|
@@ -4369,7 +4110,7 @@ async function triggerForgejo(conn, ref, inputs) {
|
|
|
4369
4110
|
body: JSON.stringify(body)
|
|
4370
4111
|
});
|
|
4371
4112
|
if (!res.ok) throw new FatalError(`Failed to trigger Forgejo workflow: ${res.status} ${res.statusText}`);
|
|
4372
|
-
log
|
|
4113
|
+
log.info(`Triggered release workflow on Forgejo (ref: ${ref})`);
|
|
4373
4114
|
}
|
|
4374
4115
|
function triggerGitHub(ref, inputs) {
|
|
4375
4116
|
const executor = createRealExecutor();
|
|
@@ -4377,7 +4118,7 @@ function triggerGitHub(ref, inputs) {
|
|
|
4377
4118
|
const cmd = `gh workflow run release.yml --ref ${ref}${inputFlags ? ` ${inputFlags}` : ""}`;
|
|
4378
4119
|
const result = executor.exec(cmd, { cwd: process.cwd() });
|
|
4379
4120
|
if (result.exitCode !== 0) throw new FatalError(`Failed to trigger GitHub workflow: ${result.stderr || result.stdout || "unknown error"}`);
|
|
4380
|
-
log
|
|
4121
|
+
log.info(`Triggered release workflow on GitHub (ref: ${ref})`);
|
|
4381
4122
|
}
|
|
4382
4123
|
//#endregion
|
|
4383
4124
|
//#region src/commands/forgejo-create-release.ts
|
|
@@ -4397,11 +4138,11 @@ const createForgejoReleaseCommand = defineCommand({
|
|
|
4397
4138
|
const executor = createRealExecutor();
|
|
4398
4139
|
const conn = resolved.conn;
|
|
4399
4140
|
if (await findRelease(executor, conn, args.tag)) {
|
|
4400
|
-
log
|
|
4141
|
+
log.info(`Release for ${args.tag} already exists — skipping`);
|
|
4401
4142
|
return;
|
|
4402
4143
|
}
|
|
4403
4144
|
await createRelease(executor, conn, args.tag);
|
|
4404
|
-
log
|
|
4145
|
+
log.info(`Created Forgejo release for ${args.tag}`);
|
|
4405
4146
|
}
|
|
4406
4147
|
});
|
|
4407
4148
|
//#endregion
|
|
@@ -4428,26 +4169,26 @@ async function mergeForgejo(conn, dryRun) {
|
|
|
4428
4169
|
const prNumber = await findOpenPr(executor, conn, HEAD_BRANCH);
|
|
4429
4170
|
if (prNumber === null) throw new FatalError(`No open PR found for branch ${HEAD_BRANCH}`);
|
|
4430
4171
|
if (dryRun) {
|
|
4431
|
-
log
|
|
4172
|
+
log.info(`[dry-run] Would merge PR #${String(prNumber)} and delete branch ${HEAD_BRANCH}`);
|
|
4432
4173
|
return;
|
|
4433
4174
|
}
|
|
4434
4175
|
await mergePr(executor, conn, prNumber, {
|
|
4435
4176
|
method: "merge",
|
|
4436
4177
|
deleteBranch: true
|
|
4437
4178
|
});
|
|
4438
|
-
log
|
|
4179
|
+
log.info(`Merged PR #${String(prNumber)} and deleted branch ${HEAD_BRANCH}`);
|
|
4439
4180
|
}
|
|
4440
4181
|
function mergeGitHub(dryRun) {
|
|
4441
4182
|
const executor = createRealExecutor();
|
|
4442
4183
|
if (dryRun) {
|
|
4443
4184
|
const prNum = executor.exec(`gh pr view ${HEAD_BRANCH} --json number --jq .number`, { cwd: process.cwd() }).stdout.trim();
|
|
4444
4185
|
if (!prNum) throw new FatalError(`No open PR found for branch ${HEAD_BRANCH}`);
|
|
4445
|
-
log
|
|
4186
|
+
log.info(`[dry-run] Would merge PR #${prNum} and delete branch ${HEAD_BRANCH}`);
|
|
4446
4187
|
return;
|
|
4447
4188
|
}
|
|
4448
4189
|
const result = executor.exec(`gh pr merge ${HEAD_BRANCH} --merge --delete-branch`, { cwd: process.cwd() });
|
|
4449
4190
|
if (result.exitCode !== 0) throw new FatalError(`Failed to merge PR: ${result.stderr || result.stdout || "unknown error"}`);
|
|
4450
|
-
log
|
|
4191
|
+
log.info(`Merged changesets PR and deleted branch ${HEAD_BRANCH}`);
|
|
4451
4192
|
}
|
|
4452
4193
|
//#endregion
|
|
4453
4194
|
//#region src/release/simple.ts
|
|
@@ -4477,20 +4218,15 @@ function readVersion(executor, cwd) {
|
|
|
4477
4218
|
if (!pkg?.version) throw new FatalError("No version field found in package.json");
|
|
4478
4219
|
return pkg.version;
|
|
4479
4220
|
}
|
|
4480
|
-
/**
|
|
4481
|
-
function
|
|
4482
|
-
|
|
4483
|
-
const name = isGitHub ? "github-actions[bot]" : "forgejo-actions[bot]";
|
|
4484
|
-
const email = isGitHub ? "github-actions[bot]@users.noreply.github.com" : "forgejo-actions[bot]@noreply.localhost";
|
|
4485
|
-
executor.exec(`git config user.name "${name}"`, { cwd: config.cwd });
|
|
4486
|
-
executor.exec(`git config user.email "${email}"`, { cwd: config.cwd });
|
|
4487
|
-
debug(config, `Configured git identity: ${name} <${email}>`);
|
|
4221
|
+
/** Resolve the platform type string for git identity configuration. */
|
|
4222
|
+
function platformType(config) {
|
|
4223
|
+
return config.platform?.type === "github" ? "github" : "forgejo";
|
|
4488
4224
|
}
|
|
4489
4225
|
/** Run the full commit-and-tag-version release flow. */
|
|
4490
4226
|
async function runSimpleRelease(executor, config) {
|
|
4491
|
-
configureGitIdentity(executor, config);
|
|
4227
|
+
configureGitIdentity(executor, platformType(config), config.cwd);
|
|
4492
4228
|
const command = buildCommand(config);
|
|
4493
|
-
log
|
|
4229
|
+
log.info(`Running: ${command}`);
|
|
4494
4230
|
const versionResult = executor.exec(command, { cwd: config.cwd });
|
|
4495
4231
|
debugExec(config, "commit-and-tag-version", versionResult);
|
|
4496
4232
|
if (versionResult.exitCode !== 0) throw new FatalError(`commit-and-tag-version failed (exit code ${String(versionResult.exitCode)}):\n${versionResult.stderr || versionResult.stdout}`);
|
|
@@ -4500,12 +4236,12 @@ async function runSimpleRelease(executor, config) {
|
|
|
4500
4236
|
debugExec(config, "git describe", tagResult);
|
|
4501
4237
|
const tag = tagResult.stdout.trim();
|
|
4502
4238
|
if (!tag) throw new FatalError("Could not determine the new tag from git describe");
|
|
4503
|
-
log
|
|
4239
|
+
log.info(`Version ${version} tagged as ${tag}`);
|
|
4504
4240
|
if (config.dryRun) {
|
|
4505
4241
|
const slidingTags = config.noSlidingTags ? [] : computeSlidingTags(version);
|
|
4506
|
-
log
|
|
4507
|
-
if (slidingTags.length > 0) log
|
|
4508
|
-
if (!config.noRelease && config.platform) log
|
|
4242
|
+
log.info(`[dry-run] Would push to origin with --follow-tags`);
|
|
4243
|
+
if (slidingTags.length > 0) log.info(`[dry-run] Would create sliding tags: ${slidingTags.join(", ")}`);
|
|
4244
|
+
if (!config.noRelease && config.platform) log.info(`[dry-run] Would create ${config.platform.type} release for ${tag}`);
|
|
4509
4245
|
return {
|
|
4510
4246
|
version,
|
|
4511
4247
|
tag,
|
|
@@ -4526,7 +4262,7 @@ async function runSimpleRelease(executor, config) {
|
|
|
4526
4262
|
debugExec(config, "git push", pushResult);
|
|
4527
4263
|
if (pushResult.exitCode !== 0) throw new FatalError(`git push failed (exit code ${String(pushResult.exitCode)}):\n${pushResult.stderr || pushResult.stdout}`);
|
|
4528
4264
|
pushed = true;
|
|
4529
|
-
log
|
|
4265
|
+
log.info("Pushed to origin");
|
|
4530
4266
|
}
|
|
4531
4267
|
let slidingTags = [];
|
|
4532
4268
|
if (!config.noSlidingTags && pushed) {
|
|
@@ -4537,8 +4273,8 @@ async function runSimpleRelease(executor, config) {
|
|
|
4537
4273
|
}
|
|
4538
4274
|
const forcePushResult = executor.exec(`git push origin ${slidingTags.join(" ")} --force`, { cwd: config.cwd });
|
|
4539
4275
|
debugExec(config, "force-push sliding tags", forcePushResult);
|
|
4540
|
-
if (forcePushResult.exitCode !== 0) log
|
|
4541
|
-
else log
|
|
4276
|
+
if (forcePushResult.exitCode !== 0) log.warn(`Warning: Failed to push sliding tags: ${forcePushResult.stderr || forcePushResult.stdout}`);
|
|
4277
|
+
else log.info(`Created sliding tags: ${slidingTags.join(", ")}`);
|
|
4542
4278
|
}
|
|
4543
4279
|
let releaseCreated = false;
|
|
4544
4280
|
if (!config.noRelease && config.platform) releaseCreated = await createPlatformRelease(executor, config, tag);
|
|
@@ -4553,21 +4289,20 @@ async function runSimpleRelease(executor, config) {
|
|
|
4553
4289
|
async function createPlatformRelease(executor, config, tag) {
|
|
4554
4290
|
if (!config.platform) return false;
|
|
4555
4291
|
if (config.platform.type === "forgejo") {
|
|
4556
|
-
if (await
|
|
4292
|
+
if (await ensureRelease(executor, config.platform.conn, tag) === "exists") {
|
|
4557
4293
|
debug(config, `Release for ${tag} already exists, skipping`);
|
|
4558
4294
|
return false;
|
|
4559
4295
|
}
|
|
4560
|
-
|
|
4561
|
-
log$2.info(`Created Forgejo release for ${tag}`);
|
|
4296
|
+
log.info(`Created Forgejo release for ${tag}`);
|
|
4562
4297
|
return true;
|
|
4563
4298
|
}
|
|
4564
4299
|
const ghResult = executor.exec(`gh release create ${tag} --generate-notes`, { cwd: config.cwd });
|
|
4565
4300
|
debugExec(config, "gh release create", ghResult);
|
|
4566
4301
|
if (ghResult.exitCode !== 0) {
|
|
4567
|
-
log
|
|
4302
|
+
log.warn(`Warning: Failed to create GitHub release: ${ghResult.stderr || ghResult.stdout}`);
|
|
4568
4303
|
return false;
|
|
4569
4304
|
}
|
|
4570
|
-
log
|
|
4305
|
+
log.info(`Created GitHub release for ${tag}`);
|
|
4571
4306
|
return true;
|
|
4572
4307
|
}
|
|
4573
4308
|
//#endregion
|
|
@@ -4584,7 +4319,7 @@ const releaseSimpleCommand = defineCommand({
|
|
|
4584
4319
|
},
|
|
4585
4320
|
verbose: {
|
|
4586
4321
|
type: "boolean",
|
|
4587
|
-
description: "Enable detailed debug logging (also enabled by
|
|
4322
|
+
description: "Enable detailed debug logging (also enabled by TOOLING_DEBUG env var)"
|
|
4588
4323
|
},
|
|
4589
4324
|
"no-push": {
|
|
4590
4325
|
type: "boolean",
|
|
@@ -4613,7 +4348,7 @@ const releaseSimpleCommand = defineCommand({
|
|
|
4613
4348
|
},
|
|
4614
4349
|
async run({ args }) {
|
|
4615
4350
|
const cwd = process.cwd();
|
|
4616
|
-
const verbose = args.verbose === true ||
|
|
4351
|
+
const verbose = args.verbose === true || isEnvVerbose();
|
|
4617
4352
|
const noRelease = args["no-release"] === true;
|
|
4618
4353
|
let platform;
|
|
4619
4354
|
if (!noRelease) {
|
|
@@ -4697,12 +4432,12 @@ const ciReporter = {
|
|
|
4697
4432
|
const localReporter = {
|
|
4698
4433
|
groupStart: (_name) => {},
|
|
4699
4434
|
groupEnd: () => {},
|
|
4700
|
-
passed: (name) => log
|
|
4701
|
-
failed: (name) => log
|
|
4702
|
-
undefinedCheck: (name) => log
|
|
4703
|
-
skippedNotDefined: (names) => log
|
|
4704
|
-
allPassed: () => log
|
|
4705
|
-
anyFailed: (names) => log
|
|
4435
|
+
passed: (name) => log.success(name),
|
|
4436
|
+
failed: (name) => log.error(`${name} failed`),
|
|
4437
|
+
undefinedCheck: (name) => log.error(`${name} not defined in package.json`),
|
|
4438
|
+
skippedNotDefined: (names) => log.info(`Skipped (not defined): ${names.join(", ")}`),
|
|
4439
|
+
allPassed: () => log.success("All checks passed"),
|
|
4440
|
+
anyFailed: (names) => log.error(`Failed checks: ${names.join(", ")}`)
|
|
4706
4441
|
};
|
|
4707
4442
|
function runRunChecks(targetDir, options = {}) {
|
|
4708
4443
|
const exec = options.execCommand ?? defaultExecCommand;
|
|
@@ -4712,6 +4447,7 @@ function runRunChecks(targetDir, options = {}) {
|
|
|
4712
4447
|
const isCI = Boolean(process.env["CI"]);
|
|
4713
4448
|
const failFast = options.failFast ?? !isCI;
|
|
4714
4449
|
const reporter = isCI ? ciReporter : localReporter;
|
|
4450
|
+
const vc = { verbose: options.verbose ?? false };
|
|
4715
4451
|
const definedScripts = getScripts(targetDir);
|
|
4716
4452
|
const addedNames = new Set(add);
|
|
4717
4453
|
const allChecks = [...CHECKS, ...add.map((name) => ({ name }))];
|
|
@@ -4727,11 +4463,13 @@ function runRunChecks(targetDir, options = {}) {
|
|
|
4727
4463
|
continue;
|
|
4728
4464
|
}
|
|
4729
4465
|
const cmd = check.args ? `pnpm run ${check.name} ${check.args}` : `pnpm run ${check.name}`;
|
|
4466
|
+
debug(vc, `Running: ${cmd} (in ${targetDir})`);
|
|
4730
4467
|
reporter.groupStart(check.name);
|
|
4731
4468
|
const start = Date.now();
|
|
4732
4469
|
const exitCode = exec(cmd, targetDir);
|
|
4733
4470
|
const elapsed = ((Date.now() - start) / 1e3).toFixed(1);
|
|
4734
4471
|
reporter.groupEnd();
|
|
4472
|
+
debug(vc, `${check.name}: exit code ${String(exitCode)}, ${elapsed}s`);
|
|
4735
4473
|
if (exitCode === 0) reporter.passed(check.name, elapsed);
|
|
4736
4474
|
else {
|
|
4737
4475
|
reporter.failed(check.name, elapsed);
|
|
@@ -4772,13 +4510,19 @@ const runChecksCommand = defineCommand({
|
|
|
4772
4510
|
type: "boolean",
|
|
4773
4511
|
description: "Stop on first failure (default: true in dev, false in CI)",
|
|
4774
4512
|
required: false
|
|
4513
|
+
},
|
|
4514
|
+
verbose: {
|
|
4515
|
+
type: "boolean",
|
|
4516
|
+
description: "Emit detailed debug logging",
|
|
4517
|
+
required: false
|
|
4775
4518
|
}
|
|
4776
4519
|
},
|
|
4777
4520
|
run({ args }) {
|
|
4778
4521
|
const exitCode = runRunChecks(path.resolve(args.dir ?? "."), {
|
|
4779
4522
|
skip: args.skip ? new Set(args.skip.split(",").map((s) => s.trim())) : void 0,
|
|
4780
4523
|
add: args.add ? args.add.split(",").map((s) => s.trim()) : void 0,
|
|
4781
|
-
failFast: args["fail-fast"] ? true : void 0
|
|
4524
|
+
failFast: args["fail-fast"] ? true : void 0,
|
|
4525
|
+
verbose: args.verbose === true || isEnvVerbose()
|
|
4782
4526
|
});
|
|
4783
4527
|
process.exitCode = exitCode;
|
|
4784
4528
|
}
|
|
@@ -4795,10 +4539,16 @@ const publishDockerCommand = defineCommand({
|
|
|
4795
4539
|
name: "docker:publish",
|
|
4796
4540
|
description: "Build, tag, and push Docker images for packages with an image:build script"
|
|
4797
4541
|
},
|
|
4798
|
-
args: {
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
|
|
4542
|
+
args: {
|
|
4543
|
+
"dry-run": {
|
|
4544
|
+
type: "boolean",
|
|
4545
|
+
description: "Build and tag images but skip login, push, and logout"
|
|
4546
|
+
},
|
|
4547
|
+
verbose: {
|
|
4548
|
+
type: "boolean",
|
|
4549
|
+
description: "Emit detailed debug logging"
|
|
4550
|
+
}
|
|
4551
|
+
},
|
|
4802
4552
|
async run({ args }) {
|
|
4803
4553
|
const config = {
|
|
4804
4554
|
cwd: process.cwd(),
|
|
@@ -4806,7 +4556,8 @@ const publishDockerCommand = defineCommand({
|
|
|
4806
4556
|
registryNamespace: requireEnv("DOCKER_REGISTRY_NAMESPACE"),
|
|
4807
4557
|
username: requireEnv("DOCKER_USERNAME"),
|
|
4808
4558
|
password: requireEnv("DOCKER_PASSWORD"),
|
|
4809
|
-
dryRun: args["dry-run"] === true
|
|
4559
|
+
dryRun: args["dry-run"] === true,
|
|
4560
|
+
verbose: args.verbose === true || isEnvVerbose()
|
|
4810
4561
|
};
|
|
4811
4562
|
runDockerPublish(createRealExecutor(), config);
|
|
4812
4563
|
}
|
|
@@ -4839,6 +4590,10 @@ const dockerBuildCommand = defineCommand({
|
|
|
4839
4590
|
type: "string",
|
|
4840
4591
|
description: "Build a single package by directory path (e.g. packages/server). Useful as an image:build script."
|
|
4841
4592
|
},
|
|
4593
|
+
verbose: {
|
|
4594
|
+
type: "boolean",
|
|
4595
|
+
description: "Emit detailed debug logging"
|
|
4596
|
+
},
|
|
4842
4597
|
_: {
|
|
4843
4598
|
type: "positional",
|
|
4844
4599
|
required: false,
|
|
@@ -4849,6 +4604,7 @@ const dockerBuildCommand = defineCommand({
|
|
|
4849
4604
|
const executor = createRealExecutor();
|
|
4850
4605
|
const rawExtra = args._ ?? [];
|
|
4851
4606
|
const extraArgs = Array.isArray(rawExtra) ? rawExtra.map(String) : [String(rawExtra)];
|
|
4607
|
+
const verbose = args.verbose === true || isEnvVerbose();
|
|
4852
4608
|
let cwd = process.cwd();
|
|
4853
4609
|
let packageDir = args.package;
|
|
4854
4610
|
if (!packageDir) {
|
|
@@ -4859,7 +4615,8 @@ const dockerBuildCommand = defineCommand({
|
|
|
4859
4615
|
runDockerBuild(executor, {
|
|
4860
4616
|
cwd,
|
|
4861
4617
|
packageDir,
|
|
4862
|
-
extraArgs: extraArgs.filter((a) => a.length > 0)
|
|
4618
|
+
extraArgs: extraArgs.filter((a) => a.length > 0),
|
|
4619
|
+
verbose
|
|
4863
4620
|
});
|
|
4864
4621
|
}
|
|
4865
4622
|
});
|
|
@@ -5076,12 +4833,6 @@ function writeTempOverlay(content) {
|
|
|
5076
4833
|
writeFileSync(filePath, content, "utf-8");
|
|
5077
4834
|
return filePath;
|
|
5078
4835
|
}
|
|
5079
|
-
function log(message) {
|
|
5080
|
-
console.log(message);
|
|
5081
|
-
}
|
|
5082
|
-
function warn(message) {
|
|
5083
|
-
console.warn(message);
|
|
5084
|
-
}
|
|
5085
4836
|
const dockerCheckCommand = defineCommand({
|
|
5086
4837
|
meta: {
|
|
5087
4838
|
name: "docker:check",
|
|
@@ -5095,12 +4846,16 @@ const dockerCheckCommand = defineCommand({
|
|
|
5095
4846
|
"poll-interval": {
|
|
5096
4847
|
type: "string",
|
|
5097
4848
|
description: "Interval between polling attempts, in ms (default: 5000)"
|
|
4849
|
+
},
|
|
4850
|
+
verbose: {
|
|
4851
|
+
type: "boolean",
|
|
4852
|
+
description: "Emit detailed debug logging"
|
|
5098
4853
|
}
|
|
5099
4854
|
},
|
|
5100
4855
|
async run({ args }) {
|
|
5101
4856
|
const cwd = process.cwd();
|
|
5102
4857
|
if (loadToolingConfig(cwd)?.dockerCheck === false) {
|
|
5103
|
-
log("Docker check is disabled in .tooling.json");
|
|
4858
|
+
log.info("Docker check is disabled in .tooling.json");
|
|
5104
4859
|
return;
|
|
5105
4860
|
}
|
|
5106
4861
|
const defaults = computeCheckDefaults(cwd);
|
|
@@ -5108,8 +4863,8 @@ const dockerCheckCommand = defineCommand({
|
|
|
5108
4863
|
if (!defaults.checkOverlay) {
|
|
5109
4864
|
const composeCwd = defaults.composeCwd ?? cwd;
|
|
5110
4865
|
const expectedOverlay = (defaults.composeFiles[0] ?? "docker-compose.yaml").replace(/\.(yaml|yml)$/, ".check.$1");
|
|
5111
|
-
warn(`Compose files found but no check overlay. Create ${path.relative(cwd, path.join(composeCwd, expectedOverlay))} to enable docker:check.`);
|
|
5112
|
-
warn("To suppress this warning, set \"dockerCheck\": false in .tooling.json.");
|
|
4866
|
+
log.warn(`Compose files found but no check overlay. Create ${path.relative(cwd, path.join(composeCwd, expectedOverlay))} to enable docker:check.`);
|
|
4867
|
+
log.warn("To suppress this warning, set \"dockerCheck\": false in .tooling.json.");
|
|
5113
4868
|
return;
|
|
5114
4869
|
}
|
|
5115
4870
|
if (!defaults.services || defaults.services.length === 0) throw new FatalError("No services found in compose files.");
|
|
@@ -5122,7 +4877,7 @@ const dockerCheckCommand = defineCommand({
|
|
|
5122
4877
|
if (rootPkg?.name) {
|
|
5123
4878
|
const dockerPackages = detectDockerPackages(fileReader, cwd, rootPkg.name);
|
|
5124
4879
|
const composeImages = extractComposeImageNames(services);
|
|
5125
|
-
for (const pkg of dockerPackages) if (!composeImages.some((img) => img === pkg.imageName || img.endsWith(`/${pkg.imageName}`))) warn(`Docker package "${pkg.dir}" (image: ${pkg.imageName}) is not referenced in any compose service.`);
|
|
4880
|
+
for (const pkg of dockerPackages) if (!composeImages.some((img) => img === pkg.imageName || img.endsWith(`/${pkg.imageName}`))) log.warn(`Docker package "${pkg.dir}" (image: ${pkg.imageName}) is not referenced in any compose service.`);
|
|
5126
4881
|
}
|
|
5127
4882
|
}
|
|
5128
4883
|
const tempOverlayPath = writeTempOverlay(generateCheckOverlay(services));
|
|
@@ -5143,7 +4898,8 @@ const dockerCheckCommand = defineCommand({
|
|
|
5143
4898
|
buildCwd: defaults.buildCwd,
|
|
5144
4899
|
healthChecks: defaults.healthChecks ? toHttpHealthChecks(defaults.healthChecks) : [],
|
|
5145
4900
|
timeoutMs: args.timeout ? Number.parseInt(args.timeout, 10) : defaults.timeoutMs,
|
|
5146
|
-
pollIntervalMs: args["poll-interval"] ? Number.parseInt(args["poll-interval"], 10) : defaults.pollIntervalMs
|
|
4901
|
+
pollIntervalMs: args["poll-interval"] ? Number.parseInt(args["poll-interval"], 10) : defaults.pollIntervalMs,
|
|
4902
|
+
verbose: args.verbose === true || isEnvVerbose()
|
|
5147
4903
|
};
|
|
5148
4904
|
const result = await runDockerCheck(createRealExecutor$1(), config);
|
|
5149
4905
|
if (!result.success) throw new FatalError(`Check failed (${result.reason}): ${result.message}`);
|
|
@@ -5159,7 +4915,7 @@ const dockerCheckCommand = defineCommand({
|
|
|
5159
4915
|
const main = defineCommand({
|
|
5160
4916
|
meta: {
|
|
5161
4917
|
name: "bst",
|
|
5162
|
-
version: "0.
|
|
4918
|
+
version: "0.35.0",
|
|
5163
4919
|
description: "Bootstrap and maintain standardized TypeScript project tooling"
|
|
5164
4920
|
},
|
|
5165
4921
|
subCommands: {
|
|
@@ -5175,7 +4931,7 @@ const main = defineCommand({
|
|
|
5175
4931
|
"docker:check": dockerCheckCommand
|
|
5176
4932
|
}
|
|
5177
4933
|
});
|
|
5178
|
-
console.log(`@bensandee/tooling v0.
|
|
4934
|
+
console.log(`@bensandee/tooling v0.35.0`);
|
|
5179
4935
|
async function run() {
|
|
5180
4936
|
await runMain(main);
|
|
5181
4937
|
process.exit(process.exitCode ?? 0);
|