@bensandee/tooling 0.13.0 → 0.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +225 -0
- package/README.md +121 -0
- package/dist/bin.d.mts +1 -0
- package/dist/bin.mjs +223 -57
- package/dist/docker-verify/index.d.mts +117 -0
- package/dist/docker-verify/index.mjs +218 -0
- package/dist/exec-CC49vrkM.mjs +7 -0
- package/dist/index.d.mts +104 -0
- package/package.json +19 -7
package/dist/bin.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { t as isExecSyncError } from "./exec-CC49vrkM.mjs";
|
|
2
3
|
import { defineCommand, runMain } from "citty";
|
|
3
4
|
import * as p from "@clack/prompts";
|
|
4
5
|
import { execSync } from "node:child_process";
|
|
@@ -106,7 +107,7 @@ function detectProject(targetDir) {
|
|
|
106
107
|
hasKnipConfig: exists("knip.json") || exists("knip.jsonc") || exists("knip.ts") || exists("knip.mts") || exists("knip.config.ts") || exists("knip.config.mts"),
|
|
107
108
|
hasRenovateConfig: exists("renovate.json") || exists("renovate.json5") || exists(".renovaterc") || exists(".renovaterc.json") || exists(".github/renovate.json") || exists(".github/renovate.json5"),
|
|
108
109
|
hasReleaseItConfig: exists(".release-it.json") || exists(".release-it.yaml") || exists(".release-it.toml"),
|
|
109
|
-
|
|
110
|
+
hasSimpleReleaseConfig: exists(".versionrc") || exists(".versionrc.json") || exists(".versionrc.js"),
|
|
110
111
|
hasChangesetsConfig: exists(".changeset/config.json"),
|
|
111
112
|
legacyConfigs: detectLegacyConfigs(targetDir)
|
|
112
113
|
};
|
|
@@ -208,7 +209,7 @@ function getMonorepoPackages(targetDir) {
|
|
|
208
209
|
function isCancelled(value) {
|
|
209
210
|
return p.isCancel(value);
|
|
210
211
|
}
|
|
211
|
-
async function runInitPrompts(targetDir) {
|
|
212
|
+
async function runInitPrompts(targetDir, saved) {
|
|
212
213
|
p.intro("@bensandee/tooling repo:init");
|
|
213
214
|
const existingPkg = readPackageJson(targetDir);
|
|
214
215
|
const detected = detectProject(targetDir);
|
|
@@ -217,7 +218,7 @@ async function runInitPrompts(targetDir) {
|
|
|
217
218
|
const detectedMonorepo = detectMonorepo(targetDir);
|
|
218
219
|
const structure = await p.select({
|
|
219
220
|
message: "Project structure",
|
|
220
|
-
initialValue: detectedMonorepo ? "monorepo" : "single",
|
|
221
|
+
initialValue: saved?.structure ?? (detectedMonorepo ? "monorepo" : "single"),
|
|
221
222
|
options: [{
|
|
222
223
|
value: "single",
|
|
223
224
|
label: "Single repo"
|
|
@@ -232,7 +233,7 @@ async function runInitPrompts(targetDir) {
|
|
|
232
233
|
}
|
|
233
234
|
const useEslintPlugin = await p.confirm({
|
|
234
235
|
message: "Include @bensandee/eslint-plugin?",
|
|
235
|
-
initialValue: true
|
|
236
|
+
initialValue: saved?.useEslintPlugin ?? true
|
|
236
237
|
});
|
|
237
238
|
if (isCancelled(useEslintPlugin)) {
|
|
238
239
|
p.cancel("Cancelled.");
|
|
@@ -241,7 +242,7 @@ async function runInitPrompts(targetDir) {
|
|
|
241
242
|
const hasExistingPrettier = detected.legacyConfigs.some((l) => l.tool === "prettier");
|
|
242
243
|
const formatter = await p.select({
|
|
243
244
|
message: "Formatter",
|
|
244
|
-
initialValue: hasExistingPrettier ? "prettier" : "oxfmt",
|
|
245
|
+
initialValue: saved?.formatter ?? (hasExistingPrettier ? "prettier" : "oxfmt"),
|
|
245
246
|
options: [{
|
|
246
247
|
value: "oxfmt",
|
|
247
248
|
label: "oxfmt",
|
|
@@ -257,7 +258,7 @@ async function runInitPrompts(targetDir) {
|
|
|
257
258
|
}
|
|
258
259
|
const setupVitest = await p.confirm({
|
|
259
260
|
message: "Set up vitest with a starter test?",
|
|
260
|
-
initialValue: !isExisting
|
|
261
|
+
initialValue: saved?.setupVitest ?? !isExisting
|
|
261
262
|
});
|
|
262
263
|
if (isCancelled(setupVitest)) {
|
|
263
264
|
p.cancel("Cancelled.");
|
|
@@ -265,6 +266,7 @@ async function runInitPrompts(targetDir) {
|
|
|
265
266
|
}
|
|
266
267
|
const ci = await p.select({
|
|
267
268
|
message: "CI workflow",
|
|
269
|
+
initialValue: saved?.ci,
|
|
268
270
|
options: [
|
|
269
271
|
{
|
|
270
272
|
value: "forgejo",
|
|
@@ -288,7 +290,7 @@ async function runInitPrompts(targetDir) {
|
|
|
288
290
|
if (ci === "github") {
|
|
289
291
|
const renovateAnswer = await p.confirm({
|
|
290
292
|
message: "Set up Renovate for automated dependency updates?",
|
|
291
|
-
initialValue: true
|
|
293
|
+
initialValue: saved?.setupRenovate ?? true
|
|
292
294
|
});
|
|
293
295
|
if (isCancelled(renovateAnswer)) {
|
|
294
296
|
p.cancel("Cancelled.");
|
|
@@ -298,7 +300,7 @@ async function runInitPrompts(targetDir) {
|
|
|
298
300
|
}
|
|
299
301
|
const releaseStrategy = await p.select({
|
|
300
302
|
message: "Release management",
|
|
301
|
-
initialValue: "none",
|
|
303
|
+
initialValue: saved?.releaseStrategy ?? "none",
|
|
302
304
|
options: [
|
|
303
305
|
{
|
|
304
306
|
value: "none",
|
|
@@ -315,9 +317,9 @@ async function runInitPrompts(targetDir) {
|
|
|
315
317
|
hint: "PR-based versioning"
|
|
316
318
|
},
|
|
317
319
|
{
|
|
318
|
-
value: "
|
|
319
|
-
label: "
|
|
320
|
-
hint: "
|
|
320
|
+
value: "simple",
|
|
321
|
+
label: "Simple",
|
|
322
|
+
hint: "uses commit-and-tag-version internally"
|
|
321
323
|
}
|
|
322
324
|
]
|
|
323
325
|
});
|
|
@@ -337,7 +339,7 @@ async function runInitPrompts(targetDir) {
|
|
|
337
339
|
p.note(detections.join("\n"), "Detected package types");
|
|
338
340
|
const applyDetected = await p.confirm({
|
|
339
341
|
message: "Apply detected tsconfig bases to packages?",
|
|
340
|
-
initialValue: true
|
|
342
|
+
initialValue: saved?.detectPackageTypes ?? true
|
|
341
343
|
});
|
|
342
344
|
if (isCancelled(applyDetected)) {
|
|
343
345
|
p.cancel("Cancelled.");
|
|
@@ -348,7 +350,7 @@ async function runInitPrompts(targetDir) {
|
|
|
348
350
|
} else {
|
|
349
351
|
const projectTypeAnswer = await p.select({
|
|
350
352
|
message: "Project type",
|
|
351
|
-
initialValue: "default",
|
|
353
|
+
initialValue: saved?.projectType ?? "default",
|
|
352
354
|
options: [
|
|
353
355
|
{
|
|
354
356
|
value: "default",
|
|
@@ -407,7 +409,7 @@ function buildDefaultConfig(targetDir, flags) {
|
|
|
407
409
|
setupVitest: !detected.hasVitestConfig,
|
|
408
410
|
ci: flags.noCi ? "none" : DEFAULT_CI,
|
|
409
411
|
setupRenovate: true,
|
|
410
|
-
releaseStrategy: detected.hasReleaseItConfig ? "release-it" : detected.
|
|
412
|
+
releaseStrategy: detected.hasReleaseItConfig ? "release-it" : detected.hasSimpleReleaseConfig ? "simple" : detected.hasChangesetsConfig ? "changesets" : "none",
|
|
411
413
|
projectType: "default",
|
|
412
414
|
detectPackageTypes: true,
|
|
413
415
|
targetDir
|
|
@@ -581,7 +583,7 @@ function addReleaseDeps(deps, config) {
|
|
|
581
583
|
deps["release-it"] = "18.1.2";
|
|
582
584
|
if (config.structure === "monorepo") deps["@release-it/bumper"] = "7.0.2";
|
|
583
585
|
break;
|
|
584
|
-
case "
|
|
586
|
+
case "simple":
|
|
585
587
|
deps["commit-and-tag-version"] = "12.5.0";
|
|
586
588
|
break;
|
|
587
589
|
case "changesets":
|
|
@@ -593,8 +595,8 @@ function addReleaseDeps(deps, config) {
|
|
|
593
595
|
function getAddedDevDepNames(config) {
|
|
594
596
|
const deps = { ...ROOT_DEV_DEPS };
|
|
595
597
|
if (config.structure !== "monorepo") Object.assign(deps, PER_PACKAGE_DEV_DEPS);
|
|
596
|
-
deps["@bensandee/config"] = "0.
|
|
597
|
-
deps["@bensandee/tooling"] = "0.
|
|
598
|
+
deps["@bensandee/config"] = "0.8.1";
|
|
599
|
+
deps["@bensandee/tooling"] = "0.14.1";
|
|
598
600
|
if (config.formatter === "oxfmt") deps["oxfmt"] = "0.35.0";
|
|
599
601
|
if (config.formatter === "prettier") deps["prettier"] = "3.8.1";
|
|
600
602
|
addReleaseDeps(deps, config);
|
|
@@ -614,9 +616,9 @@ async function generatePackageJson(ctx) {
|
|
|
614
616
|
if (ctx.config.releaseStrategy !== "none" && ctx.config.releaseStrategy !== "changesets") allScripts["trigger-release"] = "pnpm exec tooling release:trigger";
|
|
615
617
|
const devDeps = { ...ROOT_DEV_DEPS };
|
|
616
618
|
if (!isMonorepo) Object.assign(devDeps, PER_PACKAGE_DEV_DEPS);
|
|
617
|
-
devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.
|
|
618
|
-
devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.
|
|
619
|
-
if (ctx.config.useEslintPlugin) devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "0.9.
|
|
619
|
+
devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.8.1";
|
|
620
|
+
devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.14.1";
|
|
621
|
+
if (ctx.config.useEslintPlugin) devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "0.9.2";
|
|
620
622
|
if (ctx.config.formatter === "oxfmt") devDeps["oxfmt"] = "0.35.0";
|
|
621
623
|
if (ctx.config.formatter === "prettier") devDeps["prettier"] = "3.8.1";
|
|
622
624
|
addReleaseDeps(devDeps, ctx.config);
|
|
@@ -1964,25 +1966,13 @@ permissions:
|
|
|
1964
1966
|
- name: Release
|
|
1965
1967
|
env:
|
|
1966
1968
|
GITHUB_TOKEN: \${{ github.token }}
|
|
1967
|
-
|
|
1968
|
-
run: |
|
|
1969
|
-
pnpm exec commit-and-tag-version
|
|
1970
|
-
git push --follow-tags
|
|
1971
|
-
TAG=$(git describe --tags --abbrev=0)
|
|
1972
|
-
pnpm publish --no-git-checks
|
|
1973
|
-
gh release create "$TAG" --generate-notes` : `
|
|
1969
|
+
run: pnpm exec tooling release:simple` : `
|
|
1974
1970
|
- name: Release
|
|
1975
1971
|
env:
|
|
1976
1972
|
FORGEJO_SERVER_URL: \${{ github.server_url }}
|
|
1977
1973
|
FORGEJO_REPOSITORY: \${{ github.repository }}
|
|
1978
1974
|
FORGEJO_TOKEN: \${{ secrets.FORGEJO_TOKEN }}
|
|
1979
|
-
|
|
1980
|
-
run: |
|
|
1981
|
-
pnpm exec commit-and-tag-version
|
|
1982
|
-
git push --follow-tags
|
|
1983
|
-
TAG=$(git describe --tags --abbrev=0)
|
|
1984
|
-
pnpm publish --no-git-checks
|
|
1985
|
-
pnpm exec tooling release:create-forgejo-release --tag "$TAG"`;
|
|
1975
|
+
run: pnpm exec tooling release:simple`;
|
|
1986
1976
|
return `${workflowSchemaComment(ci)}name: Release
|
|
1987
1977
|
on:
|
|
1988
1978
|
workflow_dispatch:
|
|
@@ -2083,10 +2073,10 @@ function requiredReleaseSteps(strategy, nodeVersionYaml) {
|
|
|
2083
2073
|
step: { run: "pnpm release-it --ci" }
|
|
2084
2074
|
});
|
|
2085
2075
|
break;
|
|
2086
|
-
case "
|
|
2076
|
+
case "simple":
|
|
2087
2077
|
steps.push({
|
|
2088
|
-
match: { run: "
|
|
2089
|
-
step: { run: "pnpm exec
|
|
2078
|
+
match: { run: "release:simple" },
|
|
2079
|
+
step: { run: "pnpm exec tooling release:simple" }
|
|
2090
2080
|
});
|
|
2091
2081
|
break;
|
|
2092
2082
|
case "changesets":
|
|
@@ -2101,7 +2091,7 @@ function requiredReleaseSteps(strategy, nodeVersionYaml) {
|
|
|
2101
2091
|
function buildWorkflow(strategy, ci, nodeVersionYaml) {
|
|
2102
2092
|
switch (strategy) {
|
|
2103
2093
|
case "release-it": return releaseItWorkflow(ci, nodeVersionYaml);
|
|
2104
|
-
case "
|
|
2094
|
+
case "simple": return commitAndTagVersionWorkflow(ci, nodeVersionYaml);
|
|
2105
2095
|
case "changesets": return changesetsWorkflow(ci, nodeVersionYaml);
|
|
2106
2096
|
default: return null;
|
|
2107
2097
|
}
|
|
@@ -2444,7 +2434,7 @@ const ToolingConfigSchema = z.object({
|
|
|
2444
2434
|
setupRenovate: z.boolean().optional(),
|
|
2445
2435
|
releaseStrategy: z.enum([
|
|
2446
2436
|
"release-it",
|
|
2447
|
-
"
|
|
2437
|
+
"simple",
|
|
2448
2438
|
"changesets",
|
|
2449
2439
|
"none"
|
|
2450
2440
|
]).optional(),
|
|
@@ -2545,14 +2535,14 @@ const initCommand = defineCommand({
|
|
|
2545
2535
|
},
|
|
2546
2536
|
async run({ args }) {
|
|
2547
2537
|
const targetDir = path.resolve(args.dir ?? ".");
|
|
2538
|
+
const saved = loadToolingConfig(targetDir);
|
|
2548
2539
|
await runInit(args.yes ? (() => {
|
|
2549
|
-
const saved = loadToolingConfig(targetDir);
|
|
2550
2540
|
const detected = buildDefaultConfig(targetDir, {
|
|
2551
2541
|
eslintPlugin: args["eslint-plugin"] === true ? true : void 0,
|
|
2552
2542
|
noCi: args["no-ci"] === true ? true : void 0
|
|
2553
2543
|
});
|
|
2554
2544
|
return saved ? mergeWithSavedConfig(detected, saved) : detected;
|
|
2555
|
-
})() : await runInitPrompts(targetDir), args["no-prompt"] === true ? { noPrompt: true } : {});
|
|
2545
|
+
})() : await runInitPrompts(targetDir, saved), args["no-prompt"] === true ? { noPrompt: true } : {});
|
|
2556
2546
|
}
|
|
2557
2547
|
});
|
|
2558
2548
|
async function runInit(config, options = {}) {
|
|
@@ -2732,12 +2722,6 @@ function lineDiff(oldText, newText) {
|
|
|
2732
2722
|
return lines;
|
|
2733
2723
|
}
|
|
2734
2724
|
//#endregion
|
|
2735
|
-
//#region src/utils/exec.ts
|
|
2736
|
-
/** Type guard for `execSync` errors that carry stdout/stderr/status. */
|
|
2737
|
-
function isExecSyncError(err) {
|
|
2738
|
-
return err instanceof Error && "stdout" in err && typeof err.stdout === "string" && "stderr" in err && typeof err.stderr === "string" && "status" in err && typeof err.status === "number";
|
|
2739
|
-
}
|
|
2740
|
-
//#endregion
|
|
2741
2725
|
//#region src/release/executor.ts
|
|
2742
2726
|
/** Create an executor that runs real commands, fetches, and reads the filesystem. */
|
|
2743
2727
|
function createRealExecutor() {
|
|
@@ -3347,10 +3331,10 @@ function triggerGitHub(ref) {
|
|
|
3347
3331
|
p.log.info(`Triggered release workflow on GitHub (ref: ${ref})`);
|
|
3348
3332
|
}
|
|
3349
3333
|
//#endregion
|
|
3350
|
-
//#region src/commands/
|
|
3334
|
+
//#region src/commands/forgejo-create-release.ts
|
|
3351
3335
|
const createForgejoReleaseCommand = defineCommand({
|
|
3352
3336
|
meta: {
|
|
3353
|
-
name: "
|
|
3337
|
+
name: "forgejo:create-release",
|
|
3354
3338
|
description: "Create a Forgejo release for a given tag"
|
|
3355
3339
|
},
|
|
3356
3340
|
args: { tag: {
|
|
@@ -3360,7 +3344,7 @@ const createForgejoReleaseCommand = defineCommand({
|
|
|
3360
3344
|
} },
|
|
3361
3345
|
async run({ args }) {
|
|
3362
3346
|
const resolved = resolveConnection(process.cwd());
|
|
3363
|
-
if (resolved.type !== "forgejo") throw new FatalError("
|
|
3347
|
+
if (resolved.type !== "forgejo") throw new FatalError("forgejo:create-release requires a Forgejo repository");
|
|
3364
3348
|
const executor = createRealExecutor();
|
|
3365
3349
|
const conn = resolved.conn;
|
|
3366
3350
|
if (await findRelease(executor, conn, args.tag)) {
|
|
@@ -3372,11 +3356,11 @@ const createForgejoReleaseCommand = defineCommand({
|
|
|
3372
3356
|
}
|
|
3373
3357
|
});
|
|
3374
3358
|
//#endregion
|
|
3375
|
-
//#region src/commands/
|
|
3359
|
+
//#region src/commands/changesets-merge.ts
|
|
3376
3360
|
const HEAD_BRANCH = "changeset-release/main";
|
|
3377
3361
|
const releaseMergeCommand = defineCommand({
|
|
3378
3362
|
meta: {
|
|
3379
|
-
name: "
|
|
3363
|
+
name: "changesets:merge",
|
|
3380
3364
|
description: "Merge the open changesets version PR"
|
|
3381
3365
|
},
|
|
3382
3366
|
args: { "dry-run": {
|
|
@@ -3416,6 +3400,179 @@ function mergeGitHub(dryRun) {
|
|
|
3416
3400
|
p.log.info(`Merged changesets PR and deleted branch ${HEAD_BRANCH}`);
|
|
3417
3401
|
}
|
|
3418
3402
|
//#endregion
|
|
3403
|
+
//#region src/release/simple.ts
|
|
3404
|
+
/**
|
|
3405
|
+
* Compute sliding version tags from a semver version string.
|
|
3406
|
+
* For "1.2.3" returns ["v1", "v1.2"]. Strips prerelease suffixes.
|
|
3407
|
+
*/
|
|
3408
|
+
function computeSlidingTags(version) {
|
|
3409
|
+
const parts = (version.split("-")[0] ?? version).split(".");
|
|
3410
|
+
if (parts.length < 2 || !parts[0] || !parts[1]) throw new FatalError(`Invalid version format "${version}". Expected semver (X.Y.Z)`);
|
|
3411
|
+
return [`v${parts[0]}`, `v${parts[0]}.${parts[1]}`];
|
|
3412
|
+
}
|
|
3413
|
+
/** Build the commit-and-tag-version command with appropriate flags. */
|
|
3414
|
+
function buildCommand(config) {
|
|
3415
|
+
const args = ["pnpm exec commit-and-tag-version"];
|
|
3416
|
+
if (config.dryRun) args.push("--dry-run");
|
|
3417
|
+
if (config.firstRelease) args.push("--first-release");
|
|
3418
|
+
if (config.releaseAs) args.push(`--release-as ${config.releaseAs}`);
|
|
3419
|
+
if (config.prerelease) args.push(`--prerelease ${config.prerelease}`);
|
|
3420
|
+
return args.join(" ");
|
|
3421
|
+
}
|
|
3422
|
+
/** Read the current version from package.json. */
|
|
3423
|
+
function readVersion(executor, cwd) {
|
|
3424
|
+
const raw = executor.readFile(path.join(cwd, "package.json"));
|
|
3425
|
+
if (!raw) throw new FatalError("Could not read package.json");
|
|
3426
|
+
const pkg = parsePackageJson(raw);
|
|
3427
|
+
if (!pkg?.version) throw new FatalError("No version field found in package.json");
|
|
3428
|
+
return pkg.version;
|
|
3429
|
+
}
|
|
3430
|
+
/** Run the full commit-and-tag-version release flow. */
|
|
3431
|
+
async function runSimpleRelease(executor, config) {
|
|
3432
|
+
const command = buildCommand(config);
|
|
3433
|
+
p.log.info(`Running: ${command}`);
|
|
3434
|
+
const versionResult = executor.exec(command, { cwd: config.cwd });
|
|
3435
|
+
debugExec(config, "commit-and-tag-version", versionResult);
|
|
3436
|
+
if (versionResult.exitCode !== 0) throw new FatalError(`commit-and-tag-version failed (exit code ${String(versionResult.exitCode)}):\n${versionResult.stderr || versionResult.stdout}`);
|
|
3437
|
+
const version = readVersion(executor, config.cwd);
|
|
3438
|
+
debug(config, `New version: ${version}`);
|
|
3439
|
+
const tagResult = executor.exec("git describe --tags --abbrev=0", { cwd: config.cwd });
|
|
3440
|
+
debugExec(config, "git describe", tagResult);
|
|
3441
|
+
const tag = tagResult.stdout.trim();
|
|
3442
|
+
if (!tag) throw new FatalError("Could not determine the new tag from git describe");
|
|
3443
|
+
p.log.info(`Version ${version} tagged as ${tag}`);
|
|
3444
|
+
if (config.dryRun) {
|
|
3445
|
+
const slidingTags = config.noSlidingTags ? [] : computeSlidingTags(version);
|
|
3446
|
+
p.log.info(`[dry-run] Would push to origin with --follow-tags`);
|
|
3447
|
+
if (slidingTags.length > 0) p.log.info(`[dry-run] Would create sliding tags: ${slidingTags.join(", ")}`);
|
|
3448
|
+
if (!config.noRelease && config.platform) p.log.info(`[dry-run] Would create ${config.platform.type} release for ${tag}`);
|
|
3449
|
+
return {
|
|
3450
|
+
version,
|
|
3451
|
+
tag,
|
|
3452
|
+
slidingTags,
|
|
3453
|
+
pushed: false,
|
|
3454
|
+
releaseCreated: false
|
|
3455
|
+
};
|
|
3456
|
+
}
|
|
3457
|
+
let pushed = false;
|
|
3458
|
+
if (!config.noPush) {
|
|
3459
|
+
const branch = executor.exec("git rev-parse --abbrev-ref HEAD", { cwd: config.cwd }).stdout.trim() || "main";
|
|
3460
|
+
debug(config, `Pushing to origin/${branch}`);
|
|
3461
|
+
const pushResult = executor.exec(`git push --follow-tags origin ${branch}`, { cwd: config.cwd });
|
|
3462
|
+
debugExec(config, "git push", pushResult);
|
|
3463
|
+
if (pushResult.exitCode !== 0) throw new FatalError(`git push failed (exit code ${String(pushResult.exitCode)}):\n${pushResult.stderr || pushResult.stdout}`);
|
|
3464
|
+
pushed = true;
|
|
3465
|
+
p.log.info("Pushed to origin");
|
|
3466
|
+
}
|
|
3467
|
+
let slidingTags = [];
|
|
3468
|
+
if (!config.noSlidingTags && pushed) {
|
|
3469
|
+
slidingTags = computeSlidingTags(version);
|
|
3470
|
+
for (const slidingTag of slidingTags) executor.exec(`git tag -f ${slidingTag}`, { cwd: config.cwd });
|
|
3471
|
+
const forcePushResult = executor.exec(`git push origin ${slidingTags.join(" ")} --force`, { cwd: config.cwd });
|
|
3472
|
+
debugExec(config, "force-push sliding tags", forcePushResult);
|
|
3473
|
+
if (forcePushResult.exitCode !== 0) p.log.warn(`Warning: Failed to push sliding tags: ${forcePushResult.stderr || forcePushResult.stdout}`);
|
|
3474
|
+
else p.log.info(`Created sliding tags: ${slidingTags.join(", ")}`);
|
|
3475
|
+
}
|
|
3476
|
+
let releaseCreated = false;
|
|
3477
|
+
if (!config.noRelease && config.platform) releaseCreated = await createPlatformRelease(executor, config, tag);
|
|
3478
|
+
return {
|
|
3479
|
+
version,
|
|
3480
|
+
tag,
|
|
3481
|
+
slidingTags,
|
|
3482
|
+
pushed,
|
|
3483
|
+
releaseCreated
|
|
3484
|
+
};
|
|
3485
|
+
}
|
|
3486
|
+
async function createPlatformRelease(executor, config, tag) {
|
|
3487
|
+
if (!config.platform) return false;
|
|
3488
|
+
if (config.platform.type === "forgejo") {
|
|
3489
|
+
if (await findRelease(executor, config.platform.conn, tag)) {
|
|
3490
|
+
debug(config, `Release for ${tag} already exists, skipping`);
|
|
3491
|
+
return false;
|
|
3492
|
+
}
|
|
3493
|
+
await createRelease(executor, config.platform.conn, tag);
|
|
3494
|
+
p.log.info(`Created Forgejo release for ${tag}`);
|
|
3495
|
+
return true;
|
|
3496
|
+
}
|
|
3497
|
+
const ghResult = executor.exec(`gh release create ${tag} --generate-notes`, { cwd: config.cwd });
|
|
3498
|
+
debugExec(config, "gh release create", ghResult);
|
|
3499
|
+
if (ghResult.exitCode !== 0) {
|
|
3500
|
+
p.log.warn(`Warning: Failed to create GitHub release: ${ghResult.stderr || ghResult.stdout}`);
|
|
3501
|
+
return false;
|
|
3502
|
+
}
|
|
3503
|
+
p.log.info(`Created GitHub release for ${tag}`);
|
|
3504
|
+
return true;
|
|
3505
|
+
}
|
|
3506
|
+
//#endregion
|
|
3507
|
+
//#region src/commands/release-simple.ts
|
|
3508
|
+
const releaseSimpleCommand = defineCommand({
|
|
3509
|
+
meta: {
|
|
3510
|
+
name: "release:simple",
|
|
3511
|
+
description: "Run commit-and-tag-version, push, create sliding tags, and create a platform release"
|
|
3512
|
+
},
|
|
3513
|
+
args: {
|
|
3514
|
+
"dry-run": {
|
|
3515
|
+
type: "boolean",
|
|
3516
|
+
description: "Pass --dry-run to commit-and-tag-version and skip all remote operations"
|
|
3517
|
+
},
|
|
3518
|
+
verbose: {
|
|
3519
|
+
type: "boolean",
|
|
3520
|
+
description: "Enable detailed debug logging (also enabled by RELEASE_DEBUG env var)"
|
|
3521
|
+
},
|
|
3522
|
+
"no-push": {
|
|
3523
|
+
type: "boolean",
|
|
3524
|
+
description: "Run commit-and-tag-version but skip push and remote operations"
|
|
3525
|
+
},
|
|
3526
|
+
"no-sliding-tags": {
|
|
3527
|
+
type: "boolean",
|
|
3528
|
+
description: "Skip creating sliding major/minor version tags (vX, vX.Y)"
|
|
3529
|
+
},
|
|
3530
|
+
"no-release": {
|
|
3531
|
+
type: "boolean",
|
|
3532
|
+
description: "Skip Forgejo/GitHub release creation"
|
|
3533
|
+
},
|
|
3534
|
+
"first-release": {
|
|
3535
|
+
type: "boolean",
|
|
3536
|
+
description: "Pass --first-release to commit-and-tag-version (skip version bump)"
|
|
3537
|
+
},
|
|
3538
|
+
"release-as": {
|
|
3539
|
+
type: "string",
|
|
3540
|
+
description: "Force a specific version (passed to commit-and-tag-version --release-as)"
|
|
3541
|
+
},
|
|
3542
|
+
prerelease: {
|
|
3543
|
+
type: "string",
|
|
3544
|
+
description: "Create a prerelease with the given tag (e.g., beta, alpha)"
|
|
3545
|
+
}
|
|
3546
|
+
},
|
|
3547
|
+
async run({ args }) {
|
|
3548
|
+
const cwd = process.cwd();
|
|
3549
|
+
const verbose = args.verbose === true || process.env["RELEASE_DEBUG"] === "true";
|
|
3550
|
+
const noRelease = args["no-release"] === true;
|
|
3551
|
+
let platform;
|
|
3552
|
+
if (!noRelease) {
|
|
3553
|
+
const resolved = resolveConnection(cwd);
|
|
3554
|
+
if (resolved.type === "forgejo") platform = {
|
|
3555
|
+
type: "forgejo",
|
|
3556
|
+
conn: resolved.conn
|
|
3557
|
+
};
|
|
3558
|
+
else platform = { type: "github" };
|
|
3559
|
+
}
|
|
3560
|
+
const config = {
|
|
3561
|
+
cwd,
|
|
3562
|
+
dryRun: args["dry-run"] === true,
|
|
3563
|
+
verbose,
|
|
3564
|
+
noPush: args["no-push"] === true,
|
|
3565
|
+
noSlidingTags: args["no-sliding-tags"] === true,
|
|
3566
|
+
noRelease,
|
|
3567
|
+
firstRelease: args["first-release"] === true,
|
|
3568
|
+
releaseAs: args["release-as"],
|
|
3569
|
+
prerelease: args.prerelease,
|
|
3570
|
+
platform
|
|
3571
|
+
};
|
|
3572
|
+
await runSimpleRelease(createRealExecutor(), config);
|
|
3573
|
+
}
|
|
3574
|
+
});
|
|
3575
|
+
//#endregion
|
|
3419
3576
|
//#region src/commands/repo-run-checks.ts
|
|
3420
3577
|
const CHECKS = [
|
|
3421
3578
|
{ name: "build" },
|
|
@@ -3457,6 +3614,7 @@ function runRunChecks(targetDir, options = {}) {
|
|
|
3457
3614
|
const skip = options.skip ?? /* @__PURE__ */ new Set();
|
|
3458
3615
|
const add = options.add ?? [];
|
|
3459
3616
|
const isCI = Boolean(process.env["CI"]);
|
|
3617
|
+
const failFast = options.failFast ?? !isCI;
|
|
3460
3618
|
const definedScripts = getScripts(targetDir);
|
|
3461
3619
|
const addedNames = new Set(add);
|
|
3462
3620
|
const allChecks = [...CHECKS, ...add.map((name) => ({ name }))];
|
|
@@ -3480,6 +3638,7 @@ function runRunChecks(targetDir, options = {}) {
|
|
|
3480
3638
|
if (isCI) ciLog(`::error::${check.name} failed`);
|
|
3481
3639
|
p.log.error(`${check.name} failed`);
|
|
3482
3640
|
failures.push(check.name);
|
|
3641
|
+
if (failFast) return 1;
|
|
3483
3642
|
}
|
|
3484
3643
|
}
|
|
3485
3644
|
if (notDefined.length > 0) p.log.info(`Skipped (not defined): ${notDefined.join(", ")}`);
|
|
@@ -3510,12 +3669,18 @@ const runChecksCommand = defineCommand({
|
|
|
3510
3669
|
type: "string",
|
|
3511
3670
|
description: "Comma-separated list of additional check names to run (uses pnpm run <name>)",
|
|
3512
3671
|
required: false
|
|
3672
|
+
},
|
|
3673
|
+
"fail-fast": {
|
|
3674
|
+
type: "boolean",
|
|
3675
|
+
description: "Stop on first failure (default: true in dev, false in CI)",
|
|
3676
|
+
required: false
|
|
3513
3677
|
}
|
|
3514
3678
|
},
|
|
3515
3679
|
run({ args }) {
|
|
3516
3680
|
const exitCode = runRunChecks(path.resolve(args.dir ?? "."), {
|
|
3517
3681
|
skip: args.skip ? new Set(args.skip.split(",").map((s) => s.trim())) : void 0,
|
|
3518
|
-
add: args.add ? args.add.split(",").map((s) => s.trim()) : void 0
|
|
3682
|
+
add: args.add ? args.add.split(",").map((s) => s.trim()) : void 0,
|
|
3683
|
+
failFast: args["fail-fast"] === true ? true : args["fail-fast"] === false ? false : void 0
|
|
3519
3684
|
});
|
|
3520
3685
|
process.exitCode = exitCode;
|
|
3521
3686
|
}
|
|
@@ -3525,7 +3690,7 @@ const runChecksCommand = defineCommand({
|
|
|
3525
3690
|
const main = defineCommand({
|
|
3526
3691
|
meta: {
|
|
3527
3692
|
name: "tooling",
|
|
3528
|
-
version: "0.
|
|
3693
|
+
version: "0.14.1",
|
|
3529
3694
|
description: "Bootstrap and maintain standardized TypeScript project tooling"
|
|
3530
3695
|
},
|
|
3531
3696
|
subCommands: {
|
|
@@ -3535,11 +3700,12 @@ const main = defineCommand({
|
|
|
3535
3700
|
"checks:run": runChecksCommand,
|
|
3536
3701
|
"release:changesets": releaseForgejoCommand,
|
|
3537
3702
|
"release:trigger": releaseTriggerCommand,
|
|
3538
|
-
"
|
|
3539
|
-
"
|
|
3703
|
+
"forgejo:create-release": createForgejoReleaseCommand,
|
|
3704
|
+
"changesets:merge": releaseMergeCommand,
|
|
3705
|
+
"release:simple": releaseSimpleCommand
|
|
3540
3706
|
}
|
|
3541
3707
|
});
|
|
3542
|
-
console.log(`@bensandee/tooling v0.
|
|
3708
|
+
console.log(`@bensandee/tooling v0.14.1`);
|
|
3543
3709
|
runMain(main);
|
|
3544
3710
|
//#endregion
|
|
3545
3711
|
export {};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
//#region src/release/types.d.ts
|
|
4
|
+
/** Result of executing a shell command. */
|
|
5
|
+
interface ExecResult {
|
|
6
|
+
stdout: string;
|
|
7
|
+
stderr: string;
|
|
8
|
+
exitCode: number;
|
|
9
|
+
}
|
|
10
|
+
/** Options for executing a shell command. */
|
|
11
|
+
interface ExecOptions {
|
|
12
|
+
cwd?: string;
|
|
13
|
+
env?: Record<string, string>;
|
|
14
|
+
}
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region src/docker-verify/types.d.ts
|
|
17
|
+
/** Abstraction over side effects for testability. */
|
|
18
|
+
interface DockerVerifyExecutor {
|
|
19
|
+
/** Run a shell command and return the result (stdout/stderr captured). */
|
|
20
|
+
exec(command: string, options?: ExecOptions): ExecResult;
|
|
21
|
+
/** Run a shell command, streaming stdout/stderr to the console. */
|
|
22
|
+
execInherit(command: string, options?: ExecOptions): void;
|
|
23
|
+
/** Perform an HTTP request. */
|
|
24
|
+
fetch(url: string, init?: RequestInit): Promise<Response>;
|
|
25
|
+
/** Get current time in ms (mockable clock). */
|
|
26
|
+
now(): number;
|
|
27
|
+
/** Sleep for the given duration (mockable delay). */
|
|
28
|
+
sleep(ms: number): Promise<void>;
|
|
29
|
+
/** Register a process signal handler. Returns a dispose function. */
|
|
30
|
+
onSignal(signal: NodeJS.Signals, handler: () => void): () => void;
|
|
31
|
+
/** Log a message to the console. */
|
|
32
|
+
log(message: string): void;
|
|
33
|
+
/** Log an error to the console. */
|
|
34
|
+
logError(message: string): void;
|
|
35
|
+
}
|
|
36
|
+
/** A single HTTP health check definition. */
|
|
37
|
+
interface HttpHealthCheck {
|
|
38
|
+
/** Human-readable name for logging (e.g. "API", "Frontend"). */
|
|
39
|
+
name: string;
|
|
40
|
+
/** URL to fetch. */
|
|
41
|
+
url: string;
|
|
42
|
+
/** Validate the response. Return true if healthy. */
|
|
43
|
+
validate: (response: Response) => Promise<boolean>;
|
|
44
|
+
}
|
|
45
|
+
/** Docker compose configuration. */
|
|
46
|
+
interface ComposeConfig {
|
|
47
|
+
/** Working directory for docker compose commands. */
|
|
48
|
+
cwd: string;
|
|
49
|
+
/** Compose files to use (e.g. ["docker-compose.yaml", "docker-compose.verify.yaml"]). */
|
|
50
|
+
composeFiles: string[];
|
|
51
|
+
/** Optional env file for compose. */
|
|
52
|
+
envFile?: string;
|
|
53
|
+
/** Service names to monitor for container-level health. */
|
|
54
|
+
services: string[];
|
|
55
|
+
}
|
|
56
|
+
/** Full verification configuration. */
|
|
57
|
+
interface VerifyConfig {
|
|
58
|
+
/** Docker compose settings. */
|
|
59
|
+
compose: ComposeConfig;
|
|
60
|
+
/** Optional build command to run before starting compose (e.g. "pnpm image:build"). */
|
|
61
|
+
buildCommand?: string;
|
|
62
|
+
/** Working directory for the build command (defaults to compose.cwd). */
|
|
63
|
+
buildCwd?: string;
|
|
64
|
+
/** HTTP health checks to poll. All must pass for success. */
|
|
65
|
+
healthChecks: HttpHealthCheck[];
|
|
66
|
+
/** Maximum time to wait for all checks to pass, in ms. Default: 120000. */
|
|
67
|
+
timeoutMs?: number;
|
|
68
|
+
/** Interval between polling attempts, in ms. Default: 5000. */
|
|
69
|
+
pollIntervalMs?: number;
|
|
70
|
+
}
|
|
71
|
+
/** Result of the verification run. */
|
|
72
|
+
type VerifyResult = {
|
|
73
|
+
success: true;
|
|
74
|
+
elapsedMs: number;
|
|
75
|
+
} | {
|
|
76
|
+
success: false;
|
|
77
|
+
reason: "timeout" | "unhealthy-container" | "error";
|
|
78
|
+
message: string;
|
|
79
|
+
elapsedMs: number;
|
|
80
|
+
};
|
|
81
|
+
//#endregion
|
|
82
|
+
//#region src/docker-verify/executor.d.ts
|
|
83
|
+
/** Create an executor that runs real commands, fetches, and manages process signals. */
|
|
84
|
+
declare function createRealExecutor(): DockerVerifyExecutor;
|
|
85
|
+
//#endregion
|
|
86
|
+
//#region src/docker-verify/verify.d.ts
|
|
87
|
+
/** Run the full Docker image verification lifecycle. */
|
|
88
|
+
declare function runVerification(executor: DockerVerifyExecutor, config: VerifyConfig): Promise<VerifyResult>;
|
|
89
|
+
//#endregion
|
|
90
|
+
//#region src/docker-verify/compose.d.ts
|
|
91
|
+
/** Zod schema for a single container entry from `docker compose ps --format json`. */
|
|
92
|
+
declare const ContainerInfoSchema: z.ZodObject<{
|
|
93
|
+
Service: z.ZodString;
|
|
94
|
+
Health: z.ZodString;
|
|
95
|
+
}, z.core.$strip>;
|
|
96
|
+
type ContainerInfo = z.infer<typeof ContainerInfoSchema>;
|
|
97
|
+
/** Build the `docker compose` base command string from config. */
|
|
98
|
+
declare function composeCommand(config: ComposeConfig): string;
|
|
99
|
+
/** Start the compose stack in detached mode. */
|
|
100
|
+
declare function composeUp(executor: DockerVerifyExecutor, config: ComposeConfig): void;
|
|
101
|
+
/** Tear down the compose stack, removing volumes and orphans. Swallows errors. */
|
|
102
|
+
declare function composeDown(executor: DockerVerifyExecutor, config: ComposeConfig): void;
|
|
103
|
+
/** Show logs for a specific service (or all services if not specified). Swallows errors. */
|
|
104
|
+
declare function composeLogs(executor: DockerVerifyExecutor, config: ComposeConfig, service?: string): void;
|
|
105
|
+
/**
|
|
106
|
+
* Query container status via `docker compose ps --format json`.
|
|
107
|
+
* Handles both JSON array and newline-delimited JSON (varies by docker compose version).
|
|
108
|
+
*/
|
|
109
|
+
declare function composePs(executor: DockerVerifyExecutor, config: ComposeConfig): ContainerInfo[];
|
|
110
|
+
//#endregion
|
|
111
|
+
//#region src/docker-verify/health.d.ts
|
|
112
|
+
/** Look up the health status of a specific service from container info. */
|
|
113
|
+
declare function getContainerHealth(containers: ContainerInfo[], serviceName: string): string;
|
|
114
|
+
/** Run a single HTTP health check, returning true if the validator passes. */
|
|
115
|
+
declare function checkHttpHealth(executor: DockerVerifyExecutor, check: HttpHealthCheck): Promise<boolean>;
|
|
116
|
+
//#endregion
|
|
117
|
+
export { type ComposeConfig, type ContainerInfo, type DockerVerifyExecutor, type HttpHealthCheck, type VerifyConfig, type VerifyResult, checkHttpHealth, composeCommand, composeDown, composeLogs, composePs, composeUp, createRealExecutor, getContainerHealth, runVerification };
|