@bensandee/tooling 0.33.0 → 0.34.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 +207 -185
- 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/README.md
CHANGED
|
@@ -32,7 +32,7 @@ The tool auto-detects project structure, CI platform, project type, and Docker p
|
|
|
32
32
|
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
33
33
|
| `tooling repo:sync [dir]` | Detect, generate, and sync project tooling (idempotent). First run prompts for release strategy, CI platform (if not detected), and formatter (if Prettier found). Subsequent runs are non-interactive. |
|
|
34
34
|
| `tooling repo:sync --check [dir]` | Dry-run drift detection. Exits 1 if files would change. CI-friendly. |
|
|
35
|
-
| `tooling checks:run` | Run project checks (build, docker:build, typecheck, lint, test, format, knip, tooling:check, docker:check). Flags: `--skip`, `--add`, `--fail-fast`.
|
|
35
|
+
| `tooling checks:run` | Run project checks (build, docker:build, typecheck, lint, test, format, knip, tooling:check, docker:check). Flags: `--skip`, `--add`, `--fail-fast`, `--verbose`. |
|
|
36
36
|
|
|
37
37
|
**Flags:** `--yes` (accept all defaults), `--no-ci`, `--no-prompt`, `--eslint-plugin`
|
|
38
38
|
|
|
@@ -60,13 +60,13 @@ The generated `ci:check` script defaults to `pnpm check --skip 'docker:*'` since
|
|
|
60
60
|
|
|
61
61
|
### Release management
|
|
62
62
|
|
|
63
|
-
| Command | Description
|
|
64
|
-
| -------------------------------- |
|
|
65
|
-
| `tooling release:changesets` | Changesets version/publish for Forgejo CI.
|
|
66
|
-
| `tooling release:simple` | Streamlined release using commit-and-tag-version. Flags: `--release-as`, `--first-release`, `--prerelease`.
|
|
67
|
-
| `tooling release:trigger` | Trigger a release workflow.
|
|
68
|
-
| `tooling forgejo:create-release` | Create a Forgejo release from a tag.
|
|
69
|
-
| `tooling changesets:merge` | Merge a changesets version PR.
|
|
63
|
+
| Command | Description |
|
|
64
|
+
| -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
65
|
+
| `tooling release:changesets` | Changesets version/publish for Forgejo CI. Flags: `--dry-run`, `--verbose`. Env: `FORGEJO_SERVER_URL`, `FORGEJO_REPOSITORY`, `RELEASE_TOKEN`. |
|
|
66
|
+
| `tooling release:simple` | Streamlined release using commit-and-tag-version. Flags: `--release-as`, `--first-release`, `--prerelease`, `--verbose`. |
|
|
67
|
+
| `tooling release:trigger` | Trigger a release workflow. |
|
|
68
|
+
| `tooling forgejo:create-release` | Create a Forgejo release from a tag. |
|
|
69
|
+
| `tooling changesets:merge` | Merge a changesets version PR. |
|
|
70
70
|
|
|
71
71
|
#### `release:simple`
|
|
72
72
|
|
|
@@ -147,7 +147,7 @@ To give individual packages a standalone `image:build` script for local testing:
|
|
|
147
147
|
}
|
|
148
148
|
```
|
|
149
149
|
|
|
150
|
-
**Flags:** `--package <dir>` (build a single package)
|
|
150
|
+
**Flags:** `--package <dir>` (build a single package), `--verbose`
|
|
151
151
|
|
|
152
152
|
#### `docker:publish`
|
|
153
153
|
|
|
@@ -157,7 +157,7 @@ Tags generated per package: `latest`, `vX.Y.Z`, `vX.Y`, `vX`
|
|
|
157
157
|
|
|
158
158
|
Each package is tagged independently using its own version, so packages in a monorepo can have different release cadences. Packages without a `version` field are rejected at publish time.
|
|
159
159
|
|
|
160
|
-
**Flags:** `--dry-run` (build and tag only, skip login/push/logout)
|
|
160
|
+
**Flags:** `--dry-run` (build and tag only, skip login/push/logout), `--verbose`
|
|
161
161
|
|
|
162
162
|
**Required CI variables:**
|
|
163
163
|
|
|
@@ -188,6 +188,17 @@ Available override fields:
|
|
|
188
188
|
| `projectType` | string | Auto-detected from `package.json` deps |
|
|
189
189
|
| `detectPackageTypes` | boolean | `true` |
|
|
190
190
|
|
|
191
|
+
## Debug logging
|
|
192
|
+
|
|
193
|
+
All CLI commands support a `--verbose` flag for detailed debug output. Alternatively, set `TOOLING_DEBUG=true` as an environment variable — useful in CI workflows:
|
|
194
|
+
|
|
195
|
+
```yaml
|
|
196
|
+
env:
|
|
197
|
+
TOOLING_DEBUG: "true"
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Debug output is prefixed with `[debug]` and includes exec results (exit codes, stdout/stderr), compose configuration details, container health statuses, and retry attempts.
|
|
201
|
+
|
|
191
202
|
## Library API
|
|
192
203
|
|
|
193
204
|
The `"."` export provides type-only exports for programmatic use:
|
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";
|
|
@@ -13,22 +13,6 @@ import { FatalError, TransientError, UnexpectedError } from "@bensandee/common";
|
|
|
13
13
|
import { isMap, isScalar, isSeq, parse as parse$1, parseDocument, stringify } 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
|
|
@@ -1590,7 +1577,7 @@ function getAddedDevDepNames(config) {
|
|
|
1590
1577
|
const deps = { ...ROOT_DEV_DEPS };
|
|
1591
1578
|
if (config.structure !== "monorepo") Object.assign(deps, PER_PACKAGE_DEV_DEPS);
|
|
1592
1579
|
deps["@bensandee/config"] = "0.9.1";
|
|
1593
|
-
deps["@bensandee/tooling"] = "0.
|
|
1580
|
+
deps["@bensandee/tooling"] = "0.34.0";
|
|
1594
1581
|
if (config.formatter === "oxfmt") deps["oxfmt"] = {
|
|
1595
1582
|
"@changesets/cli": "2.30.0",
|
|
1596
1583
|
"@release-it/bumper": "7.0.5",
|
|
@@ -1645,7 +1632,7 @@ async function generatePackageJson(ctx) {
|
|
|
1645
1632
|
const devDeps = { ...ROOT_DEV_DEPS };
|
|
1646
1633
|
if (!isMonorepo) Object.assign(devDeps, PER_PACKAGE_DEV_DEPS);
|
|
1647
1634
|
devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.9.1";
|
|
1648
|
-
devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.
|
|
1635
|
+
devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.34.0";
|
|
1649
1636
|
if (ctx.config.useEslintPlugin) devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "0.9.2";
|
|
1650
1637
|
if (ctx.config.formatter === "oxfmt") devDeps["oxfmt"] = {
|
|
1651
1638
|
"@changesets/cli": "2.30.0",
|
|
@@ -3321,7 +3308,7 @@ function generateMigratePrompt(results, config, detected) {
|
|
|
3321
3308
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
3322
3309
|
sections.push("# Migration Prompt");
|
|
3323
3310
|
sections.push("");
|
|
3324
|
-
sections.push(`_Generated by \`@bensandee/tooling@0.
|
|
3311
|
+
sections.push(`_Generated by \`@bensandee/tooling@0.34.0 repo:sync\` on ${timestamp}_`);
|
|
3325
3312
|
sections.push("");
|
|
3326
3313
|
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
3314
|
sections.push("");
|
|
@@ -3505,11 +3492,11 @@ function contextAsDockerReader(ctx) {
|
|
|
3505
3492
|
function logDetectionSummary(ctx) {
|
|
3506
3493
|
if (ctx.config.publishDocker) {
|
|
3507
3494
|
const dockerPackages = detectDockerPackages(contextAsDockerReader(ctx), ctx.targetDir, ctx.config.name);
|
|
3508
|
-
if (dockerPackages.length > 0) log
|
|
3495
|
+
if (dockerPackages.length > 0) log.info(`Docker images: ${dockerPackages.map((pkg) => pkg.imageName).join(", ")}`);
|
|
3509
3496
|
}
|
|
3510
3497
|
if (ctx.config.publishNpm) {
|
|
3511
3498
|
const publishable = getPublishablePackages(ctx.targetDir, ctx.config.structure, ctx.packageJson);
|
|
3512
|
-
if (publishable.length > 0) log
|
|
3499
|
+
if (publishable.length > 0) log.info(`npm packages: ${publishable.map((pkg) => pkg.name).join(", ")}`);
|
|
3513
3500
|
}
|
|
3514
3501
|
}
|
|
3515
3502
|
async function runInit(config, options = {}) {
|
|
@@ -3543,7 +3530,7 @@ async function runInit(config, options = {}) {
|
|
|
3543
3530
|
const promptPath = ".tooling-migrate.md";
|
|
3544
3531
|
ctx.write(promptPath, prompt);
|
|
3545
3532
|
if (!hasChanges && options.noPrompt) {
|
|
3546
|
-
log
|
|
3533
|
+
log.success("Repository is up to date.");
|
|
3547
3534
|
return results;
|
|
3548
3535
|
}
|
|
3549
3536
|
if (results.some((r) => r.action === "archived" && r.filePath.startsWith(".husky/"))) try {
|
|
@@ -3558,13 +3545,13 @@ async function runInit(config, options = {}) {
|
|
|
3558
3545
|
if (updated.length > 0) summaryLines.push(`Updated: ${updated.map((r) => r.filePath).join(", ")}`);
|
|
3559
3546
|
note(summaryLines.join("\n"), "Summary");
|
|
3560
3547
|
if (!options.noPrompt) {
|
|
3561
|
-
log
|
|
3562
|
-
log
|
|
3548
|
+
log.info(`Migration prompt written to ${promptPath}`);
|
|
3549
|
+
log.info("In Claude Code, run: \"Execute the steps in .tooling-migrate.md\"");
|
|
3563
3550
|
}
|
|
3564
3551
|
const bensandeeDeps = getAddedDevDepNames(config).filter((name) => name.startsWith("@bensandee/"));
|
|
3565
3552
|
const hasLockfile = ctx.exists("pnpm-lock.yaml");
|
|
3566
3553
|
if (bensandeeDeps.length > 0 && hasLockfile) {
|
|
3567
|
-
log
|
|
3554
|
+
log.info("Updating @bensandee/* packages...");
|
|
3568
3555
|
try {
|
|
3569
3556
|
execSync(`pnpm update --latest ${bensandeeDeps.join(" ")}`, {
|
|
3570
3557
|
cwd: config.targetDir,
|
|
@@ -3572,7 +3559,7 @@ async function runInit(config, options = {}) {
|
|
|
3572
3559
|
timeout: 6e4
|
|
3573
3560
|
});
|
|
3574
3561
|
} catch (_error) {
|
|
3575
|
-
log
|
|
3562
|
+
log.warn("Could not update @bensandee/* packages — run pnpm install manually");
|
|
3576
3563
|
}
|
|
3577
3564
|
}
|
|
3578
3565
|
if (hasChanges && ctx.exists("package.json")) try {
|
|
@@ -3664,22 +3651,22 @@ async function runCheck(targetDir) {
|
|
|
3664
3651
|
return true;
|
|
3665
3652
|
});
|
|
3666
3653
|
if (actionable.length === 0) {
|
|
3667
|
-
log
|
|
3654
|
+
log.success("Repository is up to date.");
|
|
3668
3655
|
return 0;
|
|
3669
3656
|
}
|
|
3670
|
-
log
|
|
3657
|
+
log.warn(`${actionable.length} file(s) would be changed by repo:sync`);
|
|
3671
3658
|
for (const r of actionable) {
|
|
3672
|
-
log
|
|
3659
|
+
log.info(` ${r.action}: ${r.filePath} — ${r.description}`);
|
|
3673
3660
|
const newContent = pendingWrites.get(r.filePath);
|
|
3674
3661
|
if (!newContent) continue;
|
|
3675
3662
|
const existingPath = path.join(targetDir, r.filePath);
|
|
3676
3663
|
const existing = existsSync(existingPath) ? readFileSync(existingPath, "utf-8") : void 0;
|
|
3677
3664
|
if (!existing) {
|
|
3678
3665
|
const lineCount = newContent.split("\n").length - 1;
|
|
3679
|
-
log
|
|
3666
|
+
log.info(` + ${lineCount} new lines`);
|
|
3680
3667
|
} else {
|
|
3681
3668
|
const diff = lineDiff(existing, newContent);
|
|
3682
|
-
for (const line of diff) log
|
|
3669
|
+
for (const line of diff) log.info(` ${line}`);
|
|
3683
3670
|
}
|
|
3684
3671
|
}
|
|
3685
3672
|
return 1;
|
|
@@ -3827,6 +3814,17 @@ function reconcileTags(expectedTags, remoteTags, stdoutTags) {
|
|
|
3827
3814
|
}
|
|
3828
3815
|
//#endregion
|
|
3829
3816
|
//#region src/release/forgejo.ts
|
|
3817
|
+
const RETRY_ATTEMPTS = 3;
|
|
3818
|
+
const RETRY_BASE_DELAY_MS = 1e3;
|
|
3819
|
+
/** Safely read response body text for inclusion in error messages. */
|
|
3820
|
+
async function responseBodyText(res) {
|
|
3821
|
+
try {
|
|
3822
|
+
const text = await res.text();
|
|
3823
|
+
return text.length > 500 ? text.slice(0, 500) + "…" : text;
|
|
3824
|
+
} catch {
|
|
3825
|
+
return "(could not read response body)";
|
|
3826
|
+
}
|
|
3827
|
+
}
|
|
3830
3828
|
const PullRequestSchema = z.array(z.object({
|
|
3831
3829
|
number: z.number(),
|
|
3832
3830
|
head: z.object({ ref: z.string() })
|
|
@@ -3840,7 +3838,10 @@ const PullRequestSchema = z.array(z.object({
|
|
|
3840
3838
|
async function findOpenPr(executor, conn, head) {
|
|
3841
3839
|
const url = `${conn.serverUrl}/api/v1/repos/${conn.repository}/pulls?state=open`;
|
|
3842
3840
|
const res = await executor.fetch(url, { headers: { Authorization: `token ${conn.token}` } });
|
|
3843
|
-
if (!res.ok)
|
|
3841
|
+
if (!res.ok) {
|
|
3842
|
+
const body = await responseBodyText(res);
|
|
3843
|
+
throw new TransientError(`Failed to list PRs: ${res.status} ${res.statusText}\n${body}`);
|
|
3844
|
+
}
|
|
3844
3845
|
const parsed = PullRequestSchema.safeParse(await res.json());
|
|
3845
3846
|
if (!parsed.success) throw new UnexpectedError(`Unexpected PR list response: ${parsed.error.message}`);
|
|
3846
3847
|
return parsed.data.find((pr) => pr.head.ref === head)?.number ?? null;
|
|
@@ -3862,7 +3863,10 @@ async function createPr(executor, conn, options) {
|
|
|
3862
3863
|
},
|
|
3863
3864
|
body: JSON.stringify(payload)
|
|
3864
3865
|
});
|
|
3865
|
-
if (!res.ok)
|
|
3866
|
+
if (!res.ok) {
|
|
3867
|
+
const body = await responseBodyText(res);
|
|
3868
|
+
throw new TransientError(`Failed to create PR: ${res.status} ${res.statusText}\n${body}`);
|
|
3869
|
+
}
|
|
3866
3870
|
}
|
|
3867
3871
|
/** Update an existing pull request's title and body. */
|
|
3868
3872
|
async function updatePr(executor, conn, prNumber, options) {
|
|
@@ -3878,7 +3882,10 @@ async function updatePr(executor, conn, prNumber, options) {
|
|
|
3878
3882
|
body: options.body
|
|
3879
3883
|
})
|
|
3880
3884
|
});
|
|
3881
|
-
if (!res.ok)
|
|
3885
|
+
if (!res.ok) {
|
|
3886
|
+
const body = await responseBodyText(res);
|
|
3887
|
+
throw new TransientError(`Failed to update PR #${String(prNumber)}: ${res.status} ${res.statusText}\n${body}`);
|
|
3888
|
+
}
|
|
3882
3889
|
}
|
|
3883
3890
|
/** Merge a pull request by number. */
|
|
3884
3891
|
async function mergePr(executor, conn, prNumber, options) {
|
|
@@ -3894,7 +3901,10 @@ async function mergePr(executor, conn, prNumber, options) {
|
|
|
3894
3901
|
delete_branch_after_merge: options?.deleteBranch ?? true
|
|
3895
3902
|
})
|
|
3896
3903
|
});
|
|
3897
|
-
if (!res.ok)
|
|
3904
|
+
if (!res.ok) {
|
|
3905
|
+
const body = await responseBodyText(res);
|
|
3906
|
+
throw new TransientError(`Failed to merge PR #${String(prNumber)}: ${res.status} ${res.statusText}\n${body}`);
|
|
3907
|
+
}
|
|
3898
3908
|
}
|
|
3899
3909
|
/** Check whether a Forgejo release already exists for a given tag. */
|
|
3900
3910
|
async function findRelease(executor, conn, tag) {
|
|
@@ -3903,7 +3913,8 @@ async function findRelease(executor, conn, tag) {
|
|
|
3903
3913
|
const res = await executor.fetch(url, { headers: { Authorization: `token ${conn.token}` } });
|
|
3904
3914
|
if (res.status === 200) return true;
|
|
3905
3915
|
if (res.status === 404) return false;
|
|
3906
|
-
|
|
3916
|
+
const body = await responseBodyText(res);
|
|
3917
|
+
throw new TransientError(`Failed to check release for ${tag}: ${res.status} ${res.statusText}\n${body}`);
|
|
3907
3918
|
}
|
|
3908
3919
|
/** Create a Forgejo release for a given tag. */
|
|
3909
3920
|
async function createRelease(executor, conn, tag) {
|
|
@@ -3920,21 +3931,35 @@ async function createRelease(executor, conn, tag) {
|
|
|
3920
3931
|
body: `Published ${tag}`
|
|
3921
3932
|
})
|
|
3922
3933
|
});
|
|
3923
|
-
if (!res.ok)
|
|
3934
|
+
if (!res.ok) {
|
|
3935
|
+
const body = await responseBodyText(res);
|
|
3936
|
+
throw new TransientError(`Failed to create release for ${tag}: ${res.status} ${res.statusText}\n${body}`);
|
|
3937
|
+
}
|
|
3924
3938
|
}
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3939
|
+
/**
|
|
3940
|
+
* Ensure a Forgejo release exists for a tag, creating it if necessary.
|
|
3941
|
+
*
|
|
3942
|
+
* Handles two edge cases:
|
|
3943
|
+
* - The release already exists before we try (skips creation)
|
|
3944
|
+
* - Forgejo auto-creates a release when a tag is pushed, causing a 500 race
|
|
3945
|
+
* condition (detected by re-checking after failure)
|
|
3946
|
+
*
|
|
3947
|
+
* Retries on transient errors with exponential backoff.
|
|
3948
|
+
* Returns "created" | "exists" | "race" indicating what happened.
|
|
3949
|
+
*/
|
|
3950
|
+
async function ensureRelease(executor, conn, tag) {
|
|
3951
|
+
if (await findRelease(executor, conn, tag)) return "exists";
|
|
3952
|
+
for (let attempt = 1; attempt <= RETRY_ATTEMPTS; attempt++) try {
|
|
3953
|
+
await createRelease(executor, conn, tag);
|
|
3954
|
+
return "created";
|
|
3955
|
+
} catch (error) {
|
|
3956
|
+
if (await findRelease(executor, conn, tag)) return "race";
|
|
3957
|
+
if (attempt >= RETRY_ATTEMPTS) throw error;
|
|
3958
|
+
log.warn(`Release creation attempt ${String(attempt)}/${String(RETRY_ATTEMPTS)} failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
3959
|
+
const delay = RETRY_BASE_DELAY_MS * 2 ** (attempt - 1);
|
|
3960
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
3961
|
+
}
|
|
3962
|
+
throw new TransientError(`Failed to create release for ${tag} after ${String(RETRY_ATTEMPTS)} attempts`);
|
|
3938
3963
|
}
|
|
3939
3964
|
//#endregion
|
|
3940
3965
|
//#region src/release/version.ts
|
|
@@ -4006,7 +4031,7 @@ function buildPrContent(executor, cwd, packagesBefore) {
|
|
|
4006
4031
|
}
|
|
4007
4032
|
/** Mode 1: version packages and create/update a PR. */
|
|
4008
4033
|
async function runVersionMode(executor, config) {
|
|
4009
|
-
log
|
|
4034
|
+
log.info("Changesets detected — versioning packages");
|
|
4010
4035
|
const packagesBefore = executor.listWorkspacePackages(config.cwd);
|
|
4011
4036
|
debug(config, `Packages before versioning: ${packagesBefore.map((pkg) => `${pkg.name}@${pkg.version}`).join(", ") || "(none)"}`);
|
|
4012
4037
|
const changesetConfigPath = path.join(config.cwd, ".changeset", "config.json");
|
|
@@ -4032,19 +4057,19 @@ async function runVersionMode(executor, config) {
|
|
|
4032
4057
|
const addResult = executor.exec("git add -A", { cwd: config.cwd });
|
|
4033
4058
|
if (addResult.exitCode !== 0) throw new FatalError(`git add failed: ${addResult.stderr || addResult.stdout}`);
|
|
4034
4059
|
const remainingChangesets = executor.listChangesetFiles(config.cwd);
|
|
4035
|
-
if (remainingChangesets.length > 0) log
|
|
4060
|
+
if (remainingChangesets.length > 0) log.warn(`Changeset files still present after versioning: ${remainingChangesets.join(", ")}`);
|
|
4036
4061
|
debug(config, `Changeset files after versioning: ${remainingChangesets.length > 0 ? remainingChangesets.join(", ") : "(none — all consumed)"}`);
|
|
4037
4062
|
const commitResult = executor.exec("git commit -m \"chore: version packages\"", { cwd: config.cwd });
|
|
4038
4063
|
debugExec(config, "git commit", commitResult);
|
|
4039
4064
|
if (commitResult.exitCode !== 0) {
|
|
4040
|
-
log
|
|
4065
|
+
log.info("Nothing to commit after versioning");
|
|
4041
4066
|
return {
|
|
4042
4067
|
mode: "version",
|
|
4043
4068
|
pr: "none"
|
|
4044
4069
|
};
|
|
4045
4070
|
}
|
|
4046
4071
|
if (config.dryRun) {
|
|
4047
|
-
log
|
|
4072
|
+
log.info("[dry-run] Would push and create/update PR");
|
|
4048
4073
|
return {
|
|
4049
4074
|
mode: "version",
|
|
4050
4075
|
pr: "none"
|
|
@@ -4067,7 +4092,7 @@ async function runVersionMode(executor, config) {
|
|
|
4067
4092
|
base: "main",
|
|
4068
4093
|
body
|
|
4069
4094
|
});
|
|
4070
|
-
log
|
|
4095
|
+
log.info("Created version PR");
|
|
4071
4096
|
return {
|
|
4072
4097
|
mode: "version",
|
|
4073
4098
|
pr: "created"
|
|
@@ -4077,7 +4102,7 @@ async function runVersionMode(executor, config) {
|
|
|
4077
4102
|
title,
|
|
4078
4103
|
body
|
|
4079
4104
|
});
|
|
4080
|
-
log
|
|
4105
|
+
log.info(`Updated version PR #${String(existingPr)}`);
|
|
4081
4106
|
return {
|
|
4082
4107
|
mode: "version",
|
|
4083
4108
|
pr: "updated"
|
|
@@ -4085,24 +4110,9 @@ async function runVersionMode(executor, config) {
|
|
|
4085
4110
|
}
|
|
4086
4111
|
//#endregion
|
|
4087
4112
|
//#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
4113
|
/** Mode 2: publish to npm, push tags, and create Forgejo releases. */
|
|
4104
4114
|
async function runPublishMode(executor, config) {
|
|
4105
|
-
log
|
|
4115
|
+
log.info("No changesets — publishing packages");
|
|
4106
4116
|
const publishResult = executor.exec("pnpm changeset publish", { cwd: config.cwd });
|
|
4107
4117
|
debugExec(config, "pnpm changeset publish", publishResult);
|
|
4108
4118
|
if (publishResult.exitCode !== 0) throw new FatalError(`pnpm changeset publish failed (exit code ${String(publishResult.exitCode)}):\n${publishResult.stderr}`);
|
|
@@ -4117,11 +4127,11 @@ async function runPublishMode(executor, config) {
|
|
|
4117
4127
|
debug(config, `Reconciled tags to push: ${tagsToPush.length > 0 ? tagsToPush.join(", ") : "(none)"}`);
|
|
4118
4128
|
if (config.dryRun) {
|
|
4119
4129
|
if (tagsToPush.length === 0) {
|
|
4120
|
-
log
|
|
4130
|
+
log.info("No packages were published");
|
|
4121
4131
|
return { mode: "none" };
|
|
4122
4132
|
}
|
|
4123
|
-
log
|
|
4124
|
-
log
|
|
4133
|
+
log.info(`Tags to process: ${tagsToPush.join(", ")}`);
|
|
4134
|
+
log.info("[dry-run] Would push tags and create releases");
|
|
4125
4135
|
return {
|
|
4126
4136
|
mode: "publish",
|
|
4127
4137
|
tags: tagsToPush
|
|
@@ -4137,10 +4147,10 @@ async function runPublishMode(executor, config) {
|
|
|
4137
4147
|
for (const tag of remoteExpectedTags) if (!await findRelease(executor, conn, tag)) tagsWithMissingReleases.push(tag);
|
|
4138
4148
|
const allTags = [...tagsToPush, ...tagsWithMissingReleases];
|
|
4139
4149
|
if (allTags.length === 0) {
|
|
4140
|
-
log
|
|
4150
|
+
log.info("No packages were published");
|
|
4141
4151
|
return { mode: "none" };
|
|
4142
4152
|
}
|
|
4143
|
-
log
|
|
4153
|
+
log.info(`Tags to process: ${allTags.join(", ")}`);
|
|
4144
4154
|
const errors = [];
|
|
4145
4155
|
for (const tag of allTags) try {
|
|
4146
4156
|
if (!remoteSet.has(tag)) {
|
|
@@ -4151,24 +4161,14 @@ async function runPublishMode(executor, config) {
|
|
|
4151
4161
|
const pushTagResult = executor.exec(`git push origin refs/tags/${tag}`, { cwd: config.cwd });
|
|
4152
4162
|
if (pushTagResult.exitCode !== 0) throw new FatalError(`Failed to push tag ${tag}: ${pushTagResult.stderr || pushTagResult.stdout}`);
|
|
4153
4163
|
}
|
|
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
|
-
}
|
|
4164
|
+
if (await ensureRelease(executor, conn, tag) === "exists") log.warn(`Release for ${tag} already exists — skipping`);
|
|
4165
|
+
else log.info(`Created release for ${tag}`);
|
|
4166
4166
|
} catch (error) {
|
|
4167
4167
|
errors.push({
|
|
4168
4168
|
tag,
|
|
4169
4169
|
error
|
|
4170
4170
|
});
|
|
4171
|
-
log
|
|
4171
|
+
log.warn(`Failed to process ${tag}: ${error instanceof Error ? error.message : String(error)}`);
|
|
4172
4172
|
}
|
|
4173
4173
|
if (errors.length > 0) throw new TransientError(`Failed to create releases for: ${errors.map((e) => e.tag).join(", ")}`);
|
|
4174
4174
|
return {
|
|
@@ -4262,6 +4262,14 @@ function configureGitAuth(executor, conn, cwd) {
|
|
|
4262
4262
|
const authUrl = `https://x-access-token:${conn.token}@${host}/${conn.repository}`;
|
|
4263
4263
|
executor.exec(`git remote set-url origin ${authUrl}`, { cwd });
|
|
4264
4264
|
}
|
|
4265
|
+
/** Configure git user.name and user.email for CI bot commits. */
|
|
4266
|
+
function configureGitIdentity(executor, platform, cwd) {
|
|
4267
|
+
const isGitHub = platform === "github";
|
|
4268
|
+
const name = isGitHub ? "github-actions[bot]" : "forgejo-actions[bot]";
|
|
4269
|
+
const email = isGitHub ? "github-actions[bot]@users.noreply.github.com" : "forgejo-actions[bot]@noreply.localhost";
|
|
4270
|
+
executor.exec(`git config user.name "${name}"`, { cwd });
|
|
4271
|
+
executor.exec(`git config user.email "${email}"`, { cwd });
|
|
4272
|
+
}
|
|
4265
4273
|
//#endregion
|
|
4266
4274
|
//#region src/commands/release-changesets.ts
|
|
4267
4275
|
const releaseForgejoCommand = defineCommand({
|
|
@@ -4276,13 +4284,13 @@ const releaseForgejoCommand = defineCommand({
|
|
|
4276
4284
|
},
|
|
4277
4285
|
verbose: {
|
|
4278
4286
|
type: "boolean",
|
|
4279
|
-
description: "Enable detailed debug logging (also enabled by
|
|
4287
|
+
description: "Enable detailed debug logging (also enabled by TOOLING_DEBUG env var)"
|
|
4280
4288
|
}
|
|
4281
4289
|
},
|
|
4282
4290
|
async run({ args }) {
|
|
4283
4291
|
if ((await runRelease(buildReleaseConfig({
|
|
4284
4292
|
dryRun: args["dry-run"] === true,
|
|
4285
|
-
verbose: args.verbose === true ||
|
|
4293
|
+
verbose: args.verbose === true || isEnvVerbose()
|
|
4286
4294
|
}), createRealExecutor())).mode === "none") process.exitCode = 0;
|
|
4287
4295
|
}
|
|
4288
4296
|
});
|
|
@@ -4310,8 +4318,7 @@ async function runRelease(config, executor) {
|
|
|
4310
4318
|
debug(config, `Skipping release on non-main branch: ${branch}`);
|
|
4311
4319
|
return { mode: "none" };
|
|
4312
4320
|
}
|
|
4313
|
-
executor
|
|
4314
|
-
executor.exec("git config user.email \"forgejo-actions[bot]@noreply.localhost\"", { cwd: config.cwd });
|
|
4321
|
+
configureGitIdentity(executor, "forgejo", config.cwd);
|
|
4315
4322
|
configureGitAuth(executor, config, config.cwd);
|
|
4316
4323
|
const changesetFiles = executor.listChangesetFiles(config.cwd);
|
|
4317
4324
|
debug(config, `Changeset files found: ${changesetFiles.length > 0 ? changesetFiles.join(", ") : "(none)"}`);
|
|
@@ -4369,7 +4376,7 @@ async function triggerForgejo(conn, ref, inputs) {
|
|
|
4369
4376
|
body: JSON.stringify(body)
|
|
4370
4377
|
});
|
|
4371
4378
|
if (!res.ok) throw new FatalError(`Failed to trigger Forgejo workflow: ${res.status} ${res.statusText}`);
|
|
4372
|
-
log
|
|
4379
|
+
log.info(`Triggered release workflow on Forgejo (ref: ${ref})`);
|
|
4373
4380
|
}
|
|
4374
4381
|
function triggerGitHub(ref, inputs) {
|
|
4375
4382
|
const executor = createRealExecutor();
|
|
@@ -4377,7 +4384,7 @@ function triggerGitHub(ref, inputs) {
|
|
|
4377
4384
|
const cmd = `gh workflow run release.yml --ref ${ref}${inputFlags ? ` ${inputFlags}` : ""}`;
|
|
4378
4385
|
const result = executor.exec(cmd, { cwd: process.cwd() });
|
|
4379
4386
|
if (result.exitCode !== 0) throw new FatalError(`Failed to trigger GitHub workflow: ${result.stderr || result.stdout || "unknown error"}`);
|
|
4380
|
-
log
|
|
4387
|
+
log.info(`Triggered release workflow on GitHub (ref: ${ref})`);
|
|
4381
4388
|
}
|
|
4382
4389
|
//#endregion
|
|
4383
4390
|
//#region src/commands/forgejo-create-release.ts
|
|
@@ -4397,11 +4404,11 @@ const createForgejoReleaseCommand = defineCommand({
|
|
|
4397
4404
|
const executor = createRealExecutor();
|
|
4398
4405
|
const conn = resolved.conn;
|
|
4399
4406
|
if (await findRelease(executor, conn, args.tag)) {
|
|
4400
|
-
log
|
|
4407
|
+
log.info(`Release for ${args.tag} already exists — skipping`);
|
|
4401
4408
|
return;
|
|
4402
4409
|
}
|
|
4403
4410
|
await createRelease(executor, conn, args.tag);
|
|
4404
|
-
log
|
|
4411
|
+
log.info(`Created Forgejo release for ${args.tag}`);
|
|
4405
4412
|
}
|
|
4406
4413
|
});
|
|
4407
4414
|
//#endregion
|
|
@@ -4428,26 +4435,26 @@ async function mergeForgejo(conn, dryRun) {
|
|
|
4428
4435
|
const prNumber = await findOpenPr(executor, conn, HEAD_BRANCH);
|
|
4429
4436
|
if (prNumber === null) throw new FatalError(`No open PR found for branch ${HEAD_BRANCH}`);
|
|
4430
4437
|
if (dryRun) {
|
|
4431
|
-
log
|
|
4438
|
+
log.info(`[dry-run] Would merge PR #${String(prNumber)} and delete branch ${HEAD_BRANCH}`);
|
|
4432
4439
|
return;
|
|
4433
4440
|
}
|
|
4434
4441
|
await mergePr(executor, conn, prNumber, {
|
|
4435
4442
|
method: "merge",
|
|
4436
4443
|
deleteBranch: true
|
|
4437
4444
|
});
|
|
4438
|
-
log
|
|
4445
|
+
log.info(`Merged PR #${String(prNumber)} and deleted branch ${HEAD_BRANCH}`);
|
|
4439
4446
|
}
|
|
4440
4447
|
function mergeGitHub(dryRun) {
|
|
4441
4448
|
const executor = createRealExecutor();
|
|
4442
4449
|
if (dryRun) {
|
|
4443
4450
|
const prNum = executor.exec(`gh pr view ${HEAD_BRANCH} --json number --jq .number`, { cwd: process.cwd() }).stdout.trim();
|
|
4444
4451
|
if (!prNum) throw new FatalError(`No open PR found for branch ${HEAD_BRANCH}`);
|
|
4445
|
-
log
|
|
4452
|
+
log.info(`[dry-run] Would merge PR #${prNum} and delete branch ${HEAD_BRANCH}`);
|
|
4446
4453
|
return;
|
|
4447
4454
|
}
|
|
4448
4455
|
const result = executor.exec(`gh pr merge ${HEAD_BRANCH} --merge --delete-branch`, { cwd: process.cwd() });
|
|
4449
4456
|
if (result.exitCode !== 0) throw new FatalError(`Failed to merge PR: ${result.stderr || result.stdout || "unknown error"}`);
|
|
4450
|
-
log
|
|
4457
|
+
log.info(`Merged changesets PR and deleted branch ${HEAD_BRANCH}`);
|
|
4451
4458
|
}
|
|
4452
4459
|
//#endregion
|
|
4453
4460
|
//#region src/release/simple.ts
|
|
@@ -4477,20 +4484,15 @@ function readVersion(executor, cwd) {
|
|
|
4477
4484
|
if (!pkg?.version) throw new FatalError("No version field found in package.json");
|
|
4478
4485
|
return pkg.version;
|
|
4479
4486
|
}
|
|
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}>`);
|
|
4487
|
+
/** Resolve the platform type string for git identity configuration. */
|
|
4488
|
+
function platformType(config) {
|
|
4489
|
+
return config.platform?.type === "github" ? "github" : "forgejo";
|
|
4488
4490
|
}
|
|
4489
4491
|
/** Run the full commit-and-tag-version release flow. */
|
|
4490
4492
|
async function runSimpleRelease(executor, config) {
|
|
4491
|
-
configureGitIdentity(executor, config);
|
|
4493
|
+
configureGitIdentity(executor, platformType(config), config.cwd);
|
|
4492
4494
|
const command = buildCommand(config);
|
|
4493
|
-
log
|
|
4495
|
+
log.info(`Running: ${command}`);
|
|
4494
4496
|
const versionResult = executor.exec(command, { cwd: config.cwd });
|
|
4495
4497
|
debugExec(config, "commit-and-tag-version", versionResult);
|
|
4496
4498
|
if (versionResult.exitCode !== 0) throw new FatalError(`commit-and-tag-version failed (exit code ${String(versionResult.exitCode)}):\n${versionResult.stderr || versionResult.stdout}`);
|
|
@@ -4500,12 +4502,12 @@ async function runSimpleRelease(executor, config) {
|
|
|
4500
4502
|
debugExec(config, "git describe", tagResult);
|
|
4501
4503
|
const tag = tagResult.stdout.trim();
|
|
4502
4504
|
if (!tag) throw new FatalError("Could not determine the new tag from git describe");
|
|
4503
|
-
log
|
|
4505
|
+
log.info(`Version ${version} tagged as ${tag}`);
|
|
4504
4506
|
if (config.dryRun) {
|
|
4505
4507
|
const slidingTags = config.noSlidingTags ? [] : computeSlidingTags(version);
|
|
4506
|
-
log
|
|
4507
|
-
if (slidingTags.length > 0) log
|
|
4508
|
-
if (!config.noRelease && config.platform) log
|
|
4508
|
+
log.info(`[dry-run] Would push to origin with --follow-tags`);
|
|
4509
|
+
if (slidingTags.length > 0) log.info(`[dry-run] Would create sliding tags: ${slidingTags.join(", ")}`);
|
|
4510
|
+
if (!config.noRelease && config.platform) log.info(`[dry-run] Would create ${config.platform.type} release for ${tag}`);
|
|
4509
4511
|
return {
|
|
4510
4512
|
version,
|
|
4511
4513
|
tag,
|
|
@@ -4526,7 +4528,7 @@ async function runSimpleRelease(executor, config) {
|
|
|
4526
4528
|
debugExec(config, "git push", pushResult);
|
|
4527
4529
|
if (pushResult.exitCode !== 0) throw new FatalError(`git push failed (exit code ${String(pushResult.exitCode)}):\n${pushResult.stderr || pushResult.stdout}`);
|
|
4528
4530
|
pushed = true;
|
|
4529
|
-
log
|
|
4531
|
+
log.info("Pushed to origin");
|
|
4530
4532
|
}
|
|
4531
4533
|
let slidingTags = [];
|
|
4532
4534
|
if (!config.noSlidingTags && pushed) {
|
|
@@ -4537,8 +4539,8 @@ async function runSimpleRelease(executor, config) {
|
|
|
4537
4539
|
}
|
|
4538
4540
|
const forcePushResult = executor.exec(`git push origin ${slidingTags.join(" ")} --force`, { cwd: config.cwd });
|
|
4539
4541
|
debugExec(config, "force-push sliding tags", forcePushResult);
|
|
4540
|
-
if (forcePushResult.exitCode !== 0) log
|
|
4541
|
-
else log
|
|
4542
|
+
if (forcePushResult.exitCode !== 0) log.warn(`Warning: Failed to push sliding tags: ${forcePushResult.stderr || forcePushResult.stdout}`);
|
|
4543
|
+
else log.info(`Created sliding tags: ${slidingTags.join(", ")}`);
|
|
4542
4544
|
}
|
|
4543
4545
|
let releaseCreated = false;
|
|
4544
4546
|
if (!config.noRelease && config.platform) releaseCreated = await createPlatformRelease(executor, config, tag);
|
|
@@ -4553,21 +4555,20 @@ async function runSimpleRelease(executor, config) {
|
|
|
4553
4555
|
async function createPlatformRelease(executor, config, tag) {
|
|
4554
4556
|
if (!config.platform) return false;
|
|
4555
4557
|
if (config.platform.type === "forgejo") {
|
|
4556
|
-
if (await
|
|
4558
|
+
if (await ensureRelease(executor, config.platform.conn, tag) === "exists") {
|
|
4557
4559
|
debug(config, `Release for ${tag} already exists, skipping`);
|
|
4558
4560
|
return false;
|
|
4559
4561
|
}
|
|
4560
|
-
|
|
4561
|
-
log$2.info(`Created Forgejo release for ${tag}`);
|
|
4562
|
+
log.info(`Created Forgejo release for ${tag}`);
|
|
4562
4563
|
return true;
|
|
4563
4564
|
}
|
|
4564
4565
|
const ghResult = executor.exec(`gh release create ${tag} --generate-notes`, { cwd: config.cwd });
|
|
4565
4566
|
debugExec(config, "gh release create", ghResult);
|
|
4566
4567
|
if (ghResult.exitCode !== 0) {
|
|
4567
|
-
log
|
|
4568
|
+
log.warn(`Warning: Failed to create GitHub release: ${ghResult.stderr || ghResult.stdout}`);
|
|
4568
4569
|
return false;
|
|
4569
4570
|
}
|
|
4570
|
-
log
|
|
4571
|
+
log.info(`Created GitHub release for ${tag}`);
|
|
4571
4572
|
return true;
|
|
4572
4573
|
}
|
|
4573
4574
|
//#endregion
|
|
@@ -4584,7 +4585,7 @@ const releaseSimpleCommand = defineCommand({
|
|
|
4584
4585
|
},
|
|
4585
4586
|
verbose: {
|
|
4586
4587
|
type: "boolean",
|
|
4587
|
-
description: "Enable detailed debug logging (also enabled by
|
|
4588
|
+
description: "Enable detailed debug logging (also enabled by TOOLING_DEBUG env var)"
|
|
4588
4589
|
},
|
|
4589
4590
|
"no-push": {
|
|
4590
4591
|
type: "boolean",
|
|
@@ -4613,7 +4614,7 @@ const releaseSimpleCommand = defineCommand({
|
|
|
4613
4614
|
},
|
|
4614
4615
|
async run({ args }) {
|
|
4615
4616
|
const cwd = process.cwd();
|
|
4616
|
-
const verbose = args.verbose === true ||
|
|
4617
|
+
const verbose = args.verbose === true || isEnvVerbose();
|
|
4617
4618
|
const noRelease = args["no-release"] === true;
|
|
4618
4619
|
let platform;
|
|
4619
4620
|
if (!noRelease) {
|
|
@@ -4697,12 +4698,12 @@ const ciReporter = {
|
|
|
4697
4698
|
const localReporter = {
|
|
4698
4699
|
groupStart: (_name) => {},
|
|
4699
4700
|
groupEnd: () => {},
|
|
4700
|
-
passed: (name) => log
|
|
4701
|
-
failed: (name) => log
|
|
4702
|
-
undefinedCheck: (name) => log
|
|
4703
|
-
skippedNotDefined: (names) => log
|
|
4704
|
-
allPassed: () => log
|
|
4705
|
-
anyFailed: (names) => log
|
|
4701
|
+
passed: (name) => log.success(name),
|
|
4702
|
+
failed: (name) => log.error(`${name} failed`),
|
|
4703
|
+
undefinedCheck: (name) => log.error(`${name} not defined in package.json`),
|
|
4704
|
+
skippedNotDefined: (names) => log.info(`Skipped (not defined): ${names.join(", ")}`),
|
|
4705
|
+
allPassed: () => log.success("All checks passed"),
|
|
4706
|
+
anyFailed: (names) => log.error(`Failed checks: ${names.join(", ")}`)
|
|
4706
4707
|
};
|
|
4707
4708
|
function runRunChecks(targetDir, options = {}) {
|
|
4708
4709
|
const exec = options.execCommand ?? defaultExecCommand;
|
|
@@ -4712,6 +4713,7 @@ function runRunChecks(targetDir, options = {}) {
|
|
|
4712
4713
|
const isCI = Boolean(process.env["CI"]);
|
|
4713
4714
|
const failFast = options.failFast ?? !isCI;
|
|
4714
4715
|
const reporter = isCI ? ciReporter : localReporter;
|
|
4716
|
+
const vc = { verbose: options.verbose ?? false };
|
|
4715
4717
|
const definedScripts = getScripts(targetDir);
|
|
4716
4718
|
const addedNames = new Set(add);
|
|
4717
4719
|
const allChecks = [...CHECKS, ...add.map((name) => ({ name }))];
|
|
@@ -4727,11 +4729,13 @@ function runRunChecks(targetDir, options = {}) {
|
|
|
4727
4729
|
continue;
|
|
4728
4730
|
}
|
|
4729
4731
|
const cmd = check.args ? `pnpm run ${check.name} ${check.args}` : `pnpm run ${check.name}`;
|
|
4732
|
+
debug(vc, `Running: ${cmd} (in ${targetDir})`);
|
|
4730
4733
|
reporter.groupStart(check.name);
|
|
4731
4734
|
const start = Date.now();
|
|
4732
4735
|
const exitCode = exec(cmd, targetDir);
|
|
4733
4736
|
const elapsed = ((Date.now() - start) / 1e3).toFixed(1);
|
|
4734
4737
|
reporter.groupEnd();
|
|
4738
|
+
debug(vc, `${check.name}: exit code ${String(exitCode)}, ${elapsed}s`);
|
|
4735
4739
|
if (exitCode === 0) reporter.passed(check.name, elapsed);
|
|
4736
4740
|
else {
|
|
4737
4741
|
reporter.failed(check.name, elapsed);
|
|
@@ -4772,13 +4776,19 @@ const runChecksCommand = defineCommand({
|
|
|
4772
4776
|
type: "boolean",
|
|
4773
4777
|
description: "Stop on first failure (default: true in dev, false in CI)",
|
|
4774
4778
|
required: false
|
|
4779
|
+
},
|
|
4780
|
+
verbose: {
|
|
4781
|
+
type: "boolean",
|
|
4782
|
+
description: "Emit detailed debug logging",
|
|
4783
|
+
required: false
|
|
4775
4784
|
}
|
|
4776
4785
|
},
|
|
4777
4786
|
run({ args }) {
|
|
4778
4787
|
const exitCode = runRunChecks(path.resolve(args.dir ?? "."), {
|
|
4779
4788
|
skip: args.skip ? new Set(args.skip.split(",").map((s) => s.trim())) : void 0,
|
|
4780
4789
|
add: args.add ? args.add.split(",").map((s) => s.trim()) : void 0,
|
|
4781
|
-
failFast: args["fail-fast"] ? true : void 0
|
|
4790
|
+
failFast: args["fail-fast"] ? true : void 0,
|
|
4791
|
+
verbose: args.verbose === true || isEnvVerbose()
|
|
4782
4792
|
});
|
|
4783
4793
|
process.exitCode = exitCode;
|
|
4784
4794
|
}
|
|
@@ -4795,10 +4805,16 @@ const publishDockerCommand = defineCommand({
|
|
|
4795
4805
|
name: "docker:publish",
|
|
4796
4806
|
description: "Build, tag, and push Docker images for packages with an image:build script"
|
|
4797
4807
|
},
|
|
4798
|
-
args: {
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
|
|
4808
|
+
args: {
|
|
4809
|
+
"dry-run": {
|
|
4810
|
+
type: "boolean",
|
|
4811
|
+
description: "Build and tag images but skip login, push, and logout"
|
|
4812
|
+
},
|
|
4813
|
+
verbose: {
|
|
4814
|
+
type: "boolean",
|
|
4815
|
+
description: "Emit detailed debug logging"
|
|
4816
|
+
}
|
|
4817
|
+
},
|
|
4802
4818
|
async run({ args }) {
|
|
4803
4819
|
const config = {
|
|
4804
4820
|
cwd: process.cwd(),
|
|
@@ -4806,7 +4822,8 @@ const publishDockerCommand = defineCommand({
|
|
|
4806
4822
|
registryNamespace: requireEnv("DOCKER_REGISTRY_NAMESPACE"),
|
|
4807
4823
|
username: requireEnv("DOCKER_USERNAME"),
|
|
4808
4824
|
password: requireEnv("DOCKER_PASSWORD"),
|
|
4809
|
-
dryRun: args["dry-run"] === true
|
|
4825
|
+
dryRun: args["dry-run"] === true,
|
|
4826
|
+
verbose: args.verbose === true || isEnvVerbose()
|
|
4810
4827
|
};
|
|
4811
4828
|
runDockerPublish(createRealExecutor(), config);
|
|
4812
4829
|
}
|
|
@@ -4839,6 +4856,10 @@ const dockerBuildCommand = defineCommand({
|
|
|
4839
4856
|
type: "string",
|
|
4840
4857
|
description: "Build a single package by directory path (e.g. packages/server). Useful as an image:build script."
|
|
4841
4858
|
},
|
|
4859
|
+
verbose: {
|
|
4860
|
+
type: "boolean",
|
|
4861
|
+
description: "Emit detailed debug logging"
|
|
4862
|
+
},
|
|
4842
4863
|
_: {
|
|
4843
4864
|
type: "positional",
|
|
4844
4865
|
required: false,
|
|
@@ -4849,6 +4870,7 @@ const dockerBuildCommand = defineCommand({
|
|
|
4849
4870
|
const executor = createRealExecutor();
|
|
4850
4871
|
const rawExtra = args._ ?? [];
|
|
4851
4872
|
const extraArgs = Array.isArray(rawExtra) ? rawExtra.map(String) : [String(rawExtra)];
|
|
4873
|
+
const verbose = args.verbose === true || isEnvVerbose();
|
|
4852
4874
|
let cwd = process.cwd();
|
|
4853
4875
|
let packageDir = args.package;
|
|
4854
4876
|
if (!packageDir) {
|
|
@@ -4859,7 +4881,8 @@ const dockerBuildCommand = defineCommand({
|
|
|
4859
4881
|
runDockerBuild(executor, {
|
|
4860
4882
|
cwd,
|
|
4861
4883
|
packageDir,
|
|
4862
|
-
extraArgs: extraArgs.filter((a) => a.length > 0)
|
|
4884
|
+
extraArgs: extraArgs.filter((a) => a.length > 0),
|
|
4885
|
+
verbose
|
|
4863
4886
|
});
|
|
4864
4887
|
}
|
|
4865
4888
|
});
|
|
@@ -5076,12 +5099,6 @@ function writeTempOverlay(content) {
|
|
|
5076
5099
|
writeFileSync(filePath, content, "utf-8");
|
|
5077
5100
|
return filePath;
|
|
5078
5101
|
}
|
|
5079
|
-
function log(message) {
|
|
5080
|
-
console.log(message);
|
|
5081
|
-
}
|
|
5082
|
-
function warn(message) {
|
|
5083
|
-
console.warn(message);
|
|
5084
|
-
}
|
|
5085
5102
|
const dockerCheckCommand = defineCommand({
|
|
5086
5103
|
meta: {
|
|
5087
5104
|
name: "docker:check",
|
|
@@ -5095,12 +5112,16 @@ const dockerCheckCommand = defineCommand({
|
|
|
5095
5112
|
"poll-interval": {
|
|
5096
5113
|
type: "string",
|
|
5097
5114
|
description: "Interval between polling attempts, in ms (default: 5000)"
|
|
5115
|
+
},
|
|
5116
|
+
verbose: {
|
|
5117
|
+
type: "boolean",
|
|
5118
|
+
description: "Emit detailed debug logging"
|
|
5098
5119
|
}
|
|
5099
5120
|
},
|
|
5100
5121
|
async run({ args }) {
|
|
5101
5122
|
const cwd = process.cwd();
|
|
5102
5123
|
if (loadToolingConfig(cwd)?.dockerCheck === false) {
|
|
5103
|
-
log("Docker check is disabled in .tooling.json");
|
|
5124
|
+
log.info("Docker check is disabled in .tooling.json");
|
|
5104
5125
|
return;
|
|
5105
5126
|
}
|
|
5106
5127
|
const defaults = computeCheckDefaults(cwd);
|
|
@@ -5108,8 +5129,8 @@ const dockerCheckCommand = defineCommand({
|
|
|
5108
5129
|
if (!defaults.checkOverlay) {
|
|
5109
5130
|
const composeCwd = defaults.composeCwd ?? cwd;
|
|
5110
5131
|
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.");
|
|
5132
|
+
log.warn(`Compose files found but no check overlay. Create ${path.relative(cwd, path.join(composeCwd, expectedOverlay))} to enable docker:check.`);
|
|
5133
|
+
log.warn("To suppress this warning, set \"dockerCheck\": false in .tooling.json.");
|
|
5113
5134
|
return;
|
|
5114
5135
|
}
|
|
5115
5136
|
if (!defaults.services || defaults.services.length === 0) throw new FatalError("No services found in compose files.");
|
|
@@ -5122,7 +5143,7 @@ const dockerCheckCommand = defineCommand({
|
|
|
5122
5143
|
if (rootPkg?.name) {
|
|
5123
5144
|
const dockerPackages = detectDockerPackages(fileReader, cwd, rootPkg.name);
|
|
5124
5145
|
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.`);
|
|
5146
|
+
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
5147
|
}
|
|
5127
5148
|
}
|
|
5128
5149
|
const tempOverlayPath = writeTempOverlay(generateCheckOverlay(services));
|
|
@@ -5143,7 +5164,8 @@ const dockerCheckCommand = defineCommand({
|
|
|
5143
5164
|
buildCwd: defaults.buildCwd,
|
|
5144
5165
|
healthChecks: defaults.healthChecks ? toHttpHealthChecks(defaults.healthChecks) : [],
|
|
5145
5166
|
timeoutMs: args.timeout ? Number.parseInt(args.timeout, 10) : defaults.timeoutMs,
|
|
5146
|
-
pollIntervalMs: args["poll-interval"] ? Number.parseInt(args["poll-interval"], 10) : defaults.pollIntervalMs
|
|
5167
|
+
pollIntervalMs: args["poll-interval"] ? Number.parseInt(args["poll-interval"], 10) : defaults.pollIntervalMs,
|
|
5168
|
+
verbose: args.verbose === true || isEnvVerbose()
|
|
5147
5169
|
};
|
|
5148
5170
|
const result = await runDockerCheck(createRealExecutor$1(), config);
|
|
5149
5171
|
if (!result.success) throw new FatalError(`Check failed (${result.reason}): ${result.message}`);
|
|
@@ -5159,7 +5181,7 @@ const dockerCheckCommand = defineCommand({
|
|
|
5159
5181
|
const main = defineCommand({
|
|
5160
5182
|
meta: {
|
|
5161
5183
|
name: "bst",
|
|
5162
|
-
version: "0.
|
|
5184
|
+
version: "0.34.0",
|
|
5163
5185
|
description: "Bootstrap and maintain standardized TypeScript project tooling"
|
|
5164
5186
|
},
|
|
5165
5187
|
subCommands: {
|
|
@@ -5175,7 +5197,7 @@ const main = defineCommand({
|
|
|
5175
5197
|
"docker:check": dockerCheckCommand
|
|
5176
5198
|
}
|
|
5177
5199
|
});
|
|
5178
|
-
console.log(`@bensandee/tooling v0.
|
|
5200
|
+
console.log(`@bensandee/tooling v0.34.0`);
|
|
5179
5201
|
async function run() {
|
|
5180
5202
|
await runMain(main);
|
|
5181
5203
|
process.exit(process.exitCode ?? 0);
|
|
@@ -1,5 +1,40 @@
|
|
|
1
|
+
import * as clack from "@clack/prompts";
|
|
1
2
|
import { execSync } from "node:child_process";
|
|
2
3
|
import { z } from "zod";
|
|
4
|
+
//#region src/utils/log.ts
|
|
5
|
+
const out = (msg) => console.log(msg);
|
|
6
|
+
const isCI = Boolean(process.env["CI"]);
|
|
7
|
+
const log = isCI ? {
|
|
8
|
+
info: out,
|
|
9
|
+
warn: (msg) => out(`[warn] ${msg}`),
|
|
10
|
+
error: (msg) => out(`[error] ${msg}`),
|
|
11
|
+
success: (msg) => out(`✓ ${msg}`)
|
|
12
|
+
} : clack.log;
|
|
13
|
+
function note(body, title) {
|
|
14
|
+
if (isCI) {
|
|
15
|
+
if (title) out(`--- ${title} ---`);
|
|
16
|
+
out(body);
|
|
17
|
+
} else clack.note(body, title);
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
//#region src/utils/debug.ts
|
|
21
|
+
/** Check if verbose/debug mode is enabled via environment variables. */
|
|
22
|
+
function isEnvVerbose() {
|
|
23
|
+
return process.env["TOOLING_DEBUG"] === "true";
|
|
24
|
+
}
|
|
25
|
+
/** Log a debug message when verbose mode is enabled. */
|
|
26
|
+
function debug(config, message) {
|
|
27
|
+
if (config.verbose) log.info(`[debug] ${message}`);
|
|
28
|
+
}
|
|
29
|
+
/** Log the result of an exec call when verbose mode is enabled. */
|
|
30
|
+
function debugExec(config, label, result) {
|
|
31
|
+
if (!config.verbose) return;
|
|
32
|
+
const lines = [`[debug] ${label} (exit code ${String(result.exitCode)})`];
|
|
33
|
+
if (result.stdout.trim()) lines.push(` stdout: ${result.stdout.trim()}`);
|
|
34
|
+
if (result.stderr.trim()) lines.push(` stderr: ${result.stderr.trim()}`);
|
|
35
|
+
log.info(lines.join("\n"));
|
|
36
|
+
}
|
|
37
|
+
//#endregion
|
|
3
38
|
//#region src/utils/exec.ts
|
|
4
39
|
/** Type guard for `execSync` errors that carry stdout/stderr/status. */
|
|
5
40
|
function isExecSyncError(err) {
|
|
@@ -144,6 +179,11 @@ async function runDockerCheck(executor, config) {
|
|
|
144
179
|
const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
145
180
|
const pollIntervalMs = config.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
146
181
|
const { compose } = config;
|
|
182
|
+
const vc = { verbose: config.verbose ?? false };
|
|
183
|
+
debug(vc, `Compose files: ${compose.composeFiles.join(", ")}`);
|
|
184
|
+
debug(vc, `Services: ${compose.services.join(", ")}`);
|
|
185
|
+
debug(vc, `Health checks: ${config.healthChecks.map((c) => c.name).join(", ") || "(none)"}`);
|
|
186
|
+
debug(vc, `Timeout: ${String(timeoutMs)}ms, poll interval: ${String(pollIntervalMs)}ms`);
|
|
147
187
|
const cleanup = () => composeDown(executor, compose);
|
|
148
188
|
let cleaningUp = false;
|
|
149
189
|
const gracefulShutdown = () => {
|
|
@@ -171,9 +211,11 @@ async function runDockerCheck(executor, config) {
|
|
|
171
211
|
const healthStatus = new Map(config.healthChecks.map((c) => [c.name, false]));
|
|
172
212
|
while (executor.now() - startTime < timeoutMs) {
|
|
173
213
|
const containers = composePs(executor, compose);
|
|
214
|
+
debug(vc, `compose ps: ${containers.length} container(s)`);
|
|
174
215
|
let allContainersHealthy = true;
|
|
175
216
|
for (const service of compose.services) {
|
|
176
217
|
const status = getContainerHealth(containers, service);
|
|
218
|
+
debug(vc, `Container ${service}: ${status || "(no healthcheck)"}`);
|
|
177
219
|
if (status === "unhealthy") {
|
|
178
220
|
executor.logError(`Container ${service} is unhealthy`);
|
|
179
221
|
composeLogs(executor, compose, service);
|
|
@@ -233,4 +275,4 @@ async function runDockerCheck(executor, config) {
|
|
|
233
275
|
}
|
|
234
276
|
}
|
|
235
277
|
//#endregion
|
|
236
|
-
export { composeDown as a, composeUp as c, composeCommand as i, createRealExecutor as l, checkHttpHealth as n, composeLogs as o, getContainerHealth as r, composePs as s, runDockerCheck as t, isExecSyncError as u };
|
|
278
|
+
export { composeDown as a, composeUp as c, debug as d, debugExec as f, note as h, composeCommand as i, createRealExecutor as l, log as m, checkHttpHealth as n, composeLogs as o, isEnvVerbose as p, getContainerHealth as r, composePs as s, runDockerCheck as t, isExecSyncError as u };
|
|
@@ -67,6 +67,8 @@ interface CheckConfig {
|
|
|
67
67
|
timeoutMs?: number;
|
|
68
68
|
/** Interval between polling attempts, in ms. Default: 5000. */
|
|
69
69
|
pollIntervalMs?: number;
|
|
70
|
+
/** If true, emit detailed debug logging. */
|
|
71
|
+
verbose?: boolean;
|
|
70
72
|
}
|
|
71
73
|
/** Result of the docker check run. */
|
|
72
74
|
type CheckResult = {
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as composeDown, c as composeUp, i as composeCommand, l as createRealExecutor, n as checkHttpHealth, o as composeLogs, r as getContainerHealth, s as composePs, t as runDockerCheck } from "../check-
|
|
1
|
+
import { a as composeDown, c as composeUp, i as composeCommand, l as createRealExecutor, n as checkHttpHealth, o as composeLogs, r as getContainerHealth, s as composePs, t as runDockerCheck } from "../check-Ceom_OgJ.mjs";
|
|
2
2
|
export { checkHttpHealth, composeCommand, composeDown, composeLogs, composePs, composeUp, createRealExecutor, getContainerHealth, runDockerCheck };
|