@theholocron/cli 2.0.0-alpha.8 → 2.0.0-alpha.9

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/cli.mjs +140 -7
  2. package/package.json +1 -1
package/dist/cli.mjs CHANGED
@@ -1130,6 +1130,78 @@ jobs:
1130
1130
  name: Upload results to Codecov
1131
1131
  with:
1132
1132
  token: \${{ secrets.CODECOV_TOKEN }}
1133
+ `,
1134
+ "sync-github": `\
1135
+ name: Sync GitHub Templates
1136
+
1137
+ # Builds the holocron CLI from source and pushes updated workflow templates
1138
+ # and composite actions to downstream .github repos. Runs whenever the
1139
+ # template source files change on main or alpha.
1140
+ #
1141
+ # Secrets required:
1142
+ # SYNC_TOKEN — fine-grained PAT or GitHub App token with Contents write
1143
+ # access to the primary and secondary repos.
1144
+
1145
+ on: # yamllint disable-line rule:truthy
1146
+ workflow_call:
1147
+ inputs:
1148
+ primary-repo:
1149
+ description: >
1150
+ Primary .github repo — receives composite actions, reusable workflows,
1151
+ and thin-caller templates. Requires a PR (branch protection assumed).
1152
+ type: string
1153
+ required: false
1154
+ default: theholocron/.github
1155
+ secondary-repos:
1156
+ description: >
1157
+ Space-separated list of secondary repos (reusable workflows + thin
1158
+ callers only, no composite actions). Pushed directly to main.
1159
+ type: string
1160
+ required: false
1161
+ default: ""
1162
+ sync-branch:
1163
+ description: Branch name used for the primary-repo PR
1164
+ type: string
1165
+ required: false
1166
+ default: chore/sync-templates
1167
+ secrets:
1168
+ SYNC_TOKEN:
1169
+ required: true
1170
+
1171
+ jobs:
1172
+ sync:
1173
+ name: Sync templates
1174
+ runs-on: ubuntu-latest
1175
+ timeout-minutes: 15
1176
+ permissions:
1177
+ contents: read
1178
+ steps:
1179
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
1180
+ name: Checkout repository
1181
+
1182
+ - uses: theholocron/.github/.github/actions/setup@main
1183
+ name: Setup
1184
+
1185
+ - run: pnpm build
1186
+ name: Build CLI
1187
+
1188
+ - name: Sync primary repo (PR)
1189
+ run: >
1190
+ node packages/cli/dist/cli.mjs sync-github
1191
+ --repo \${{ inputs.primary-repo }}
1192
+ --branch \${{ inputs.sync-branch }}
1193
+ --pr
1194
+ env:
1195
+ GITHUB_TOKEN: \${{ secrets.SYNC_TOKEN }}
1196
+
1197
+ - name: Sync secondary repos (direct push)
1198
+ if: \${{ inputs.secondary-repos != '' }}
1199
+ run: |
1200
+ for repo in \${{ inputs.secondary-repos }}; do
1201
+ node packages/cli/dist/cli.mjs sync-github --repo "$repo"
1202
+ done
1203
+ env:
1204
+ GITHUB_TOKEN: \${{ secrets.SYNC_TOKEN }}
1133
1205
  `,
1134
1206
  typecheck: `\
1135
1207
  name: Typecheck
@@ -1393,6 +1465,32 @@ jobs:
1393
1465
  audit:
1394
1466
  uses: ${ref("audit")}
1395
1467
  secrets: inherit
1468
+ `,
1469
+ "sync-github": `\
1470
+ name: Sync GitHub Templates
1471
+
1472
+ on: # yamllint disable-line rule:truthy
1473
+ push:
1474
+ branches: [main, alpha]
1475
+ paths:
1476
+ - packages/cli/src/templates/index.ts
1477
+ - packages/cli/src/commands/setup-workflows.ts
1478
+
1479
+ concurrency:
1480
+ group: sync-github-\${{ github.ref }}
1481
+ cancel-in-progress: true
1482
+
1483
+ permissions:
1484
+ contents: read
1485
+
1486
+ jobs:
1487
+ sync:
1488
+ name: Sync
1489
+ uses: ${ref("sync-github")}
1490
+ with:
1491
+ secondary-repos: theholocron/.github-private
1492
+ secrets:
1493
+ SYNC_TOKEN: \${{ secrets.SYNC_TOKEN }}
1396
1494
  `
1397
1495
  };
1398
1496
  const KNOWN_WORKFLOWS = new Set(Object.keys(WORKFLOW_TEMPLATES));
@@ -1420,9 +1518,9 @@ function thinCallerHeader() {
1420
1518
  ``
1421
1519
  ].join("\n");
1422
1520
  }
1423
- function buildBatch() {
1521
+ function buildBatch(repo) {
1424
1522
  const files = [];
1425
- for (const [name, content] of Object.entries(ACTIONS)) files.push({
1523
+ if (repo === DEFAULT_REPO) for (const [name, content] of Object.entries(ACTIONS)) files.push({
1426
1524
  path: `.github/actions/${name}.yml`,
1427
1525
  content: reusableHeader(`packages/cli/src/templates/index.ts`) + content
1428
1526
  });
@@ -1440,7 +1538,7 @@ async function runSyncGithub(input) {
1440
1538
  const print = input.print ?? ((line) => console.log(line));
1441
1539
  const repo = input.repo ?? DEFAULT_REPO;
1442
1540
  const [owner, repoName] = repo.split("/");
1443
- const { token, dryRun = false } = input;
1541
+ const { token, dryRun = false, branch, createPr = false } = input;
1444
1542
  const message = input.message ?? `chore: sync from theholocron/holocron`;
1445
1543
  const fetchFn = input.fetch ?? globalThis.fetch;
1446
1544
  const headers = {
@@ -1450,19 +1548,21 @@ async function runSyncGithub(input) {
1450
1548
  "X-GitHub-Api-Version": "2022-11-28"
1451
1549
  };
1452
1550
  print(`holocron sync-github${dryRun ? " (dry-run)" : ""}`);
1453
- print(` repo: ${repo}`);
1551
+ print(` repo: ${repo}`);
1552
+ if (branch) print(` branch: ${branch}`);
1454
1553
  print("");
1455
- const batch = buildBatch();
1554
+ const batch = buildBatch(repo);
1456
1555
  let created = 0;
1457
1556
  let updated = 0;
1458
1557
  let unchanged = 0;
1459
1558
  for (const file of batch) {
1559
+ const ref = branch ? `?ref=${encodeURIComponent(branch)}` : "";
1460
1560
  const url = `${API_BASE}/repos/${owner}/${repoName}/contents/${file.path}`;
1461
1561
  const newContent = Buffer.from(file.content, "utf8").toString("base64");
1462
1562
  let existingSha;
1463
1563
  let existingContent;
1464
1564
  try {
1465
- const getRes = await fetchFn(url, { headers });
1565
+ const getRes = await fetchFn(`${url}${ref}`, { headers });
1466
1566
  if (getRes.ok) {
1467
1567
  const data = await getRes.json();
1468
1568
  existingSha = data.sha;
@@ -1486,6 +1586,7 @@ async function runSyncGithub(input) {
1486
1586
  content: newContent
1487
1587
  };
1488
1588
  if (existingSha) body.sha = existingSha;
1589
+ if (branch) body.branch = branch;
1489
1590
  const putRes = await fetchFn(url, {
1490
1591
  method: "PUT",
1491
1592
  headers,
@@ -1507,13 +1608,36 @@ async function runSyncGithub(input) {
1507
1608
  if (existingSha) updated++;
1508
1609
  else created++;
1509
1610
  }
1611
+ const changed = created + updated;
1510
1612
  print("");
1511
1613
  print(` ${created} created, ${updated} updated, ${unchanged} unchanged`);
1614
+ let prUrl;
1615
+ if (branch && createPr && changed > 0 && !dryRun) {
1616
+ const prRes = await fetchFn(`${API_BASE}/repos/${owner}/${repoName}/pulls`, {
1617
+ method: "POST",
1618
+ headers,
1619
+ body: JSON.stringify({
1620
+ title: message,
1621
+ head: branch,
1622
+ base: "main",
1623
+ body: "Auto-generated by `holocron sync-github`. Review and merge to apply template updates."
1624
+ })
1625
+ });
1626
+ if (prRes.ok) {
1627
+ prUrl = (await prRes.json()).html_url;
1628
+ print(` → PR opened: ${prUrl}`);
1629
+ } else {
1630
+ const err = await prRes.json();
1631
+ if (err.errors?.some((e) => e.message.includes("already exists"))) print(` → PR already open for ${branch} — branch updated, ready to merge`);
1632
+ else print(` ⚠ PR creation failed: ${err.message ?? prRes.status}`);
1633
+ }
1634
+ }
1512
1635
  return {
1513
1636
  status: dryRun ? "dry-run" : "ok",
1514
1637
  created,
1515
1638
  updated,
1516
- unchanged
1639
+ unchanged,
1640
+ prUrl
1517
1641
  };
1518
1642
  }
1519
1643
  //#endregion
@@ -3493,6 +3617,13 @@ await yargs(hideBin(process.argv)).scriptName("holocron").usage("$0 <command> [o
3493
3617
  type: "string",
3494
3618
  default: "theholocron/.github",
3495
3619
  describe: "Target org/repo (default: theholocron/.github)"
3620
+ }).option("branch", {
3621
+ type: "string",
3622
+ describe: "Push to this branch instead of the default branch (enables PR-based workflow for protected repos)"
3623
+ }).option("pr", {
3624
+ type: "boolean",
3625
+ default: false,
3626
+ describe: "Open a PR after pushing to --branch (no-op without --branch)"
3496
3627
  }).option("message", {
3497
3628
  type: "string",
3498
3629
  describe: "Commit message (default: chore: sync from theholocron/holocron)"
@@ -3507,6 +3638,8 @@ await yargs(hideBin(process.argv)).scriptName("holocron").usage("$0 <command> [o
3507
3638
  token,
3508
3639
  repo: argv.repo,
3509
3640
  dryRun: argv.dryRun,
3641
+ ...argv.branch ? { branch: argv.branch } : {},
3642
+ ...argv.pr ? { createPr: true } : {},
3510
3643
  ...argv.message ? { message: argv.message } : {}
3511
3644
  })).status === "fail") process.exitCode = 1;
3512
3645
  }).command("config show", "Print the resolved holocron config", () => {}, async (argv) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theholocron/cli",
3
- "version": "2.0.0-alpha.8",
3
+ "version": "2.0.0-alpha.9",
4
4
  "description": "The Holocron CLI — a pluggable, capability-based orchestrator for spinning up and operating software projects.",
5
5
  "homepage": "https://github.com/theholocron/holocron/tree/main/packages/cli#readme",
6
6
  "bugs": "https://github.com/theholocron/holocron/issues",