@bensandee/tooling 0.31.0 → 0.32.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.
Files changed (2) hide show
  1. package/dist/bin.mjs +192 -234
  2. package/package.json +8 -10
package/dist/bin.mjs CHANGED
@@ -970,10 +970,6 @@ function runDockerPublish(executor, config) {
970
970
  //#region src/utils/yaml-merge.ts
971
971
  const IGNORE_PATTERN = "@bensandee/tooling:ignore";
972
972
  const FORGEJO_SCHEMA_COMMENT = "# yaml-language-server: $schema=../../.vscode/forgejo-workflow.schema.json\n";
973
- /** Returns a yaml-language-server schema comment for Forgejo workflows, empty string otherwise. */
974
- function workflowSchemaComment(ci) {
975
- return ci === "forgejo" ? FORGEJO_SCHEMA_COMMENT : "";
976
- }
977
973
  /** Prepend the Forgejo schema comment if it's not already present. No-op for GitHub. */
978
974
  function ensureSchemaComment(content, ci) {
979
975
  if (ci !== "forgejo") return content;
@@ -1028,6 +1024,29 @@ function mergeLefthookCommands(existing, requiredCommands) {
1028
1024
  };
1029
1025
  }
1030
1026
  }
1027
+ /** Extract only the mergeable steps (those with a match) as RequiredSteps. */
1028
+ function toRequiredSteps(steps) {
1029
+ return steps.filter((s) => s.match !== void 0).map((s) => ({
1030
+ match: s.match,
1031
+ step: s.step
1032
+ }));
1033
+ }
1034
+ /** Build a complete workflow YAML string from structured options. Single source of truth for both new files and merge steps. */
1035
+ function buildWorkflowYaml(options) {
1036
+ const doc = { name: options.name };
1037
+ if (options.enableEmailNotifications) doc["enable-email-notifications"] = true;
1038
+ doc["on"] = options.on;
1039
+ if (options.permissions) doc["permissions"] = options.permissions;
1040
+ if (options.concurrency) doc["concurrency"] = options.concurrency;
1041
+ doc["jobs"] = { [options.jobName]: {
1042
+ "runs-on": options.runsOn ?? "ubuntu-latest",
1043
+ steps: options.steps.map((s) => s.step)
1044
+ } };
1045
+ return ensureSchemaComment(stringify(doc, {
1046
+ lineWidth: 0,
1047
+ nullStr: ""
1048
+ }), options.ci);
1049
+ }
1031
1050
  /**
1032
1051
  * Ensure required steps exist in a workflow job's steps array.
1033
1052
  * Only adds missing steps at the end — never modifies existing ones.
@@ -1169,43 +1188,22 @@ function computeNodeVersionYaml(ctx) {
1169
1188
  }
1170
1189
  //#endregion
1171
1190
  //#region src/generators/publish-ci.ts
1172
- function deployWorkflow(ci, nodeVersionYaml) {
1173
- return `${workflowSchemaComment(ci)}name: Publish
1174
- on:
1175
- workflow_dispatch:
1176
-
1177
- jobs:
1178
- publish:
1179
- runs-on: ubuntu-latest
1180
- steps:
1181
- - uses: actions/checkout@v4
1182
- - uses: pnpm/action-setup@v4
1183
- - uses: actions/setup-node@v4
1184
- with:
1185
- ${nodeVersionYaml}
1186
- - run: pnpm install --frozen-lockfile
1187
- - name: Publish Docker images
1188
- env:
1189
- DOCKER_REGISTRY_HOST: ${actionsExpr("vars.DOCKER_REGISTRY_HOST")}
1190
- DOCKER_REGISTRY_NAMESPACE: ${actionsExpr("vars.DOCKER_REGISTRY_NAMESPACE")}
1191
- DOCKER_USERNAME: ${actionsExpr("secrets.DOCKER_USERNAME")}
1192
- DOCKER_PASSWORD: ${actionsExpr("secrets.DOCKER_PASSWORD")}
1193
- run: pnpm exec bst docker:publish
1194
- `;
1195
- }
1196
- function requiredDeploySteps() {
1191
+ function publishSteps(nodeVersionYaml) {
1197
1192
  return [
1198
1193
  {
1199
1194
  match: { uses: "actions/checkout" },
1200
- step: { uses: "actions/checkout@v4" }
1195
+ step: { uses: "actions/checkout@v6" }
1201
1196
  },
1202
1197
  {
1203
1198
  match: { uses: "pnpm/action-setup" },
1204
- step: { uses: "pnpm/action-setup@v4" }
1199
+ step: { uses: "pnpm/action-setup@v5" }
1205
1200
  },
1206
1201
  {
1207
1202
  match: { uses: "actions/setup-node" },
1208
- step: { uses: "actions/setup-node@v4" }
1203
+ step: {
1204
+ uses: "actions/setup-node@v6",
1205
+ with: nodeVersionYaml.startsWith("node-version-file") ? { "node-version-file": "package.json" } : { "node-version": "24" }
1206
+ }
1209
1207
  },
1210
1208
  {
1211
1209
  match: { run: "pnpm install" },
@@ -1213,7 +1211,16 @@ function requiredDeploySteps() {
1213
1211
  },
1214
1212
  {
1215
1213
  match: { run: "docker:publish" },
1216
- step: { run: "pnpm exec bst docker:publish" }
1214
+ step: {
1215
+ name: "Publish Docker images",
1216
+ env: {
1217
+ DOCKER_REGISTRY_HOST: actionsExpr("vars.DOCKER_REGISTRY_HOST"),
1218
+ DOCKER_REGISTRY_NAMESPACE: actionsExpr("vars.DOCKER_REGISTRY_NAMESPACE"),
1219
+ DOCKER_USERNAME: actionsExpr("secrets.DOCKER_USERNAME"),
1220
+ DOCKER_PASSWORD: actionsExpr("secrets.DOCKER_PASSWORD")
1221
+ },
1222
+ run: "pnpm exec bst docker:publish"
1223
+ }
1217
1224
  }
1218
1225
  ];
1219
1226
  }
@@ -1255,8 +1262,14 @@ async function generateDeployCi(ctx) {
1255
1262
  };
1256
1263
  const isGitHub = ctx.config.ci === "github";
1257
1264
  const workflowPath = isGitHub ? ".github/workflows/publish.yml" : ".forgejo/workflows/publish.yml";
1258
- const nodeVersionYaml = computeNodeVersionYaml(ctx);
1259
- const content = deployWorkflow(ctx.config.ci, nodeVersionYaml);
1265
+ const steps = publishSteps(computeNodeVersionYaml(ctx));
1266
+ const content = buildWorkflowYaml({
1267
+ ci: ctx.config.ci,
1268
+ name: "Publish",
1269
+ on: { workflow_dispatch: null },
1270
+ jobName: "publish",
1271
+ steps
1272
+ });
1260
1273
  if (ctx.exists(workflowPath)) {
1261
1274
  const raw = ctx.read(workflowPath);
1262
1275
  if (raw) {
@@ -1276,7 +1289,7 @@ async function generateDeployCi(ctx) {
1276
1289
  description: "Publish workflow already up to date"
1277
1290
  };
1278
1291
  }
1279
- const merged = mergeWorkflowSteps(existing, "publish", requiredDeploySteps());
1292
+ const merged = mergeWorkflowSteps(existing, "publish", toRequiredSteps(steps));
1280
1293
  const withComment = ensureSchemaComment(merged.content, ctx.config.ci);
1281
1294
  if (!merged.changed) {
1282
1295
  if (withComment !== raw) {
@@ -1362,6 +1375,7 @@ const PER_PACKAGE_DEV_DEPS = {
1362
1375
  "@types/node": {
1363
1376
  "@changesets/cli": "2.30.0",
1364
1377
  "@release-it/bumper": "7.0.5",
1378
+ "@types/node": "24.12.0",
1365
1379
  "commit-and-tag-version": "12.7.0",
1366
1380
  "knip": "5.87.0",
1367
1381
  "lefthook": "2.1.4",
@@ -1369,7 +1383,6 @@ const PER_PACKAGE_DEV_DEPS = {
1369
1383
  "oxlint": "1.56.0",
1370
1384
  "prettier": "3.8.1",
1371
1385
  "release-it": "19.2.4",
1372
- "@types/node": "24.12.0",
1373
1386
  "tsdown": "0.21.4",
1374
1387
  "typescript": "5.9.3",
1375
1388
  "vitest": "4.1.0",
@@ -1378,6 +1391,7 @@ const PER_PACKAGE_DEV_DEPS = {
1378
1391
  tsdown: {
1379
1392
  "@changesets/cli": "2.30.0",
1380
1393
  "@release-it/bumper": "7.0.5",
1394
+ "@types/node": "24.12.0",
1381
1395
  "commit-and-tag-version": "12.7.0",
1382
1396
  "knip": "5.87.0",
1383
1397
  "lefthook": "2.1.4",
@@ -1385,7 +1399,6 @@ const PER_PACKAGE_DEV_DEPS = {
1385
1399
  "oxlint": "1.56.0",
1386
1400
  "prettier": "3.8.1",
1387
1401
  "release-it": "19.2.4",
1388
- "@types/node": "24.12.0",
1389
1402
  "tsdown": "0.21.4",
1390
1403
  "typescript": "5.9.3",
1391
1404
  "vitest": "4.1.0",
@@ -1394,6 +1407,7 @@ const PER_PACKAGE_DEV_DEPS = {
1394
1407
  typescript: {
1395
1408
  "@changesets/cli": "2.30.0",
1396
1409
  "@release-it/bumper": "7.0.5",
1410
+ "@types/node": "24.12.0",
1397
1411
  "commit-and-tag-version": "12.7.0",
1398
1412
  "knip": "5.87.0",
1399
1413
  "lefthook": "2.1.4",
@@ -1401,7 +1415,6 @@ const PER_PACKAGE_DEV_DEPS = {
1401
1415
  "oxlint": "1.56.0",
1402
1416
  "prettier": "3.8.1",
1403
1417
  "release-it": "19.2.4",
1404
- "@types/node": "24.12.0",
1405
1418
  "tsdown": "0.21.4",
1406
1419
  "typescript": "5.9.3",
1407
1420
  "vitest": "4.1.0",
@@ -1410,6 +1423,7 @@ const PER_PACKAGE_DEV_DEPS = {
1410
1423
  vitest: {
1411
1424
  "@changesets/cli": "2.30.0",
1412
1425
  "@release-it/bumper": "7.0.5",
1426
+ "@types/node": "24.12.0",
1413
1427
  "commit-and-tag-version": "12.7.0",
1414
1428
  "knip": "5.87.0",
1415
1429
  "lefthook": "2.1.4",
@@ -1417,7 +1431,6 @@ const PER_PACKAGE_DEV_DEPS = {
1417
1431
  "oxlint": "1.56.0",
1418
1432
  "prettier": "3.8.1",
1419
1433
  "release-it": "19.2.4",
1420
- "@types/node": "24.12.0",
1421
1434
  "tsdown": "0.21.4",
1422
1435
  "typescript": "5.9.3",
1423
1436
  "vitest": "4.1.0",
@@ -1429,6 +1442,7 @@ const ROOT_DEV_DEPS = {
1429
1442
  knip: {
1430
1443
  "@changesets/cli": "2.30.0",
1431
1444
  "@release-it/bumper": "7.0.5",
1445
+ "@types/node": "24.12.0",
1432
1446
  "commit-and-tag-version": "12.7.0",
1433
1447
  "knip": "5.87.0",
1434
1448
  "lefthook": "2.1.4",
@@ -1436,7 +1450,6 @@ const ROOT_DEV_DEPS = {
1436
1450
  "oxlint": "1.56.0",
1437
1451
  "prettier": "3.8.1",
1438
1452
  "release-it": "19.2.4",
1439
- "@types/node": "24.12.0",
1440
1453
  "tsdown": "0.21.4",
1441
1454
  "typescript": "5.9.3",
1442
1455
  "vitest": "4.1.0",
@@ -1445,6 +1458,7 @@ const ROOT_DEV_DEPS = {
1445
1458
  lefthook: {
1446
1459
  "@changesets/cli": "2.30.0",
1447
1460
  "@release-it/bumper": "7.0.5",
1461
+ "@types/node": "24.12.0",
1448
1462
  "commit-and-tag-version": "12.7.0",
1449
1463
  "knip": "5.87.0",
1450
1464
  "lefthook": "2.1.4",
@@ -1452,7 +1466,6 @@ const ROOT_DEV_DEPS = {
1452
1466
  "oxlint": "1.56.0",
1453
1467
  "prettier": "3.8.1",
1454
1468
  "release-it": "19.2.4",
1455
- "@types/node": "24.12.0",
1456
1469
  "tsdown": "0.21.4",
1457
1470
  "typescript": "5.9.3",
1458
1471
  "vitest": "4.1.0",
@@ -1461,6 +1474,7 @@ const ROOT_DEV_DEPS = {
1461
1474
  oxlint: {
1462
1475
  "@changesets/cli": "2.30.0",
1463
1476
  "@release-it/bumper": "7.0.5",
1477
+ "@types/node": "24.12.0",
1464
1478
  "commit-and-tag-version": "12.7.0",
1465
1479
  "knip": "5.87.0",
1466
1480
  "lefthook": "2.1.4",
@@ -1468,7 +1482,6 @@ const ROOT_DEV_DEPS = {
1468
1482
  "oxlint": "1.56.0",
1469
1483
  "prettier": "3.8.1",
1470
1484
  "release-it": "19.2.4",
1471
- "@types/node": "24.12.0",
1472
1485
  "tsdown": "0.21.4",
1473
1486
  "typescript": "5.9.3",
1474
1487
  "vitest": "4.1.0",
@@ -1504,6 +1517,7 @@ function addReleaseDeps(deps, config) {
1504
1517
  deps["release-it"] = {
1505
1518
  "@changesets/cli": "2.30.0",
1506
1519
  "@release-it/bumper": "7.0.5",
1520
+ "@types/node": "24.12.0",
1507
1521
  "commit-and-tag-version": "12.7.0",
1508
1522
  "knip": "5.87.0",
1509
1523
  "lefthook": "2.1.4",
@@ -1511,7 +1525,6 @@ function addReleaseDeps(deps, config) {
1511
1525
  "oxlint": "1.56.0",
1512
1526
  "prettier": "3.8.1",
1513
1527
  "release-it": "19.2.4",
1514
- "@types/node": "24.12.0",
1515
1528
  "tsdown": "0.21.4",
1516
1529
  "typescript": "5.9.3",
1517
1530
  "vitest": "4.1.0",
@@ -1520,6 +1533,7 @@ function addReleaseDeps(deps, config) {
1520
1533
  if (config.structure === "monorepo") deps["@release-it/bumper"] = {
1521
1534
  "@changesets/cli": "2.30.0",
1522
1535
  "@release-it/bumper": "7.0.5",
1536
+ "@types/node": "24.12.0",
1523
1537
  "commit-and-tag-version": "12.7.0",
1524
1538
  "knip": "5.87.0",
1525
1539
  "lefthook": "2.1.4",
@@ -1527,7 +1541,6 @@ function addReleaseDeps(deps, config) {
1527
1541
  "oxlint": "1.56.0",
1528
1542
  "prettier": "3.8.1",
1529
1543
  "release-it": "19.2.4",
1530
- "@types/node": "24.12.0",
1531
1544
  "tsdown": "0.21.4",
1532
1545
  "typescript": "5.9.3",
1533
1546
  "vitest": "4.1.0",
@@ -1538,6 +1551,7 @@ function addReleaseDeps(deps, config) {
1538
1551
  deps["commit-and-tag-version"] = {
1539
1552
  "@changesets/cli": "2.30.0",
1540
1553
  "@release-it/bumper": "7.0.5",
1554
+ "@types/node": "24.12.0",
1541
1555
  "commit-and-tag-version": "12.7.0",
1542
1556
  "knip": "5.87.0",
1543
1557
  "lefthook": "2.1.4",
@@ -1545,7 +1559,6 @@ function addReleaseDeps(deps, config) {
1545
1559
  "oxlint": "1.56.0",
1546
1560
  "prettier": "3.8.1",
1547
1561
  "release-it": "19.2.4",
1548
- "@types/node": "24.12.0",
1549
1562
  "tsdown": "0.21.4",
1550
1563
  "typescript": "5.9.3",
1551
1564
  "vitest": "4.1.0",
@@ -1556,6 +1569,7 @@ function addReleaseDeps(deps, config) {
1556
1569
  deps["@changesets/cli"] = {
1557
1570
  "@changesets/cli": "2.30.0",
1558
1571
  "@release-it/bumper": "7.0.5",
1572
+ "@types/node": "24.12.0",
1559
1573
  "commit-and-tag-version": "12.7.0",
1560
1574
  "knip": "5.87.0",
1561
1575
  "lefthook": "2.1.4",
@@ -1563,7 +1577,6 @@ function addReleaseDeps(deps, config) {
1563
1577
  "oxlint": "1.56.0",
1564
1578
  "prettier": "3.8.1",
1565
1579
  "release-it": "19.2.4",
1566
- "@types/node": "24.12.0",
1567
1580
  "tsdown": "0.21.4",
1568
1581
  "typescript": "5.9.3",
1569
1582
  "vitest": "4.1.0",
@@ -1577,10 +1590,11 @@ function getAddedDevDepNames(config) {
1577
1590
  const deps = { ...ROOT_DEV_DEPS };
1578
1591
  if (config.structure !== "monorepo") Object.assign(deps, PER_PACKAGE_DEV_DEPS);
1579
1592
  deps["@bensandee/config"] = "0.9.1";
1580
- deps["@bensandee/tooling"] = "0.31.0";
1593
+ deps["@bensandee/tooling"] = "0.32.0";
1581
1594
  if (config.formatter === "oxfmt") deps["oxfmt"] = {
1582
1595
  "@changesets/cli": "2.30.0",
1583
1596
  "@release-it/bumper": "7.0.5",
1597
+ "@types/node": "24.12.0",
1584
1598
  "commit-and-tag-version": "12.7.0",
1585
1599
  "knip": "5.87.0",
1586
1600
  "lefthook": "2.1.4",
@@ -1588,7 +1602,6 @@ function getAddedDevDepNames(config) {
1588
1602
  "oxlint": "1.56.0",
1589
1603
  "prettier": "3.8.1",
1590
1604
  "release-it": "19.2.4",
1591
- "@types/node": "24.12.0",
1592
1605
  "tsdown": "0.21.4",
1593
1606
  "typescript": "5.9.3",
1594
1607
  "vitest": "4.1.0",
@@ -1597,6 +1610,7 @@ function getAddedDevDepNames(config) {
1597
1610
  if (config.formatter === "prettier") deps["prettier"] = {
1598
1611
  "@changesets/cli": "2.30.0",
1599
1612
  "@release-it/bumper": "7.0.5",
1613
+ "@types/node": "24.12.0",
1600
1614
  "commit-and-tag-version": "12.7.0",
1601
1615
  "knip": "5.87.0",
1602
1616
  "lefthook": "2.1.4",
@@ -1604,7 +1618,6 @@ function getAddedDevDepNames(config) {
1604
1618
  "oxlint": "1.56.0",
1605
1619
  "prettier": "3.8.1",
1606
1620
  "release-it": "19.2.4",
1607
- "@types/node": "24.12.0",
1608
1621
  "tsdown": "0.21.4",
1609
1622
  "typescript": "5.9.3",
1610
1623
  "vitest": "4.1.0",
@@ -1632,11 +1645,12 @@ async function generatePackageJson(ctx) {
1632
1645
  const devDeps = { ...ROOT_DEV_DEPS };
1633
1646
  if (!isMonorepo) Object.assign(devDeps, PER_PACKAGE_DEV_DEPS);
1634
1647
  devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.9.1";
1635
- devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.31.0";
1648
+ devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.32.0";
1636
1649
  if (ctx.config.useEslintPlugin) devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "0.9.2";
1637
1650
  if (ctx.config.formatter === "oxfmt") devDeps["oxfmt"] = {
1638
1651
  "@changesets/cli": "2.30.0",
1639
1652
  "@release-it/bumper": "7.0.5",
1653
+ "@types/node": "24.12.0",
1640
1654
  "commit-and-tag-version": "12.7.0",
1641
1655
  "knip": "5.87.0",
1642
1656
  "lefthook": "2.1.4",
@@ -1644,7 +1658,6 @@ async function generatePackageJson(ctx) {
1644
1658
  "oxlint": "1.56.0",
1645
1659
  "prettier": "3.8.1",
1646
1660
  "release-it": "19.2.4",
1647
- "@types/node": "24.12.0",
1648
1661
  "tsdown": "0.21.4",
1649
1662
  "typescript": "5.9.3",
1650
1663
  "vitest": "4.1.0",
@@ -1653,6 +1666,7 @@ async function generatePackageJson(ctx) {
1653
1666
  if (ctx.config.formatter === "prettier") devDeps["prettier"] = {
1654
1667
  "@changesets/cli": "2.30.0",
1655
1668
  "@release-it/bumper": "7.0.5",
1669
+ "@types/node": "24.12.0",
1656
1670
  "commit-and-tag-version": "12.7.0",
1657
1671
  "knip": "5.87.0",
1658
1672
  "lefthook": "2.1.4",
@@ -1660,7 +1674,6 @@ async function generatePackageJson(ctx) {
1660
1674
  "oxlint": "1.56.0",
1661
1675
  "prettier": "3.8.1",
1662
1676
  "release-it": "19.2.4",
1663
- "@types/node": "24.12.0",
1664
1677
  "tsdown": "0.21.4",
1665
1678
  "typescript": "5.9.3",
1666
1679
  "vitest": "4.1.0",
@@ -1741,6 +1754,7 @@ async function generatePackageJson(ctx) {
1741
1754
  packageManager: `pnpm@${{
1742
1755
  "@changesets/cli": "2.30.0",
1743
1756
  "@release-it/bumper": "7.0.5",
1757
+ "@types/node": "24.12.0",
1744
1758
  "commit-and-tag-version": "12.7.0",
1745
1759
  "knip": "5.87.0",
1746
1760
  "lefthook": "2.1.4",
@@ -1748,7 +1762,6 @@ async function generatePackageJson(ctx) {
1748
1762
  "oxlint": "1.56.0",
1749
1763
  "prettier": "3.8.1",
1750
1764
  "release-it": "19.2.4",
1751
- "@types/node": "24.12.0",
1752
1765
  "tsdown": "0.21.4",
1753
1766
  "typescript": "5.9.3",
1754
1767
  "vitest": "4.1.0",
@@ -2174,50 +2187,20 @@ const CI_CONCURRENCY = {
2174
2187
  group: `ci-${actionsExpr("github.ref")}`,
2175
2188
  "cancel-in-progress": actionsExpr("github.ref != 'refs/heads/main'")
2176
2189
  };
2177
- function ciWorkflow(nodeVersionYaml, isForgejo, isChangesets) {
2178
- const emailNotifications = isForgejo ? "\nenable-email-notifications: true\n" : "";
2179
- const concurrencyBlock = isChangesets ? `
2180
- concurrency:
2181
- group: ci-${actionsExpr("github.ref")}
2182
- cancel-in-progress: ${actionsExpr("github.ref != 'refs/heads/main'")}
2183
- ` : "";
2184
- return `${workflowSchemaComment(isForgejo ? "forgejo" : "github")}name: CI
2185
- ${emailNotifications}on:
2186
- push:
2187
- branches: [main]
2188
- tags-ignore:
2189
- - "**"
2190
- pull_request:
2191
- ${concurrencyBlock}
2192
- jobs:
2193
- check:
2194
- runs-on: ubuntu-latest
2195
- steps:
2196
- - uses: actions/checkout@v4
2197
- - uses: pnpm/action-setup@v4
2198
- - uses: actions/setup-node@v4
2199
- with:
2200
- ${nodeVersionYaml}
2201
- cache: pnpm
2202
- - run: pnpm install --frozen-lockfile
2203
- - name: Run all checks
2204
- run: pnpm ci:check
2205
- `;
2206
- }
2207
- function requiredCheckSteps(nodeVersionYaml) {
2190
+ function checkSteps(nodeVersionYaml) {
2208
2191
  return [
2209
2192
  {
2210
2193
  match: { uses: "actions/checkout" },
2211
- step: { uses: "actions/checkout@v4" }
2194
+ step: { uses: "actions/checkout@v6" }
2212
2195
  },
2213
2196
  {
2214
2197
  match: { uses: "pnpm/action-setup" },
2215
- step: { uses: "pnpm/action-setup@v4" }
2198
+ step: { uses: "pnpm/action-setup@v5" }
2216
2199
  },
2217
2200
  {
2218
2201
  match: { uses: "actions/setup-node" },
2219
2202
  step: {
2220
- uses: "actions/setup-node@v4",
2203
+ uses: "actions/setup-node@v6",
2221
2204
  with: {
2222
2205
  ...nodeVersionYaml.startsWith("node-version-file") ? { "node-version-file": "package.json" } : { "node-version": "24" },
2223
2206
  cache: "pnpm"
@@ -2248,14 +2231,29 @@ async function generateCi(ctx) {
2248
2231
  description: "CI workflow not requested"
2249
2232
  };
2250
2233
  const isGitHub = ctx.config.ci === "github";
2234
+ const isForgejo = !isGitHub;
2251
2235
  const isChangesets = ctx.config.releaseStrategy === "changesets";
2252
- const nodeVersionYaml = computeNodeVersionYaml(ctx);
2236
+ const steps = checkSteps(computeNodeVersionYaml(ctx));
2237
+ const content = buildWorkflowYaml({
2238
+ ci: ctx.config.ci,
2239
+ name: "CI",
2240
+ on: {
2241
+ push: {
2242
+ branches: ["main"],
2243
+ "tags-ignore": ["**"]
2244
+ },
2245
+ pull_request: null
2246
+ },
2247
+ enableEmailNotifications: isForgejo,
2248
+ ...isChangesets && { concurrency: CI_CONCURRENCY },
2249
+ jobName: "check",
2250
+ steps
2251
+ });
2253
2252
  const filePath = ciWorkflowPath(ctx.config.ci, ctx.config.releaseStrategy);
2254
- const content = ciWorkflow(nodeVersionYaml, !isGitHub, isChangesets);
2255
2253
  if (ctx.exists(filePath)) {
2256
2254
  const existing = ctx.read(filePath);
2257
2255
  if (existing) {
2258
- let result = mergeWorkflowSteps(existing, "check", requiredCheckSteps(nodeVersionYaml));
2256
+ let result = mergeWorkflowSteps(existing, "check", toRequiredSteps(steps));
2259
2257
  const withTagsIgnore = ensureWorkflowTagsIgnore(result.content);
2260
2258
  result = {
2261
2259
  content: withTagsIgnore.content,
@@ -2760,83 +2758,84 @@ async function generateChangesets(ctx) {
2760
2758
  }
2761
2759
  //#endregion
2762
2760
  //#region src/generators/release-ci.ts
2761
+ /** Common setup steps shared by release-it and simple strategies. */
2763
2762
  function commonSteps(nodeVersionYaml, publishesNpm) {
2764
- return ` - uses: actions/checkout@v4
2765
- with:
2766
- fetch-depth: 0
2767
- - uses: pnpm/action-setup@v4
2768
- - uses: actions/setup-node@v4
2769
- with:
2770
- ${nodeVersionYaml}
2771
- cache: pnpm${publishesNpm ? `\n registry-url: "https://registry.npmjs.org"` : ""}
2772
- - run: pnpm install --frozen-lockfile`;
2773
- }
2774
- function releaseItWorkflow(ci, nodeVersionYaml, publishesNpm) {
2775
- const isGitHub = ci === "github";
2776
- const permissions = isGitHub ? `
2777
- permissions:
2778
- contents: write
2779
- ` : "";
2780
- const tokenEnv = isGitHub ? `GITHUB_TOKEN: \${{ github.token }}` : `RELEASE_TOKEN: \${{ secrets.RELEASE_TOKEN }}`;
2781
- const npmEnv = publishesNpm ? `\n NODE_AUTH_TOKEN: \${{ secrets.NPM_TOKEN }}` : "";
2782
- return `${workflowSchemaComment(ci)}name: Release
2783
- on:
2784
- workflow_dispatch:
2785
- ${permissions}
2786
- jobs:
2787
- release:
2788
- runs-on: ubuntu-latest
2789
- steps:
2790
- ${commonSteps(nodeVersionYaml, publishesNpm)}
2791
- - run: pnpm release-it --ci
2792
- env:
2793
- ${tokenEnv}${npmEnv}
2794
- `;
2763
+ const isNodeVersionFile = nodeVersionYaml.startsWith("node-version-file");
2764
+ return [
2765
+ {
2766
+ match: { uses: "actions/checkout" },
2767
+ step: {
2768
+ uses: "actions/checkout@v6",
2769
+ with: { "fetch-depth": 0 }
2770
+ }
2771
+ },
2772
+ {
2773
+ match: { uses: "pnpm/action-setup" },
2774
+ step: { uses: "pnpm/action-setup@v5" }
2775
+ },
2776
+ {
2777
+ match: { uses: "actions/setup-node" },
2778
+ step: {
2779
+ uses: "actions/setup-node@v6",
2780
+ with: {
2781
+ ...isNodeVersionFile ? { "node-version-file": "package.json" } : { "node-version": "24" },
2782
+ cache: "pnpm",
2783
+ ...publishesNpm && { "registry-url": "https://registry.npmjs.org" }
2784
+ }
2785
+ }
2786
+ },
2787
+ {
2788
+ match: { run: "pnpm install" },
2789
+ step: { run: "pnpm install --frozen-lockfile" }
2790
+ }
2791
+ ];
2795
2792
  }
2796
- function dockerPublishStep() {
2797
- return `
2798
- - name: Publish Docker images
2799
- if: success()
2800
- env:
2801
- DOCKER_REGISTRY_HOST: ${actionsExpr("vars.DOCKER_REGISTRY_HOST")}
2802
- DOCKER_REGISTRY_NAMESPACE: ${actionsExpr("vars.DOCKER_REGISTRY_NAMESPACE")}
2803
- DOCKER_USERNAME: ${actionsExpr("secrets.DOCKER_USERNAME")}
2804
- DOCKER_PASSWORD: ${actionsExpr("secrets.DOCKER_PASSWORD")}
2805
- run: pnpm exec bst docker:publish`;
2806
- }
2807
- function commitAndTagVersionWorkflow(ci, nodeVersionYaml, publishesNpm, hasDocker) {
2808
- const isGitHub = ci === "github";
2809
- const permissions = isGitHub ? `
2810
- permissions:
2811
- contents: write
2812
- ` : "";
2813
- const gitConfigStep = `
2814
- - name: Configure git
2815
- run: |
2816
- git config user.name "${isGitHub ? "github-actions[bot]" : "forgejo-actions[bot]"}"
2817
- git config user.email "${isGitHub ? "github-actions[bot]@users.noreply.github.com" : "forgejo-actions[bot]@noreply.localhost"}"`;
2818
- const releaseStep = isGitHub ? `
2819
- - name: Release
2820
- env:
2821
- GITHUB_TOKEN: \${{ github.token }}
2822
- run: pnpm exec bst release:simple` : `
2823
- - name: Release
2824
- env:
2825
- FORGEJO_SERVER_URL: \${{ github.server_url }}
2826
- FORGEJO_REPOSITORY: \${{ github.repository }}
2827
- RELEASE_TOKEN: \${{ secrets.RELEASE_TOKEN }}
2828
- run: pnpm exec bst release:simple`;
2829
- const dockerStep = hasDocker ? dockerPublishStep() : "";
2830
- return `${workflowSchemaComment(ci)}name: Release
2831
- on:
2832
- workflow_dispatch:
2833
- ${permissions}
2834
- jobs:
2835
- release:
2836
- runs-on: ubuntu-latest
2837
- steps:
2838
- ${commonSteps(nodeVersionYaml, publishesNpm)}${gitConfigStep}${releaseStep}${dockerStep}
2839
- `;
2793
+ function releaseItSteps(ci, nodeVersionYaml, publishesNpm) {
2794
+ const tokenEnv = ci === "github" ? { GITHUB_TOKEN: actionsExpr("github.token") } : { RELEASE_TOKEN: actionsExpr("secrets.RELEASE_TOKEN") };
2795
+ const npmEnv = publishesNpm ? { NODE_AUTH_TOKEN: actionsExpr("secrets.NPM_TOKEN") } : {};
2796
+ return [...commonSteps(nodeVersionYaml, publishesNpm), {
2797
+ match: { run: "release-it" },
2798
+ step: {
2799
+ run: "pnpm release-it --ci",
2800
+ env: {
2801
+ ...tokenEnv,
2802
+ ...npmEnv
2803
+ }
2804
+ }
2805
+ }];
2806
+ }
2807
+ function simpleReleaseSteps(ci, nodeVersionYaml, publishesNpm, hasDocker) {
2808
+ const releaseStep = {
2809
+ match: { run: "release:simple" },
2810
+ step: {
2811
+ name: "Release",
2812
+ env: ci === "github" ? { GITHUB_TOKEN: actionsExpr("github.token") } : {
2813
+ FORGEJO_SERVER_URL: actionsExpr("github.server_url"),
2814
+ FORGEJO_REPOSITORY: actionsExpr("github.repository"),
2815
+ RELEASE_TOKEN: actionsExpr("secrets.RELEASE_TOKEN")
2816
+ },
2817
+ run: "pnpm exec bst release:simple"
2818
+ }
2819
+ };
2820
+ const dockerStep = {
2821
+ match: { run: "docker:publish" },
2822
+ step: {
2823
+ name: "Publish Docker images",
2824
+ if: "success()",
2825
+ env: {
2826
+ DOCKER_REGISTRY_HOST: actionsExpr("vars.DOCKER_REGISTRY_HOST"),
2827
+ DOCKER_REGISTRY_NAMESPACE: actionsExpr("vars.DOCKER_REGISTRY_NAMESPACE"),
2828
+ DOCKER_USERNAME: actionsExpr("secrets.DOCKER_USERNAME"),
2829
+ DOCKER_PASSWORD: actionsExpr("secrets.DOCKER_PASSWORD")
2830
+ },
2831
+ run: "pnpm exec bst docker:publish"
2832
+ }
2833
+ };
2834
+ return [
2835
+ ...commonSteps(nodeVersionYaml, publishesNpm),
2836
+ releaseStep,
2837
+ ...hasDocker ? [dockerStep] : []
2838
+ ];
2840
2839
  }
2841
2840
  /** Build the required release step for the check job (changesets). */
2842
2841
  function changesetsReleaseStep(ci, publishesNpm) {
@@ -2870,69 +2869,10 @@ function changesetsReleaseStep(ci, publishesNpm) {
2870
2869
  }
2871
2870
  };
2872
2871
  }
2873
- function requiredReleaseSteps(strategy, nodeVersionYaml, publishesNpm, hasDocker) {
2874
- const isNodeVersionFile = nodeVersionYaml.startsWith("node-version-file");
2875
- const steps = [
2876
- {
2877
- match: { uses: "actions/checkout" },
2878
- step: {
2879
- uses: "actions/checkout@v4",
2880
- with: { "fetch-depth": 0 }
2881
- }
2882
- },
2883
- {
2884
- match: { uses: "pnpm/action-setup" },
2885
- step: { uses: "pnpm/action-setup@v4" }
2886
- },
2887
- {
2888
- match: { uses: "actions/setup-node" },
2889
- step: {
2890
- uses: "actions/setup-node@v4",
2891
- with: {
2892
- ...isNodeVersionFile ? { "node-version-file": "package.json" } : { "node-version": "24" },
2893
- cache: "pnpm",
2894
- ...publishesNpm && { "registry-url": "https://registry.npmjs.org" }
2895
- }
2896
- }
2897
- },
2898
- {
2899
- match: { run: "pnpm install" },
2900
- step: { run: "pnpm install --frozen-lockfile" }
2901
- }
2902
- ];
2903
- switch (strategy) {
2904
- case "release-it":
2905
- steps.push({
2906
- match: { run: "release-it" },
2907
- step: { run: "pnpm release-it --ci" }
2908
- });
2909
- break;
2910
- case "simple":
2911
- steps.push({
2912
- match: { run: "release:simple" },
2913
- step: { run: "pnpm exec bst release:simple" }
2914
- });
2915
- if (hasDocker) steps.push({
2916
- match: { run: "docker:publish" },
2917
- step: {
2918
- name: "Publish Docker images",
2919
- run: "pnpm exec bst docker:publish"
2920
- }
2921
- });
2922
- break;
2923
- case "changesets":
2924
- steps.push({
2925
- match: { run: "changeset" },
2926
- step: { run: "pnpm exec bst release:changesets" }
2927
- });
2928
- break;
2929
- }
2930
- return steps;
2931
- }
2932
- function buildWorkflow(strategy, ci, nodeVersionYaml, publishesNpm, hasDocker) {
2872
+ function buildSteps(strategy, ci, nodeVersionYaml, publishesNpm, hasDocker) {
2933
2873
  switch (strategy) {
2934
- case "release-it": return releaseItWorkflow(ci, nodeVersionYaml, publishesNpm);
2935
- case "simple": return commitAndTagVersionWorkflow(ci, nodeVersionYaml, publishesNpm, hasDocker);
2874
+ case "release-it": return releaseItSteps(ci, nodeVersionYaml, publishesNpm);
2875
+ case "simple": return simpleReleaseSteps(ci, nodeVersionYaml, publishesNpm, hasDocker);
2936
2876
  default: return null;
2937
2877
  }
2938
2878
  }
@@ -2983,12 +2923,20 @@ async function generateReleaseCi(ctx) {
2983
2923
  const workflowPath = isGitHub ? ".github/workflows/release.yml" : ".forgejo/workflows/release.yml";
2984
2924
  const nodeVersionYaml = computeNodeVersionYaml(ctx);
2985
2925
  const hasDocker = hasDockerPackages(ctx);
2986
- const content = buildWorkflow(ctx.config.releaseStrategy, ctx.config.ci, nodeVersionYaml, publishesNpm, hasDocker);
2987
- if (!content) return {
2926
+ const steps = buildSteps(ctx.config.releaseStrategy, ctx.config.ci, nodeVersionYaml, publishesNpm, hasDocker);
2927
+ if (!steps) return {
2988
2928
  filePath,
2989
2929
  action: "skipped",
2990
2930
  description: "Release CI workflow not applicable"
2991
2931
  };
2932
+ const content = buildWorkflowYaml({
2933
+ ci: ctx.config.ci,
2934
+ name: "Release",
2935
+ on: { workflow_dispatch: null },
2936
+ ...isGitHub && { permissions: { contents: "write" } },
2937
+ jobName: "release",
2938
+ steps
2939
+ });
2992
2940
  if (ctx.exists(workflowPath)) {
2993
2941
  const raw = ctx.read(workflowPath);
2994
2942
  if (raw) {
@@ -3008,7 +2956,7 @@ async function generateReleaseCi(ctx) {
3008
2956
  description: "Release workflow already up to date"
3009
2957
  };
3010
2958
  }
3011
- const merged = mergeWorkflowSteps(existing, "release", requiredReleaseSteps(ctx.config.releaseStrategy, nodeVersionYaml, publishesNpm, hasDocker));
2959
+ const merged = mergeWorkflowSteps(existing, "release", toRequiredSteps(steps));
3012
2960
  const withComment = ensureSchemaComment(merged.content, ctx.config.ci);
3013
2961
  if (!merged.changed) {
3014
2962
  if (withComment !== raw) {
@@ -3337,7 +3285,7 @@ function generateMigratePrompt(results, config, detected) {
3337
3285
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
3338
3286
  sections.push("# Migration Prompt");
3339
3287
  sections.push("");
3340
- sections.push(`_Generated by \`@bensandee/tooling@0.31.0 repo:sync\` on ${timestamp}_`);
3288
+ sections.push(`_Generated by \`@bensandee/tooling@0.32.0 repo:sync\` on ${timestamp}_`);
3341
3289
  sections.push("");
3342
3290
  sections.push("The following prompt was generated by `@bensandee/tooling repo:sync`. Paste it into Claude Code or another AI assistant to finish migrating this repository.");
3343
3291
  sections.push("");
@@ -4473,8 +4421,18 @@ function readVersion(executor, cwd) {
4473
4421
  if (!pkg?.version) throw new FatalError("No version field found in package.json");
4474
4422
  return pkg.version;
4475
4423
  }
4424
+ /** Configure git identity for CI bot commits. */
4425
+ function configureGitIdentity(executor, config) {
4426
+ const isGitHub = config.platform?.type === "github";
4427
+ const name = isGitHub ? "github-actions[bot]" : "forgejo-actions[bot]";
4428
+ const email = isGitHub ? "github-actions[bot]@users.noreply.github.com" : "forgejo-actions[bot]@noreply.localhost";
4429
+ executor.exec(`git config user.name "${name}"`, { cwd: config.cwd });
4430
+ executor.exec(`git config user.email "${email}"`, { cwd: config.cwd });
4431
+ debug(config, `Configured git identity: ${name} <${email}>`);
4432
+ }
4476
4433
  /** Run the full commit-and-tag-version release flow. */
4477
4434
  async function runSimpleRelease(executor, config) {
4435
+ configureGitIdentity(executor, config);
4478
4436
  const command = buildCommand(config);
4479
4437
  log$2.info(`Running: ${command}`);
4480
4438
  const versionResult = executor.exec(command, { cwd: config.cwd });
@@ -5145,7 +5103,7 @@ const dockerCheckCommand = defineCommand({
5145
5103
  const main = defineCommand({
5146
5104
  meta: {
5147
5105
  name: "bst",
5148
- version: "0.31.0",
5106
+ version: "0.32.0",
5149
5107
  description: "Bootstrap and maintain standardized TypeScript project tooling"
5150
5108
  },
5151
5109
  subCommands: {
@@ -5161,7 +5119,7 @@ const main = defineCommand({
5161
5119
  "docker:check": dockerCheckCommand
5162
5120
  }
5163
5121
  });
5164
- console.log(`@bensandee/tooling v0.31.0`);
5122
+ console.log(`@bensandee/tooling v0.32.0`);
5165
5123
  async function run() {
5166
5124
  await runMain(main);
5167
5125
  process.exit(process.exitCode ?? 0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bensandee/tooling",
3
- "version": "0.31.0",
3
+ "version": "0.32.0",
4
4
  "description": "CLI tool to bootstrap and maintain standardized TypeScript project tooling",
5
5
  "bin": {
6
6
  "bst": "./dist/bin.mjs"
@@ -40,23 +40,21 @@
40
40
  "@bensandee/common": "0.1.2"
41
41
  },
42
42
  "devDependencies": {
43
- "@types/node": "24.12.0",
44
- "@types/picomatch": "4.0.2",
45
- "tsdown": "0.21.4",
46
- "typescript": "5.9.3",
47
- "vitest": "4.1.0",
48
- "@bensandee/config": "0.9.1"
49
- },
50
- "optionalDependencies": {
51
43
  "@changesets/cli": "2.30.0",
52
44
  "@release-it/bumper": "7.0.5",
45
+ "@types/node": "24.12.0",
46
+ "@types/picomatch": "4.0.2",
53
47
  "commit-and-tag-version": "12.7.0",
54
48
  "knip": "5.87.0",
55
49
  "lefthook": "2.1.4",
56
50
  "oxfmt": "0.41.0",
57
51
  "oxlint": "1.56.0",
58
52
  "prettier": "3.8.1",
59
- "release-it": "19.2.4"
53
+ "release-it": "19.2.4",
54
+ "tsdown": "0.21.4",
55
+ "typescript": "5.9.3",
56
+ "vitest": "4.1.0",
57
+ "@bensandee/config": "0.9.1"
60
58
  },
61
59
  "scripts": {
62
60
  "build": "tsdown",