@bensandee/tooling 0.61.0 → 0.62.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 +5 -1
- package/dist/bin.mjs +305 -104
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -137,7 +137,7 @@ Opt out entirely with `"pnpmWorkspace": false` in `.tooling.json`; the generator
|
|
|
137
137
|
| Command | Description |
|
|
138
138
|
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
139
139
|
| `tooling release:changesets` | Changesets version/publish for Forgejo CI. Flags: `--dry-run`, `--verbose`. Env: `FORGEJO_SERVER_URL`, `FORGEJO_REPOSITORY`, `RELEASE_TOKEN`. |
|
|
140
|
-
| `tooling release:simple` | Streamlined release using commit-and-tag-version. Flags: `--release-as`, `--first-release`, `--prerelease`, `--verbose`.
|
|
140
|
+
| `tooling release:simple` | Streamlined release using commit-and-tag-version. Flags: `--release-as`, `--first-release`, `--prerelease`, `--verbose`. Env: `RELEASE_TOKEN` (required on every platform). |
|
|
141
141
|
| `tooling release:trigger` | Trigger a release workflow. |
|
|
142
142
|
| `tooling workflow:dispatch` | Dispatch a `workflow_dispatch` event for any workflow in any repo on the same git host. Flags: `--repo`, `--workflow` (required), `--ref`, `--input key=value` (repeatable). |
|
|
143
143
|
| `tooling forgejo:create-release` | Create a Forgejo release from a tag. |
|
|
@@ -170,6 +170,10 @@ tooling release:simple --release-as major --prerelease beta # → 2.0.0-beta.0
|
|
|
170
170
|
|
|
171
171
|
The generated release workflow exposes these as optional `workflow_dispatch` inputs (`bump` and `prerelease`), so bumps can also be controlled from the CI UI.
|
|
172
172
|
|
|
173
|
+
**Authentication:** `release:simple` requires `RELEASE_TOKEN` (a Personal Access Token with write access on the target forge) on every platform — Forgejo, GitHub, and GitHub Enterprise alike. The runner's auto-injected token (`github.token` / Forgejo Actions' internal auth) is never used: `actions/checkout@v6` writes that token to git config as an `http.<serverUrl>/.extraheader`, which silently overrides URL-embedded credentials and authenticates pushes as the runner pseudo-user. On Forgejo that pseudo-user is `uid=-2`, which fails the pre-receive hook (`Internal Server Error`); on GitHub Enterprise the same shape can mask custom hook rejections. `release:simple` actively unsets the extraheader and rewrites the remote URL with `RELEASE_TOKEN` before pushing.
|
|
174
|
+
|
|
175
|
+
GitHub consumers that previously relied on `github.token` must mint a PAT (scope: `contents: write`) and store it as the `RELEASE_TOKEN` secret on the repo or org.
|
|
176
|
+
|
|
173
177
|
#### `workflow:dispatch`
|
|
174
178
|
|
|
175
179
|
Triggers a `workflow_dispatch` event for an arbitrary workflow in an arbitrary repo on the same git host — useful for release→deploy patterns where one repo's CI kicks off a workflow in another (e.g. an infra repo). `release:trigger` is a special case of this command, pinned to `release.yml` in the current repo.
|
package/dist/bin.mjs
CHANGED
|
@@ -844,15 +844,24 @@ function parseGitUrl(urlStr) {
|
|
|
844
844
|
}
|
|
845
845
|
}
|
|
846
846
|
/**
|
|
847
|
-
* Configure the git remote origin to
|
|
847
|
+
* Configure the git remote origin to push as `token` regardless of what
|
|
848
|
+
* `actions/checkout` left behind.
|
|
848
849
|
*
|
|
849
|
-
*
|
|
850
|
-
*
|
|
851
|
-
*
|
|
850
|
+
* `actions/checkout@v6` injects an HTTP Authorization header into git config
|
|
851
|
+
* under `http.<serverUrl>/.extraheader`. That header overrides Basic auth
|
|
852
|
+
* embedded in the remote URL, so a `git remote set-url` alone is not enough —
|
|
853
|
+
* the runner's pseudo-user token still wins and the push authenticates as
|
|
854
|
+
* uid=-2 on Forgejo (and as the runner identity on GitHub Enterprise with
|
|
855
|
+
* custom pre-receive hooks). Unset the extraheader before rewriting the URL.
|
|
852
856
|
*/
|
|
853
|
-
function configureGitAuth(executor,
|
|
854
|
-
const host =
|
|
855
|
-
|
|
857
|
+
function configureGitAuth(executor, coords, cwd) {
|
|
858
|
+
const host = coords.serverUrl.replace(/^https?:\/\//, "");
|
|
859
|
+
executor.exec("git", [
|
|
860
|
+
"config",
|
|
861
|
+
"--unset-all",
|
|
862
|
+
`http.${coords.serverUrl}/.extraheader`
|
|
863
|
+
], { cwd });
|
|
864
|
+
const authUrl = `https://x-access-token:${coords.token}@${host}/${coords.repository}`;
|
|
856
865
|
executor.exec("git", [
|
|
857
866
|
"remote",
|
|
858
867
|
"set-url",
|
|
@@ -1580,11 +1589,21 @@ function buildWorkflowYaml(options) {
|
|
|
1580
1589
|
doc["on"] = options.on;
|
|
1581
1590
|
if (options.permissions) doc["permissions"] = options.permissions;
|
|
1582
1591
|
if (options.concurrency) doc["concurrency"] = options.concurrency;
|
|
1583
|
-
|
|
1592
|
+
const jobs = { [options.jobName]: {
|
|
1584
1593
|
"runs-on": options.runsOn ?? "ubuntu-latest",
|
|
1585
1594
|
...options.jobEnv && { env: options.jobEnv },
|
|
1595
|
+
...options.jobOutputs && { outputs: options.jobOutputs },
|
|
1586
1596
|
steps: options.steps.map((s) => s.step)
|
|
1587
1597
|
} };
|
|
1598
|
+
for (const [name, def] of Object.entries(options.extraJobs ?? {})) jobs[name] = {
|
|
1599
|
+
...def.needs && { needs: def.needs },
|
|
1600
|
+
...def.if && { if: def.if },
|
|
1601
|
+
...def["continue-on-error"] && { "continue-on-error": true },
|
|
1602
|
+
"runs-on": def.runsOn ?? "ubuntu-latest",
|
|
1603
|
+
...def.env && { env: def.env },
|
|
1604
|
+
steps: def.steps.map((s) => s.step)
|
|
1605
|
+
};
|
|
1606
|
+
doc["jobs"] = jobs;
|
|
1588
1607
|
const readable = stringify(doc, {
|
|
1589
1608
|
lineWidth: 0,
|
|
1590
1609
|
nullStr: ""
|
|
@@ -2206,7 +2225,7 @@ async function generatePackageJson(ctx) {
|
|
|
2206
2225
|
const devDeps = { ...ROOT_DEV_DEPS };
|
|
2207
2226
|
if (!isMonorepo) Object.assign(devDeps, PER_PACKAGE_DEV_DEPS);
|
|
2208
2227
|
devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.11.3";
|
|
2209
|
-
devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.
|
|
2228
|
+
devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.62.0";
|
|
2210
2229
|
if (ctx.config.useEslintPlugin) devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "0.13.0";
|
|
2211
2230
|
if (ctx.config.formatter === "oxfmt") devDeps["oxfmt"] = {
|
|
2212
2231
|
"@changesets/cli": "2.31.0",
|
|
@@ -3172,6 +3191,67 @@ async function responseBodyText(res) {
|
|
|
3172
3191
|
return "(could not read response body)";
|
|
3173
3192
|
}
|
|
3174
3193
|
}
|
|
3194
|
+
const SwaggerEnvelopeSchema = z.union([z.object({ message: z.literal("") }), z.object({
|
|
3195
|
+
message: z.string(),
|
|
3196
|
+
url: z.string().regex(/\/api\/swagger$/)
|
|
3197
|
+
})]);
|
|
3198
|
+
/**
|
|
3199
|
+
* Detect Forgejo's signature "route not handled" response shape — empty body
|
|
3200
|
+
* or a JSON envelope with either an empty `message` or a `url` pointing at
|
|
3201
|
+
* `/api/swagger`. Forgejo returns this from a 500 (and sometimes 404) when no
|
|
3202
|
+
* route handles the request, even though the status code suggests a
|
|
3203
|
+
* server-side fault.
|
|
3204
|
+
*/
|
|
3205
|
+
function isForgejoGenericServerErrorBody(body) {
|
|
3206
|
+
if (body === "") return true;
|
|
3207
|
+
try {
|
|
3208
|
+
const parsed = JSON.parse(body);
|
|
3209
|
+
return SwaggerEnvelopeSchema.safeParse(parsed).success;
|
|
3210
|
+
} catch (_error) {
|
|
3211
|
+
return false;
|
|
3212
|
+
}
|
|
3213
|
+
}
|
|
3214
|
+
const DISPATCH_VERB_PATTERN = /\bdispatch\b/i;
|
|
3215
|
+
/**
|
|
3216
|
+
* Map a non-2xx Forgejo response to a human-readable hint.
|
|
3217
|
+
*
|
|
3218
|
+
* Returns the most-likely cause first; callers still surface the raw body so an
|
|
3219
|
+
* uncommon failure mode isn't masked. Returns "" for unmapped codes (2xx/3xx,
|
|
3220
|
+
* teapots) so the caller can decide whether to include a Hint line at all.
|
|
3221
|
+
*/
|
|
3222
|
+
function forgejoErrorHint(status, body, operation) {
|
|
3223
|
+
const isDispatch = DISPATCH_VERB_PATTERN.test(operation.verb);
|
|
3224
|
+
const resource = operation.resource ?? "the resource";
|
|
3225
|
+
if (status === 401 || status === 403) return `Forgejo rejected the token. It needs the scope required for "${operation.verb}" on ${resource}.`;
|
|
3226
|
+
if (status === 404) {
|
|
3227
|
+
const base = `"${resource}" was not found on the server. Verify the name${operation.ref ? `, that it exists on ${operation.ref}` : ""}, and that the token can see it.`;
|
|
3228
|
+
return isDispatch ? `${base} For dispatch, also confirm the workflow has an "on: workflow_dispatch:" trigger.` : base;
|
|
3229
|
+
}
|
|
3230
|
+
if (status === 400 || status === 422) {
|
|
3231
|
+
const base = `Forgejo rejected the request payload for "${operation.verb}". Check for missing or invalid fields.`;
|
|
3232
|
+
return isDispatch ? `${base} For dispatch, common causes are a missing "on: workflow_dispatch:" trigger or a required input not being provided.` : base;
|
|
3233
|
+
}
|
|
3234
|
+
if (status >= 500 && status < 600) {
|
|
3235
|
+
if (isForgejoGenericServerErrorBody(body)) {
|
|
3236
|
+
const ref = operation.ref ? ` on ${operation.ref}` : "";
|
|
3237
|
+
return `Forgejo returned a generic server error. Most often this means the requested route is unavailable${isDispatch ? ` (e.g. "${resource}" doesn't exist${ref}, or the workflow lacks an "on: workflow_dispatch:" trigger)` : ""}. Less commonly: the token lacks scope, or it is a server-side bug.`;
|
|
3238
|
+
}
|
|
3239
|
+
return `Forgejo server error — usually transient. Retry; if it persists, check the server logs.`;
|
|
3240
|
+
}
|
|
3241
|
+
return "";
|
|
3242
|
+
}
|
|
3243
|
+
/**
|
|
3244
|
+
* Build a richer error message for a non-2xx Forgejo API response.
|
|
3245
|
+
*
|
|
3246
|
+
* Reads, truncates, and redacts the body via {@link responseBodyText}; appends
|
|
3247
|
+
* a `Hint:` line when {@link forgejoErrorHint} has one; appends the raw body as
|
|
3248
|
+
* a `Response:` block. Callers still pick the error class (Transient vs Fatal).
|
|
3249
|
+
*/
|
|
3250
|
+
async function forgejoErrorMessage(res, operation) {
|
|
3251
|
+
const body = await responseBodyText(res);
|
|
3252
|
+
const hint = forgejoErrorHint(res.status, body, operation);
|
|
3253
|
+
return `Failed to ${operation.verb} on Forgejo: ${String(res.status)} ${res.statusText}` + (hint ? `\nHint: ${hint}` : "") + (body ? `\nResponse: ${body}` : "");
|
|
3254
|
+
}
|
|
3175
3255
|
const PullRequestSchema = z.array(z.object({
|
|
3176
3256
|
number: z.number(),
|
|
3177
3257
|
head: z.object({ ref: z.string() })
|
|
@@ -3185,10 +3265,10 @@ const PullRequestSchema = z.array(z.object({
|
|
|
3185
3265
|
async function findOpenPr(executor, conn, head) {
|
|
3186
3266
|
const url = `${conn.serverUrl}/api/v1/repos/${conn.repository}/pulls?state=open`;
|
|
3187
3267
|
const res = await executor.fetch(url, { headers: { Authorization: `token ${conn.token}` } });
|
|
3188
|
-
if (!res.ok) {
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
}
|
|
3268
|
+
if (!res.ok) throw new TransientError(await forgejoErrorMessage(res, {
|
|
3269
|
+
verb: "list PRs",
|
|
3270
|
+
resource: conn.repository
|
|
3271
|
+
}));
|
|
3192
3272
|
const parsed = PullRequestSchema.safeParse(await res.json());
|
|
3193
3273
|
if (!parsed.success) throw new UnexpectedError(`Unexpected PR list response: ${parsed.error.message}`);
|
|
3194
3274
|
return parsed.data.find((pr) => pr.head.ref === head)?.number ?? null;
|
|
@@ -3210,10 +3290,10 @@ async function createPr(executor, conn, options) {
|
|
|
3210
3290
|
},
|
|
3211
3291
|
body: JSON.stringify(payload)
|
|
3212
3292
|
});
|
|
3213
|
-
if (!res.ok) {
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
}
|
|
3293
|
+
if (!res.ok) throw new TransientError(await forgejoErrorMessage(res, {
|
|
3294
|
+
verb: "create PR",
|
|
3295
|
+
resource: `${options.head}→${options.base}`
|
|
3296
|
+
}));
|
|
3217
3297
|
}
|
|
3218
3298
|
/** Update an existing pull request's title and body. */
|
|
3219
3299
|
async function updatePr(executor, conn, prNumber, options) {
|
|
@@ -3229,10 +3309,10 @@ async function updatePr(executor, conn, prNumber, options) {
|
|
|
3229
3309
|
body: options.body
|
|
3230
3310
|
})
|
|
3231
3311
|
});
|
|
3232
|
-
if (!res.ok) {
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
}
|
|
3312
|
+
if (!res.ok) throw new TransientError(await forgejoErrorMessage(res, {
|
|
3313
|
+
verb: "update PR",
|
|
3314
|
+
resource: `#${String(prNumber)}`
|
|
3315
|
+
}));
|
|
3236
3316
|
}
|
|
3237
3317
|
/** Merge a pull request by number. */
|
|
3238
3318
|
async function mergePr(executor, conn, prNumber, options) {
|
|
@@ -3248,10 +3328,10 @@ async function mergePr(executor, conn, prNumber, options) {
|
|
|
3248
3328
|
delete_branch_after_merge: options?.deleteBranch ?? true
|
|
3249
3329
|
})
|
|
3250
3330
|
});
|
|
3251
|
-
if (!res.ok) {
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
}
|
|
3331
|
+
if (!res.ok) throw new TransientError(await forgejoErrorMessage(res, {
|
|
3332
|
+
verb: "merge PR",
|
|
3333
|
+
resource: `#${String(prNumber)}`
|
|
3334
|
+
}));
|
|
3255
3335
|
}
|
|
3256
3336
|
/** Check whether a Forgejo release already exists for a given tag. */
|
|
3257
3337
|
async function findRelease(executor, conn, tag) {
|
|
@@ -3260,8 +3340,10 @@ async function findRelease(executor, conn, tag) {
|
|
|
3260
3340
|
const res = await executor.fetch(url, { headers: { Authorization: `token ${conn.token}` } });
|
|
3261
3341
|
if (res.status === 200) return true;
|
|
3262
3342
|
if (res.status === 404) return false;
|
|
3263
|
-
|
|
3264
|
-
|
|
3343
|
+
throw new TransientError(await forgejoErrorMessage(res, {
|
|
3344
|
+
verb: "check release",
|
|
3345
|
+
resource: tag
|
|
3346
|
+
}));
|
|
3265
3347
|
}
|
|
3266
3348
|
/** Create a Forgejo release for a given tag. */
|
|
3267
3349
|
async function createRelease(executor, conn, tag) {
|
|
@@ -3278,10 +3360,10 @@ async function createRelease(executor, conn, tag) {
|
|
|
3278
3360
|
body: `Published ${tag}`
|
|
3279
3361
|
})
|
|
3280
3362
|
});
|
|
3281
|
-
if (!res.ok) {
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
}
|
|
3363
|
+
if (!res.ok) throw new TransientError(await forgejoErrorMessage(res, {
|
|
3364
|
+
verb: "create release",
|
|
3365
|
+
resource: tag
|
|
3366
|
+
}));
|
|
3285
3367
|
}
|
|
3286
3368
|
/**
|
|
3287
3369
|
* Ensure a Forgejo release exists for a tag, creating it if necessary.
|
|
@@ -3313,10 +3395,10 @@ async function uploadReleaseAsset(executor, conn, tag, options) {
|
|
|
3313
3395
|
const encodedTag = encodeURIComponent(tag);
|
|
3314
3396
|
const releaseUrl = `${conn.serverUrl}/api/v1/repos/${conn.repository}/releases/tags/${encodedTag}`;
|
|
3315
3397
|
const releaseRes = await executor.fetch(releaseUrl, { headers: { Authorization: `token ${conn.token}` } });
|
|
3316
|
-
if (!releaseRes.ok) {
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
}
|
|
3398
|
+
if (!releaseRes.ok) throw new TransientError(await forgejoErrorMessage(releaseRes, {
|
|
3399
|
+
verb: "get release",
|
|
3400
|
+
resource: tag
|
|
3401
|
+
}));
|
|
3320
3402
|
const parsed = z.object({ id: z.number() }).safeParse(await releaseRes.json());
|
|
3321
3403
|
if (!parsed.success) throw new UnexpectedError(`Unexpected release response: ${parsed.error.message}`);
|
|
3322
3404
|
const releaseId = parsed.data.id;
|
|
@@ -3328,10 +3410,10 @@ async function uploadReleaseAsset(executor, conn, tag, options) {
|
|
|
3328
3410
|
headers: { Authorization: `token ${conn.token}` },
|
|
3329
3411
|
body: form
|
|
3330
3412
|
});
|
|
3331
|
-
if (!res.ok) {
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
}
|
|
3413
|
+
if (!res.ok) throw new TransientError(await forgejoErrorMessage(res, {
|
|
3414
|
+
verb: "upload asset",
|
|
3415
|
+
resource: `${options.assetName} to ${tag}`
|
|
3416
|
+
}));
|
|
3335
3417
|
}
|
|
3336
3418
|
const ActionSecretListSchema = z.object({
|
|
3337
3419
|
secrets: z.array(z.object({ name: z.string() })),
|
|
@@ -3362,10 +3444,10 @@ async function createAccessToken(executor, serverUrl, username, password, tokenN
|
|
|
3362
3444
|
scopes
|
|
3363
3445
|
})
|
|
3364
3446
|
});
|
|
3365
|
-
if (!res.ok) {
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
}
|
|
3447
|
+
if (!res.ok) throw new TransientError(await forgejoErrorMessage(res, {
|
|
3448
|
+
verb: "create access token",
|
|
3449
|
+
resource: username
|
|
3450
|
+
}));
|
|
3369
3451
|
const parsed = AccessTokenResponseSchema.safeParse(await res.json());
|
|
3370
3452
|
if (!parsed.success) throw new UnexpectedError(`Unexpected token response: ${parsed.error.message}`);
|
|
3371
3453
|
return parsed.data.sha1;
|
|
@@ -3386,10 +3468,10 @@ async function setActionSecret(executor, conn, secretName, secretValue) {
|
|
|
3386
3468
|
},
|
|
3387
3469
|
body: JSON.stringify({ data: secretValue })
|
|
3388
3470
|
});
|
|
3389
|
-
if (!res.ok) {
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
}
|
|
3471
|
+
if (!res.ok) throw new TransientError(await forgejoErrorMessage(res, {
|
|
3472
|
+
verb: "set secret",
|
|
3473
|
+
resource: secretName
|
|
3474
|
+
}));
|
|
3393
3475
|
}
|
|
3394
3476
|
/**
|
|
3395
3477
|
* Set (or overwrite) a Forgejo Actions secret at the user level.
|
|
@@ -3412,10 +3494,10 @@ async function setUserActionSecret(executor, serverUrl, authorization, secretNam
|
|
|
3412
3494
|
headers,
|
|
3413
3495
|
body: JSON.stringify({ data: secretValue })
|
|
3414
3496
|
});
|
|
3415
|
-
if (!res.ok) {
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
}
|
|
3497
|
+
if (!res.ok) throw new TransientError(await forgejoErrorMessage(res, {
|
|
3498
|
+
verb: "set user secret",
|
|
3499
|
+
resource: secretName
|
|
3500
|
+
}));
|
|
3419
3501
|
}
|
|
3420
3502
|
/**
|
|
3421
3503
|
* List Forgejo Actions secret names for a repository.
|
|
@@ -3431,10 +3513,10 @@ async function listActionSecrets(executor, conn) {
|
|
|
3431
3513
|
for (;;) {
|
|
3432
3514
|
const url = `${conn.serverUrl}/api/v1/repos/${owner}/${repo}/actions/secrets?page=${String(page)}&limit=${String(limit)}`;
|
|
3433
3515
|
const res = await executor.fetch(url, { headers: { Authorization: `token ${conn.token}` } });
|
|
3434
|
-
if (!res.ok) {
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
}
|
|
3516
|
+
if (!res.ok) throw new TransientError(await forgejoErrorMessage(res, {
|
|
3517
|
+
verb: "list secrets",
|
|
3518
|
+
resource: `${owner}/${repo}`
|
|
3519
|
+
}));
|
|
3438
3520
|
const parsed = ActionSecretListSchema.safeParse(await res.json());
|
|
3439
3521
|
if (!parsed.success) throw new UnexpectedError(`Unexpected secrets list response: ${parsed.error.message}`);
|
|
3440
3522
|
for (const secret of parsed.data.secrets) names.push(secret.name);
|
|
@@ -3455,10 +3537,10 @@ async function deleteActionSecret(executor, conn, secretName) {
|
|
|
3455
3537
|
method: "DELETE",
|
|
3456
3538
|
headers: { Authorization: `token ${conn.token}` }
|
|
3457
3539
|
});
|
|
3458
|
-
if (!res.ok) {
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
}
|
|
3540
|
+
if (!res.ok) throw new TransientError(await forgejoErrorMessage(res, {
|
|
3541
|
+
verb: "delete secret",
|
|
3542
|
+
resource: secretName
|
|
3543
|
+
}));
|
|
3462
3544
|
}
|
|
3463
3545
|
const WebhookSchema = z.object({
|
|
3464
3546
|
id: z.number(),
|
|
@@ -3467,6 +3549,11 @@ const WebhookSchema = z.object({
|
|
|
3467
3549
|
config: z.record(z.string(), z.string()).optional()
|
|
3468
3550
|
});
|
|
3469
3551
|
const WebhookListSchema = z.array(WebhookSchema);
|
|
3552
|
+
/**
|
|
3553
|
+
* Returns a status struct instead of throwing, so it deliberately does not use
|
|
3554
|
+
* {@link forgejoErrorMessage} — the caller branches on `status` rather than
|
|
3555
|
+
* catching an error.
|
|
3556
|
+
*/
|
|
3470
3557
|
async function fetchHooks(executor, conn, endpoint) {
|
|
3471
3558
|
const res = await executor.fetch(`${conn.serverUrl}/api/v1${endpoint}`, { headers: { Authorization: `token ${conn.token}` } });
|
|
3472
3559
|
if (res.status === 401 || res.status === 403) return {
|
|
@@ -3531,17 +3618,17 @@ async function hasAdminAccess(executor, conn) {
|
|
|
3531
3618
|
const OrgSchema = z.object({ username: z.string() });
|
|
3532
3619
|
const OrgListSchema = z.array(OrgSchema);
|
|
3533
3620
|
const PAGE_LIMIT = 50;
|
|
3534
|
-
async function paginate(executor, conn, endpoint, extract) {
|
|
3621
|
+
async function paginate(executor, conn, endpoint, verb, extract) {
|
|
3535
3622
|
const all = [];
|
|
3536
3623
|
let page = 1;
|
|
3537
3624
|
const separator = endpoint.includes("?") ? "&" : "?";
|
|
3538
3625
|
for (;;) {
|
|
3539
3626
|
const url = `${conn.serverUrl}/api/v1${endpoint}${separator}page=${String(page)}&limit=${String(PAGE_LIMIT)}`;
|
|
3540
3627
|
const res = await executor.fetch(url, { headers: { Authorization: `token ${conn.token}` } });
|
|
3541
|
-
if (!res.ok) {
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
}
|
|
3628
|
+
if (!res.ok) throw new TransientError(await forgejoErrorMessage(res, {
|
|
3629
|
+
verb,
|
|
3630
|
+
resource: `${endpoint} (page ${String(page)})`
|
|
3631
|
+
}));
|
|
3545
3632
|
const items = extract(await res.json());
|
|
3546
3633
|
all.push(...items);
|
|
3547
3634
|
if (items.length < PAGE_LIMIT) break;
|
|
@@ -3555,7 +3642,7 @@ async function paginate(executor, conn, endpoint, extract) {
|
|
|
3555
3642
|
*/
|
|
3556
3643
|
async function listOrgs(executor, conn, mode) {
|
|
3557
3644
|
const endpoint = mode === "admin" ? "/admin/orgs" : "/user/orgs";
|
|
3558
|
-
return (await paginate(executor, conn, endpoint, (raw) => {
|
|
3645
|
+
return (await paginate(executor, conn, endpoint, "list orgs", (raw) => {
|
|
3559
3646
|
const parsed = OrgListSchema.safeParse(raw);
|
|
3560
3647
|
if (!parsed.success) throw new UnexpectedError(`Unexpected ${endpoint} response: ${parsed.error.message}`);
|
|
3561
3648
|
return parsed.data;
|
|
@@ -3564,7 +3651,7 @@ async function listOrgs(executor, conn, mode) {
|
|
|
3564
3651
|
const RepoSearchSchema = z.object({ data: z.array(z.object({ full_name: z.string() })) });
|
|
3565
3652
|
/** List repositories visible to the token via `/repos/search`. */
|
|
3566
3653
|
async function searchRepos(executor, conn) {
|
|
3567
|
-
return (await paginate(executor, conn, "/repos/search", (raw) => {
|
|
3654
|
+
return (await paginate(executor, conn, "/repos/search", "list repos", (raw) => {
|
|
3568
3655
|
const parsed = RepoSearchSchema.safeParse(raw);
|
|
3569
3656
|
if (!parsed.success) throw new UnexpectedError(`Unexpected /repos/search response: ${parsed.error.message}`);
|
|
3570
3657
|
return parsed.data.data;
|
|
@@ -3772,7 +3859,11 @@ async function generateGitignore(ctx) {
|
|
|
3772
3859
|
}
|
|
3773
3860
|
//#endregion
|
|
3774
3861
|
//#region src/generators/release-ci.ts
|
|
3775
|
-
const DEFAULT_POST_RELEASE_COMMAND = [
|
|
3862
|
+
const DEFAULT_POST_RELEASE_COMMAND = [
|
|
3863
|
+
"pnpm exec bst webhook:send",
|
|
3864
|
+
" \"$POST_RELEASE_HOOK_URL\"",
|
|
3865
|
+
" --tag \"$RELEASE_TAG\""
|
|
3866
|
+
].join("");
|
|
3776
3867
|
/** Map secret names to `${{ secrets.<NAME> }}` env entries. */
|
|
3777
3868
|
function secretsEnv(secrets) {
|
|
3778
3869
|
const env = {};
|
|
@@ -3798,6 +3889,26 @@ function postReleaseHookStep(opts) {
|
|
|
3798
3889
|
} };
|
|
3799
3890
|
}
|
|
3800
3891
|
/**
|
|
3892
|
+
* Post-release hook as a separate job with `continue-on-error: true` so a
|
|
3893
|
+
* webhook failure (HTTP 5xx, DNS, etc.) shows up as a red job in the run UI
|
|
3894
|
+
* without failing the overall workflow conclusion.
|
|
3895
|
+
*/
|
|
3896
|
+
function postReleaseHookJob(opts) {
|
|
3897
|
+
return {
|
|
3898
|
+
needs: opts.needsJob,
|
|
3899
|
+
if: opts.publishCondition,
|
|
3900
|
+
"continue-on-error": true,
|
|
3901
|
+
steps: [...commonSteps(opts.nodeVersionYaml, opts.publishesNpm), { step: {
|
|
3902
|
+
name: "Post-release hook",
|
|
3903
|
+
env: {
|
|
3904
|
+
...hookSecretEnv(opts.secrets),
|
|
3905
|
+
RELEASE_TAG: opts.releaseTagExpr
|
|
3906
|
+
},
|
|
3907
|
+
run: opts.command ?? DEFAULT_POST_RELEASE_COMMAND
|
|
3908
|
+
} }]
|
|
3909
|
+
};
|
|
3910
|
+
}
|
|
3911
|
+
/**
|
|
3801
3912
|
* Step that dispatches a `workflow_dispatch` event to another repo after a
|
|
3802
3913
|
* release, triggering a downstream deploy. The release tag is forwarded from
|
|
3803
3914
|
* the `release` step's `tag` output.
|
|
@@ -3845,7 +3956,7 @@ function commonSteps(nodeVersionYaml, publishesNpm) {
|
|
|
3845
3956
|
];
|
|
3846
3957
|
}
|
|
3847
3958
|
function releaseItSteps(ci, nodeVersionYaml, publishesNpm) {
|
|
3848
|
-
const tokenEnv = ci === "github" ? { GITHUB_TOKEN: actionsExpr("
|
|
3959
|
+
const tokenEnv = ci === "github" ? { GITHUB_TOKEN: actionsExpr("secrets.RELEASE_TOKEN") } : { RELEASE_TOKEN: actionsExpr("secrets.RELEASE_TOKEN") };
|
|
3849
3960
|
const npmEnv = publishesNpm ? { NODE_AUTH_TOKEN: actionsExpr("secrets.NPM_TOKEN") } : {};
|
|
3850
3961
|
return [...commonSteps(nodeVersionYaml, publishesNpm), { step: {
|
|
3851
3962
|
run: "pnpm release-it --ci",
|
|
@@ -3881,7 +3992,11 @@ function simpleWorkflowDispatchTrigger() {
|
|
|
3881
3992
|
}
|
|
3882
3993
|
/** Platform connection env so a `bst` step can resolve the git host. */
|
|
3883
3994
|
function connectionEnv(ci) {
|
|
3884
|
-
|
|
3995
|
+
if (ci === "github") return {
|
|
3996
|
+
RELEASE_TOKEN: actionsExpr("secrets.RELEASE_TOKEN"),
|
|
3997
|
+
GITHUB_TOKEN: actionsExpr("secrets.RELEASE_TOKEN")
|
|
3998
|
+
};
|
|
3999
|
+
return {
|
|
3885
4000
|
FORGEJO_SERVER_URL: actionsExpr("github.server_url"),
|
|
3886
4001
|
FORGEJO_REPOSITORY: actionsExpr("github.repository"),
|
|
3887
4002
|
RELEASE_TOKEN: actionsExpr("secrets.RELEASE_TOKEN")
|
|
@@ -3970,15 +4085,27 @@ async function generateReleaseCi(ctx) {
|
|
|
3970
4085
|
secrets: hookSecrets
|
|
3971
4086
|
}));
|
|
3972
4087
|
}
|
|
4088
|
+
let extraJobs;
|
|
4089
|
+
let jobOutputs;
|
|
3973
4090
|
if (ctx.config.releaseStrategy === "release-it") steps.push(postReleaseHookStep({
|
|
3974
4091
|
command: getPostReleaseHookCommand(ctx),
|
|
3975
4092
|
secrets: hookSecrets
|
|
3976
4093
|
}));
|
|
3977
|
-
else if (ctx.config.releaseStrategy === "simple")
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
4094
|
+
else if (ctx.config.releaseStrategy === "simple") {
|
|
4095
|
+
jobOutputs = {
|
|
4096
|
+
pushed: actionsExpr("steps.release.outputs.pushed"),
|
|
4097
|
+
tag: actionsExpr("steps.release.outputs.tag")
|
|
4098
|
+
};
|
|
4099
|
+
extraJobs = { "post-release-hook": postReleaseHookJob({
|
|
4100
|
+
needsJob: "release",
|
|
4101
|
+
publishCondition: "needs.release.outputs.pushed == 'true'",
|
|
4102
|
+
releaseTagExpr: actionsExpr("needs.release.outputs.tag"),
|
|
4103
|
+
command: getPostReleaseHookCommand(ctx),
|
|
4104
|
+
secrets: hookSecrets,
|
|
4105
|
+
nodeVersionYaml,
|
|
4106
|
+
publishesNpm
|
|
4107
|
+
}) };
|
|
4108
|
+
}
|
|
3982
4109
|
const on = ctx.config.releaseStrategy === "simple" ? simpleWorkflowDispatchTrigger() : { workflow_dispatch: null };
|
|
3983
4110
|
const content = buildWorkflowYaml({
|
|
3984
4111
|
ci: ctx.config.ci,
|
|
@@ -3986,7 +4113,9 @@ async function generateReleaseCi(ctx) {
|
|
|
3986
4113
|
on,
|
|
3987
4114
|
...isGitHub && { permissions: { contents: "write" } },
|
|
3988
4115
|
jobName: "release",
|
|
3989
|
-
|
|
4116
|
+
...jobOutputs && { jobOutputs },
|
|
4117
|
+
steps,
|
|
4118
|
+
...extraJobs && { extraJobs }
|
|
3990
4119
|
});
|
|
3991
4120
|
const alreadyExists = ctx.exists(workflowPath);
|
|
3992
4121
|
const existing = alreadyExists ? ctx.read(workflowPath) : void 0;
|
|
@@ -4043,7 +4172,7 @@ function changesetsReleaseStep(ci, publishesNpm) {
|
|
|
4043
4172
|
version: "pnpm changeset version"
|
|
4044
4173
|
},
|
|
4045
4174
|
env: {
|
|
4046
|
-
GITHUB_TOKEN: actionsExpr("
|
|
4175
|
+
GITHUB_TOKEN: actionsExpr("secrets.RELEASE_TOKEN"),
|
|
4047
4176
|
...publishesNpm && { NPM_TOKEN: actionsExpr("secrets.NPM_TOKEN") }
|
|
4048
4177
|
}
|
|
4049
4178
|
} };
|
|
@@ -4112,8 +4241,16 @@ async function generateCi(ctx) {
|
|
|
4112
4241
|
const hasAssets = isChangesets && hasReleaseAssets(ctx);
|
|
4113
4242
|
const host = hasDocker ? resolveGenTimeHost(ctx) : null;
|
|
4114
4243
|
const publishedIf = "steps.release.outputs.published == 'true'";
|
|
4244
|
+
const githubChangesetsDeriveTagStep = { step: {
|
|
4245
|
+
name: "Derive release tag",
|
|
4246
|
+
id: "derive-tag",
|
|
4247
|
+
if: publishedIf,
|
|
4248
|
+
env: { PUBLISHED_PACKAGES: actionsExpr("steps.release.outputs.publishedPackages") },
|
|
4249
|
+
run: "echo \"tag=$(echo \"$PUBLISHED_PACKAGES\" | jq -r '.[0] | \"\\(.name)@\\(.version)\"')\" >> \"$GITHUB_OUTPUT\""
|
|
4250
|
+
} };
|
|
4251
|
+
const changesetsTagOutputExpr = isGitHub ? actionsExpr("steps.derive-tag.outputs.tag") : actionsExpr("steps.release.outputs.tag");
|
|
4115
4252
|
const releaseEnv = {};
|
|
4116
|
-
if (isGitHub) releaseEnv["GITHUB_TOKEN"] = actionsExpr("
|
|
4253
|
+
if (isGitHub) releaseEnv["GITHUB_TOKEN"] = actionsExpr("secrets.RELEASE_TOKEN");
|
|
4117
4254
|
else {
|
|
4118
4255
|
releaseEnv["FORGEJO_SERVER_URL"] = actionsExpr("github.server_url");
|
|
4119
4256
|
releaseEnv["FORGEJO_REPOSITORY"] = actionsExpr("github.repository");
|
|
@@ -4123,6 +4260,7 @@ async function generateCi(ctx) {
|
|
|
4123
4260
|
...checkSteps(nodeVersionYaml, isChangesets && publishesNpm, coverage.enabled),
|
|
4124
4261
|
...emitCoverageSteps ? coverageHistorySteps() : [],
|
|
4125
4262
|
...isChangesets ? [changesetsReleaseStep(ctx.config.ci, publishesNpm)] : [],
|
|
4263
|
+
...isChangesets && isGitHub ? [githubChangesetsDeriveTagStep] : [],
|
|
4126
4264
|
...hasAssets ? [{ step: {
|
|
4127
4265
|
name: "Upload Release Assets",
|
|
4128
4266
|
if: publishedIf,
|
|
@@ -4141,13 +4279,25 @@ async function generateCi(ctx) {
|
|
|
4141
4279
|
releaseEnv,
|
|
4142
4280
|
condition: publishedIf,
|
|
4143
4281
|
secrets: hookSecrets
|
|
4144
|
-
})] : [],
|
|
4145
|
-
...isChangesets ? [postReleaseHookStep({
|
|
4146
|
-
command: hookCommand,
|
|
4147
|
-
secrets: hookSecrets,
|
|
4148
|
-
condition: publishedIf
|
|
4149
4282
|
})] : []
|
|
4150
4283
|
];
|
|
4284
|
+
let extraJobs;
|
|
4285
|
+
let jobOutputs;
|
|
4286
|
+
if (isChangesets) {
|
|
4287
|
+
jobOutputs = {
|
|
4288
|
+
published: actionsExpr("steps.release.outputs.published"),
|
|
4289
|
+
tag: changesetsTagOutputExpr
|
|
4290
|
+
};
|
|
4291
|
+
extraJobs = { "post-release-hook": postReleaseHookJob({
|
|
4292
|
+
needsJob: "check",
|
|
4293
|
+
publishCondition: "needs.check.outputs.published == 'true'",
|
|
4294
|
+
releaseTagExpr: actionsExpr("needs.check.outputs.tag"),
|
|
4295
|
+
command: hookCommand,
|
|
4296
|
+
secrets: hookSecrets,
|
|
4297
|
+
nodeVersionYaml,
|
|
4298
|
+
publishesNpm
|
|
4299
|
+
}) };
|
|
4300
|
+
}
|
|
4151
4301
|
const content = buildWorkflowYaml({
|
|
4152
4302
|
ci: ctx.config.ci,
|
|
4153
4303
|
name: "CI",
|
|
@@ -4163,7 +4313,9 @@ async function generateCi(ctx) {
|
|
|
4163
4313
|
...isChangesets && { concurrency: CI_CONCURRENCY },
|
|
4164
4314
|
jobName: "check",
|
|
4165
4315
|
...emitCoverageSteps && { jobEnv: IS_MAIN_JOB_ENV },
|
|
4166
|
-
|
|
4316
|
+
...jobOutputs && { jobOutputs },
|
|
4317
|
+
steps,
|
|
4318
|
+
...extraJobs && { extraJobs }
|
|
4167
4319
|
});
|
|
4168
4320
|
const filePath = ciWorkflowPath(ctx.config.ci, ctx.config.releaseStrategy);
|
|
4169
4321
|
const alreadyExists = ctx.exists(filePath);
|
|
@@ -5273,7 +5425,7 @@ async function runGenerators(ctx) {
|
|
|
5273
5425
|
function generateMigratePrompt(results, config, detected) {
|
|
5274
5426
|
const sections = [];
|
|
5275
5427
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
5276
|
-
sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.
|
|
5428
|
+
sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.62.0 repo:sync\` on ${timestamp}_\n`);
|
|
5277
5429
|
const changed = results.filter((r) => r.action === "created" || r.action === "updated" || r.action === "archived");
|
|
5278
5430
|
if (changed.length > 0) {
|
|
5279
5431
|
const items = changed.map((r) => {
|
|
@@ -6239,10 +6391,11 @@ async function dispatchForgejo(conn, options, executor) {
|
|
|
6239
6391
|
},
|
|
6240
6392
|
body: JSON.stringify(body)
|
|
6241
6393
|
});
|
|
6242
|
-
if (!res.ok) {
|
|
6243
|
-
|
|
6244
|
-
|
|
6245
|
-
|
|
6394
|
+
if (!res.ok) throw new FatalError(await forgejoErrorMessage(res, {
|
|
6395
|
+
verb: "dispatch workflow",
|
|
6396
|
+
resource: options.workflow,
|
|
6397
|
+
ref: options.ref
|
|
6398
|
+
}));
|
|
6246
6399
|
log.info(`Dispatched ${options.workflow} on ${repo} (ref: ${options.ref})`);
|
|
6247
6400
|
}
|
|
6248
6401
|
function dispatchGitHub(options, executor) {
|
|
@@ -6472,6 +6625,39 @@ function readVersion(executor, cwd) {
|
|
|
6472
6625
|
function platformType(config) {
|
|
6473
6626
|
return config.platform?.type === "github" ? "github" : "forgejo";
|
|
6474
6627
|
}
|
|
6628
|
+
/**
|
|
6629
|
+
* Resolve the serverUrl + repository the push will target so the auth URL
|
|
6630
|
+
* can be rewritten in `configureGitAuth`. For Forgejo we already have these
|
|
6631
|
+
* in the connection. For GitHub, prefer the Actions-injected env vars
|
|
6632
|
+
* (also set on Forgejo runners that emulate GitHub Actions) and fall back to
|
|
6633
|
+
* parsing package.json so local invocations still work.
|
|
6634
|
+
*/
|
|
6635
|
+
function resolveAuthCoordinates(executor, config, token) {
|
|
6636
|
+
if (config.platform?.type === "forgejo") return {
|
|
6637
|
+
serverUrl: config.platform.conn.serverUrl,
|
|
6638
|
+
repository: config.platform.conn.repository,
|
|
6639
|
+
token
|
|
6640
|
+
};
|
|
6641
|
+
const envServer = process.env["GITHUB_SERVER_URL"]?.trim();
|
|
6642
|
+
const envRepo = process.env["GITHUB_REPOSITORY"]?.trim();
|
|
6643
|
+
if (envServer && envRepo) return {
|
|
6644
|
+
serverUrl: envServer,
|
|
6645
|
+
repository: envRepo,
|
|
6646
|
+
token
|
|
6647
|
+
};
|
|
6648
|
+
const pkgRaw = executor.readFile(path.join(config.cwd, "package.json"));
|
|
6649
|
+
if (!pkgRaw) return void 0;
|
|
6650
|
+
const repoField = parsePackageJson(pkgRaw)?.repository;
|
|
6651
|
+
const urlStr = typeof repoField === "string" ? repoField : repoField?.url;
|
|
6652
|
+
if (!urlStr) return void 0;
|
|
6653
|
+
const parsed = parseGitUrl(urlStr);
|
|
6654
|
+
if (!parsed) return void 0;
|
|
6655
|
+
return {
|
|
6656
|
+
serverUrl: `${parsed.protocol}//${parsed.hostname}`,
|
|
6657
|
+
repository: parsed.repository,
|
|
6658
|
+
token
|
|
6659
|
+
};
|
|
6660
|
+
}
|
|
6475
6661
|
/** Check whether a "tag already exists" failure can be recovered from. */
|
|
6476
6662
|
function recoverFromExistingTag(executor, config, output) {
|
|
6477
6663
|
const match = /tag '([^']+)' already exists/.exec(output);
|
|
@@ -6486,6 +6672,8 @@ function recoverFromExistingTag(executor, config, output) {
|
|
|
6486
6672
|
}
|
|
6487
6673
|
/** Run the full commit-and-tag-version release flow. */
|
|
6488
6674
|
async function runSimpleRelease(executor, config) {
|
|
6675
|
+
const releaseToken = process.env["RELEASE_TOKEN"]?.trim();
|
|
6676
|
+
if (!releaseToken) throw new FatalError("RELEASE_TOKEN environment variable is required for release:simple. Create a PAT with write:repository (Forgejo) or contents: write (GitHub) scope on the target forge, store it as the RELEASE_TOKEN secret on your repo (or org), and expose it as env to this step.");
|
|
6489
6677
|
const platform = platformType(config);
|
|
6490
6678
|
debug(config, `Platform: ${platform}`);
|
|
6491
6679
|
debug(config, `Config: dryRun=${String(config.dryRun)} noPush=${String(config.noPush)} noSlidingTags=${String(config.noSlidingTags)} noRelease=${String(config.noRelease)}`);
|
|
@@ -6529,10 +6717,11 @@ async function runSimpleRelease(executor, config) {
|
|
|
6529
6717
|
}
|
|
6530
6718
|
let pushed = false;
|
|
6531
6719
|
if (!config.noPush) {
|
|
6532
|
-
|
|
6533
|
-
|
|
6534
|
-
|
|
6535
|
-
|
|
6720
|
+
const authCoords = resolveAuthCoordinates(executor, config, releaseToken);
|
|
6721
|
+
if (authCoords) {
|
|
6722
|
+
configureGitAuth(executor, authCoords, config.cwd);
|
|
6723
|
+
debug(config, `Configured git remote auth for ${authCoords.repository}`);
|
|
6724
|
+
} else debug(config, "Skipping git remote auth: could not resolve server URL / repository");
|
|
6536
6725
|
const branch = executor.exec("git", [
|
|
6537
6726
|
"rev-parse",
|
|
6538
6727
|
"--abbrev-ref",
|
|
@@ -8450,7 +8639,8 @@ function maskSecret(secret) {
|
|
|
8450
8639
|
}
|
|
8451
8640
|
/**
|
|
8452
8641
|
* Send a webhook and log the result. Intended for CLI usage.
|
|
8453
|
-
*
|
|
8642
|
+
* Returns the underlying result so the caller can decide whether to
|
|
8643
|
+
* surface a non-zero exit. Never throws.
|
|
8454
8644
|
*/
|
|
8455
8645
|
async function sendWebhookWithLogging(url, secret, payload) {
|
|
8456
8646
|
const result = await sendWebhook({
|
|
@@ -8460,10 +8650,27 @@ async function sendWebhookWithLogging(url, secret, payload) {
|
|
|
8460
8650
|
});
|
|
8461
8651
|
const context = `url=${url}, secret=${maskSecret(secret)}`;
|
|
8462
8652
|
if (result.ok) log.info(`Post-release hook completed successfully (${context})`);
|
|
8463
|
-
else log.
|
|
8653
|
+
else log.error(`Post-release hook failed: ${result.error ?? "unknown error"} (${context})`);
|
|
8654
|
+
return result;
|
|
8464
8655
|
}
|
|
8465
8656
|
//#endregion
|
|
8466
8657
|
//#region src/commands/webhook-send.ts
|
|
8658
|
+
/**
|
|
8659
|
+
* Choose the payload source for `webhook:send`. When `--tag` or
|
|
8660
|
+
* `--repository` is set, build from flags via `buildSimplePayload` — this
|
|
8661
|
+
* makes the explicit tag win over `GITHUB_REF`, which is often `refs/heads/main`
|
|
8662
|
+
* (not a tag) when the post-release hook fires.
|
|
8663
|
+
*/
|
|
8664
|
+
function resolveWebhookPayload(args, env = process.env) {
|
|
8665
|
+
if (args.tag || args.repository) return buildSimplePayload({
|
|
8666
|
+
tag: args.tag ?? "",
|
|
8667
|
+
repository: args.repository ?? env["GITHUB_REPOSITORY"] ?? "",
|
|
8668
|
+
serverUrl: args["server-url"] ?? env["GITHUB_SERVER_URL"] ?? "https://github.com",
|
|
8669
|
+
actor: args.actor ?? env["GITHUB_ACTOR"] ?? "github-actions[bot]",
|
|
8670
|
+
prerelease: args.prerelease ?? false
|
|
8671
|
+
});
|
|
8672
|
+
return buildPayloadFromEnv();
|
|
8673
|
+
}
|
|
8467
8674
|
const webhookSendCommand = defineCommand({
|
|
8468
8675
|
meta: {
|
|
8469
8676
|
name: "webhook:send",
|
|
@@ -8501,14 +8708,8 @@ const webhookSendCommand = defineCommand({
|
|
|
8501
8708
|
return;
|
|
8502
8709
|
}
|
|
8503
8710
|
const secret = process.env["WEBHOOK_SECRET"];
|
|
8504
|
-
const payload = args
|
|
8505
|
-
|
|
8506
|
-
repository: args.repository ?? process.env["GITHUB_REPOSITORY"] ?? "",
|
|
8507
|
-
serverUrl: args["server-url"] ?? process.env["GITHUB_SERVER_URL"] ?? "https://github.com",
|
|
8508
|
-
actor: args.actor ?? process.env["GITHUB_ACTOR"] ?? "github-actions[bot]",
|
|
8509
|
-
prerelease: args.prerelease ?? false
|
|
8510
|
-
}) : buildPayloadFromEnv();
|
|
8511
|
-
await sendWebhookWithLogging(args.url, secret || void 0, payload);
|
|
8711
|
+
const payload = resolveWebhookPayload(args);
|
|
8712
|
+
if (!(await sendWebhookWithLogging(args.url, secret || void 0, payload)).ok) process.exitCode = 1;
|
|
8512
8713
|
}
|
|
8513
8714
|
});
|
|
8514
8715
|
//#endregion
|
|
@@ -8921,7 +9122,7 @@ const out = (msg) => console.log(msg);
|
|
|
8921
9122
|
const main = defineCommand({
|
|
8922
9123
|
meta: {
|
|
8923
9124
|
name: "bst",
|
|
8924
|
-
version: "0.
|
|
9125
|
+
version: "0.62.0",
|
|
8925
9126
|
description: "Bootstrap and maintain standardized TypeScript project tooling"
|
|
8926
9127
|
},
|
|
8927
9128
|
subCommands: {
|
|
@@ -8994,7 +9195,7 @@ const main = defineCommand({
|
|
|
8994
9195
|
})
|
|
8995
9196
|
}
|
|
8996
9197
|
});
|
|
8997
|
-
console.log(`@bensandee/tooling v0.
|
|
9198
|
+
console.log(`@bensandee/tooling v0.62.0`);
|
|
8998
9199
|
async function run() {
|
|
8999
9200
|
await runMain(main);
|
|
9000
9201
|
process.exit(process.exitCode ?? 0);
|