@bensandee/tooling 0.45.1 → 0.46.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.mjs +318 -397
- package/package.json +3 -3
package/dist/bin.mjs
CHANGED
|
@@ -4,16 +4,16 @@ import { defineCommand, runMain } from "citty";
|
|
|
4
4
|
import * as p from "@clack/prompts";
|
|
5
5
|
import { isCancel, select } from "@clack/prompts";
|
|
6
6
|
import path from "node:path";
|
|
7
|
-
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { appendFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
8
8
|
import { execFileSync, execSync } from "node:child_process";
|
|
9
9
|
import JSON5 from "json5";
|
|
10
10
|
import { parse } from "jsonc-parser";
|
|
11
11
|
import { z } from "zod";
|
|
12
12
|
import { FatalError, TransientError, UnexpectedError } from "@bensandee/common";
|
|
13
13
|
import { isMap, isScalar, parse as parse$1, parseDocument, stringify, visit } from "yaml";
|
|
14
|
-
import { createHmac } from "node:crypto";
|
|
15
14
|
import picomatch from "picomatch";
|
|
16
15
|
import { tmpdir } from "node:os";
|
|
16
|
+
import { createHmac } from "node:crypto";
|
|
17
17
|
//#region src/types.ts
|
|
18
18
|
const LEGACY_TOOLS = [
|
|
19
19
|
"eslint",
|
|
@@ -1167,6 +1167,18 @@ function hasEnginesNode(ctx) {
|
|
|
1167
1167
|
function computeNodeVersionYaml(ctx) {
|
|
1168
1168
|
return hasEnginesNode(ctx) ? "node-version-file: package.json" : "node-version: \"24\"";
|
|
1169
1169
|
}
|
|
1170
|
+
/** Build the docker publish env block, omitting registry vars when derivable at runtime. */
|
|
1171
|
+
function dockerPublishEnv(registryDerivable) {
|
|
1172
|
+
return {
|
|
1173
|
+
...registryDerivable ? {} : {
|
|
1174
|
+
DOCKER_REGISTRY_HOST: actionsExpr("vars.DOCKER_REGISTRY_HOST"),
|
|
1175
|
+
DOCKER_REGISTRY_NAMESPACE: actionsExpr("vars.DOCKER_REGISTRY_NAMESPACE")
|
|
1176
|
+
},
|
|
1177
|
+
DOCKER_USERNAME: actionsExpr("secrets.DOCKER_USERNAME"),
|
|
1178
|
+
DOCKER_PASSWORD: actionsExpr("secrets.DOCKER_PASSWORD"),
|
|
1179
|
+
RELEASE_DEBUG: actionsExpr("vars.RELEASE_DEBUG || 'false'")
|
|
1180
|
+
};
|
|
1181
|
+
}
|
|
1170
1182
|
//#endregion
|
|
1171
1183
|
//#region src/generators/publish-ci.ts
|
|
1172
1184
|
/**
|
|
@@ -1179,18 +1191,6 @@ function hasRepositoryUrl(ctx) {
|
|
|
1179
1191
|
if (!raw) return false;
|
|
1180
1192
|
return parsePackageJson(raw)?.repository !== void 0;
|
|
1181
1193
|
}
|
|
1182
|
-
/** Build the docker publish env block, omitting registry vars when derivable at runtime. */
|
|
1183
|
-
function dockerPublishEnv$1(registryDerivable) {
|
|
1184
|
-
return {
|
|
1185
|
-
...registryDerivable ? {} : {
|
|
1186
|
-
DOCKER_REGISTRY_HOST: actionsExpr("vars.DOCKER_REGISTRY_HOST"),
|
|
1187
|
-
DOCKER_REGISTRY_NAMESPACE: actionsExpr("vars.DOCKER_REGISTRY_NAMESPACE")
|
|
1188
|
-
},
|
|
1189
|
-
DOCKER_USERNAME: actionsExpr("secrets.DOCKER_USERNAME"),
|
|
1190
|
-
DOCKER_PASSWORD: actionsExpr("secrets.DOCKER_PASSWORD"),
|
|
1191
|
-
RELEASE_DEBUG: actionsExpr("vars.RELEASE_DEBUG || 'false'")
|
|
1192
|
-
};
|
|
1193
|
-
}
|
|
1194
1194
|
function publishSteps(nodeVersionYaml, registryDerivable) {
|
|
1195
1195
|
return [
|
|
1196
1196
|
{ step: { uses: "actions/checkout@v6" } },
|
|
@@ -1202,13 +1202,13 @@ function publishSteps(nodeVersionYaml, registryDerivable) {
|
|
|
1202
1202
|
{ step: { run: "pnpm install --frozen-lockfile" } },
|
|
1203
1203
|
{ step: {
|
|
1204
1204
|
name: "Publish Docker images",
|
|
1205
|
-
env: dockerPublishEnv
|
|
1205
|
+
env: dockerPublishEnv(registryDerivable),
|
|
1206
1206
|
run: "pnpm exec bst docker:publish"
|
|
1207
1207
|
} }
|
|
1208
1208
|
];
|
|
1209
1209
|
}
|
|
1210
1210
|
const DockerMapSchema = z.object({ docker: z.record(z.string(), z.unknown()).optional() });
|
|
1211
|
-
const PostReleaseHookSchema
|
|
1211
|
+
const PostReleaseHookSchema = z.object({
|
|
1212
1212
|
postReleaseHook: z.string().optional(),
|
|
1213
1213
|
postReleaseHookSecrets: z.array(z.string()).optional()
|
|
1214
1214
|
});
|
|
@@ -1217,7 +1217,7 @@ function getPostReleaseHookCommand(ctx) {
|
|
|
1217
1217
|
const raw = ctx.read(".tooling.json");
|
|
1218
1218
|
if (!raw) return void 0;
|
|
1219
1219
|
try {
|
|
1220
|
-
const result = PostReleaseHookSchema
|
|
1220
|
+
const result = PostReleaseHookSchema.safeParse(JSON.parse(raw));
|
|
1221
1221
|
if (!result.success) return void 0;
|
|
1222
1222
|
return result.data.postReleaseHook;
|
|
1223
1223
|
} catch (error) {
|
|
@@ -1230,7 +1230,7 @@ function getPostReleaseHookSecrets(ctx) {
|
|
|
1230
1230
|
const raw = ctx.read(".tooling.json");
|
|
1231
1231
|
if (!raw) return void 0;
|
|
1232
1232
|
try {
|
|
1233
|
-
const result = PostReleaseHookSchema
|
|
1233
|
+
const result = PostReleaseHookSchema.safeParse(JSON.parse(raw));
|
|
1234
1234
|
if (!result.success) return void 0;
|
|
1235
1235
|
return result.data.postReleaseHookSecrets;
|
|
1236
1236
|
} catch (error) {
|
|
@@ -1584,7 +1584,7 @@ async function generatePackageJson(ctx) {
|
|
|
1584
1584
|
const devDeps = { ...ROOT_DEV_DEPS };
|
|
1585
1585
|
if (!isMonorepo) Object.assign(devDeps, PER_PACKAGE_DEV_DEPS);
|
|
1586
1586
|
devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.10.1";
|
|
1587
|
-
devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.
|
|
1587
|
+
devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.46.0";
|
|
1588
1588
|
if (ctx.config.useEslintPlugin) devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "0.12.0";
|
|
1589
1589
|
if (ctx.config.formatter === "oxfmt") devDeps["oxfmt"] = {
|
|
1590
1590
|
"@changesets/cli": "2.30.0",
|
|
@@ -2208,9 +2208,10 @@ function hookSecretEnv(secrets) {
|
|
|
2208
2208
|
}
|
|
2209
2209
|
/** Step that runs after release, gated on the POST_RELEASE_HOOK_URL secret being set. */
|
|
2210
2210
|
function postReleaseHookStep(opts) {
|
|
2211
|
+
const ifCondition = opts?.condition ?? (opts?.guardMain ? "github.ref == 'refs/heads/main'" : void 0);
|
|
2211
2212
|
return { step: {
|
|
2212
2213
|
name: "Post-release hook",
|
|
2213
|
-
...
|
|
2214
|
+
...ifCondition && { if: ifCondition },
|
|
2214
2215
|
env: hookSecretEnv(opts?.secrets),
|
|
2215
2216
|
run: opts?.command ?? DEFAULT_POST_RELEASE_COMMAND
|
|
2216
2217
|
} };
|
|
@@ -2282,35 +2283,25 @@ function simpleReleaseCommand() {
|
|
|
2282
2283
|
"pnpm exec bst release:simple $FLAGS"
|
|
2283
2284
|
].join("\n");
|
|
2284
2285
|
}
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
DOCKER_REGISTRY_NAMESPACE: actionsExpr("vars.DOCKER_REGISTRY_NAMESPACE")
|
|
2291
|
-
},
|
|
2292
|
-
DOCKER_USERNAME: actionsExpr("secrets.DOCKER_USERNAME"),
|
|
2293
|
-
DOCKER_PASSWORD: actionsExpr("secrets.DOCKER_PASSWORD"),
|
|
2294
|
-
RELEASE_DEBUG: actionsExpr("vars.RELEASE_DEBUG || 'false'")
|
|
2286
|
+
function simpleReleaseSteps(ci, nodeVersionYaml, publishesNpm, hasDocker, registryDerivable) {
|
|
2287
|
+
const releaseEnv = ci === "github" ? { GITHUB_TOKEN: actionsExpr("github.token") } : {
|
|
2288
|
+
FORGEJO_SERVER_URL: actionsExpr("github.server_url"),
|
|
2289
|
+
FORGEJO_REPOSITORY: actionsExpr("github.repository"),
|
|
2290
|
+
RELEASE_TOKEN: actionsExpr("secrets.RELEASE_TOKEN")
|
|
2295
2291
|
};
|
|
2296
|
-
|
|
2297
|
-
function simpleReleaseSteps(ci, nodeVersionYaml, publishesNpm, hasDocker, registryDerivable, hookSecrets) {
|
|
2292
|
+
const pushedIf = "steps.release.outputs.pushed == 'true'";
|
|
2298
2293
|
const releaseStep = { step: {
|
|
2294
|
+
id: "release",
|
|
2299
2295
|
name: "Release",
|
|
2300
2296
|
env: {
|
|
2301
|
-
...
|
|
2302
|
-
FORGEJO_SERVER_URL: actionsExpr("github.server_url"),
|
|
2303
|
-
FORGEJO_REPOSITORY: actionsExpr("github.repository"),
|
|
2304
|
-
RELEASE_TOKEN: actionsExpr("secrets.RELEASE_TOKEN")
|
|
2305
|
-
},
|
|
2306
|
-
...hookSecretEnv(hookSecrets),
|
|
2297
|
+
...releaseEnv,
|
|
2307
2298
|
RELEASE_DEBUG: actionsExpr("vars.RELEASE_DEBUG || 'false'")
|
|
2308
2299
|
},
|
|
2309
2300
|
run: simpleReleaseCommand()
|
|
2310
2301
|
} };
|
|
2311
2302
|
const dockerStep = { step: {
|
|
2312
2303
|
name: "Publish Docker images",
|
|
2313
|
-
if:
|
|
2304
|
+
if: pushedIf,
|
|
2314
2305
|
env: dockerPublishEnv(registryDerivable),
|
|
2315
2306
|
run: "pnpm exec bst docker:publish"
|
|
2316
2307
|
} };
|
|
@@ -2320,10 +2311,10 @@ function simpleReleaseSteps(ci, nodeVersionYaml, publishesNpm, hasDocker, regist
|
|
|
2320
2311
|
...hasDocker ? [dockerStep] : []
|
|
2321
2312
|
];
|
|
2322
2313
|
}
|
|
2323
|
-
function buildSteps(strategy, ci, nodeVersionYaml, publishesNpm, hasDocker, registryDerivable
|
|
2314
|
+
function buildSteps(strategy, ci, nodeVersionYaml, publishesNpm, hasDocker, registryDerivable) {
|
|
2324
2315
|
switch (strategy) {
|
|
2325
2316
|
case "release-it": return releaseItSteps(ci, nodeVersionYaml, publishesNpm);
|
|
2326
|
-
case "simple": return simpleReleaseSteps(ci, nodeVersionYaml, publishesNpm, hasDocker, registryDerivable
|
|
2317
|
+
case "simple": return simpleReleaseSteps(ci, nodeVersionYaml, publishesNpm, hasDocker, registryDerivable);
|
|
2327
2318
|
default: return null;
|
|
2328
2319
|
}
|
|
2329
2320
|
}
|
|
@@ -2346,7 +2337,7 @@ async function generateReleaseCi(ctx) {
|
|
|
2346
2337
|
const hasDocker = hasDockerPackages(ctx);
|
|
2347
2338
|
const registryDerivable = hasRepositoryUrl(ctx);
|
|
2348
2339
|
const hookSecrets = getPostReleaseHookSecrets(ctx);
|
|
2349
|
-
const steps = buildSteps(ctx.config.releaseStrategy, ctx.config.ci, nodeVersionYaml, publishesNpm, hasDocker, registryDerivable
|
|
2340
|
+
const steps = buildSteps(ctx.config.releaseStrategy, ctx.config.ci, nodeVersionYaml, publishesNpm, hasDocker, registryDerivable);
|
|
2350
2341
|
if (!steps) return {
|
|
2351
2342
|
filePath,
|
|
2352
2343
|
action: "skipped",
|
|
@@ -2356,6 +2347,11 @@ async function generateReleaseCi(ctx) {
|
|
|
2356
2347
|
command: getPostReleaseHookCommand(ctx),
|
|
2357
2348
|
secrets: hookSecrets
|
|
2358
2349
|
}));
|
|
2350
|
+
else if (ctx.config.releaseStrategy === "simple") steps.push(postReleaseHookStep({
|
|
2351
|
+
command: getPostReleaseHookCommand(ctx),
|
|
2352
|
+
secrets: hookSecrets,
|
|
2353
|
+
condition: "steps.release.outputs.pushed == 'true'"
|
|
2354
|
+
}));
|
|
2359
2355
|
const on = ctx.config.releaseStrategy === "simple" ? simpleWorkflowDispatchTrigger() : { workflow_dispatch: null };
|
|
2360
2356
|
const content = buildWorkflowYaml({
|
|
2361
2357
|
ci: ctx.config.ci,
|
|
@@ -2391,8 +2387,9 @@ async function generateReleaseCi(ctx) {
|
|
|
2391
2387
|
//#endregion
|
|
2392
2388
|
//#region src/generators/ci.ts
|
|
2393
2389
|
/** Build the release step for the check job (changesets strategy). */
|
|
2394
|
-
function changesetsReleaseStep(ci, publishesNpm
|
|
2390
|
+
function changesetsReleaseStep(ci, publishesNpm) {
|
|
2395
2391
|
if (ci === "github") return { step: {
|
|
2392
|
+
id: "release",
|
|
2396
2393
|
uses: "changesets/action@v1",
|
|
2397
2394
|
if: "github.ref == 'refs/heads/main'",
|
|
2398
2395
|
with: {
|
|
@@ -2405,6 +2402,7 @@ function changesetsReleaseStep(ci, publishesNpm, hookSecrets) {
|
|
|
2405
2402
|
}
|
|
2406
2403
|
} };
|
|
2407
2404
|
return { step: {
|
|
2405
|
+
id: "release",
|
|
2408
2406
|
name: "Release",
|
|
2409
2407
|
if: "github.ref == 'refs/heads/main'",
|
|
2410
2408
|
env: {
|
|
@@ -2412,7 +2410,6 @@ function changesetsReleaseStep(ci, publishesNpm, hookSecrets) {
|
|
|
2412
2410
|
FORGEJO_REPOSITORY: actionsExpr("github.repository"),
|
|
2413
2411
|
RELEASE_TOKEN: actionsExpr("secrets.RELEASE_TOKEN"),
|
|
2414
2412
|
...publishesNpm && { NODE_AUTH_TOKEN: actionsExpr("secrets.NPM_TOKEN") },
|
|
2415
|
-
...hookSecretEnv(hookSecrets),
|
|
2416
2413
|
RELEASE_DEBUG: actionsExpr("vars.RELEASE_DEBUG || 'false'")
|
|
2417
2414
|
},
|
|
2418
2415
|
run: "pnpm exec bst release:changesets"
|
|
@@ -2462,14 +2459,22 @@ async function generateCi(ctx) {
|
|
|
2462
2459
|
const publishesNpm = ctx.config.publishNpm === true;
|
|
2463
2460
|
const hookCommand = getPostReleaseHookCommand(ctx);
|
|
2464
2461
|
const hookSecrets = getPostReleaseHookSecrets(ctx);
|
|
2465
|
-
const
|
|
2462
|
+
const hasDocker = isChangesets && hasDockerPackages(ctx);
|
|
2463
|
+
const registryDerivable = hasDocker && hasRepositoryUrl(ctx);
|
|
2464
|
+
const publishedIf = "steps.release.outputs.published == 'true'";
|
|
2466
2465
|
const steps = [
|
|
2467
2466
|
...checkSteps(nodeVersionYaml, isChangesets && publishesNpm),
|
|
2468
|
-
...isChangesets ? [changesetsReleaseStep(ctx.config.ci, publishesNpm
|
|
2469
|
-
...
|
|
2467
|
+
...isChangesets ? [changesetsReleaseStep(ctx.config.ci, publishesNpm)] : [],
|
|
2468
|
+
...hasDocker ? [{ step: {
|
|
2469
|
+
name: "Publish Docker images",
|
|
2470
|
+
if: publishedIf,
|
|
2471
|
+
env: dockerPublishEnv(registryDerivable),
|
|
2472
|
+
run: "pnpm exec bst docker:publish"
|
|
2473
|
+
} }] : [],
|
|
2474
|
+
...isChangesets ? [postReleaseHookStep({
|
|
2470
2475
|
command: hookCommand,
|
|
2471
2476
|
secrets: hookSecrets,
|
|
2472
|
-
|
|
2477
|
+
condition: publishedIf
|
|
2473
2478
|
})] : []
|
|
2474
2479
|
];
|
|
2475
2480
|
const content = buildWorkflowYaml({
|
|
@@ -3344,7 +3349,7 @@ async function runGenerators(ctx) {
|
|
|
3344
3349
|
function generateMigratePrompt(results, config, detected) {
|
|
3345
3350
|
const sections = [];
|
|
3346
3351
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
3347
|
-
sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.
|
|
3352
|
+
sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.46.0 repo:sync\` on ${timestamp}_\n`);
|
|
3348
3353
|
const changed = results.filter((r) => r.action === "created" || r.action === "updated" || r.action === "archived");
|
|
3349
3354
|
if (changed.length > 0) {
|
|
3350
3355
|
const items = changed.map((r) => {
|
|
@@ -4251,6 +4256,111 @@ async function runVersionMode(executor, config) {
|
|
|
4251
4256
|
};
|
|
4252
4257
|
}
|
|
4253
4258
|
//#endregion
|
|
4259
|
+
//#region src/release/publish.ts
|
|
4260
|
+
/** Mode 2: publish to npm, push tags, and create Forgejo releases. */
|
|
4261
|
+
async function runPublishMode(executor, config) {
|
|
4262
|
+
log.info("No changesets — publishing packages");
|
|
4263
|
+
const publishResult = executor.exec("pnpm", ["changeset", "publish"], { cwd: config.cwd });
|
|
4264
|
+
debugExec(config, "pnpm changeset publish", publishResult);
|
|
4265
|
+
if (publishResult.exitCode !== 0) throw new FatalError(`pnpm changeset publish failed (exit code ${String(publishResult.exitCode)}):\n${publishResult.stderr}`);
|
|
4266
|
+
const stdoutTags = parseNewTags(publishResult.stdout + "\n" + publishResult.stderr);
|
|
4267
|
+
debug(config, `Tags from publish stdout: ${stdoutTags.length > 0 ? stdoutTags.join(", ") : "(none)"}`);
|
|
4268
|
+
const expectedTags = computeExpectedTags(executor.listWorkspacePackages(config.cwd));
|
|
4269
|
+
debug(config, `Expected tags from workspace packages: ${expectedTags.length > 0 ? expectedTags.join(", ") : "(none)"}`);
|
|
4270
|
+
const remoteTags = parseRemoteTags(executor.exec("git", [
|
|
4271
|
+
"ls-remote",
|
|
4272
|
+
"--tags",
|
|
4273
|
+
"origin"
|
|
4274
|
+
], { cwd: config.cwd }).stdout);
|
|
4275
|
+
debug(config, `Remote tags: ${remoteTags.length > 0 ? remoteTags.join(", ") : "(none)"}`);
|
|
4276
|
+
const remoteSet = new Set(remoteTags);
|
|
4277
|
+
const tagsToPush = reconcileTags(expectedTags, remoteTags, stdoutTags);
|
|
4278
|
+
debug(config, `Reconciled tags to push: ${tagsToPush.length > 0 ? tagsToPush.join(", ") : "(none)"}`);
|
|
4279
|
+
if (config.dryRun) {
|
|
4280
|
+
if (tagsToPush.length === 0) {
|
|
4281
|
+
log.info("No packages were published");
|
|
4282
|
+
return { mode: "none" };
|
|
4283
|
+
}
|
|
4284
|
+
log.info(`Tags to process: ${tagsToPush.join(", ")}`);
|
|
4285
|
+
log.info("[dry-run] Would push tags and create releases");
|
|
4286
|
+
return {
|
|
4287
|
+
mode: "publish",
|
|
4288
|
+
tags: tagsToPush
|
|
4289
|
+
};
|
|
4290
|
+
}
|
|
4291
|
+
const conn = {
|
|
4292
|
+
serverUrl: config.serverUrl,
|
|
4293
|
+
repository: config.repository,
|
|
4294
|
+
token: config.token
|
|
4295
|
+
};
|
|
4296
|
+
const remoteExpectedTags = expectedTags.filter((t) => remoteSet.has(t) && !tagsToPush.includes(t));
|
|
4297
|
+
const tagsWithMissingReleases = [];
|
|
4298
|
+
for (const tag of remoteExpectedTags) if (!await findRelease(executor, conn, tag)) tagsWithMissingReleases.push(tag);
|
|
4299
|
+
const allTags = [...tagsToPush, ...tagsWithMissingReleases];
|
|
4300
|
+
if (allTags.length === 0) {
|
|
4301
|
+
log.info("No packages were published");
|
|
4302
|
+
return { mode: "none" };
|
|
4303
|
+
}
|
|
4304
|
+
log.info(`Tags to process: ${allTags.join(", ")}`);
|
|
4305
|
+
const errors = [];
|
|
4306
|
+
for (const tag of allTags) try {
|
|
4307
|
+
if (!remoteSet.has(tag)) {
|
|
4308
|
+
if (executor.exec("git", [
|
|
4309
|
+
"tag",
|
|
4310
|
+
"-l",
|
|
4311
|
+
tag
|
|
4312
|
+
], { cwd: config.cwd }).stdout.trim() === "") {
|
|
4313
|
+
const tagResult = executor.exec("git", ["tag", tag], { cwd: config.cwd });
|
|
4314
|
+
if (tagResult.exitCode !== 0) throw new FatalError(`Failed to create tag ${tag}: ${tagResult.stderr || tagResult.stdout}`);
|
|
4315
|
+
}
|
|
4316
|
+
const pushTagResult = executor.exec("git", [
|
|
4317
|
+
"push",
|
|
4318
|
+
"origin",
|
|
4319
|
+
`refs/tags/${tag}`
|
|
4320
|
+
], { cwd: config.cwd });
|
|
4321
|
+
if (pushTagResult.exitCode !== 0) throw new FatalError(`Failed to push tag ${tag}: ${pushTagResult.stderr || pushTagResult.stdout}`);
|
|
4322
|
+
}
|
|
4323
|
+
if (await ensureRelease(executor, conn, tag) === "exists") log.warn(`Release for ${tag} already exists — skipping`);
|
|
4324
|
+
else log.info(`Created release for ${tag}`);
|
|
4325
|
+
} catch (error) {
|
|
4326
|
+
errors.push({
|
|
4327
|
+
tag,
|
|
4328
|
+
error
|
|
4329
|
+
});
|
|
4330
|
+
log.warn(`Failed to process ${tag}: ${error instanceof Error ? error.message : String(error)}`);
|
|
4331
|
+
}
|
|
4332
|
+
if (errors.length > 0) throw new TransientError(`Failed to create releases for: ${errors.map((e) => e.tag).join(", ")}`);
|
|
4333
|
+
const slidingTags = allTags.flatMap((t) => {
|
|
4334
|
+
const atIndex = t.lastIndexOf("@");
|
|
4335
|
+
if (atIndex <= 0) return [];
|
|
4336
|
+
const name = t.slice(0, atIndex);
|
|
4337
|
+
return computeSlidingTags(t.slice(atIndex + 1)).map((suffix) => `${name}@${suffix}`);
|
|
4338
|
+
});
|
|
4339
|
+
if (slidingTags.length > 0) {
|
|
4340
|
+
for (const tag of slidingTags) {
|
|
4341
|
+
const tagResult = executor.exec("git", [
|
|
4342
|
+
"tag",
|
|
4343
|
+
"-f",
|
|
4344
|
+
tag
|
|
4345
|
+
], { cwd: config.cwd });
|
|
4346
|
+
if (tagResult.exitCode !== 0) log.warn(`Warning: Failed to create sliding tag ${tag}: ${tagResult.stderr || tagResult.stdout}`);
|
|
4347
|
+
}
|
|
4348
|
+
const forcePushResult = executor.exec("git", [
|
|
4349
|
+
"push",
|
|
4350
|
+
"origin",
|
|
4351
|
+
...slidingTags.map((t) => `refs/tags/${t}`),
|
|
4352
|
+
"--force"
|
|
4353
|
+
], { cwd: config.cwd });
|
|
4354
|
+
debugExec(config, "force-push sliding tags", forcePushResult);
|
|
4355
|
+
if (forcePushResult.exitCode !== 0) log.warn(`Warning: Failed to push sliding tags: ${forcePushResult.stderr || forcePushResult.stdout}`);
|
|
4356
|
+
else log.info(`Created sliding tags: ${slidingTags.join(", ")}`);
|
|
4357
|
+
}
|
|
4358
|
+
return {
|
|
4359
|
+
mode: "publish",
|
|
4360
|
+
tags: allTags
|
|
4361
|
+
};
|
|
4362
|
+
}
|
|
4363
|
+
//#endregion
|
|
4254
4364
|
//#region src/release/connection.ts
|
|
4255
4365
|
const RepositorySchema = z.union([z.string(), z.object({ url: z.string() })]);
|
|
4256
4366
|
/**
|
|
@@ -4364,345 +4474,56 @@ function configureGitIdentity(executor, platform, cwd) {
|
|
|
4364
4474
|
], { cwd });
|
|
4365
4475
|
}
|
|
4366
4476
|
//#endregion
|
|
4367
|
-
//#region src/release
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
draft: false,
|
|
4378
|
-
prerelease: false,
|
|
4379
|
-
author: { login: actor }
|
|
4380
|
-
},
|
|
4381
|
-
repository: {
|
|
4382
|
-
full_name: options.repository,
|
|
4383
|
-
html_url: `${options.serverUrl}/${options.repository}`
|
|
4477
|
+
//#region src/commands/release-changesets.ts
|
|
4478
|
+
const releaseForgejoCommand = defineCommand({
|
|
4479
|
+
meta: {
|
|
4480
|
+
name: "release:changesets",
|
|
4481
|
+
description: "Changesets version/publish for Forgejo CI"
|
|
4482
|
+
},
|
|
4483
|
+
args: {
|
|
4484
|
+
"dry-run": {
|
|
4485
|
+
type: "boolean",
|
|
4486
|
+
description: "Skip push, API calls, and publishing side effects"
|
|
4384
4487
|
},
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4488
|
+
verbose: {
|
|
4489
|
+
type: "boolean",
|
|
4490
|
+
description: "Enable detailed debug logging (also enabled by TOOLING_DEBUG env var)"
|
|
4491
|
+
}
|
|
4492
|
+
},
|
|
4493
|
+
async run({ args }) {
|
|
4494
|
+
const result = await runRelease(buildReleaseConfig({
|
|
4495
|
+
dryRun: args["dry-run"] === true,
|
|
4496
|
+
verbose: args.verbose === true || isEnvVerbose()
|
|
4497
|
+
}), createRealExecutor());
|
|
4498
|
+
if (result.mode === "publish") {
|
|
4499
|
+
const outputFile = process.env["GITHUB_OUTPUT"];
|
|
4500
|
+
if (outputFile) try {
|
|
4501
|
+
appendFileSync(outputFile, "published=true\n");
|
|
4502
|
+
} catch (_error) {}
|
|
4503
|
+
}
|
|
4504
|
+
if (result.mode === "none") process.exitCode = 0;
|
|
4505
|
+
}
|
|
4506
|
+
});
|
|
4507
|
+
/** Build release config from environment / package.json and CLI flags. */
|
|
4508
|
+
function buildReleaseConfig(flags) {
|
|
4509
|
+
const resolved = resolveConnection(process.cwd());
|
|
4510
|
+
if (resolved.type !== "forgejo") throw new FatalError("release:changesets requires a Forgejo repository");
|
|
4393
4511
|
return {
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
draft: false,
|
|
4399
|
-
prerelease: options.prerelease ?? false,
|
|
4400
|
-
author: { login: actor }
|
|
4401
|
-
},
|
|
4402
|
-
repository: {
|
|
4403
|
-
full_name: repository,
|
|
4404
|
-
html_url: repository ? `${serverUrl}/${repository}` : ""
|
|
4405
|
-
},
|
|
4406
|
-
sender: { login: actor }
|
|
4512
|
+
...resolved.conn,
|
|
4513
|
+
cwd: process.cwd(),
|
|
4514
|
+
dryRun: flags.dryRun ?? false,
|
|
4515
|
+
verbose: flags.verbose ?? false
|
|
4407
4516
|
};
|
|
4408
4517
|
}
|
|
4409
|
-
/**
|
|
4410
|
-
function
|
|
4411
|
-
const repository = process.env["GITHUB_REPOSITORY"];
|
|
4518
|
+
/** Resolve the current branch from CI env vars or git. */
|
|
4519
|
+
function getCurrentBranch(executor, cwd) {
|
|
4412
4520
|
const ref = process.env["GITHUB_REF"];
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
release: {
|
|
4420
|
-
tag_name: tagName,
|
|
4421
|
-
name: tagName,
|
|
4422
|
-
draft: false,
|
|
4423
|
-
prerelease: false,
|
|
4424
|
-
author: { login: actor }
|
|
4425
|
-
},
|
|
4426
|
-
repository: {
|
|
4427
|
-
full_name: repository,
|
|
4428
|
-
html_url: `${serverUrl}/${repository}`
|
|
4429
|
-
},
|
|
4430
|
-
sender: { login: actor }
|
|
4431
|
-
};
|
|
4432
|
-
}
|
|
4433
|
-
/**
|
|
4434
|
-
* Compute the HMAC-SHA256 hex digest of data using the given secret.
|
|
4435
|
-
* Returns the signature in GitHub-style format: `sha256=<hex>`.
|
|
4436
|
-
*/
|
|
4437
|
-
function computeSignature(secret, data) {
|
|
4438
|
-
const hmac = createHmac("sha256", secret);
|
|
4439
|
-
hmac.update(data);
|
|
4440
|
-
return `sha256=${hmac.digest("hex")}`;
|
|
4441
|
-
}
|
|
4442
|
-
/**
|
|
4443
|
-
* POST to a webhook URL with GitHub-style HMAC-SHA256 authentication.
|
|
4444
|
-
*
|
|
4445
|
-
* Sends a JSON-serialized release payload (or empty string if no payload).
|
|
4446
|
-
* If a secret is provided, computes the HMAC-SHA256 signature of the body
|
|
4447
|
-
* and includes it in the `X-Hub-Signature-256` header.
|
|
4448
|
-
*
|
|
4449
|
-
* Never throws — returns a result object indicating success or failure.
|
|
4450
|
-
*/
|
|
4451
|
-
async function sendWebhook(options) {
|
|
4452
|
-
const { url, secret, payload, fetcher = globalThis.fetch } = options;
|
|
4453
|
-
const body = payload ? JSON.stringify(payload) : "";
|
|
4454
|
-
const headers = { "Content-Type": "application/json" };
|
|
4455
|
-
if (secret) headers["X-Hub-Signature-256"] = computeSignature(secret, body);
|
|
4456
|
-
try {
|
|
4457
|
-
const response = await fetcher(url, {
|
|
4458
|
-
method: "POST",
|
|
4459
|
-
headers,
|
|
4460
|
-
body
|
|
4461
|
-
});
|
|
4462
|
-
if (!response.ok) return {
|
|
4463
|
-
ok: false,
|
|
4464
|
-
status: response.status,
|
|
4465
|
-
error: `HTTP ${String(response.status)}: ${await response.text().catch(() => "")}`
|
|
4466
|
-
};
|
|
4467
|
-
return {
|
|
4468
|
-
ok: true,
|
|
4469
|
-
status: response.status
|
|
4470
|
-
};
|
|
4471
|
-
} catch (error) {
|
|
4472
|
-
return {
|
|
4473
|
-
ok: false,
|
|
4474
|
-
error: error instanceof Error ? error.message : String(error)
|
|
4475
|
-
};
|
|
4476
|
-
}
|
|
4477
|
-
}
|
|
4478
|
-
/**
|
|
4479
|
-
* Send a webhook and log the result. Intended for CLI usage.
|
|
4480
|
-
* Never throws.
|
|
4481
|
-
*/
|
|
4482
|
-
async function sendWebhookWithLogging(url, secret, payload) {
|
|
4483
|
-
const result = await sendWebhook({
|
|
4484
|
-
url,
|
|
4485
|
-
secret,
|
|
4486
|
-
payload
|
|
4487
|
-
});
|
|
4488
|
-
if (result.ok) log.info("Post-release hook completed successfully");
|
|
4489
|
-
else log.warn(`Post-release hook failed: ${result.error ?? "unknown error"}`);
|
|
4490
|
-
}
|
|
4491
|
-
//#endregion
|
|
4492
|
-
//#region src/release/hook.ts
|
|
4493
|
-
const PostReleaseHookSchema = z.object({ postReleaseHook: z.string().optional() });
|
|
4494
|
-
/** Read the postReleaseHook command override from .tooling.json at runtime. */
|
|
4495
|
-
function loadHookCommand(executor, cwd) {
|
|
4496
|
-
const configPath = path.join(cwd, ".tooling.json");
|
|
4497
|
-
const raw = executor.readFile(configPath);
|
|
4498
|
-
if (!raw) return void 0;
|
|
4499
|
-
try {
|
|
4500
|
-
const result = PostReleaseHookSchema.safeParse(JSON.parse(raw));
|
|
4501
|
-
if (!result.success) return void 0;
|
|
4502
|
-
return result.data.postReleaseHook;
|
|
4503
|
-
} catch (_error) {
|
|
4504
|
-
return;
|
|
4505
|
-
}
|
|
4506
|
-
}
|
|
4507
|
-
/**
|
|
4508
|
-
* Execute the post-release hook after a successful publish.
|
|
4509
|
-
*
|
|
4510
|
-
* Uses the `postReleaseHook` command from `.tooling.json` if configured,
|
|
4511
|
-
* otherwise POSTs to `POST_RELEASE_HOOK_URL` with HMAC-SHA256 authentication
|
|
4512
|
-
* using `WEBHOOK_SECRET`. Both env vars must be set by the CI workflow.
|
|
4513
|
-
*
|
|
4514
|
-
* Failures are logged as warnings — they never fail the release.
|
|
4515
|
-
*/
|
|
4516
|
-
async function runPostReleaseHook(executor, config, payload) {
|
|
4517
|
-
if (config.dryRun) {
|
|
4518
|
-
debug(config, "Skipping post-release hook (dry-run)");
|
|
4519
|
-
return;
|
|
4520
|
-
}
|
|
4521
|
-
const customCommand = loadHookCommand(executor, config.cwd);
|
|
4522
|
-
if (customCommand) {
|
|
4523
|
-
debug(config, `Running custom post-release hook: ${customCommand}`);
|
|
4524
|
-
executeHookCommand(executor, config, customCommand);
|
|
4525
|
-
return;
|
|
4526
|
-
}
|
|
4527
|
-
const hookUrl = process.env["POST_RELEASE_HOOK_URL"];
|
|
4528
|
-
if (!hookUrl) {
|
|
4529
|
-
debug(config, "No POST_RELEASE_HOOK_URL set — skipping post-release hook");
|
|
4530
|
-
return;
|
|
4531
|
-
}
|
|
4532
|
-
const hookSecret = process.env["WEBHOOK_SECRET"];
|
|
4533
|
-
debug(config, `Running post-release hook: POST ${hookUrl}`);
|
|
4534
|
-
const result = await sendWebhook({
|
|
4535
|
-
url: hookUrl,
|
|
4536
|
-
secret: hookSecret || void 0,
|
|
4537
|
-
payload,
|
|
4538
|
-
fetcher: executor.fetch.bind(executor)
|
|
4539
|
-
});
|
|
4540
|
-
if (result.ok) log.info("Post-release hook completed successfully");
|
|
4541
|
-
else log.warn(`Post-release hook failed: ${result.error ?? "unknown error"}`);
|
|
4542
|
-
}
|
|
4543
|
-
function executeHookCommand(executor, config, command) {
|
|
4544
|
-
try {
|
|
4545
|
-
const result = executor.exec("sh", ["-c", command], { cwd: config.cwd });
|
|
4546
|
-
if (result.exitCode !== 0) log.warn(`Post-release hook failed (exit code ${String(result.exitCode)}): ${result.stderr || result.stdout}`);
|
|
4547
|
-
else log.info("Post-release hook completed successfully");
|
|
4548
|
-
} catch (error) {
|
|
4549
|
-
log.warn(`Post-release hook error: ${error instanceof Error ? error.message : String(error)}`);
|
|
4550
|
-
}
|
|
4551
|
-
}
|
|
4552
|
-
//#endregion
|
|
4553
|
-
//#region src/release/publish.ts
|
|
4554
|
-
/** Mode 2: publish to npm, push tags, and create Forgejo releases. */
|
|
4555
|
-
async function runPublishMode(executor, config) {
|
|
4556
|
-
log.info("No changesets — publishing packages");
|
|
4557
|
-
const publishResult = executor.exec("pnpm", ["changeset", "publish"], { cwd: config.cwd });
|
|
4558
|
-
debugExec(config, "pnpm changeset publish", publishResult);
|
|
4559
|
-
if (publishResult.exitCode !== 0) throw new FatalError(`pnpm changeset publish failed (exit code ${String(publishResult.exitCode)}):\n${publishResult.stderr}`);
|
|
4560
|
-
const stdoutTags = parseNewTags(publishResult.stdout + "\n" + publishResult.stderr);
|
|
4561
|
-
debug(config, `Tags from publish stdout: ${stdoutTags.length > 0 ? stdoutTags.join(", ") : "(none)"}`);
|
|
4562
|
-
const expectedTags = computeExpectedTags(executor.listWorkspacePackages(config.cwd));
|
|
4563
|
-
debug(config, `Expected tags from workspace packages: ${expectedTags.length > 0 ? expectedTags.join(", ") : "(none)"}`);
|
|
4564
|
-
const remoteTags = parseRemoteTags(executor.exec("git", [
|
|
4565
|
-
"ls-remote",
|
|
4566
|
-
"--tags",
|
|
4567
|
-
"origin"
|
|
4568
|
-
], { cwd: config.cwd }).stdout);
|
|
4569
|
-
debug(config, `Remote tags: ${remoteTags.length > 0 ? remoteTags.join(", ") : "(none)"}`);
|
|
4570
|
-
const remoteSet = new Set(remoteTags);
|
|
4571
|
-
const tagsToPush = reconcileTags(expectedTags, remoteTags, stdoutTags);
|
|
4572
|
-
debug(config, `Reconciled tags to push: ${tagsToPush.length > 0 ? tagsToPush.join(", ") : "(none)"}`);
|
|
4573
|
-
if (config.dryRun) {
|
|
4574
|
-
if (tagsToPush.length === 0) {
|
|
4575
|
-
log.info("No packages were published");
|
|
4576
|
-
return { mode: "none" };
|
|
4577
|
-
}
|
|
4578
|
-
log.info(`Tags to process: ${tagsToPush.join(", ")}`);
|
|
4579
|
-
log.info("[dry-run] Would push tags and create releases");
|
|
4580
|
-
return {
|
|
4581
|
-
mode: "publish",
|
|
4582
|
-
tags: tagsToPush
|
|
4583
|
-
};
|
|
4584
|
-
}
|
|
4585
|
-
const conn = {
|
|
4586
|
-
serverUrl: config.serverUrl,
|
|
4587
|
-
repository: config.repository,
|
|
4588
|
-
token: config.token
|
|
4589
|
-
};
|
|
4590
|
-
const remoteExpectedTags = expectedTags.filter((t) => remoteSet.has(t) && !tagsToPush.includes(t));
|
|
4591
|
-
const tagsWithMissingReleases = [];
|
|
4592
|
-
for (const tag of remoteExpectedTags) if (!await findRelease(executor, conn, tag)) tagsWithMissingReleases.push(tag);
|
|
4593
|
-
const allTags = [...tagsToPush, ...tagsWithMissingReleases];
|
|
4594
|
-
if (allTags.length === 0) {
|
|
4595
|
-
log.info("No packages were published");
|
|
4596
|
-
return { mode: "none" };
|
|
4597
|
-
}
|
|
4598
|
-
log.info(`Tags to process: ${allTags.join(", ")}`);
|
|
4599
|
-
const errors = [];
|
|
4600
|
-
for (const tag of allTags) try {
|
|
4601
|
-
if (!remoteSet.has(tag)) {
|
|
4602
|
-
if (executor.exec("git", [
|
|
4603
|
-
"tag",
|
|
4604
|
-
"-l",
|
|
4605
|
-
tag
|
|
4606
|
-
], { cwd: config.cwd }).stdout.trim() === "") {
|
|
4607
|
-
const tagResult = executor.exec("git", ["tag", tag], { cwd: config.cwd });
|
|
4608
|
-
if (tagResult.exitCode !== 0) throw new FatalError(`Failed to create tag ${tag}: ${tagResult.stderr || tagResult.stdout}`);
|
|
4609
|
-
}
|
|
4610
|
-
const pushTagResult = executor.exec("git", [
|
|
4611
|
-
"push",
|
|
4612
|
-
"origin",
|
|
4613
|
-
`refs/tags/${tag}`
|
|
4614
|
-
], { cwd: config.cwd });
|
|
4615
|
-
if (pushTagResult.exitCode !== 0) throw new FatalError(`Failed to push tag ${tag}: ${pushTagResult.stderr || pushTagResult.stdout}`);
|
|
4616
|
-
}
|
|
4617
|
-
if (await ensureRelease(executor, conn, tag) === "exists") log.warn(`Release for ${tag} already exists — skipping`);
|
|
4618
|
-
else log.info(`Created release for ${tag}`);
|
|
4619
|
-
} catch (error) {
|
|
4620
|
-
errors.push({
|
|
4621
|
-
tag,
|
|
4622
|
-
error
|
|
4623
|
-
});
|
|
4624
|
-
log.warn(`Failed to process ${tag}: ${error instanceof Error ? error.message : String(error)}`);
|
|
4625
|
-
}
|
|
4626
|
-
if (errors.length > 0) throw new TransientError(`Failed to create releases for: ${errors.map((e) => e.tag).join(", ")}`);
|
|
4627
|
-
const slidingTags = allTags.flatMap((t) => {
|
|
4628
|
-
const atIndex = t.lastIndexOf("@");
|
|
4629
|
-
if (atIndex <= 0) return [];
|
|
4630
|
-
const name = t.slice(0, atIndex);
|
|
4631
|
-
return computeSlidingTags(t.slice(atIndex + 1)).map((suffix) => `${name}@${suffix}`);
|
|
4632
|
-
});
|
|
4633
|
-
if (slidingTags.length > 0) {
|
|
4634
|
-
for (const tag of slidingTags) {
|
|
4635
|
-
const tagResult = executor.exec("git", [
|
|
4636
|
-
"tag",
|
|
4637
|
-
"-f",
|
|
4638
|
-
tag
|
|
4639
|
-
], { cwd: config.cwd });
|
|
4640
|
-
if (tagResult.exitCode !== 0) log.warn(`Warning: Failed to create sliding tag ${tag}: ${tagResult.stderr || tagResult.stdout}`);
|
|
4641
|
-
}
|
|
4642
|
-
const forcePushResult = executor.exec("git", [
|
|
4643
|
-
"push",
|
|
4644
|
-
"origin",
|
|
4645
|
-
...slidingTags.map((t) => `refs/tags/${t}`),
|
|
4646
|
-
"--force"
|
|
4647
|
-
], { cwd: config.cwd });
|
|
4648
|
-
debugExec(config, "force-push sliding tags", forcePushResult);
|
|
4649
|
-
if (forcePushResult.exitCode !== 0) log.warn(`Warning: Failed to push sliding tags: ${forcePushResult.stderr || forcePushResult.stdout}`);
|
|
4650
|
-
else log.info(`Created sliding tags: ${slidingTags.join(", ")}`);
|
|
4651
|
-
}
|
|
4652
|
-
await runPostReleaseHook(executor, config, buildChangesetsPayload({
|
|
4653
|
-
tags: allTags,
|
|
4654
|
-
repository: config.repository,
|
|
4655
|
-
serverUrl: config.serverUrl
|
|
4656
|
-
}));
|
|
4657
|
-
return {
|
|
4658
|
-
mode: "publish",
|
|
4659
|
-
tags: allTags
|
|
4660
|
-
};
|
|
4661
|
-
}
|
|
4662
|
-
//#endregion
|
|
4663
|
-
//#region src/commands/release-changesets.ts
|
|
4664
|
-
const releaseForgejoCommand = defineCommand({
|
|
4665
|
-
meta: {
|
|
4666
|
-
name: "release:changesets",
|
|
4667
|
-
description: "Changesets version/publish for Forgejo CI"
|
|
4668
|
-
},
|
|
4669
|
-
args: {
|
|
4670
|
-
"dry-run": {
|
|
4671
|
-
type: "boolean",
|
|
4672
|
-
description: "Skip push, API calls, and publishing side effects"
|
|
4673
|
-
},
|
|
4674
|
-
verbose: {
|
|
4675
|
-
type: "boolean",
|
|
4676
|
-
description: "Enable detailed debug logging (also enabled by TOOLING_DEBUG env var)"
|
|
4677
|
-
}
|
|
4678
|
-
},
|
|
4679
|
-
async run({ args }) {
|
|
4680
|
-
if ((await runRelease(buildReleaseConfig({
|
|
4681
|
-
dryRun: args["dry-run"] === true,
|
|
4682
|
-
verbose: args.verbose === true || isEnvVerbose()
|
|
4683
|
-
}), createRealExecutor())).mode === "none") process.exitCode = 0;
|
|
4684
|
-
}
|
|
4685
|
-
});
|
|
4686
|
-
/** Build release config from environment / package.json and CLI flags. */
|
|
4687
|
-
function buildReleaseConfig(flags) {
|
|
4688
|
-
const resolved = resolveConnection(process.cwd());
|
|
4689
|
-
if (resolved.type !== "forgejo") throw new FatalError("release:changesets requires a Forgejo repository");
|
|
4690
|
-
return {
|
|
4691
|
-
...resolved.conn,
|
|
4692
|
-
cwd: process.cwd(),
|
|
4693
|
-
dryRun: flags.dryRun ?? false,
|
|
4694
|
-
verbose: flags.verbose ?? false
|
|
4695
|
-
};
|
|
4696
|
-
}
|
|
4697
|
-
/** Resolve the current branch from CI env vars or git. */
|
|
4698
|
-
function getCurrentBranch(executor, cwd) {
|
|
4699
|
-
const ref = process.env["GITHUB_REF"];
|
|
4700
|
-
if (ref?.startsWith("refs/heads/")) return ref.slice(11);
|
|
4701
|
-
return executor.exec("git", [
|
|
4702
|
-
"rev-parse",
|
|
4703
|
-
"--abbrev-ref",
|
|
4704
|
-
"HEAD"
|
|
4705
|
-
], { cwd }).stdout.trim();
|
|
4521
|
+
if (ref?.startsWith("refs/heads/")) return ref.slice(11);
|
|
4522
|
+
return executor.exec("git", [
|
|
4523
|
+
"rev-parse",
|
|
4524
|
+
"--abbrev-ref",
|
|
4525
|
+
"HEAD"
|
|
4526
|
+
], { cwd }).stdout.trim();
|
|
4706
4527
|
}
|
|
4707
4528
|
/** Core release logic — testable with a mock executor. */
|
|
4708
4529
|
async function runRelease(config, executor) {
|
|
@@ -4970,16 +4791,6 @@ async function runSimpleRelease(executor, config) {
|
|
|
4970
4791
|
}
|
|
4971
4792
|
let releaseCreated = false;
|
|
4972
4793
|
if (!config.noRelease && config.platform) releaseCreated = await createPlatformRelease(executor, config, tag);
|
|
4973
|
-
if (pushed) {
|
|
4974
|
-
const conn = config.platform?.type === "forgejo" ? config.platform.conn : void 0;
|
|
4975
|
-
await runPostReleaseHook(executor, config, buildSimplePayload({
|
|
4976
|
-
tag,
|
|
4977
|
-
repository: conn?.repository,
|
|
4978
|
-
serverUrl: conn?.serverUrl,
|
|
4979
|
-
prerelease: Boolean(config.prerelease),
|
|
4980
|
-
platform: config.platform?.type
|
|
4981
|
-
}));
|
|
4982
|
-
}
|
|
4983
4794
|
return {
|
|
4984
4795
|
version,
|
|
4985
4796
|
tag,
|
|
@@ -5078,7 +4889,12 @@ const releaseSimpleCommand = defineCommand({
|
|
|
5078
4889
|
prerelease: args.prerelease,
|
|
5079
4890
|
platform
|
|
5080
4891
|
};
|
|
5081
|
-
await runSimpleRelease(createRealExecutor(), config)
|
|
4892
|
+
if ((await runSimpleRelease(createRealExecutor(), config)).pushed) {
|
|
4893
|
+
const outputFile = process.env["GITHUB_OUTPUT"];
|
|
4894
|
+
if (outputFile) try {
|
|
4895
|
+
appendFileSync(outputFile, "pushed=true\n");
|
|
4896
|
+
} catch (_error) {}
|
|
4897
|
+
}
|
|
5082
4898
|
}
|
|
5083
4899
|
});
|
|
5084
4900
|
//#endregion
|
|
@@ -5088,13 +4904,13 @@ const CHECKS = [
|
|
|
5088
4904
|
{ name: "typecheck" },
|
|
5089
4905
|
{ name: "lint" },
|
|
5090
4906
|
{ name: "test" },
|
|
5091
|
-
{ name: "docker:build" },
|
|
5092
4907
|
{
|
|
5093
4908
|
name: "format",
|
|
5094
4909
|
args: "--check"
|
|
5095
4910
|
},
|
|
5096
4911
|
{ name: "knip" },
|
|
5097
4912
|
{ name: "tooling:check" },
|
|
4913
|
+
{ name: "docker:build" },
|
|
5098
4914
|
{ name: "docker:check" }
|
|
5099
4915
|
];
|
|
5100
4916
|
/** Check if a name matches any skip pattern. Supports glob syntax via picomatch. */
|
|
@@ -6105,6 +5921,111 @@ const forgejoSecretsCommand = defineCommand({
|
|
|
6105
5921
|
}
|
|
6106
5922
|
});
|
|
6107
5923
|
//#endregion
|
|
5924
|
+
//#region src/release/webhook.ts
|
|
5925
|
+
/** Build a webhook payload from simple release context. */
|
|
5926
|
+
function buildSimplePayload(options) {
|
|
5927
|
+
const repository = options.repository ?? "";
|
|
5928
|
+
const serverUrl = options.serverUrl ?? "https://github.com";
|
|
5929
|
+
const actor = options.actor ?? ciActorName(options.platform ?? "github");
|
|
5930
|
+
return {
|
|
5931
|
+
action: "published",
|
|
5932
|
+
release: {
|
|
5933
|
+
tag_name: options.tag,
|
|
5934
|
+
name: options.tag,
|
|
5935
|
+
draft: false,
|
|
5936
|
+
prerelease: options.prerelease ?? false,
|
|
5937
|
+
author: { login: actor }
|
|
5938
|
+
},
|
|
5939
|
+
repository: {
|
|
5940
|
+
full_name: repository,
|
|
5941
|
+
html_url: repository ? `${serverUrl}/${repository}` : ""
|
|
5942
|
+
},
|
|
5943
|
+
sender: { login: actor }
|
|
5944
|
+
};
|
|
5945
|
+
}
|
|
5946
|
+
/** Build a webhook payload from GitHub Actions environment variables. */
|
|
5947
|
+
function buildPayloadFromEnv() {
|
|
5948
|
+
const repository = process.env["GITHUB_REPOSITORY"];
|
|
5949
|
+
const ref = process.env["GITHUB_REF"];
|
|
5950
|
+
const serverUrl = process.env["GITHUB_SERVER_URL"] ?? "https://github.com";
|
|
5951
|
+
const actor = process.env["GITHUB_ACTOR"] ?? "github-actions[bot]";
|
|
5952
|
+
if (!repository || !ref) return void 0;
|
|
5953
|
+
const tagName = ref.startsWith("refs/tags/") ? ref.slice(10) : ref;
|
|
5954
|
+
return {
|
|
5955
|
+
action: "published",
|
|
5956
|
+
release: {
|
|
5957
|
+
tag_name: tagName,
|
|
5958
|
+
name: tagName,
|
|
5959
|
+
draft: false,
|
|
5960
|
+
prerelease: false,
|
|
5961
|
+
author: { login: actor }
|
|
5962
|
+
},
|
|
5963
|
+
repository: {
|
|
5964
|
+
full_name: repository,
|
|
5965
|
+
html_url: `${serverUrl}/${repository}`
|
|
5966
|
+
},
|
|
5967
|
+
sender: { login: actor }
|
|
5968
|
+
};
|
|
5969
|
+
}
|
|
5970
|
+
/**
|
|
5971
|
+
* Compute the HMAC-SHA256 hex digest of data using the given secret.
|
|
5972
|
+
* Returns the signature in GitHub-style format: `sha256=<hex>`.
|
|
5973
|
+
*/
|
|
5974
|
+
function computeSignature(secret, data) {
|
|
5975
|
+
const hmac = createHmac("sha256", secret);
|
|
5976
|
+
hmac.update(data);
|
|
5977
|
+
return `sha256=${hmac.digest("hex")}`;
|
|
5978
|
+
}
|
|
5979
|
+
/**
|
|
5980
|
+
* POST to a webhook URL with GitHub-style HMAC-SHA256 authentication.
|
|
5981
|
+
*
|
|
5982
|
+
* Sends a JSON-serialized release payload (or empty string if no payload).
|
|
5983
|
+
* If a secret is provided, computes the HMAC-SHA256 signature of the body
|
|
5984
|
+
* and includes it in the `X-Hub-Signature-256` header.
|
|
5985
|
+
*
|
|
5986
|
+
* Never throws — returns a result object indicating success or failure.
|
|
5987
|
+
*/
|
|
5988
|
+
async function sendWebhook(options) {
|
|
5989
|
+
const { url, secret, payload, fetcher = globalThis.fetch } = options;
|
|
5990
|
+
const body = payload ? JSON.stringify(payload) : "";
|
|
5991
|
+
const headers = { "Content-Type": "application/json" };
|
|
5992
|
+
if (secret) headers["X-Hub-Signature-256"] = computeSignature(secret, body);
|
|
5993
|
+
try {
|
|
5994
|
+
const response = await fetcher(url, {
|
|
5995
|
+
method: "POST",
|
|
5996
|
+
headers,
|
|
5997
|
+
body
|
|
5998
|
+
});
|
|
5999
|
+
if (!response.ok) return {
|
|
6000
|
+
ok: false,
|
|
6001
|
+
status: response.status,
|
|
6002
|
+
error: `HTTP ${String(response.status)}: ${await response.text().catch(() => "")}`
|
|
6003
|
+
};
|
|
6004
|
+
return {
|
|
6005
|
+
ok: true,
|
|
6006
|
+
status: response.status
|
|
6007
|
+
};
|
|
6008
|
+
} catch (error) {
|
|
6009
|
+
return {
|
|
6010
|
+
ok: false,
|
|
6011
|
+
error: error instanceof Error ? error.message : String(error)
|
|
6012
|
+
};
|
|
6013
|
+
}
|
|
6014
|
+
}
|
|
6015
|
+
/**
|
|
6016
|
+
* Send a webhook and log the result. Intended for CLI usage.
|
|
6017
|
+
* Never throws.
|
|
6018
|
+
*/
|
|
6019
|
+
async function sendWebhookWithLogging(url, secret, payload) {
|
|
6020
|
+
const result = await sendWebhook({
|
|
6021
|
+
url,
|
|
6022
|
+
secret,
|
|
6023
|
+
payload
|
|
6024
|
+
});
|
|
6025
|
+
if (result.ok) log.info("Post-release hook completed successfully");
|
|
6026
|
+
else log.warn(`Post-release hook failed: ${result.error ?? "unknown error"}`);
|
|
6027
|
+
}
|
|
6028
|
+
//#endregion
|
|
6108
6029
|
//#region src/commands/webhook-send.ts
|
|
6109
6030
|
const webhookSendCommand = defineCommand({
|
|
6110
6031
|
meta: {
|
|
@@ -6155,7 +6076,7 @@ const webhookSendCommand = defineCommand({
|
|
|
6155
6076
|
const main = defineCommand({
|
|
6156
6077
|
meta: {
|
|
6157
6078
|
name: "bst",
|
|
6158
|
-
version: "0.
|
|
6079
|
+
version: "0.46.0",
|
|
6159
6080
|
description: "Bootstrap and maintain standardized TypeScript project tooling"
|
|
6160
6081
|
},
|
|
6161
6082
|
subCommands: {
|
|
@@ -6175,7 +6096,7 @@ const main = defineCommand({
|
|
|
6175
6096
|
"webhook:send": webhookSendCommand
|
|
6176
6097
|
}
|
|
6177
6098
|
});
|
|
6178
|
-
console.log(`@bensandee/tooling v0.
|
|
6099
|
+
console.log(`@bensandee/tooling v0.46.0`);
|
|
6179
6100
|
async function run() {
|
|
6180
6101
|
await runMain(main);
|
|
6181
6102
|
process.exit(process.exitCode ?? 0);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bensandee/tooling",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.46.0",
|
|
4
4
|
"description": "CLI tool to bootstrap and maintain standardized TypeScript project tooling",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -54,8 +54,8 @@
|
|
|
54
54
|
"prettier": "3.8.1",
|
|
55
55
|
"release-it": "19.2.4",
|
|
56
56
|
"tsdown": "0.21.6",
|
|
57
|
-
"@bensandee/
|
|
58
|
-
"@bensandee/
|
|
57
|
+
"@bensandee/claude-hooks": "0.6.3",
|
|
58
|
+
"@bensandee/config": "0.10.1"
|
|
59
59
|
},
|
|
60
60
|
"scripts": {
|
|
61
61
|
"build": "tsdown",
|