@bensandee/tooling 0.43.0 → 0.44.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 +1 -0
- package/dist/bin.mjs +100 -32
- package/package.json +3 -3
- package/tooling.schema.json +1 -1
package/README.md
CHANGED
|
@@ -96,6 +96,7 @@ To skip a workflow file entirely, add `# @bensandee/tooling:ignore` as the first
|
|
|
96
96
|
| `tooling release:trigger` | Trigger a release workflow. |
|
|
97
97
|
| `tooling forgejo:create-release` | Create a Forgejo release from a tag. |
|
|
98
98
|
| `tooling changesets:merge` | Merge a changesets version PR. |
|
|
99
|
+
| `tooling webhook:send <url>` | POST to a webhook URL with HMAC-SHA256 auth. Secret from `WEBHOOK_SECRET` env var. |
|
|
99
100
|
|
|
100
101
|
#### `release:simple`
|
|
101
102
|
|
package/dist/bin.mjs
CHANGED
|
@@ -11,6 +11,7 @@ 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";
|
|
14
15
|
import picomatch from "picomatch";
|
|
15
16
|
import { tmpdir } from "node:os";
|
|
16
17
|
//#region src/types.ts
|
|
@@ -626,7 +627,7 @@ const ToolingConfigSchema = z.strictObject({
|
|
|
626
627
|
dockerCheck: z.union([z.literal(false), DockerCheckConfigSchema]).optional().meta({ description: "Docker health check configuration or false to disable" }),
|
|
627
628
|
registryHost: z.string().optional().meta({ description: "Docker registry hostname (e.g. ghcr.io, code.example.com)" }),
|
|
628
629
|
registryNamespace: z.string().optional().meta({ description: "Docker registry namespace (e.g. ghcr.io/myorg, code.example.com/myorg)" }),
|
|
629
|
-
postReleaseHook: z.string().optional().meta({ description: "Override the post-release hook command (default:
|
|
630
|
+
postReleaseHook: z.string().optional().meta({ description: "Override the post-release hook command (default: POST to POST_RELEASE_HOOK_URL with HMAC-SHA256 auth)" }),
|
|
630
631
|
postReleaseHookSecrets: z.array(z.string()).optional().meta({ description: "Additional secret names to inject as env vars in the post-release hook step (e.g. ['WEBHOOK_SECRET'])" })
|
|
631
632
|
});
|
|
632
633
|
/** Load saved tooling config from the target directory. Returns undefined if missing, throws on invalid. */
|
|
@@ -1571,7 +1572,7 @@ async function generatePackageJson(ctx) {
|
|
|
1571
1572
|
const devDeps = { ...ROOT_DEV_DEPS };
|
|
1572
1573
|
if (!isMonorepo) Object.assign(devDeps, PER_PACKAGE_DEV_DEPS);
|
|
1573
1574
|
devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.10.1";
|
|
1574
|
-
devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.
|
|
1575
|
+
devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.44.0";
|
|
1575
1576
|
if (ctx.config.useEslintPlugin) devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "0.12.0";
|
|
1576
1577
|
if (ctx.config.formatter === "oxfmt") devDeps["oxfmt"] = {
|
|
1577
1578
|
"@changesets/cli": "2.30.0",
|
|
@@ -2181,16 +2182,14 @@ async function generateGitignore(ctx) {
|
|
|
2181
2182
|
//#region src/generators/release-ci.ts
|
|
2182
2183
|
const DEFAULT_POST_RELEASE_COMMAND = [
|
|
2183
2184
|
"if [ -n \"$POST_RELEASE_HOOK_URL\" ]; then",
|
|
2184
|
-
"
|
|
2185
|
-
" $",
|
|
2186
|
-
"{POST_RELEASE_HOOK_SECRET:+-H \"Authorization: Bearer $POST_RELEASE_HOOK_SECRET\"}",
|
|
2185
|
+
" pnpm dlx @bensandee/tooling webhook:send",
|
|
2187
2186
|
" \"$POST_RELEASE_HOOK_URL\"; fi"
|
|
2188
2187
|
].join("");
|
|
2189
|
-
/** Build env vars for hook secrets to inject into a
|
|
2188
|
+
/** Build env vars for hook secrets to inject into a workflow step. */
|
|
2190
2189
|
function hookSecretEnv(secrets) {
|
|
2191
2190
|
const env = {
|
|
2192
2191
|
POST_RELEASE_HOOK_URL: actionsExpr("secrets.POST_RELEASE_HOOK_URL"),
|
|
2193
|
-
|
|
2192
|
+
WEBHOOK_SECRET: actionsExpr("secrets.POST_RELEASE_HOOK_SECRET")
|
|
2194
2193
|
};
|
|
2195
2194
|
for (const name of secrets ?? []) env[name] = actionsExpr(`secrets.${name}`);
|
|
2196
2195
|
return env;
|
|
@@ -3333,7 +3332,7 @@ async function runGenerators(ctx) {
|
|
|
3333
3332
|
function generateMigratePrompt(results, config, detected) {
|
|
3334
3333
|
const sections = [];
|
|
3335
3334
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
3336
|
-
sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.
|
|
3335
|
+
sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.44.0 repo:sync\` on ${timestamp}_\n`);
|
|
3337
3336
|
const changed = results.filter((r) => r.action === "created" || r.action === "updated" || r.action === "archived");
|
|
3338
3337
|
if (changed.length > 0) {
|
|
3339
3338
|
const items = changed.map((r) => {
|
|
@@ -4240,6 +4239,64 @@ async function runVersionMode(executor, config) {
|
|
|
4240
4239
|
};
|
|
4241
4240
|
}
|
|
4242
4241
|
//#endregion
|
|
4242
|
+
//#region src/release/webhook.ts
|
|
4243
|
+
/**
|
|
4244
|
+
* Compute the HMAC-SHA256 hex digest of data using the given secret.
|
|
4245
|
+
* Returns the signature in GitHub-style format: `sha256=<hex>`.
|
|
4246
|
+
*/
|
|
4247
|
+
function computeSignature(secret, data) {
|
|
4248
|
+
const hmac = createHmac("sha256", secret);
|
|
4249
|
+
hmac.update(data);
|
|
4250
|
+
return `sha256=${hmac.digest("hex")}`;
|
|
4251
|
+
}
|
|
4252
|
+
/**
|
|
4253
|
+
* POST to a webhook URL with GitHub-style HMAC-SHA256 authentication.
|
|
4254
|
+
*
|
|
4255
|
+
* Sends an empty body. If a secret is provided, computes the HMAC-SHA256
|
|
4256
|
+
* signature and includes it in the `X-Hub-Signature-256` header.
|
|
4257
|
+
*
|
|
4258
|
+
* Never throws — returns a result object indicating success or failure.
|
|
4259
|
+
*/
|
|
4260
|
+
async function sendWebhook(options) {
|
|
4261
|
+
const { url, secret, fetcher = globalThis.fetch } = options;
|
|
4262
|
+
const body = "";
|
|
4263
|
+
const headers = { "Content-Type": "application/json" };
|
|
4264
|
+
if (secret) headers["X-Hub-Signature-256"] = computeSignature(secret, body);
|
|
4265
|
+
try {
|
|
4266
|
+
const response = await fetcher(url, {
|
|
4267
|
+
method: "POST",
|
|
4268
|
+
headers,
|
|
4269
|
+
body
|
|
4270
|
+
});
|
|
4271
|
+
if (!response.ok) return {
|
|
4272
|
+
ok: false,
|
|
4273
|
+
status: response.status,
|
|
4274
|
+
error: `HTTP ${String(response.status)}: ${await response.text().catch(() => "")}`
|
|
4275
|
+
};
|
|
4276
|
+
return {
|
|
4277
|
+
ok: true,
|
|
4278
|
+
status: response.status
|
|
4279
|
+
};
|
|
4280
|
+
} catch (error) {
|
|
4281
|
+
return {
|
|
4282
|
+
ok: false,
|
|
4283
|
+
error: error instanceof Error ? error.message : String(error)
|
|
4284
|
+
};
|
|
4285
|
+
}
|
|
4286
|
+
}
|
|
4287
|
+
/**
|
|
4288
|
+
* Send a webhook and log the result. Intended for CLI usage.
|
|
4289
|
+
* Never throws.
|
|
4290
|
+
*/
|
|
4291
|
+
async function sendWebhookWithLogging(url, secret) {
|
|
4292
|
+
const result = await sendWebhook({
|
|
4293
|
+
url,
|
|
4294
|
+
secret
|
|
4295
|
+
});
|
|
4296
|
+
if (result.ok) log.info("Post-release hook completed successfully");
|
|
4297
|
+
else log.warn(`Post-release hook failed: ${result.error ?? "unknown error"}`);
|
|
4298
|
+
}
|
|
4299
|
+
//#endregion
|
|
4243
4300
|
//#region src/release/hook.ts
|
|
4244
4301
|
const PostReleaseHookSchema = z.object({ postReleaseHook: z.string().optional() });
|
|
4245
4302
|
/** Read the postReleaseHook command override from .tooling.json at runtime. */
|
|
@@ -4259,12 +4316,12 @@ function loadHookCommand(executor, cwd) {
|
|
|
4259
4316
|
* Execute the post-release hook after a successful publish.
|
|
4260
4317
|
*
|
|
4261
4318
|
* Uses the `postReleaseHook` command from `.tooling.json` if configured,
|
|
4262
|
-
* otherwise
|
|
4263
|
-
* `
|
|
4319
|
+
* otherwise POSTs to `POST_RELEASE_HOOK_URL` with HMAC-SHA256 authentication
|
|
4320
|
+
* using `WEBHOOK_SECRET`. Both env vars must be set by the CI workflow.
|
|
4264
4321
|
*
|
|
4265
4322
|
* Failures are logged as warnings — they never fail the release.
|
|
4266
4323
|
*/
|
|
4267
|
-
function runPostReleaseHook(executor, config) {
|
|
4324
|
+
async function runPostReleaseHook(executor, config) {
|
|
4268
4325
|
if (config.dryRun) {
|
|
4269
4326
|
debug(config, "Skipping post-release hook (dry-run)");
|
|
4270
4327
|
return;
|
|
@@ -4280,22 +4337,15 @@ function runPostReleaseHook(executor, config) {
|
|
|
4280
4337
|
debug(config, "No POST_RELEASE_HOOK_URL set — skipping post-release hook");
|
|
4281
4338
|
return;
|
|
4282
4339
|
}
|
|
4283
|
-
const hookSecret = process.env["
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
try {
|
|
4293
|
-
const result = executor.exec("curl", args, { cwd: config.cwd });
|
|
4294
|
-
if (result.exitCode !== 0) log.warn(`Post-release hook failed (exit code ${String(result.exitCode)}): ${result.stderr || result.stdout}`);
|
|
4295
|
-
else log.info("Post-release hook completed successfully");
|
|
4296
|
-
} catch (error) {
|
|
4297
|
-
log.warn(`Post-release hook error: ${error instanceof Error ? error.message : String(error)}`);
|
|
4298
|
-
}
|
|
4340
|
+
const hookSecret = process.env["WEBHOOK_SECRET"];
|
|
4341
|
+
debug(config, `Running post-release hook: POST ${hookUrl}`);
|
|
4342
|
+
const result = await sendWebhook({
|
|
4343
|
+
url: hookUrl,
|
|
4344
|
+
secret: hookSecret || void 0,
|
|
4345
|
+
fetcher: executor.fetch.bind(executor)
|
|
4346
|
+
});
|
|
4347
|
+
if (result.ok) log.info("Post-release hook completed successfully");
|
|
4348
|
+
else log.warn(`Post-release hook failed: ${result.error ?? "unknown error"}`);
|
|
4299
4349
|
}
|
|
4300
4350
|
function executeHookCommand(executor, config, command) {
|
|
4301
4351
|
try {
|
|
@@ -4406,7 +4456,7 @@ async function runPublishMode(executor, config) {
|
|
|
4406
4456
|
if (forcePushResult.exitCode !== 0) log.warn(`Warning: Failed to push sliding tags: ${forcePushResult.stderr || forcePushResult.stdout}`);
|
|
4407
4457
|
else log.info(`Created sliding tags: ${slidingTags.join(", ")}`);
|
|
4408
4458
|
}
|
|
4409
|
-
runPostReleaseHook(executor, config);
|
|
4459
|
+
await runPostReleaseHook(executor, config);
|
|
4410
4460
|
return {
|
|
4411
4461
|
mode: "publish",
|
|
4412
4462
|
tags: allTags
|
|
@@ -4830,7 +4880,7 @@ async function runSimpleRelease(executor, config) {
|
|
|
4830
4880
|
}
|
|
4831
4881
|
let releaseCreated = false;
|
|
4832
4882
|
if (!config.noRelease && config.platform) releaseCreated = await createPlatformRelease(executor, config, tag);
|
|
4833
|
-
if (pushed) runPostReleaseHook(executor, config);
|
|
4883
|
+
if (pushed) await runPostReleaseHook(executor, config);
|
|
4834
4884
|
return {
|
|
4835
4885
|
version,
|
|
4836
4886
|
tag,
|
|
@@ -5955,11 +6005,28 @@ const forgejoSecretsCommand = defineCommand({
|
|
|
5955
6005
|
}
|
|
5956
6006
|
});
|
|
5957
6007
|
//#endregion
|
|
6008
|
+
//#region src/commands/webhook-send.ts
|
|
6009
|
+
const webhookSendCommand = defineCommand({
|
|
6010
|
+
meta: {
|
|
6011
|
+
name: "webhook:send",
|
|
6012
|
+
description: "Send a POST request to a webhook URL with GitHub-style HMAC-SHA256 authentication"
|
|
6013
|
+
},
|
|
6014
|
+
args: { url: {
|
|
6015
|
+
type: "positional",
|
|
6016
|
+
description: "Webhook URL to POST to",
|
|
6017
|
+
required: true
|
|
6018
|
+
} },
|
|
6019
|
+
async run({ args }) {
|
|
6020
|
+
const secret = process.env["WEBHOOK_SECRET"];
|
|
6021
|
+
await sendWebhookWithLogging(args.url, secret || void 0);
|
|
6022
|
+
}
|
|
6023
|
+
});
|
|
6024
|
+
//#endregion
|
|
5958
6025
|
//#region src/bin.ts
|
|
5959
6026
|
const main = defineCommand({
|
|
5960
6027
|
meta: {
|
|
5961
6028
|
name: "bst",
|
|
5962
|
-
version: "0.
|
|
6029
|
+
version: "0.44.0",
|
|
5963
6030
|
description: "Bootstrap and maintain standardized TypeScript project tooling"
|
|
5964
6031
|
},
|
|
5965
6032
|
subCommands: {
|
|
@@ -5975,10 +6042,11 @@ const main = defineCommand({
|
|
|
5975
6042
|
"docker:check": dockerCheckCommand,
|
|
5976
6043
|
"hooks:audit": hooksAuditCommand,
|
|
5977
6044
|
"setup:secrets": setupSecretsCommand,
|
|
5978
|
-
"forgejo:secrets": forgejoSecretsCommand
|
|
6045
|
+
"forgejo:secrets": forgejoSecretsCommand,
|
|
6046
|
+
"webhook:send": webhookSendCommand
|
|
5979
6047
|
}
|
|
5980
6048
|
});
|
|
5981
|
-
console.log(`@bensandee/tooling v0.
|
|
6049
|
+
console.log(`@bensandee/tooling v0.44.0`);
|
|
5982
6050
|
async function run() {
|
|
5983
6051
|
await runMain(main);
|
|
5984
6052
|
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.44.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/config": "0.10.1",
|
|
58
|
+
"@bensandee/claude-hooks": "0.6.1"
|
|
59
59
|
},
|
|
60
60
|
"scripts": {
|
|
61
61
|
"build": "tsdown",
|
package/tooling.schema.json
CHANGED
|
@@ -170,7 +170,7 @@
|
|
|
170
170
|
"type": "string"
|
|
171
171
|
},
|
|
172
172
|
"postReleaseHook": {
|
|
173
|
-
"description": "Override the post-release hook command (default:
|
|
173
|
+
"description": "Override the post-release hook command (default: POST to POST_RELEASE_HOOK_URL with HMAC-SHA256 auth)",
|
|
174
174
|
"type": "string"
|
|
175
175
|
},
|
|
176
176
|
"postReleaseHookSecrets": {
|