@bensandee/tooling 0.51.0 → 0.52.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 +243 -24
- package/package.json +2 -2
package/dist/bin.mjs
CHANGED
|
@@ -1369,7 +1369,7 @@ const PER_PACKAGE_DEV_DEPS = {
|
|
|
1369
1369
|
"@types/node": {
|
|
1370
1370
|
"@changesets/cli": "2.30.0",
|
|
1371
1371
|
"@release-it/bumper": "7.0.5",
|
|
1372
|
-
"@types/node": "24.12.
|
|
1372
|
+
"@types/node": "24.12.2",
|
|
1373
1373
|
"commit-and-tag-version": "12.7.1",
|
|
1374
1374
|
"knip": "6.3.1",
|
|
1375
1375
|
"lefthook": "2.1.5",
|
|
@@ -1385,7 +1385,7 @@ const PER_PACKAGE_DEV_DEPS = {
|
|
|
1385
1385
|
tsdown: {
|
|
1386
1386
|
"@changesets/cli": "2.30.0",
|
|
1387
1387
|
"@release-it/bumper": "7.0.5",
|
|
1388
|
-
"@types/node": "24.12.
|
|
1388
|
+
"@types/node": "24.12.2",
|
|
1389
1389
|
"commit-and-tag-version": "12.7.1",
|
|
1390
1390
|
"knip": "6.3.1",
|
|
1391
1391
|
"lefthook": "2.1.5",
|
|
@@ -1401,7 +1401,7 @@ const PER_PACKAGE_DEV_DEPS = {
|
|
|
1401
1401
|
typescript: {
|
|
1402
1402
|
"@changesets/cli": "2.30.0",
|
|
1403
1403
|
"@release-it/bumper": "7.0.5",
|
|
1404
|
-
"@types/node": "24.12.
|
|
1404
|
+
"@types/node": "24.12.2",
|
|
1405
1405
|
"commit-and-tag-version": "12.7.1",
|
|
1406
1406
|
"knip": "6.3.1",
|
|
1407
1407
|
"lefthook": "2.1.5",
|
|
@@ -1417,7 +1417,7 @@ const PER_PACKAGE_DEV_DEPS = {
|
|
|
1417
1417
|
vitest: {
|
|
1418
1418
|
"@changesets/cli": "2.30.0",
|
|
1419
1419
|
"@release-it/bumper": "7.0.5",
|
|
1420
|
-
"@types/node": "24.12.
|
|
1420
|
+
"@types/node": "24.12.2",
|
|
1421
1421
|
"commit-and-tag-version": "12.7.1",
|
|
1422
1422
|
"knip": "6.3.1",
|
|
1423
1423
|
"lefthook": "2.1.5",
|
|
@@ -1436,7 +1436,7 @@ const ROOT_DEV_DEPS = {
|
|
|
1436
1436
|
knip: {
|
|
1437
1437
|
"@changesets/cli": "2.30.0",
|
|
1438
1438
|
"@release-it/bumper": "7.0.5",
|
|
1439
|
-
"@types/node": "24.12.
|
|
1439
|
+
"@types/node": "24.12.2",
|
|
1440
1440
|
"commit-and-tag-version": "12.7.1",
|
|
1441
1441
|
"knip": "6.3.1",
|
|
1442
1442
|
"lefthook": "2.1.5",
|
|
@@ -1452,7 +1452,7 @@ const ROOT_DEV_DEPS = {
|
|
|
1452
1452
|
lefthook: {
|
|
1453
1453
|
"@changesets/cli": "2.30.0",
|
|
1454
1454
|
"@release-it/bumper": "7.0.5",
|
|
1455
|
-
"@types/node": "24.12.
|
|
1455
|
+
"@types/node": "24.12.2",
|
|
1456
1456
|
"commit-and-tag-version": "12.7.1",
|
|
1457
1457
|
"knip": "6.3.1",
|
|
1458
1458
|
"lefthook": "2.1.5",
|
|
@@ -1468,7 +1468,7 @@ const ROOT_DEV_DEPS = {
|
|
|
1468
1468
|
oxlint: {
|
|
1469
1469
|
"@changesets/cli": "2.30.0",
|
|
1470
1470
|
"@release-it/bumper": "7.0.5",
|
|
1471
|
-
"@types/node": "24.12.
|
|
1471
|
+
"@types/node": "24.12.2",
|
|
1472
1472
|
"commit-and-tag-version": "12.7.1",
|
|
1473
1473
|
"knip": "6.3.1",
|
|
1474
1474
|
"lefthook": "2.1.5",
|
|
@@ -1509,7 +1509,7 @@ function addReleaseDeps(deps, config) {
|
|
|
1509
1509
|
deps["release-it"] = {
|
|
1510
1510
|
"@changesets/cli": "2.30.0",
|
|
1511
1511
|
"@release-it/bumper": "7.0.5",
|
|
1512
|
-
"@types/node": "24.12.
|
|
1512
|
+
"@types/node": "24.12.2",
|
|
1513
1513
|
"commit-and-tag-version": "12.7.1",
|
|
1514
1514
|
"knip": "6.3.1",
|
|
1515
1515
|
"lefthook": "2.1.5",
|
|
@@ -1525,7 +1525,7 @@ function addReleaseDeps(deps, config) {
|
|
|
1525
1525
|
if (config.structure === "monorepo") deps["@release-it/bumper"] = {
|
|
1526
1526
|
"@changesets/cli": "2.30.0",
|
|
1527
1527
|
"@release-it/bumper": "7.0.5",
|
|
1528
|
-
"@types/node": "24.12.
|
|
1528
|
+
"@types/node": "24.12.2",
|
|
1529
1529
|
"commit-and-tag-version": "12.7.1",
|
|
1530
1530
|
"knip": "6.3.1",
|
|
1531
1531
|
"lefthook": "2.1.5",
|
|
@@ -1543,7 +1543,7 @@ function addReleaseDeps(deps, config) {
|
|
|
1543
1543
|
deps["commit-and-tag-version"] = {
|
|
1544
1544
|
"@changesets/cli": "2.30.0",
|
|
1545
1545
|
"@release-it/bumper": "7.0.5",
|
|
1546
|
-
"@types/node": "24.12.
|
|
1546
|
+
"@types/node": "24.12.2",
|
|
1547
1547
|
"commit-and-tag-version": "12.7.1",
|
|
1548
1548
|
"knip": "6.3.1",
|
|
1549
1549
|
"lefthook": "2.1.5",
|
|
@@ -1561,7 +1561,7 @@ function addReleaseDeps(deps, config) {
|
|
|
1561
1561
|
deps["@changesets/cli"] = {
|
|
1562
1562
|
"@changesets/cli": "2.30.0",
|
|
1563
1563
|
"@release-it/bumper": "7.0.5",
|
|
1564
|
-
"@types/node": "24.12.
|
|
1564
|
+
"@types/node": "24.12.2",
|
|
1565
1565
|
"commit-and-tag-version": "12.7.1",
|
|
1566
1566
|
"knip": "6.3.1",
|
|
1567
1567
|
"lefthook": "2.1.5",
|
|
@@ -1596,12 +1596,12 @@ async function generatePackageJson(ctx) {
|
|
|
1596
1596
|
const devDeps = { ...ROOT_DEV_DEPS };
|
|
1597
1597
|
if (!isMonorepo) Object.assign(devDeps, PER_PACKAGE_DEV_DEPS);
|
|
1598
1598
|
devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.10.2";
|
|
1599
|
-
devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.
|
|
1599
|
+
devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.52.0";
|
|
1600
1600
|
if (ctx.config.useEslintPlugin) devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "0.12.0";
|
|
1601
1601
|
if (ctx.config.formatter === "oxfmt") devDeps["oxfmt"] = {
|
|
1602
1602
|
"@changesets/cli": "2.30.0",
|
|
1603
1603
|
"@release-it/bumper": "7.0.5",
|
|
1604
|
-
"@types/node": "24.12.
|
|
1604
|
+
"@types/node": "24.12.2",
|
|
1605
1605
|
"commit-and-tag-version": "12.7.1",
|
|
1606
1606
|
"knip": "6.3.1",
|
|
1607
1607
|
"lefthook": "2.1.5",
|
|
@@ -1617,7 +1617,7 @@ async function generatePackageJson(ctx) {
|
|
|
1617
1617
|
if (ctx.config.formatter === "prettier") devDeps["prettier"] = {
|
|
1618
1618
|
"@changesets/cli": "2.30.0",
|
|
1619
1619
|
"@release-it/bumper": "7.0.5",
|
|
1620
|
-
"@types/node": "24.12.
|
|
1620
|
+
"@types/node": "24.12.2",
|
|
1621
1621
|
"commit-and-tag-version": "12.7.1",
|
|
1622
1622
|
"knip": "6.3.1",
|
|
1623
1623
|
"lefthook": "2.1.5",
|
|
@@ -1711,7 +1711,7 @@ async function generatePackageJson(ctx) {
|
|
|
1711
1711
|
packageManager: `pnpm@${{
|
|
1712
1712
|
"@changesets/cli": "2.30.0",
|
|
1713
1713
|
"@release-it/bumper": "7.0.5",
|
|
1714
|
-
"@types/node": "24.12.
|
|
1714
|
+
"@types/node": "24.12.2",
|
|
1715
1715
|
"commit-and-tag-version": "12.7.1",
|
|
1716
1716
|
"knip": "6.3.1",
|
|
1717
1717
|
"lefthook": "2.1.5",
|
|
@@ -3370,7 +3370,7 @@ async function runGenerators(ctx) {
|
|
|
3370
3370
|
function generateMigratePrompt(results, config, detected) {
|
|
3371
3371
|
const sections = [];
|
|
3372
3372
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
3373
|
-
sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.
|
|
3373
|
+
sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.52.0 repo:sync\` on ${timestamp}_\n`);
|
|
3374
3374
|
const changed = results.filter((r) => r.action === "created" || r.action === "updated" || r.action === "archived");
|
|
3375
3375
|
if (changed.length > 0) {
|
|
3376
3376
|
const items = changed.map((r) => {
|
|
@@ -4125,6 +4125,116 @@ async function deleteActionSecret(executor, conn, secretName) {
|
|
|
4125
4125
|
throw new TransientError(`Failed to delete secret ${secretName}: ${String(res.status)} ${res.statusText}\n${body}`);
|
|
4126
4126
|
}
|
|
4127
4127
|
}
|
|
4128
|
+
const WebhookSchema = z.object({
|
|
4129
|
+
id: z.number(),
|
|
4130
|
+
type: z.string(),
|
|
4131
|
+
active: z.boolean(),
|
|
4132
|
+
config: z.record(z.string(), z.string()).optional()
|
|
4133
|
+
});
|
|
4134
|
+
const WebhookListSchema = z.array(WebhookSchema);
|
|
4135
|
+
async function fetchHooks(executor, conn, endpoint) {
|
|
4136
|
+
const res = await executor.fetch(`${conn.serverUrl}/api/v1${endpoint}`, { headers: { Authorization: `token ${conn.token}` } });
|
|
4137
|
+
if (res.status === 401 || res.status === 403) return {
|
|
4138
|
+
status: "unauthorized",
|
|
4139
|
+
httpStatus: res.status
|
|
4140
|
+
};
|
|
4141
|
+
if (!res.ok) {
|
|
4142
|
+
const body = await responseBodyText(res);
|
|
4143
|
+
return {
|
|
4144
|
+
status: "error",
|
|
4145
|
+
httpStatus: res.status,
|
|
4146
|
+
message: `${res.statusText}\n${body}`
|
|
4147
|
+
};
|
|
4148
|
+
}
|
|
4149
|
+
const parsed = WebhookListSchema.safeParse(await res.json());
|
|
4150
|
+
if (!parsed.success) throw new UnexpectedError(`Unexpected webhook list response for ${endpoint}: ${parsed.error.message}`);
|
|
4151
|
+
return {
|
|
4152
|
+
status: "ok",
|
|
4153
|
+
hooks: parsed.data
|
|
4154
|
+
};
|
|
4155
|
+
}
|
|
4156
|
+
/** List system-level webhooks. Requires admin token (read:admin scope). */
|
|
4157
|
+
async function listSystemHooks(executor, conn) {
|
|
4158
|
+
return fetchHooks(executor, conn, "/admin/hooks");
|
|
4159
|
+
}
|
|
4160
|
+
/** List webhooks owned by the authenticated user. Requires read:user scope. */
|
|
4161
|
+
async function listUserHooks(executor, conn) {
|
|
4162
|
+
return fetchHooks(executor, conn, "/user/hooks");
|
|
4163
|
+
}
|
|
4164
|
+
/** List webhooks for an organization. Requires read:organization scope. */
|
|
4165
|
+
async function listOrgHooks(executor, conn, org) {
|
|
4166
|
+
return fetchHooks(executor, conn, `/orgs/${encodeURIComponent(org)}/hooks`);
|
|
4167
|
+
}
|
|
4168
|
+
/** List webhooks for a repository. Requires read:repository + repo admin. */
|
|
4169
|
+
async function listRepoHooks(executor, conn, repository) {
|
|
4170
|
+
return fetchHooks(executor, conn, `/repos/${repository}/hooks`);
|
|
4171
|
+
}
|
|
4172
|
+
const AuthenticatedUserSchema = z.object({ login: z.string() });
|
|
4173
|
+
/** Return the login of the authenticated user, or null if the token is invalid. */
|
|
4174
|
+
async function getAuthenticatedUser(executor, conn) {
|
|
4175
|
+
const res = await executor.fetch(`${conn.serverUrl}/api/v1/user`, { headers: { Authorization: `token ${conn.token}` } });
|
|
4176
|
+
if (!res.ok) return {
|
|
4177
|
+
ok: false,
|
|
4178
|
+
httpStatus: res.status
|
|
4179
|
+
};
|
|
4180
|
+
const parsed = AuthenticatedUserSchema.safeParse(await res.json());
|
|
4181
|
+
if (!parsed.success) throw new UnexpectedError(`Unexpected /user response: ${parsed.error.message}`);
|
|
4182
|
+
return {
|
|
4183
|
+
ok: true,
|
|
4184
|
+
login: parsed.data.login
|
|
4185
|
+
};
|
|
4186
|
+
}
|
|
4187
|
+
/**
|
|
4188
|
+
* Probe whether the token has admin access by requesting `/admin/users?limit=1`.
|
|
4189
|
+
*
|
|
4190
|
+
* Returns true on 2xx, false on 401/403. Any other status is treated as not-admin
|
|
4191
|
+
* to keep enumeration conservative.
|
|
4192
|
+
*/
|
|
4193
|
+
async function hasAdminAccess(executor, conn) {
|
|
4194
|
+
return (await executor.fetch(`${conn.serverUrl}/api/v1/admin/users?limit=1`, { headers: { Authorization: `token ${conn.token}` } })).ok;
|
|
4195
|
+
}
|
|
4196
|
+
const OrgSchema = z.object({ username: z.string() });
|
|
4197
|
+
const OrgListSchema = z.array(OrgSchema);
|
|
4198
|
+
const PAGE_LIMIT = 50;
|
|
4199
|
+
async function paginate(executor, conn, endpoint, extract) {
|
|
4200
|
+
const all = [];
|
|
4201
|
+
let page = 1;
|
|
4202
|
+
const separator = endpoint.includes("?") ? "&" : "?";
|
|
4203
|
+
for (;;) {
|
|
4204
|
+
const url = `${conn.serverUrl}/api/v1${endpoint}${separator}page=${String(page)}&limit=${String(PAGE_LIMIT)}`;
|
|
4205
|
+
const res = await executor.fetch(url, { headers: { Authorization: `token ${conn.token}` } });
|
|
4206
|
+
if (!res.ok) {
|
|
4207
|
+
const body = await responseBodyText(res);
|
|
4208
|
+
throw new TransientError(`Failed to fetch ${endpoint} (page ${String(page)}): ${String(res.status)} ${res.statusText}\n${body}`);
|
|
4209
|
+
}
|
|
4210
|
+
const items = extract(await res.json());
|
|
4211
|
+
all.push(...items);
|
|
4212
|
+
if (items.length < PAGE_LIMIT) break;
|
|
4213
|
+
page++;
|
|
4214
|
+
}
|
|
4215
|
+
return all;
|
|
4216
|
+
}
|
|
4217
|
+
/**
|
|
4218
|
+
* List organizations. When `mode` is "admin", uses `/admin/orgs` (all orgs on
|
|
4219
|
+
* the instance). When "user", uses `/user/orgs` (only the token user's orgs).
|
|
4220
|
+
*/
|
|
4221
|
+
async function listOrgs(executor, conn, mode) {
|
|
4222
|
+
const endpoint = mode === "admin" ? "/admin/orgs" : "/user/orgs";
|
|
4223
|
+
return (await paginate(executor, conn, endpoint, (raw) => {
|
|
4224
|
+
const parsed = OrgListSchema.safeParse(raw);
|
|
4225
|
+
if (!parsed.success) throw new UnexpectedError(`Unexpected ${endpoint} response: ${parsed.error.message}`);
|
|
4226
|
+
return parsed.data;
|
|
4227
|
+
})).map((o) => o.username);
|
|
4228
|
+
}
|
|
4229
|
+
const RepoSearchSchema = z.object({ data: z.array(z.object({ full_name: z.string() })) });
|
|
4230
|
+
/** List repositories visible to the token via `/repos/search`. */
|
|
4231
|
+
async function searchRepos(executor, conn) {
|
|
4232
|
+
return (await paginate(executor, conn, "/repos/search", (raw) => {
|
|
4233
|
+
const parsed = RepoSearchSchema.safeParse(raw);
|
|
4234
|
+
if (!parsed.success) throw new UnexpectedError(`Unexpected /repos/search response: ${parsed.error.message}`);
|
|
4235
|
+
return parsed.data.data;
|
|
4236
|
+
})).map((r) => r.full_name);
|
|
4237
|
+
}
|
|
4128
4238
|
//#endregion
|
|
4129
4239
|
//#region src/release/version.ts
|
|
4130
4240
|
const BRANCH = "changeset-release/main";
|
|
@@ -5480,15 +5590,33 @@ function parseComposeServices(cwd, composeFiles) {
|
|
|
5480
5590
|
function stripComposeVariables(image) {
|
|
5481
5591
|
return image.replace(/\$\{[^}]*\}/g, "");
|
|
5482
5592
|
}
|
|
5593
|
+
/** Resolve a compose service image string to its bare name (variables and :tag stripped). */
|
|
5594
|
+
function bareImageName(image) {
|
|
5595
|
+
return stripComposeVariables(image).split(":")[0] || void 0;
|
|
5596
|
+
}
|
|
5483
5597
|
/** Extract deduplicated bare image names (without tags) from parsed services. */
|
|
5484
5598
|
function extractComposeImageNames(services) {
|
|
5485
5599
|
const names = /* @__PURE__ */ new Set();
|
|
5486
5600
|
for (const service of services) if (service.image) {
|
|
5487
|
-
const bare =
|
|
5601
|
+
const bare = bareImageName(service.image);
|
|
5488
5602
|
if (bare) names.add(bare);
|
|
5489
5603
|
}
|
|
5490
5604
|
return [...names];
|
|
5491
5605
|
}
|
|
5606
|
+
/**
|
|
5607
|
+
* Whether a service's image refers to an image this repo builds locally.
|
|
5608
|
+
* Matches the bare image name (variables/tag stripped) against the list of
|
|
5609
|
+
* detected Docker package image names, using the same exact-or-suffix match
|
|
5610
|
+
* as the cross-reference warning in docker:check.
|
|
5611
|
+
*/
|
|
5612
|
+
function isLocallyBuiltService(service, locallyBuiltImageNames) {
|
|
5613
|
+
if (!service.image) return false;
|
|
5614
|
+
const bare = bareImageName(service.image);
|
|
5615
|
+
if (!bare) return false;
|
|
5616
|
+
if (locallyBuiltImageNames.has(bare)) return true;
|
|
5617
|
+
for (const name of locallyBuiltImageNames) if (bare.endsWith(`/${name}`)) return true;
|
|
5618
|
+
return false;
|
|
5619
|
+
}
|
|
5492
5620
|
/** Generate health checks from parsed services: services with exposed ports get HTTP checks, unless they define a compose-level healthcheck. */
|
|
5493
5621
|
function deriveHealthChecks(services) {
|
|
5494
5622
|
return services.filter((s) => s.hostPort !== void 0 && !s.hasHealthcheck).map((s) => ({
|
|
@@ -5519,14 +5647,21 @@ const CHECK_HEALTHCHECK = {
|
|
|
5519
5647
|
};
|
|
5520
5648
|
/**
|
|
5521
5649
|
* Generate a check overlay YAML string from parsed services.
|
|
5522
|
-
* Sets `restart: "no"` on all services,
|
|
5523
|
-
* for services that define a healthcheck in the base compose file
|
|
5650
|
+
* Sets `restart: "no"` on all services, overrides healthcheck intervals
|
|
5651
|
+
* for services that define a healthcheck in the base compose file, and
|
|
5652
|
+
* forces `pull_policy: never` on services whose image is locally built by
|
|
5653
|
+
* this repo (matched against `locallyBuiltImageNames`). The override
|
|
5654
|
+
* prevents pull failures when the consumer's base compose sets
|
|
5655
|
+
* `pull_policy: always` for prod deploys against images that only exist
|
|
5656
|
+
* in the local Docker cache after `docker:build`.
|
|
5524
5657
|
*/
|
|
5525
|
-
function generateCheckOverlay(services) {
|
|
5658
|
+
function generateCheckOverlay(services, locallyBuiltImageNames = []) {
|
|
5659
|
+
const localSet = new Set(locallyBuiltImageNames);
|
|
5526
5660
|
const serviceOverrides = {};
|
|
5527
5661
|
for (const service of services) {
|
|
5528
5662
|
const override = { restart: "no" };
|
|
5529
5663
|
if (service.hasHealthcheck) override["healthcheck"] = { ...CHECK_HEALTHCHECK };
|
|
5664
|
+
if (isLocallyBuiltService(service, localSet)) override["pull_policy"] = "never";
|
|
5530
5665
|
serviceOverrides[service.name] = override;
|
|
5531
5666
|
}
|
|
5532
5667
|
return stringify({ services: serviceOverrides });
|
|
@@ -5637,15 +5772,19 @@ const dockerCheckCommand = defineCommand({
|
|
|
5637
5772
|
const services = parseComposeServices(composeCwd, defaults.composeFiles);
|
|
5638
5773
|
const fileReader = createFileReader();
|
|
5639
5774
|
const rootPkgRaw = fileReader.readFile(path.join(cwd, "package.json"));
|
|
5775
|
+
const locallyBuiltImageNames = [];
|
|
5640
5776
|
if (rootPkgRaw) {
|
|
5641
5777
|
const rootPkg = parsePackageJson(rootPkgRaw);
|
|
5642
5778
|
if (rootPkg?.name) {
|
|
5643
5779
|
const dockerPackages = detectDockerPackages(fileReader, cwd, rootPkg.name);
|
|
5644
5780
|
const composeImages = extractComposeImageNames(services);
|
|
5645
|
-
for (const pkg of dockerPackages)
|
|
5781
|
+
for (const pkg of dockerPackages) {
|
|
5782
|
+
locallyBuiltImageNames.push(pkg.imageName);
|
|
5783
|
+
if (!composeImages.some((img) => img === pkg.imageName || img.endsWith(`/${pkg.imageName}`))) log.warn(`Docker package "${pkg.dir}" (image: ${pkg.imageName}) is not referenced in any compose service.`);
|
|
5784
|
+
}
|
|
5646
5785
|
}
|
|
5647
5786
|
}
|
|
5648
|
-
const tempOverlayPath = writeTempOverlay(generateCheckOverlay(services));
|
|
5787
|
+
const tempOverlayPath = writeTempOverlay(generateCheckOverlay(services, locallyBuiltImageNames));
|
|
5649
5788
|
const composeFiles = [
|
|
5650
5789
|
...defaults.composeFiles,
|
|
5651
5790
|
tempOverlayPath,
|
|
@@ -6317,6 +6456,85 @@ const forgejoSecretsCommand = defineCommand({
|
|
|
6317
6456
|
}
|
|
6318
6457
|
});
|
|
6319
6458
|
//#endregion
|
|
6459
|
+
//#region src/commands/forgejo-webhooks.ts
|
|
6460
|
+
/** Render a WebhookFetchStatus, logging warnings for unauthorized/error cases. */
|
|
6461
|
+
function collectGroup(label, scopeHint, status) {
|
|
6462
|
+
if (status.status === "ok") return status.hooks.length > 0 ? {
|
|
6463
|
+
label,
|
|
6464
|
+
hooks: status.hooks
|
|
6465
|
+
} : null;
|
|
6466
|
+
if (status.status === "unauthorized") {
|
|
6467
|
+
log.warn(`${label}: HTTP ${String(status.httpStatus)} — token likely missing scope: ${scopeHint}`);
|
|
6468
|
+
return null;
|
|
6469
|
+
}
|
|
6470
|
+
log.warn(`${label}: HTTP ${String(status.httpStatus)} ${status.message}`);
|
|
6471
|
+
return null;
|
|
6472
|
+
}
|
|
6473
|
+
async function runListWebhooks(executor, conn) {
|
|
6474
|
+
const userResult = await getAuthenticatedUser(executor, conn);
|
|
6475
|
+
if (!userResult.ok) throw new FatalError(`Token is invalid or lacks read:user scope (HTTP ${String(userResult.httpStatus)})`);
|
|
6476
|
+
const user = userResult.login;
|
|
6477
|
+
const isAdmin = await hasAdminAccess(executor, conn);
|
|
6478
|
+
const groups = [];
|
|
6479
|
+
const systemGroup = collectGroup("System", "read:admin", await listSystemHooks(executor, conn));
|
|
6480
|
+
if (systemGroup) groups.push(systemGroup);
|
|
6481
|
+
const userGroup = collectGroup(`User: ${user}`, "read:user", await listUserHooks(executor, conn));
|
|
6482
|
+
if (userGroup) groups.push(userGroup);
|
|
6483
|
+
const orgs = await listOrgs(executor, conn, isAdmin ? "admin" : "user");
|
|
6484
|
+
for (const org of orgs) {
|
|
6485
|
+
const orgGroup = collectGroup(`Org: ${org}`, "read:organization", await listOrgHooks(executor, conn, org));
|
|
6486
|
+
if (orgGroup) groups.push(orgGroup);
|
|
6487
|
+
}
|
|
6488
|
+
const repos = await searchRepos(executor, conn);
|
|
6489
|
+
for (const repo of repos) {
|
|
6490
|
+
const repoGroup = collectGroup(`Repo: ${repo}`, "read:repository + repo admin", await listRepoHooks(executor, conn, repo));
|
|
6491
|
+
if (repoGroup) groups.push(repoGroup);
|
|
6492
|
+
}
|
|
6493
|
+
return {
|
|
6494
|
+
user,
|
|
6495
|
+
isAdmin,
|
|
6496
|
+
groups,
|
|
6497
|
+
totalCount: groups.reduce((sum, g) => sum + g.hooks.length, 0)
|
|
6498
|
+
};
|
|
6499
|
+
}
|
|
6500
|
+
function renderText(inventory, serverUrl) {
|
|
6501
|
+
log.info(`Forgejo Webhook Inventory — ${serverUrl}`);
|
|
6502
|
+
log.info(`Token user: ${inventory.user}${inventory.isAdmin ? " (admin)" : ""}`);
|
|
6503
|
+
if (inventory.groups.length === 0) {
|
|
6504
|
+
log.info("No webhooks found.");
|
|
6505
|
+
return;
|
|
6506
|
+
}
|
|
6507
|
+
for (const group of inventory.groups) {
|
|
6508
|
+
log.info(`${group.label} (${String(group.hooks.length)} webhook(s))`);
|
|
6509
|
+
for (const hook of group.hooks) {
|
|
6510
|
+
const url = hook.config?.["url"] ?? "n/a";
|
|
6511
|
+
log.info(` id=${String(hook.id)} type=${hook.type} active=${String(hook.active)} url=${url}`);
|
|
6512
|
+
}
|
|
6513
|
+
}
|
|
6514
|
+
log.info(`Total: ${String(inventory.totalCount)} webhook(s)`);
|
|
6515
|
+
if (inventory.isAdmin) log.info("Note: User-level webhooks for other accounts cannot be enumerated via the API. Check the admin UI or run this command with tokens from individual user accounts.");
|
|
6516
|
+
}
|
|
6517
|
+
const forgejoWebhooksCommand = defineCommand({
|
|
6518
|
+
meta: {
|
|
6519
|
+
name: "forgejo:webhooks",
|
|
6520
|
+
description: "Enumerate webhooks across a Forgejo instance (system, user, org, and repo levels)"
|
|
6521
|
+
},
|
|
6522
|
+
args: { json: {
|
|
6523
|
+
type: "boolean",
|
|
6524
|
+
description: "Emit results as JSON instead of a human-readable summary"
|
|
6525
|
+
} },
|
|
6526
|
+
async run({ args }) {
|
|
6527
|
+
const resolved = resolveConnection(process.cwd());
|
|
6528
|
+
if (resolved.type !== "forgejo") throw new FatalError("forgejo:webhooks requires a Forgejo repository");
|
|
6529
|
+
const inventory = await runListWebhooks(createRealExecutor(), resolved.conn);
|
|
6530
|
+
if (args.json) {
|
|
6531
|
+
console.log(JSON.stringify(inventory, null, 2));
|
|
6532
|
+
return;
|
|
6533
|
+
}
|
|
6534
|
+
renderText(inventory, resolved.conn.serverUrl);
|
|
6535
|
+
}
|
|
6536
|
+
});
|
|
6537
|
+
//#endregion
|
|
6320
6538
|
//#region src/release/webhook.ts
|
|
6321
6539
|
/** Build a webhook payload from simple release context. */
|
|
6322
6540
|
function buildSimplePayload(options) {
|
|
@@ -6475,7 +6693,7 @@ const webhookSendCommand = defineCommand({
|
|
|
6475
6693
|
const main = defineCommand({
|
|
6476
6694
|
meta: {
|
|
6477
6695
|
name: "bst",
|
|
6478
|
-
version: "0.
|
|
6696
|
+
version: "0.52.0",
|
|
6479
6697
|
description: "Bootstrap and maintain standardized TypeScript project tooling"
|
|
6480
6698
|
},
|
|
6481
6699
|
subCommands: {
|
|
@@ -6493,10 +6711,11 @@ const main = defineCommand({
|
|
|
6493
6711
|
"hooks:sync": hooksSyncCommand,
|
|
6494
6712
|
"setup:secrets": setupSecretsCommand,
|
|
6495
6713
|
"forgejo:secrets": forgejoSecretsCommand,
|
|
6714
|
+
"forgejo:webhooks": forgejoWebhooksCommand,
|
|
6496
6715
|
"webhook:send": webhookSendCommand
|
|
6497
6716
|
}
|
|
6498
6717
|
});
|
|
6499
|
-
console.log(`@bensandee/tooling v0.
|
|
6718
|
+
console.log(`@bensandee/tooling v0.52.0`);
|
|
6500
6719
|
async function run() {
|
|
6501
6720
|
await runMain(main);
|
|
6502
6721
|
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.52.0",
|
|
4
4
|
"description": "CLI tool to bootstrap and maintain standardized TypeScript project tooling",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@changesets/cli": "2.30.0",
|
|
50
50
|
"@release-it/bumper": "7.0.5",
|
|
51
|
-
"@types/node": "24.12.
|
|
51
|
+
"@types/node": "24.12.2",
|
|
52
52
|
"@types/picomatch": "4.0.3",
|
|
53
53
|
"commit-and-tag-version": "12.7.1",
|
|
54
54
|
"prettier": "3.8.2",
|