@bensandee/tooling 0.30.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 (3) hide show
  1. package/README.md +1 -1
  2. package/dist/bin.mjs +193 -217
  3. package/package.json +8 -10
package/README.md CHANGED
@@ -77,7 +77,7 @@ The generated `ci:check` script defaults to `pnpm check --skip 'docker:*'` since
77
77
 
78
78
  Docker packages are discovered automatically. Any package with a `Dockerfile` or `docker/Dockerfile` is a Docker package. Image names are derived as `{root-package-name}-{package-name}`, build context defaults to `.` (project root). For single-package repos, `Dockerfile` or `docker/Dockerfile` at the project root is checked.
79
79
 
80
- When Docker packages are present, `repo:sync` generates a deploy workflow (`.forgejo/workflows/publish.yml` or `.github/workflows/publish.yml`) triggered on version tags (`v*.*.*`) that runs `pnpm exec tooling docker:publish`.
80
+ When Docker packages are present, `repo:sync` generates a publish workflow (`.forgejo/workflows/publish.yml` or `.github/workflows/publish.yml`) triggered via `workflow_dispatch` for manual runs. For the `simple` release strategy, docker publishing is also added as a step in the release workflow so it runs automatically after each release.
81
81
 
82
82
  #### Overrides
83
83
 
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,45 +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
- push:
1176
- tags:
1177
- - "v[0-9]+.[0-9]+.[0-9]+"
1178
-
1179
- jobs:
1180
- publish:
1181
- runs-on: ubuntu-latest
1182
- steps:
1183
- - uses: actions/checkout@v4
1184
- - uses: pnpm/action-setup@v4
1185
- - uses: actions/setup-node@v4
1186
- with:
1187
- ${nodeVersionYaml}
1188
- - run: pnpm install --frozen-lockfile
1189
- - name: Publish Docker images
1190
- env:
1191
- DOCKER_REGISTRY_HOST: ${actionsExpr("vars.DOCKER_REGISTRY_HOST")}
1192
- DOCKER_REGISTRY_NAMESPACE: ${actionsExpr("vars.DOCKER_REGISTRY_NAMESPACE")}
1193
- DOCKER_USERNAME: ${actionsExpr("secrets.DOCKER_USERNAME")}
1194
- DOCKER_PASSWORD: ${actionsExpr("secrets.DOCKER_PASSWORD")}
1195
- run: pnpm exec bst docker:publish
1196
- `;
1197
- }
1198
- function requiredDeploySteps() {
1191
+ function publishSteps(nodeVersionYaml) {
1199
1192
  return [
1200
1193
  {
1201
1194
  match: { uses: "actions/checkout" },
1202
- step: { uses: "actions/checkout@v4" }
1195
+ step: { uses: "actions/checkout@v6" }
1203
1196
  },
1204
1197
  {
1205
1198
  match: { uses: "pnpm/action-setup" },
1206
- step: { uses: "pnpm/action-setup@v4" }
1199
+ step: { uses: "pnpm/action-setup@v5" }
1207
1200
  },
1208
1201
  {
1209
1202
  match: { uses: "actions/setup-node" },
1210
- 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
+ }
1211
1207
  },
1212
1208
  {
1213
1209
  match: { run: "pnpm install" },
@@ -1215,7 +1211,16 @@ function requiredDeploySteps() {
1215
1211
  },
1216
1212
  {
1217
1213
  match: { run: "docker:publish" },
1218
- 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
+ }
1219
1224
  }
1220
1225
  ];
1221
1226
  }
@@ -1257,8 +1262,14 @@ async function generateDeployCi(ctx) {
1257
1262
  };
1258
1263
  const isGitHub = ctx.config.ci === "github";
1259
1264
  const workflowPath = isGitHub ? ".github/workflows/publish.yml" : ".forgejo/workflows/publish.yml";
1260
- const nodeVersionYaml = computeNodeVersionYaml(ctx);
1261
- 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
+ });
1262
1273
  if (ctx.exists(workflowPath)) {
1263
1274
  const raw = ctx.read(workflowPath);
1264
1275
  if (raw) {
@@ -1278,7 +1289,7 @@ async function generateDeployCi(ctx) {
1278
1289
  description: "Publish workflow already up to date"
1279
1290
  };
1280
1291
  }
1281
- const merged = mergeWorkflowSteps(existing, "publish", requiredDeploySteps());
1292
+ const merged = mergeWorkflowSteps(existing, "publish", toRequiredSteps(steps));
1282
1293
  const withComment = ensureSchemaComment(merged.content, ctx.config.ci);
1283
1294
  if (!merged.changed) {
1284
1295
  if (withComment !== raw) {
@@ -1364,6 +1375,7 @@ const PER_PACKAGE_DEV_DEPS = {
1364
1375
  "@types/node": {
1365
1376
  "@changesets/cli": "2.30.0",
1366
1377
  "@release-it/bumper": "7.0.5",
1378
+ "@types/node": "24.12.0",
1367
1379
  "commit-and-tag-version": "12.7.0",
1368
1380
  "knip": "5.87.0",
1369
1381
  "lefthook": "2.1.4",
@@ -1371,7 +1383,6 @@ const PER_PACKAGE_DEV_DEPS = {
1371
1383
  "oxlint": "1.56.0",
1372
1384
  "prettier": "3.8.1",
1373
1385
  "release-it": "19.2.4",
1374
- "@types/node": "24.12.0",
1375
1386
  "tsdown": "0.21.4",
1376
1387
  "typescript": "5.9.3",
1377
1388
  "vitest": "4.1.0",
@@ -1380,6 +1391,7 @@ const PER_PACKAGE_DEV_DEPS = {
1380
1391
  tsdown: {
1381
1392
  "@changesets/cli": "2.30.0",
1382
1393
  "@release-it/bumper": "7.0.5",
1394
+ "@types/node": "24.12.0",
1383
1395
  "commit-and-tag-version": "12.7.0",
1384
1396
  "knip": "5.87.0",
1385
1397
  "lefthook": "2.1.4",
@@ -1387,7 +1399,6 @@ const PER_PACKAGE_DEV_DEPS = {
1387
1399
  "oxlint": "1.56.0",
1388
1400
  "prettier": "3.8.1",
1389
1401
  "release-it": "19.2.4",
1390
- "@types/node": "24.12.0",
1391
1402
  "tsdown": "0.21.4",
1392
1403
  "typescript": "5.9.3",
1393
1404
  "vitest": "4.1.0",
@@ -1396,6 +1407,7 @@ const PER_PACKAGE_DEV_DEPS = {
1396
1407
  typescript: {
1397
1408
  "@changesets/cli": "2.30.0",
1398
1409
  "@release-it/bumper": "7.0.5",
1410
+ "@types/node": "24.12.0",
1399
1411
  "commit-and-tag-version": "12.7.0",
1400
1412
  "knip": "5.87.0",
1401
1413
  "lefthook": "2.1.4",
@@ -1403,7 +1415,6 @@ const PER_PACKAGE_DEV_DEPS = {
1403
1415
  "oxlint": "1.56.0",
1404
1416
  "prettier": "3.8.1",
1405
1417
  "release-it": "19.2.4",
1406
- "@types/node": "24.12.0",
1407
1418
  "tsdown": "0.21.4",
1408
1419
  "typescript": "5.9.3",
1409
1420
  "vitest": "4.1.0",
@@ -1412,6 +1423,7 @@ const PER_PACKAGE_DEV_DEPS = {
1412
1423
  vitest: {
1413
1424
  "@changesets/cli": "2.30.0",
1414
1425
  "@release-it/bumper": "7.0.5",
1426
+ "@types/node": "24.12.0",
1415
1427
  "commit-and-tag-version": "12.7.0",
1416
1428
  "knip": "5.87.0",
1417
1429
  "lefthook": "2.1.4",
@@ -1419,7 +1431,6 @@ const PER_PACKAGE_DEV_DEPS = {
1419
1431
  "oxlint": "1.56.0",
1420
1432
  "prettier": "3.8.1",
1421
1433
  "release-it": "19.2.4",
1422
- "@types/node": "24.12.0",
1423
1434
  "tsdown": "0.21.4",
1424
1435
  "typescript": "5.9.3",
1425
1436
  "vitest": "4.1.0",
@@ -1431,6 +1442,7 @@ const ROOT_DEV_DEPS = {
1431
1442
  knip: {
1432
1443
  "@changesets/cli": "2.30.0",
1433
1444
  "@release-it/bumper": "7.0.5",
1445
+ "@types/node": "24.12.0",
1434
1446
  "commit-and-tag-version": "12.7.0",
1435
1447
  "knip": "5.87.0",
1436
1448
  "lefthook": "2.1.4",
@@ -1438,7 +1450,6 @@ const ROOT_DEV_DEPS = {
1438
1450
  "oxlint": "1.56.0",
1439
1451
  "prettier": "3.8.1",
1440
1452
  "release-it": "19.2.4",
1441
- "@types/node": "24.12.0",
1442
1453
  "tsdown": "0.21.4",
1443
1454
  "typescript": "5.9.3",
1444
1455
  "vitest": "4.1.0",
@@ -1447,6 +1458,7 @@ const ROOT_DEV_DEPS = {
1447
1458
  lefthook: {
1448
1459
  "@changesets/cli": "2.30.0",
1449
1460
  "@release-it/bumper": "7.0.5",
1461
+ "@types/node": "24.12.0",
1450
1462
  "commit-and-tag-version": "12.7.0",
1451
1463
  "knip": "5.87.0",
1452
1464
  "lefthook": "2.1.4",
@@ -1454,7 +1466,6 @@ const ROOT_DEV_DEPS = {
1454
1466
  "oxlint": "1.56.0",
1455
1467
  "prettier": "3.8.1",
1456
1468
  "release-it": "19.2.4",
1457
- "@types/node": "24.12.0",
1458
1469
  "tsdown": "0.21.4",
1459
1470
  "typescript": "5.9.3",
1460
1471
  "vitest": "4.1.0",
@@ -1463,6 +1474,7 @@ const ROOT_DEV_DEPS = {
1463
1474
  oxlint: {
1464
1475
  "@changesets/cli": "2.30.0",
1465
1476
  "@release-it/bumper": "7.0.5",
1477
+ "@types/node": "24.12.0",
1466
1478
  "commit-and-tag-version": "12.7.0",
1467
1479
  "knip": "5.87.0",
1468
1480
  "lefthook": "2.1.4",
@@ -1470,7 +1482,6 @@ const ROOT_DEV_DEPS = {
1470
1482
  "oxlint": "1.56.0",
1471
1483
  "prettier": "3.8.1",
1472
1484
  "release-it": "19.2.4",
1473
- "@types/node": "24.12.0",
1474
1485
  "tsdown": "0.21.4",
1475
1486
  "typescript": "5.9.3",
1476
1487
  "vitest": "4.1.0",
@@ -1506,6 +1517,7 @@ function addReleaseDeps(deps, config) {
1506
1517
  deps["release-it"] = {
1507
1518
  "@changesets/cli": "2.30.0",
1508
1519
  "@release-it/bumper": "7.0.5",
1520
+ "@types/node": "24.12.0",
1509
1521
  "commit-and-tag-version": "12.7.0",
1510
1522
  "knip": "5.87.0",
1511
1523
  "lefthook": "2.1.4",
@@ -1513,7 +1525,6 @@ function addReleaseDeps(deps, config) {
1513
1525
  "oxlint": "1.56.0",
1514
1526
  "prettier": "3.8.1",
1515
1527
  "release-it": "19.2.4",
1516
- "@types/node": "24.12.0",
1517
1528
  "tsdown": "0.21.4",
1518
1529
  "typescript": "5.9.3",
1519
1530
  "vitest": "4.1.0",
@@ -1522,6 +1533,7 @@ function addReleaseDeps(deps, config) {
1522
1533
  if (config.structure === "monorepo") deps["@release-it/bumper"] = {
1523
1534
  "@changesets/cli": "2.30.0",
1524
1535
  "@release-it/bumper": "7.0.5",
1536
+ "@types/node": "24.12.0",
1525
1537
  "commit-and-tag-version": "12.7.0",
1526
1538
  "knip": "5.87.0",
1527
1539
  "lefthook": "2.1.4",
@@ -1529,7 +1541,6 @@ function addReleaseDeps(deps, config) {
1529
1541
  "oxlint": "1.56.0",
1530
1542
  "prettier": "3.8.1",
1531
1543
  "release-it": "19.2.4",
1532
- "@types/node": "24.12.0",
1533
1544
  "tsdown": "0.21.4",
1534
1545
  "typescript": "5.9.3",
1535
1546
  "vitest": "4.1.0",
@@ -1540,6 +1551,7 @@ function addReleaseDeps(deps, config) {
1540
1551
  deps["commit-and-tag-version"] = {
1541
1552
  "@changesets/cli": "2.30.0",
1542
1553
  "@release-it/bumper": "7.0.5",
1554
+ "@types/node": "24.12.0",
1543
1555
  "commit-and-tag-version": "12.7.0",
1544
1556
  "knip": "5.87.0",
1545
1557
  "lefthook": "2.1.4",
@@ -1547,7 +1559,6 @@ function addReleaseDeps(deps, config) {
1547
1559
  "oxlint": "1.56.0",
1548
1560
  "prettier": "3.8.1",
1549
1561
  "release-it": "19.2.4",
1550
- "@types/node": "24.12.0",
1551
1562
  "tsdown": "0.21.4",
1552
1563
  "typescript": "5.9.3",
1553
1564
  "vitest": "4.1.0",
@@ -1558,6 +1569,7 @@ function addReleaseDeps(deps, config) {
1558
1569
  deps["@changesets/cli"] = {
1559
1570
  "@changesets/cli": "2.30.0",
1560
1571
  "@release-it/bumper": "7.0.5",
1572
+ "@types/node": "24.12.0",
1561
1573
  "commit-and-tag-version": "12.7.0",
1562
1574
  "knip": "5.87.0",
1563
1575
  "lefthook": "2.1.4",
@@ -1565,7 +1577,6 @@ function addReleaseDeps(deps, config) {
1565
1577
  "oxlint": "1.56.0",
1566
1578
  "prettier": "3.8.1",
1567
1579
  "release-it": "19.2.4",
1568
- "@types/node": "24.12.0",
1569
1580
  "tsdown": "0.21.4",
1570
1581
  "typescript": "5.9.3",
1571
1582
  "vitest": "4.1.0",
@@ -1579,10 +1590,11 @@ function getAddedDevDepNames(config) {
1579
1590
  const deps = { ...ROOT_DEV_DEPS };
1580
1591
  if (config.structure !== "monorepo") Object.assign(deps, PER_PACKAGE_DEV_DEPS);
1581
1592
  deps["@bensandee/config"] = "0.9.1";
1582
- deps["@bensandee/tooling"] = "0.30.0";
1593
+ deps["@bensandee/tooling"] = "0.32.0";
1583
1594
  if (config.formatter === "oxfmt") deps["oxfmt"] = {
1584
1595
  "@changesets/cli": "2.30.0",
1585
1596
  "@release-it/bumper": "7.0.5",
1597
+ "@types/node": "24.12.0",
1586
1598
  "commit-and-tag-version": "12.7.0",
1587
1599
  "knip": "5.87.0",
1588
1600
  "lefthook": "2.1.4",
@@ -1590,7 +1602,6 @@ function getAddedDevDepNames(config) {
1590
1602
  "oxlint": "1.56.0",
1591
1603
  "prettier": "3.8.1",
1592
1604
  "release-it": "19.2.4",
1593
- "@types/node": "24.12.0",
1594
1605
  "tsdown": "0.21.4",
1595
1606
  "typescript": "5.9.3",
1596
1607
  "vitest": "4.1.0",
@@ -1599,6 +1610,7 @@ function getAddedDevDepNames(config) {
1599
1610
  if (config.formatter === "prettier") deps["prettier"] = {
1600
1611
  "@changesets/cli": "2.30.0",
1601
1612
  "@release-it/bumper": "7.0.5",
1613
+ "@types/node": "24.12.0",
1602
1614
  "commit-and-tag-version": "12.7.0",
1603
1615
  "knip": "5.87.0",
1604
1616
  "lefthook": "2.1.4",
@@ -1606,7 +1618,6 @@ function getAddedDevDepNames(config) {
1606
1618
  "oxlint": "1.56.0",
1607
1619
  "prettier": "3.8.1",
1608
1620
  "release-it": "19.2.4",
1609
- "@types/node": "24.12.0",
1610
1621
  "tsdown": "0.21.4",
1611
1622
  "typescript": "5.9.3",
1612
1623
  "vitest": "4.1.0",
@@ -1634,11 +1645,12 @@ async function generatePackageJson(ctx) {
1634
1645
  const devDeps = { ...ROOT_DEV_DEPS };
1635
1646
  if (!isMonorepo) Object.assign(devDeps, PER_PACKAGE_DEV_DEPS);
1636
1647
  devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.9.1";
1637
- devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.30.0";
1648
+ devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.32.0";
1638
1649
  if (ctx.config.useEslintPlugin) devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "0.9.2";
1639
1650
  if (ctx.config.formatter === "oxfmt") devDeps["oxfmt"] = {
1640
1651
  "@changesets/cli": "2.30.0",
1641
1652
  "@release-it/bumper": "7.0.5",
1653
+ "@types/node": "24.12.0",
1642
1654
  "commit-and-tag-version": "12.7.0",
1643
1655
  "knip": "5.87.0",
1644
1656
  "lefthook": "2.1.4",
@@ -1646,7 +1658,6 @@ async function generatePackageJson(ctx) {
1646
1658
  "oxlint": "1.56.0",
1647
1659
  "prettier": "3.8.1",
1648
1660
  "release-it": "19.2.4",
1649
- "@types/node": "24.12.0",
1650
1661
  "tsdown": "0.21.4",
1651
1662
  "typescript": "5.9.3",
1652
1663
  "vitest": "4.1.0",
@@ -1655,6 +1666,7 @@ async function generatePackageJson(ctx) {
1655
1666
  if (ctx.config.formatter === "prettier") devDeps["prettier"] = {
1656
1667
  "@changesets/cli": "2.30.0",
1657
1668
  "@release-it/bumper": "7.0.5",
1669
+ "@types/node": "24.12.0",
1658
1670
  "commit-and-tag-version": "12.7.0",
1659
1671
  "knip": "5.87.0",
1660
1672
  "lefthook": "2.1.4",
@@ -1662,7 +1674,6 @@ async function generatePackageJson(ctx) {
1662
1674
  "oxlint": "1.56.0",
1663
1675
  "prettier": "3.8.1",
1664
1676
  "release-it": "19.2.4",
1665
- "@types/node": "24.12.0",
1666
1677
  "tsdown": "0.21.4",
1667
1678
  "typescript": "5.9.3",
1668
1679
  "vitest": "4.1.0",
@@ -1743,6 +1754,7 @@ async function generatePackageJson(ctx) {
1743
1754
  packageManager: `pnpm@${{
1744
1755
  "@changesets/cli": "2.30.0",
1745
1756
  "@release-it/bumper": "7.0.5",
1757
+ "@types/node": "24.12.0",
1746
1758
  "commit-and-tag-version": "12.7.0",
1747
1759
  "knip": "5.87.0",
1748
1760
  "lefthook": "2.1.4",
@@ -1750,7 +1762,6 @@ async function generatePackageJson(ctx) {
1750
1762
  "oxlint": "1.56.0",
1751
1763
  "prettier": "3.8.1",
1752
1764
  "release-it": "19.2.4",
1753
- "@types/node": "24.12.0",
1754
1765
  "tsdown": "0.21.4",
1755
1766
  "typescript": "5.9.3",
1756
1767
  "vitest": "4.1.0",
@@ -2176,50 +2187,20 @@ const CI_CONCURRENCY = {
2176
2187
  group: `ci-${actionsExpr("github.ref")}`,
2177
2188
  "cancel-in-progress": actionsExpr("github.ref != 'refs/heads/main'")
2178
2189
  };
2179
- function ciWorkflow(nodeVersionYaml, isForgejo, isChangesets) {
2180
- const emailNotifications = isForgejo ? "\nenable-email-notifications: true\n" : "";
2181
- const concurrencyBlock = isChangesets ? `
2182
- concurrency:
2183
- group: ci-${actionsExpr("github.ref")}
2184
- cancel-in-progress: ${actionsExpr("github.ref != 'refs/heads/main'")}
2185
- ` : "";
2186
- return `${workflowSchemaComment(isForgejo ? "forgejo" : "github")}name: CI
2187
- ${emailNotifications}on:
2188
- push:
2189
- branches: [main]
2190
- tags-ignore:
2191
- - "**"
2192
- pull_request:
2193
- ${concurrencyBlock}
2194
- jobs:
2195
- check:
2196
- runs-on: ubuntu-latest
2197
- steps:
2198
- - uses: actions/checkout@v4
2199
- - uses: pnpm/action-setup@v4
2200
- - uses: actions/setup-node@v4
2201
- with:
2202
- ${nodeVersionYaml}
2203
- cache: pnpm
2204
- - run: pnpm install --frozen-lockfile
2205
- - name: Run all checks
2206
- run: pnpm ci:check
2207
- `;
2208
- }
2209
- function requiredCheckSteps(nodeVersionYaml) {
2190
+ function checkSteps(nodeVersionYaml) {
2210
2191
  return [
2211
2192
  {
2212
2193
  match: { uses: "actions/checkout" },
2213
- step: { uses: "actions/checkout@v4" }
2194
+ step: { uses: "actions/checkout@v6" }
2214
2195
  },
2215
2196
  {
2216
2197
  match: { uses: "pnpm/action-setup" },
2217
- step: { uses: "pnpm/action-setup@v4" }
2198
+ step: { uses: "pnpm/action-setup@v5" }
2218
2199
  },
2219
2200
  {
2220
2201
  match: { uses: "actions/setup-node" },
2221
2202
  step: {
2222
- uses: "actions/setup-node@v4",
2203
+ uses: "actions/setup-node@v6",
2223
2204
  with: {
2224
2205
  ...nodeVersionYaml.startsWith("node-version-file") ? { "node-version-file": "package.json" } : { "node-version": "24" },
2225
2206
  cache: "pnpm"
@@ -2250,14 +2231,29 @@ async function generateCi(ctx) {
2250
2231
  description: "CI workflow not requested"
2251
2232
  };
2252
2233
  const isGitHub = ctx.config.ci === "github";
2234
+ const isForgejo = !isGitHub;
2253
2235
  const isChangesets = ctx.config.releaseStrategy === "changesets";
2254
- 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
+ });
2255
2252
  const filePath = ciWorkflowPath(ctx.config.ci, ctx.config.releaseStrategy);
2256
- const content = ciWorkflow(nodeVersionYaml, !isGitHub, isChangesets);
2257
2253
  if (ctx.exists(filePath)) {
2258
2254
  const existing = ctx.read(filePath);
2259
2255
  if (existing) {
2260
- let result = mergeWorkflowSteps(existing, "check", requiredCheckSteps(nodeVersionYaml));
2256
+ let result = mergeWorkflowSteps(existing, "check", toRequiredSteps(steps));
2261
2257
  const withTagsIgnore = ensureWorkflowTagsIgnore(result.content);
2262
2258
  result = {
2263
2259
  content: withTagsIgnore.content,
@@ -2762,71 +2758,84 @@ async function generateChangesets(ctx) {
2762
2758
  }
2763
2759
  //#endregion
2764
2760
  //#region src/generators/release-ci.ts
2761
+ /** Common setup steps shared by release-it and simple strategies. */
2765
2762
  function commonSteps(nodeVersionYaml, publishesNpm) {
2766
- return ` - uses: actions/checkout@v4
2767
- with:
2768
- fetch-depth: 0
2769
- - uses: pnpm/action-setup@v4
2770
- - uses: actions/setup-node@v4
2771
- with:
2772
- ${nodeVersionYaml}
2773
- cache: pnpm${publishesNpm ? `\n registry-url: "https://registry.npmjs.org"` : ""}
2774
- - run: pnpm install --frozen-lockfile`;
2775
- }
2776
- function releaseItWorkflow(ci, nodeVersionYaml, publishesNpm) {
2777
- const isGitHub = ci === "github";
2778
- const permissions = isGitHub ? `
2779
- permissions:
2780
- contents: write
2781
- ` : "";
2782
- const tokenEnv = isGitHub ? `GITHUB_TOKEN: \${{ github.token }}` : `RELEASE_TOKEN: \${{ secrets.RELEASE_TOKEN }}`;
2783
- const npmEnv = publishesNpm ? `\n NODE_AUTH_TOKEN: \${{ secrets.NPM_TOKEN }}` : "";
2784
- return `${workflowSchemaComment(ci)}name: Release
2785
- on:
2786
- workflow_dispatch:
2787
- ${permissions}
2788
- jobs:
2789
- release:
2790
- runs-on: ubuntu-latest
2791
- steps:
2792
- ${commonSteps(nodeVersionYaml, publishesNpm)}
2793
- - run: pnpm release-it --ci
2794
- env:
2795
- ${tokenEnv}${npmEnv}
2796
- `;
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
+ ];
2797
2792
  }
2798
- function commitAndTagVersionWorkflow(ci, nodeVersionYaml, publishesNpm) {
2799
- const isGitHub = ci === "github";
2800
- const permissions = isGitHub ? `
2801
- permissions:
2802
- contents: write
2803
- ` : "";
2804
- const gitConfigStep = `
2805
- - name: Configure git
2806
- run: |
2807
- git config user.name "${isGitHub ? "github-actions[bot]" : "forgejo-actions[bot]"}"
2808
- git config user.email "${isGitHub ? "github-actions[bot]@users.noreply.github.com" : "forgejo-actions[bot]@noreply.localhost"}"`;
2809
- const releaseStep = isGitHub ? `
2810
- - name: Release
2811
- env:
2812
- GITHUB_TOKEN: \${{ github.token }}
2813
- run: pnpm exec bst release:simple` : `
2814
- - name: Release
2815
- env:
2816
- FORGEJO_SERVER_URL: \${{ github.server_url }}
2817
- FORGEJO_REPOSITORY: \${{ github.repository }}
2818
- RELEASE_TOKEN: \${{ secrets.RELEASE_TOKEN }}
2819
- run: pnpm exec bst release:simple`;
2820
- return `${workflowSchemaComment(ci)}name: Release
2821
- on:
2822
- workflow_dispatch:
2823
- ${permissions}
2824
- jobs:
2825
- release:
2826
- runs-on: ubuntu-latest
2827
- steps:
2828
- ${commonSteps(nodeVersionYaml, publishesNpm)}${gitConfigStep}${releaseStep}
2829
- `;
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
+ ];
2830
2839
  }
2831
2840
  /** Build the required release step for the check job (changesets). */
2832
2841
  function changesetsReleaseStep(ci, publishesNpm) {
@@ -2860,62 +2869,10 @@ function changesetsReleaseStep(ci, publishesNpm) {
2860
2869
  }
2861
2870
  };
2862
2871
  }
2863
- function requiredReleaseSteps(strategy, nodeVersionYaml, publishesNpm) {
2864
- const isNodeVersionFile = nodeVersionYaml.startsWith("node-version-file");
2865
- const steps = [
2866
- {
2867
- match: { uses: "actions/checkout" },
2868
- step: {
2869
- uses: "actions/checkout@v4",
2870
- with: { "fetch-depth": 0 }
2871
- }
2872
- },
2873
- {
2874
- match: { uses: "pnpm/action-setup" },
2875
- step: { uses: "pnpm/action-setup@v4" }
2876
- },
2877
- {
2878
- match: { uses: "actions/setup-node" },
2879
- step: {
2880
- uses: "actions/setup-node@v4",
2881
- with: {
2882
- ...isNodeVersionFile ? { "node-version-file": "package.json" } : { "node-version": "24" },
2883
- cache: "pnpm",
2884
- ...publishesNpm && { "registry-url": "https://registry.npmjs.org" }
2885
- }
2886
- }
2887
- },
2888
- {
2889
- match: { run: "pnpm install" },
2890
- step: { run: "pnpm install --frozen-lockfile" }
2891
- }
2892
- ];
2872
+ function buildSteps(strategy, ci, nodeVersionYaml, publishesNpm, hasDocker) {
2893
2873
  switch (strategy) {
2894
- case "release-it":
2895
- steps.push({
2896
- match: { run: "release-it" },
2897
- step: { run: "pnpm release-it --ci" }
2898
- });
2899
- break;
2900
- case "simple":
2901
- steps.push({
2902
- match: { run: "release:simple" },
2903
- step: { run: "pnpm exec bst release:simple" }
2904
- });
2905
- break;
2906
- case "changesets":
2907
- steps.push({
2908
- match: { run: "changeset" },
2909
- step: { run: "pnpm exec bst release:changesets" }
2910
- });
2911
- break;
2912
- }
2913
- return steps;
2914
- }
2915
- function buildWorkflow(strategy, ci, nodeVersionYaml, publishesNpm) {
2916
- switch (strategy) {
2917
- case "release-it": return releaseItWorkflow(ci, nodeVersionYaml, publishesNpm);
2918
- case "simple": return commitAndTagVersionWorkflow(ci, nodeVersionYaml, publishesNpm);
2874
+ case "release-it": return releaseItSteps(ci, nodeVersionYaml, publishesNpm);
2875
+ case "simple": return simpleReleaseSteps(ci, nodeVersionYaml, publishesNpm, hasDocker);
2919
2876
  default: return null;
2920
2877
  }
2921
2878
  }
@@ -2965,12 +2922,21 @@ async function generateReleaseCi(ctx) {
2965
2922
  const isGitHub = ctx.config.ci === "github";
2966
2923
  const workflowPath = isGitHub ? ".github/workflows/release.yml" : ".forgejo/workflows/release.yml";
2967
2924
  const nodeVersionYaml = computeNodeVersionYaml(ctx);
2968
- const content = buildWorkflow(ctx.config.releaseStrategy, ctx.config.ci, nodeVersionYaml, publishesNpm);
2969
- if (!content) return {
2925
+ const hasDocker = hasDockerPackages(ctx);
2926
+ const steps = buildSteps(ctx.config.releaseStrategy, ctx.config.ci, nodeVersionYaml, publishesNpm, hasDocker);
2927
+ if (!steps) return {
2970
2928
  filePath,
2971
2929
  action: "skipped",
2972
2930
  description: "Release CI workflow not applicable"
2973
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
+ });
2974
2940
  if (ctx.exists(workflowPath)) {
2975
2941
  const raw = ctx.read(workflowPath);
2976
2942
  if (raw) {
@@ -2990,7 +2956,7 @@ async function generateReleaseCi(ctx) {
2990
2956
  description: "Release workflow already up to date"
2991
2957
  };
2992
2958
  }
2993
- const merged = mergeWorkflowSteps(existing, "release", requiredReleaseSteps(ctx.config.releaseStrategy, nodeVersionYaml, publishesNpm));
2959
+ const merged = mergeWorkflowSteps(existing, "release", toRequiredSteps(steps));
2994
2960
  const withComment = ensureSchemaComment(merged.content, ctx.config.ci);
2995
2961
  if (!merged.changed) {
2996
2962
  if (withComment !== raw) {
@@ -3319,7 +3285,7 @@ function generateMigratePrompt(results, config, detected) {
3319
3285
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
3320
3286
  sections.push("# Migration Prompt");
3321
3287
  sections.push("");
3322
- sections.push(`_Generated by \`@bensandee/tooling@0.30.0 repo:sync\` on ${timestamp}_`);
3288
+ sections.push(`_Generated by \`@bensandee/tooling@0.32.0 repo:sync\` on ${timestamp}_`);
3323
3289
  sections.push("");
3324
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.");
3325
3291
  sections.push("");
@@ -4455,8 +4421,18 @@ function readVersion(executor, cwd) {
4455
4421
  if (!pkg?.version) throw new FatalError("No version field found in package.json");
4456
4422
  return pkg.version;
4457
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
+ }
4458
4433
  /** Run the full commit-and-tag-version release flow. */
4459
4434
  async function runSimpleRelease(executor, config) {
4435
+ configureGitIdentity(executor, config);
4460
4436
  const command = buildCommand(config);
4461
4437
  log$2.info(`Running: ${command}`);
4462
4438
  const versionResult = executor.exec(command, { cwd: config.cwd });
@@ -5127,7 +5103,7 @@ const dockerCheckCommand = defineCommand({
5127
5103
  const main = defineCommand({
5128
5104
  meta: {
5129
5105
  name: "bst",
5130
- version: "0.30.0",
5106
+ version: "0.32.0",
5131
5107
  description: "Bootstrap and maintain standardized TypeScript project tooling"
5132
5108
  },
5133
5109
  subCommands: {
@@ -5143,7 +5119,7 @@ const main = defineCommand({
5143
5119
  "docker:check": dockerCheckCommand
5144
5120
  }
5145
5121
  });
5146
- console.log(`@bensandee/tooling v0.30.0`);
5122
+ console.log(`@bensandee/tooling v0.32.0`);
5147
5123
  async function run() {
5148
5124
  await runMain(main);
5149
5125
  process.exit(process.exitCode ?? 0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bensandee/tooling",
3
- "version": "0.30.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",