@theholocron/cli 3.58.1 → 3.59.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -11,9 +11,9 @@ import { Entry, findCredentials } from "@napi-rs/keyring";
11
11
  import { pathToFileURL } from "node:url";
12
12
  import ora from "ora";
13
13
  import chalk from "chalk";
14
+ import { createLogger, parseLogLevel, resolveAxiomFromEnv } from "@theholocron/logger";
14
15
  import { execFile, execFileSync, spawnSync } from "node:child_process";
15
16
  import { homedir } from "node:os";
16
- import { createLogger, parseLogLevel, resolveAxiomFromEnv } from "@theholocron/logger";
17
17
  import { access, copyFile, mkdir, readFile, readdir, rm, stat, symlink, unlink, writeFile } from "node:fs/promises";
18
18
  import { createHash } from "node:crypto";
19
19
  import { generateReadme } from "@theholocron/components-doc/markdown";
@@ -512,6 +512,94 @@ async function tryLoadHint(importer, packageName) {
512
512
  }
513
513
  }
514
514
  //#endregion
515
+ //#region src/logger.ts
516
+ /**
517
+ * CLI-side wiring for `@theholocron/logger`.
518
+ *
519
+ * `logger` is the operational-output channel — internal state, debug
520
+ * traces, errors, structured context that routes to Axiom. It runs in
521
+ * parallel to `print` (user-facing UX output) and does not replace it.
522
+ */
523
+ /**
524
+ * Resolve the explicit level to hand to `createLogger`, in priority order:
525
+ *
526
+ * 1. `--verbose` → `"debug"` 2. `--quiet` → `"error"`
527
+ * 3. `HOLOCRON_LOG_LEVEL` env var
528
+ * 4. `holocron.config` `log.level` (`configLevel`)
529
+ *
530
+ * Returns `undefined` when nothing applies — `createLogger` then defaults
531
+ * to `"info"`. Resolving the full chain here (rather than passing
532
+ * `configLevel` straight through) keeps config below the env var.
533
+ */
534
+ function resolveLogLevel(argv, configLevel) {
535
+ if (argv.verbose) return "debug";
536
+ if (argv.quiet) return "error";
537
+ return parseLogLevel(env.get("HOLOCRON_LOG_LEVEL")) ?? configLevel;
538
+ }
539
+ let root;
540
+ let rootLevel;
541
+ let rootCommand;
542
+ let rootAxiomKey;
543
+ /**
544
+ * Resolve Axiom credentials for the CLI. Env vars win — same contract as
545
+ * `@theholocron/logger`'s `resolveAxiomFromEnv`. Failing that, the CLI-only
546
+ * bridge pairs the OS-keyring token (`axiom.<org>` then bare `axiom`) with a
547
+ * dataset from `HOLOCRON_AXIOM_DATASET` / `AXIOM_DATASET` or
548
+ * `holocron.config` `log.axiom.dataset`. Returns `undefined` unless both a
549
+ * token and a dataset are found.
550
+ */
551
+ function resolveCliAxiom(opts) {
552
+ const fromEnv = resolveAxiomFromEnv();
553
+ if (fromEnv) return fromEnv;
554
+ const dataset = env.get("HOLOCRON_AXIOM_DATASET") || env.get("AXIOM_DATASET") || opts.configAxiomDataset;
555
+ if (!dataset) return void 0;
556
+ const org = opts.org ?? env.get("HOLOCRON_ORG");
557
+ const token = (org ? getToken(`axiom.${org}`) : null) ?? getToken("axiom");
558
+ return token ? {
559
+ dataset,
560
+ token
561
+ } : void 0;
562
+ }
563
+ /**
564
+ * The process-wide root logger. Built once (from `cli.ts`'s middleware,
565
+ * with the command name + flags + env). Rebuilt at most once more when a
566
+ * command's handler supplies `holocron.config` context the flag/env-only
567
+ * first pass could not have known — `log.level` (unless `--verbose` /
568
+ * `--quiet` already fixed it) or `log.axiom.dataset` + the resolved org
569
+ * for a keyring-backed Axiom transport. That rebuild generates a fresh
570
+ * `runId`, which is harmless: nothing logs between the middleware and the
571
+ * handler.
572
+ */
573
+ function buildCliLogger(argv, opts = {}) {
574
+ const { command, configLevel } = opts;
575
+ if (command) rootCommand = command;
576
+ const level = resolveLogLevel(argv, configLevel);
577
+ const axiom = resolveCliAxiom(opts);
578
+ const axiomKey = axiom?.dataset;
579
+ const rebuildForConfig = configLevel !== void 0 && level !== rootLevel && !argv.verbose && !argv.quiet;
580
+ if (!root || rebuildForConfig || axiomKey !== void 0 && axiomKey !== rootAxiomKey) {
581
+ const built = createLogger({
582
+ ...level ? { level } : {},
583
+ ...axiom ? { axiom } : {}
584
+ });
585
+ root = {
586
+ logger: rootCommand ? built.logger.child({ command: rootCommand }) : built.logger,
587
+ runId: built.runId
588
+ };
589
+ rootLevel = level;
590
+ rootAxiomKey = axiomKey;
591
+ }
592
+ return root;
593
+ }
594
+ /** Lazily-memoized `Logger` for module-level call sites with no `argv` in scope. */
595
+ function getLogger() {
596
+ return (root ??= createLogger()).logger;
597
+ }
598
+ /** The current root logger's correlation id, if a root has been built. */
599
+ function getRunId() {
600
+ return root?.runId;
601
+ }
602
+ //#endregion
515
603
  //#region src/plugin/loader.ts
516
604
  /**
517
605
  * `PluginLoader` — loads provider plugins per the resolved config and
@@ -684,9 +772,14 @@ function prStateLabel(pr) {
684
772
  }
685
773
  async function runCleanupPreview(input) {
686
774
  const print = input.print ?? ((line) => console.log(line));
775
+ const logger = input.logger ?? getLogger();
687
776
  // c8 ignore next -- real PluginLoader construction is integration-level; unit tests always supply loader
688
777
  const loader = input.loader ?? new PluginLoader(input.loaded.resolved, input.context);
689
778
  await loader.load();
779
+ logger.info({
780
+ pr: input.prNumber,
781
+ project: input.project
782
+ }, "cleanup-preview: start");
690
783
  if (!loader.has("source")) throw new Error("source capability is not configured — add a source provider to holocron.config.json");
691
784
  const source = loader.get("source");
692
785
  if (!source.getPullRequest) throw new Error(`${source.providerName} source provider does not support getPullRequest`);
@@ -716,6 +809,13 @@ async function runCleanupPreview(input) {
716
809
  }
717
810
  if (deployments.length === 0) {
718
811
  print(style.dim(`No deployments found for branch ${branch}.`));
812
+ logger.info({
813
+ pr: pr.number,
814
+ branch,
815
+ found: 0,
816
+ deleted: 0,
817
+ status: "none"
818
+ }, "cleanup-preview: done");
719
819
  return {
720
820
  pr,
721
821
  branch,
@@ -768,6 +868,13 @@ async function runCleanupPreview(input) {
768
868
  try {
769
869
  const count = await deploy.deletePreviewDeployments(input.project, selected);
770
870
  print(style.success(`Deleted ${count} deployment${count === 1 ? "" : "s"}.`));
871
+ logger.info({
872
+ pr: pr.number,
873
+ branch,
874
+ found: deployments.length,
875
+ deleted: count,
876
+ status: "ok"
877
+ }, "cleanup-preview: done");
771
878
  return {
772
879
  pr,
773
880
  branch,
@@ -778,6 +885,13 @@ async function runCleanupPreview(input) {
778
885
  } catch (err) {
779
886
  const message = err instanceof Error ? err.message : String(err);
780
887
  print(style.fail(message));
888
+ logger.warn({
889
+ pr: pr.number,
890
+ branch,
891
+ found: deployments.length,
892
+ reason: message,
893
+ status: "fail"
894
+ }, "cleanup-preview: done");
781
895
  return {
782
896
  pr,
783
897
  branch,
@@ -894,9 +1008,16 @@ async function runClone(input) {
894
1008
  //#region src/commands/deploy.ts
895
1009
  async function runDeploy(input) {
896
1010
  const print = input.print ?? ((line) => console.log(line));
1011
+ const logger = input.logger ?? getLogger();
897
1012
  const loader = input.loader ?? new PluginLoader(input.loaded.resolved, input.context);
898
1013
  await loader.load();
899
1014
  const dryRun = input.context.dryRun ?? false;
1015
+ logger.info({
1016
+ branch: input.branch,
1017
+ target: input.target ?? "preview",
1018
+ projectId: input.projectId,
1019
+ dryRun: dryRun || void 0
1020
+ }, "deploy: start");
900
1021
  print(style.header(`Holocron deploy — branch=${input.branch}${input.target ? `, target=${input.target}` : " (preview)"}${dryRun ? " (dry-run)" : ""}`));
901
1022
  if (!loader.has("deployment")) throw new Error("deployment capability is not configured — add a `deployment` provider to holocron.config.json");
902
1023
  const deploy = loader.get("deployment");
@@ -916,6 +1037,11 @@ async function runDeploy(input) {
916
1037
  ...input.target ? { target: input.target } : {}
917
1038
  }));
918
1039
  print(` ${style.success(`${record.status} — ${record.url}`)}`);
1040
+ logger.info({
1041
+ status: record.status,
1042
+ url: record.url,
1043
+ id: record.id
1044
+ }, "deploy: triggered");
919
1045
  return {
920
1046
  deployment: record,
921
1047
  status: "ok"
@@ -923,6 +1049,10 @@ async function runDeploy(input) {
923
1049
  } catch (err) {
924
1050
  const message = err instanceof Error ? err.message : String(err);
925
1051
  print(` ${style.fail(message)}`);
1052
+ logger.warn({
1053
+ branch: input.branch,
1054
+ reason: message
1055
+ }, "deploy: failed");
926
1056
  return {
927
1057
  deployment: null,
928
1058
  status: "fail",
@@ -931,94 +1061,6 @@ async function runDeploy(input) {
931
1061
  }
932
1062
  }
933
1063
  //#endregion
934
- //#region src/logger.ts
935
- /**
936
- * CLI-side wiring for `@theholocron/logger`.
937
- *
938
- * `logger` is the operational-output channel — internal state, debug
939
- * traces, errors, structured context that routes to Axiom. It runs in
940
- * parallel to `print` (user-facing UX output) and does not replace it.
941
- */
942
- /**
943
- * Resolve the explicit level to hand to `createLogger`, in priority order:
944
- *
945
- * 1. `--verbose` → `"debug"` 2. `--quiet` → `"error"`
946
- * 3. `HOLOCRON_LOG_LEVEL` env var
947
- * 4. `holocron.config` `log.level` (`configLevel`)
948
- *
949
- * Returns `undefined` when nothing applies — `createLogger` then defaults
950
- * to `"info"`. Resolving the full chain here (rather than passing
951
- * `configLevel` straight through) keeps config below the env var.
952
- */
953
- function resolveLogLevel(argv, configLevel) {
954
- if (argv.verbose) return "debug";
955
- if (argv.quiet) return "error";
956
- return parseLogLevel(env.get("HOLOCRON_LOG_LEVEL")) ?? configLevel;
957
- }
958
- let root;
959
- let rootLevel;
960
- let rootCommand;
961
- let rootAxiomKey;
962
- /**
963
- * Resolve Axiom credentials for the CLI. Env vars win — same contract as
964
- * `@theholocron/logger`'s `resolveAxiomFromEnv`. Failing that, the CLI-only
965
- * bridge pairs the OS-keyring token (`axiom.<org>` then bare `axiom`) with a
966
- * dataset from `HOLOCRON_AXIOM_DATASET` / `AXIOM_DATASET` or
967
- * `holocron.config` `log.axiom.dataset`. Returns `undefined` unless both a
968
- * token and a dataset are found.
969
- */
970
- function resolveCliAxiom(opts) {
971
- const fromEnv = resolveAxiomFromEnv();
972
- if (fromEnv) return fromEnv;
973
- const dataset = env.get("HOLOCRON_AXIOM_DATASET") || env.get("AXIOM_DATASET") || opts.configAxiomDataset;
974
- if (!dataset) return void 0;
975
- const org = opts.org ?? env.get("HOLOCRON_ORG");
976
- const token = (org ? getToken(`axiom.${org}`) : null) ?? getToken("axiom");
977
- return token ? {
978
- dataset,
979
- token
980
- } : void 0;
981
- }
982
- /**
983
- * The process-wide root logger. Built once (from `cli.ts`'s middleware,
984
- * with the command name + flags + env). Rebuilt at most once more when a
985
- * command's handler supplies `holocron.config` context the flag/env-only
986
- * first pass could not have known — `log.level` (unless `--verbose` /
987
- * `--quiet` already fixed it) or `log.axiom.dataset` + the resolved org
988
- * for a keyring-backed Axiom transport. That rebuild generates a fresh
989
- * `runId`, which is harmless: nothing logs between the middleware and the
990
- * handler.
991
- */
992
- function buildCliLogger(argv, opts = {}) {
993
- const { command, configLevel } = opts;
994
- if (command) rootCommand = command;
995
- const level = resolveLogLevel(argv, configLevel);
996
- const axiom = resolveCliAxiom(opts);
997
- const axiomKey = axiom?.dataset;
998
- const rebuildForConfig = configLevel !== void 0 && level !== rootLevel && !argv.verbose && !argv.quiet;
999
- if (!root || rebuildForConfig || axiomKey !== void 0 && axiomKey !== rootAxiomKey) {
1000
- const built = createLogger({
1001
- ...level ? { level } : {},
1002
- ...axiom ? { axiom } : {}
1003
- });
1004
- root = {
1005
- logger: rootCommand ? built.logger.child({ command: rootCommand }) : built.logger,
1006
- runId: built.runId
1007
- };
1008
- rootLevel = level;
1009
- rootAxiomKey = axiomKey;
1010
- }
1011
- return root;
1012
- }
1013
- /** Lazily-memoized `Logger` for module-level call sites with no `argv` in scope. */
1014
- function getLogger() {
1015
- return (root ??= createLogger()).logger;
1016
- }
1017
- /** The current root logger's correlation id, if a root has been built. */
1018
- function getRunId() {
1019
- return root?.runId;
1020
- }
1021
- //#endregion
1022
1064
  //#region src/commands/doctor.ts
1023
1065
  async function runDoctor(input) {
1024
1066
  const print = input.print ?? ((line) => console.log(line));
@@ -2871,8 +2913,13 @@ function describeScope(scope) {
2871
2913
  //#region src/commands/secrets-sync.ts
2872
2914
  async function runSecretsSync(input) {
2873
2915
  const print = input.print ?? ((line) => console.log(line));
2916
+ const logger = input.logger ?? getLogger();
2874
2917
  const loader = input.loader ?? new PluginLoader(input.loaded.resolved, input.context);
2875
2918
  await withSpinner("Loading plugins…", () => loader.load());
2919
+ logger.info({
2920
+ environment: input.environmentId,
2921
+ dryRun: (input.context.dryRun ?? false) || void 0
2922
+ }, "secrets sync: start");
2876
2923
  const dryRun = input.context.dryRun ?? false;
2877
2924
  const targets = input.targets ?? ["production", "preview"];
2878
2925
  print(style.header(`Holocron secrets sync — environment ${input.environmentId}${dryRun ? " (dry-run)" : ""}`));
@@ -2918,6 +2965,13 @@ async function runSecretsSync(input) {
2918
2965
  }
2919
2966
  }
2920
2967
  }
2968
+ for (const r of rows) logger[r.status === "fail" ? "warn" : "info"]({
2969
+ destination: r.destination,
2970
+ scope: r.scope,
2971
+ key: r.key,
2972
+ status: r.status,
2973
+ ...r.message ? { detail: r.message } : {}
2974
+ }, `secrets sync: ${r.key} → ${r.destination}`);
2921
2975
  const summary = rows.reduce((acc, r) => {
2922
2976
  if (r.status === "ok") acc.ok += 1;
2923
2977
  else if (r.status === "fail") acc.fail += 1;
@@ -2930,6 +2984,10 @@ async function runSecretsSync(input) {
2930
2984
  skip: 0,
2931
2985
  dryRun: 0
2932
2986
  });
2987
+ logger[summary.fail > 0 ? "warn" : "info"]({
2988
+ ...summary,
2989
+ keys: rows.length
2990
+ }, "secrets sync: done");
2933
2991
  print("");
2934
2992
  const summaryLine = `${summary.ok} ok, ${summary.fail} fail, ${summary.skip} skipped${dryRun ? `, ${summary.dryRun} would-do` : ""}`;
2935
2993
  print(summary.fail > 0 ? style.fail(summaryLine) : style.success(summaryLine));
@@ -5299,12 +5357,23 @@ const LOCAL_STEPS = /* @__PURE__ */ new Set([
5299
5357
  ]);
5300
5358
  async function runSync(input) {
5301
5359
  const print = input.print ?? ((line) => console.log(line));
5360
+ const logger = input.logger ?? getLogger();
5302
5361
  const loader = input.loader ?? new PluginLoader(input.loaded.resolved, input.context);
5303
5362
  const config = input.loaded.resolved;
5304
5363
  const dryRun = input.context.dryRun ?? false;
5305
5364
  const requestedSteps = input.steps;
5306
5365
  const steps = [];
5366
+ logger.info({
5367
+ config: config.name,
5368
+ steps: requestedSteps ?? "all",
5369
+ dryRun: dryRun || void 0
5370
+ }, "sync: start");
5307
5371
  await loader.load();
5372
+ for (const failure of loader.loadFailures()) logger.warn({
5373
+ capability: failure.key,
5374
+ provider: failure.provider,
5375
+ reason: failure.error.message
5376
+ }, `sync: plugin ${failure.provider} unavailable`);
5308
5377
  print(`Holocron sync — ${config.name}${dryRun ? " (dry-run)" : ""}`);
5309
5378
  print(` config: ${input.loaded.filepath}`);
5310
5379
  print("");
@@ -5572,6 +5641,12 @@ async function runSync(input) {
5572
5641
  print(formatSyncStep(result));
5573
5642
  }
5574
5643
  }
5644
+ for (const s of steps) logger[s.status === "fail" ? "warn" : "info"]({
5645
+ capability: s.capability,
5646
+ step: s.step,
5647
+ status: s.status,
5648
+ ...s.message ? { detail: s.message } : {}
5649
+ }, `sync: ${s.step}`);
5575
5650
  const summary = steps.reduce((acc, s) => {
5576
5651
  if (s.status === "ok") acc.ok += 1;
5577
5652
  else if (s.status === "fail") acc.fail += 1;
@@ -5584,6 +5659,7 @@ async function runSync(input) {
5584
5659
  skip: 0,
5585
5660
  dryRun: 0
5586
5661
  });
5662
+ logger[summary.fail > 0 ? "warn" : "info"]({ ...summary }, "sync: done");
5587
5663
  print("");
5588
5664
  print(` ${summary.ok} ok, ${summary.fail} fail, ${summary.skip} skipped${dryRun ? `, ${summary.dryRun} would-do` : ""}`);
5589
5665
  return {
@@ -5710,13 +5786,13 @@ var security_default = "name: Security\n\non: # yamllint disable-line rule:truth
5710
5786
  var stale_default = "name: Stale\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n days-before-stale:\n description: Days of inactivity before an issue or PR is marked stale (applies to both unless type-specific value is set)\n type: number\n required: false\n default: 30\n days-before-close:\n description: Days after stale label before closing (applies to both unless type-specific value is set)\n type: number\n required: false\n default: 5\n days-before-issue-stale:\n description: Days of inactivity before an issue is marked stale; overrides days-before-stale when set to a non-negative value\n type: number\n required: false\n default: -1\n days-before-issue-close:\n description: Days after stale label before closing an issue; overrides days-before-close when set to a non-negative value\n type: number\n required: false\n default: -1\n days-before-pr-stale:\n description: Days of inactivity before a PR is marked stale; overrides days-before-stale when set to a non-negative value\n type: number\n required: false\n default: -1\n days-before-pr-close:\n description: Days after stale label before closing a PR; overrides days-before-close when set to a non-negative value\n type: number\n required: false\n default: -1\n exempt-issue-labels:\n description: Comma-separated labels that exempt an issue from being marked stale\n type: string\n required: false\n default: \"in-progress,wip\"\n exempt-pr-labels:\n description: Comma-separated labels that exempt a PR from being marked stale\n type: string\n required: false\n default: \"\"\n exempt-all-issue-milestones:\n description: Issues assigned to any milestone are never marked stale\n type: boolean\n required: false\n default: true\n exempt-all-pr-milestones:\n description: PRs assigned to any milestone are never marked stale\n type: boolean\n required: false\n default: true\n exempt-all-issue-projects:\n description: Issues assigned to any project are never marked stale\n type: boolean\n required: false\n default: true\n exempt-all-pr-projects:\n description: PRs assigned to any project are never marked stale\n type: boolean\n required: false\n default: true\n\njobs:\n stale:\n name: Mark stale issues and pull requests\n permissions:\n contents: write\n issues: write\n pull-requests: write\n runs-on: ubuntu-latest\n timeout-minutes: 10\n steps:\n - uses: actions/stale@1e223db275d687790206a7acac4d1a11bd6fe629 # v10.4.0\n name: Run Stale\n with:\n close-issue-message: >\n This issue was closed because it has been stalled for\n ${{ inputs.days-before-close }} days with no activity.\n days-before-close: ${{ inputs.days-before-close }}\n days-before-issue-close: ${{ inputs.days-before-issue-close }}\n days-before-issue-stale: ${{ inputs.days-before-issue-stale }}\n days-before-pr-close: ${{ inputs.days-before-pr-close }}\n days-before-pr-stale: ${{ inputs.days-before-pr-stale }}\n days-before-stale: ${{ inputs.days-before-stale }}\n exempt-all-issue-milestones: ${{ inputs.exempt-all-issue-milestones }}\n exempt-all-issue-projects: ${{ inputs.exempt-all-issue-projects }}\n exempt-all-pr-milestones: ${{ inputs.exempt-all-pr-milestones }}\n exempt-all-pr-projects: ${{ inputs.exempt-all-pr-projects }}\n exempt-issue-labels: ${{ inputs.exempt-issue-labels }}\n exempt-pr-labels: ${{ inputs.exempt-pr-labels }}\n stale-issue-label: wontfix\n stale-issue-message: >\n This issue is stale because it has been open ${{ inputs.days-before-stale }}\n days with no activity. Remove the stale label or comment, or this will be\n closed in ${{ inputs.days-before-close }} days.\n stale-pr-label: wontfix\n stale-pr-message: >\n This PR is stale because it has been open ${{ inputs.days-before-stale }}\n days with no activity. Remove the stale label or comment, or this will be\n closed in ${{ inputs.days-before-close }} days.\n";
5711
5787
  //#endregion
5712
5788
  //#region src/templates/workflows/sync.yml
5713
- var sync_default = "name: Sync\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n steps:\n description: >\n Sync steps to run (default: all). Valid values:\n labels, properties, teams, topics, keywords, description, homepage, readme, workflows, wiki.\n Pass a space-separated list to run a subset, e.g. \"readme\" or \"readme wiki\".\n type: string\n required: false\n secrets:\n HOLOCRON_ADMIN_TOKEN:\n description: Fine-grained PAT with admin scopes (labels, properties, teams).\n required: false\n HOLOCRON_AXIOM_TOKEN:\n description: >\n Axiom API token. When set alongside the HOLOCRON_AXIOM_DATASET\n repo/org variable, the CLI ships this run's structured logs to Axiom.\n required: false\n HOLOCRON_DEPLOY_TOKEN:\n description: Fine-grained PAT for GitHub Pages configuration.\n required: false\n HOLOCRON_ISSUES_TOKEN:\n description: Fine-grained PAT for issue management.\n required: false\n HOLOCRON_ORG_TOKEN:\n description: Org-scoped fine-grained PAT for team sync and org properties.\n required: false\n HOLOCRON_READ_TOKEN:\n description: Fine-grained PAT for read-only GitHub API calls.\n required: false\n HOLOCRON_SYNC_TOKEN:\n required: false\n GH_TOKEN:\n description: >\n Generic GitHub token fallback for gh CLI calls. Used when\n HOLOCRON_SYNC_TOKEN is not set.\n required: false\n\njobs:\n sync:\n name: Sync repo from config\n runs-on: ubuntu-latest\n timeout-minutes: 10\n permissions:\n contents: write\n pull-requests: write\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n with:\n token: ${{ secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN || github.token }}\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - run: pnpm build\n name: Build CLI\n\n - name: Run holocron sync\n # Call the built entry directly — `pnpm exec holocron` relies on a\n # node_modules/.bin/holocron symlink that pnpm cannot create at install\n # time (dist/ does not exist yet), same as sync-github.yml.\n run: |\n if [ -n \"$STEPS\" ]; then\n # shellcheck disable=SC2086\n node packages/cli/dist/cli.mjs sync --steps $STEPS\n else\n node packages/cli/dist/cli.mjs sync\n fi\n env:\n HOLOCRON_ADMIN_TOKEN: ${{ secrets.HOLOCRON_ADMIN_TOKEN }}\n HOLOCRON_AXIOM_TOKEN: ${{ secrets.HOLOCRON_AXIOM_TOKEN }}\n HOLOCRON_AXIOM_DATASET: ${{ vars.HOLOCRON_AXIOM_DATASET }}\n HOLOCRON_DEPLOY_TOKEN: ${{ secrets.HOLOCRON_DEPLOY_TOKEN }}\n HOLOCRON_ISSUES_TOKEN: ${{ secrets.HOLOCRON_ISSUES_TOKEN }}\n HOLOCRON_ORG_TOKEN: ${{ secrets.HOLOCRON_ORG_TOKEN }}\n HOLOCRON_READ_TOKEN: ${{ secrets.HOLOCRON_READ_TOKEN }}\n HOLOCRON_SYNC_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN }}\n STEPS: ${{ inputs.steps }}\n\n - name: Format generated files\n run: pnpm exec prettier --write README.md docs/src/content/docs/index.mdx 2>/dev/null || true\n\n - uses: theholocron/.github/.github/actions/auto-commit@main\n id: auto-commit\n name: Commit sync changes\n with:\n token: ${{ secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN || github.token }}\n branch: chore/auto-sync\n commit-message: \"chore: sync from holocron.config\"\n commit-options: \"--no-verify\"\n\n - name: Open PR if changes were committed\n if: steps.auto-commit.outputs.changes-detected == 'true'\n run: |\n gh pr create \\\n --title \"chore: sync README and repo metadata\" \\\n --body \"Automated sync triggered by changes to config or package files. Merge to apply.\" \\\n --base main \\\n --head chore/auto-sync \\\n || echo \"PR already open — branch updated.\"\n env:\n GH_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN || github.token }}\n\n - name: Broadcast wiki sync if navbar changed\n if: >-\n steps.auto-commit.outputs.changes-detected == 'true' &&\n github.event_name == 'push' &&\n (inputs.steps == '' || contains(inputs.steps, 'wiki'))\n run: |\n if git diff HEAD~1 --name-only | grep -q 'fern/docs.yml'; then\n gh workflow run sync-dispatch.yml \\\n --repo theholocron/.github \\\n --field \"steps=wiki\" \\\n || echo \"skipping broadcast — insufficient permissions\"\n fi\n env:\n GH_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN || github.token }}\n";
5789
+ var sync_default = "name: Sync\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n steps:\n description: >\n Sync steps to run (default: all). Valid values:\n labels, properties, teams, topics, keywords, description, homepage, readme, workflows, wiki.\n Pass a space-separated list to run a subset, e.g. \"readme\" or \"readme wiki\".\n type: string\n required: false\n secrets:\n HOLOCRON_ADMIN_TOKEN:\n description: Fine-grained PAT with admin scopes (labels, properties, teams).\n required: false\n HOLOCRON_AXIOM_TOKEN:\n description: >\n Axiom API token. When set alongside the HOLOCRON_AXIOM_DATASET\n repo/org variable, the CLI ships this run's structured logs to Axiom.\n Falls back to the vendor-native AXIOM_TOKEN secret.\n required: false\n AXIOM_TOKEN:\n description: Vendor-native fallback for HOLOCRON_AXIOM_TOKEN.\n required: false\n HOLOCRON_DEPLOY_TOKEN:\n description: Fine-grained PAT for GitHub Pages configuration.\n required: false\n HOLOCRON_ISSUES_TOKEN:\n description: Fine-grained PAT for issue management.\n required: false\n HOLOCRON_ORG_TOKEN:\n description: Org-scoped fine-grained PAT for team sync and org properties.\n required: false\n HOLOCRON_READ_TOKEN:\n description: Fine-grained PAT for read-only GitHub API calls.\n required: false\n HOLOCRON_SYNC_TOKEN:\n required: false\n GH_TOKEN:\n description: >\n Generic GitHub token fallback for gh CLI calls. Used when\n HOLOCRON_SYNC_TOKEN is not set.\n required: false\n\njobs:\n sync:\n name: Sync repo from config\n runs-on: ubuntu-latest\n timeout-minutes: 10\n permissions:\n contents: write\n pull-requests: write\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n with:\n token: ${{ secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN || github.token }}\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - run: pnpm build\n name: Build CLI\n\n - name: Run holocron sync\n # Call the built entry directly — `pnpm exec holocron` relies on a\n # node_modules/.bin/holocron symlink that pnpm cannot create at install\n # time (dist/ does not exist yet), same as sync-github.yml.\n run: |\n if [ -n \"$STEPS\" ]; then\n # shellcheck disable=SC2086\n node packages/cli/dist/cli.mjs sync --steps $STEPS\n else\n node packages/cli/dist/cli.mjs sync\n fi\n env:\n HOLOCRON_ADMIN_TOKEN: ${{ secrets.HOLOCRON_ADMIN_TOKEN }}\n HOLOCRON_AXIOM_TOKEN: ${{ secrets.HOLOCRON_AXIOM_TOKEN || secrets.AXIOM_TOKEN }}\n HOLOCRON_AXIOM_DATASET: ${{ vars.HOLOCRON_AXIOM_DATASET || vars.AXIOM_DATASET }}\n HOLOCRON_DEPLOY_TOKEN: ${{ secrets.HOLOCRON_DEPLOY_TOKEN }}\n HOLOCRON_ISSUES_TOKEN: ${{ secrets.HOLOCRON_ISSUES_TOKEN }}\n HOLOCRON_ORG_TOKEN: ${{ secrets.HOLOCRON_ORG_TOKEN }}\n HOLOCRON_READ_TOKEN: ${{ secrets.HOLOCRON_READ_TOKEN }}\n HOLOCRON_SYNC_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN }}\n STEPS: ${{ inputs.steps }}\n\n - name: Format generated files\n run: |\n pnpm exec prettier --write README.md docs/src/content/docs/index.mdx 2>/dev/null || true\n # `holocron sync` writes package.json fields (keywords, description,\n # homepage) with a plain assignment, which appends new keys at the\n # end — re-apply the canonical order the lint config enforces.\n pnpm exec eslint --fix --no-warn-ignored package.json 2>/dev/null || true\n\n - uses: theholocron/.github/.github/actions/auto-commit@main\n id: auto-commit\n name: Commit sync changes\n with:\n token: ${{ secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN || github.token }}\n branch: chore/auto-sync\n commit-message: \"chore: sync from holocron.config\"\n commit-options: \"--no-verify\"\n\n - name: Open PR if changes were committed\n if: steps.auto-commit.outputs.changes-detected == 'true'\n run: |\n gh pr create \\\n --title \"chore: sync README and repo metadata\" \\\n --body \"Automated sync triggered by changes to config or package files. Merge to apply.\" \\\n --base main \\\n --head chore/auto-sync \\\n || echo \"PR already open — branch updated.\"\n env:\n GH_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN || github.token }}\n\n - name: Broadcast wiki sync if navbar changed\n if: >-\n steps.auto-commit.outputs.changes-detected == 'true' &&\n github.event_name == 'push' &&\n (inputs.steps == '' || contains(inputs.steps, 'wiki'))\n run: |\n if git diff HEAD~1 --name-only | grep -q 'fern/docs.yml'; then\n gh workflow run sync-dispatch.yml \\\n --repo theholocron/.github \\\n --field \"steps=wiki\" \\\n || echo \"skipping broadcast — insufficient permissions\"\n fi\n env:\n GH_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN || github.token }}\n";
5714
5790
  //#endregion
5715
5791
  //#region src/templates/workflows/sync-dispatch.yml
5716
5792
  var sync_dispatch_default = "name: Sync Dispatch\n\non: # yamllint disable-line rule:truthy\n workflow_dispatch:\n inputs:\n steps:\n description: >\n Sync steps to pass to each repo's sync.yml. Default is \"readme\"\n (only README marker blocks are updated).\n type: string\n required: false\n default: readme\n\npermissions:\n contents: read\n\njobs:\n broadcast:\n name: Broadcast sync to all repos\n runs-on: ubuntu-latest\n timeout-minutes: 15\n steps:\n - name: Dispatch sync to all repos with sync.yml\n run: |\n gh api /orgs/theholocron/repos --paginate --jq '.[].name' \\\n | while IFS= read -r repo; do\n gh api \"/repos/theholocron/$repo/contents/.github/workflows/sync.yml\" --silent 2>/dev/null || continue\n echo \"Dispatching sync to theholocron/$repo\"\n gh workflow run sync.yml \\\n --repo \"theholocron/$repo\" \\\n --field \"steps=$STEPS\" \\\n || echo \"Warning: could not dispatch to theholocron/$repo — skipping\"\n done\n env:\n GH_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN }}\n STEPS: ${{ inputs.steps }}\n";
5717
5793
  //#endregion
5718
5794
  //#region src/templates/workflows/sync-github.yml
5719
- var sync_github_default = "name: Sync workflow templates\n\n# Builds the holocron CLI from source and pushes updated workflow templates\n# and composite actions to downstream .github repos. Runs whenever the\n# template source files change on main or alpha.\n#\n# Secrets required:\n# HOLOCRON_SYNC_TOKEN — fine-grained PAT (resource owner: org) with:\n# Actions: Read and write (dispatch workflow runs via gh workflow run)\n# Contents: Read and write (git trees, blobs, refs)\n# Pull requests: Read and write (open sync PR)\n# Workflows: Read and write (write .github/workflows/*.yml)\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n primary-repo:\n description: >\n Primary .github repo — receives composite actions, reusable workflows,\n and thin-caller templates. Requires a PR (branch protection assumed).\n type: string\n required: false\n default: theholocron/.github\n secondary-repos:\n description: >\n Space-separated list of secondary repos (reusable workflows + thin\n callers only, no composite actions). Changes are delivered via pull\n request, same as the primary repo.\n type: string\n required: false\n default: \"\"\n sync-branch:\n description: Branch name used for the primary and secondary repo PRs\n type: string\n required: false\n default: chore/sync-templates\n secrets:\n HOLOCRON_SYNC_TOKEN:\n required: true\n HOLOCRON_READ_TOKEN:\n description: >\n Fine-grained PAT for read-only GitHub API calls (e.g. resolving git\n committer identity via `gh api user`). Falls back to HOLOCRON_SYNC_TOKEN.\n required: false\n GH_TOKEN:\n description: >\n Generic GitHub token fallback for `gh` CLI calls. Used when neither\n HOLOCRON_READ_TOKEN nor HOLOCRON_SYNC_TOKEN is set.\n required: false\n HOLOCRON_AXIOM_TOKEN:\n description: >\n Axiom API token. When set alongside the HOLOCRON_AXIOM_DATASET\n repo/org variable, the CLI ships this run's structured logs to Axiom.\n required: false\n TURBO_TOKEN:\n required: false\n\njobs:\n sync:\n name: Sync templates\n runs-on: ubuntu-latest\n timeout-minutes: 15\n permissions:\n contents: read\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n HOLOCRON_AXIOM_TOKEN: ${{ secrets.HOLOCRON_AXIOM_TOKEN }}\n HOLOCRON_AXIOM_DATASET: ${{ vars.HOLOCRON_AXIOM_DATASET }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - run: pnpm build\n name: Build CLI\n\n - name: Cache actionlint\n id: cache-actionlint\n uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0\n with:\n path: /tmp/actionlint\n key: actionlint-v1.7.7-linux-amd64\n\n - name: Download actionlint\n if: steps.cache-actionlint.outputs.cache-hit != 'true'\n run: |\n curl -fsSL https://github.com/rhysd/actionlint/releases/download/v1.7.7/actionlint_1.7.7_linux_amd64.tar.gz \\\n | tar -xz -C /tmp actionlint\n\n - name: Validate generated workflows\n run: |\n node packages/cli/dist/cli.mjs sync-github \\\n --repo \"$PRIMARY_REPO\" \\\n --output-dir /tmp/sync-validate\n /tmp/actionlint /tmp/sync-validate/.github/workflows/*.yml\n env:\n PRIMARY_REPO: ${{ inputs.primary-repo }}\n\n - name: Sync primary repo (PR)\n run: |\n GIT_NAME=$(gh api user --jq .name 2>/dev/null || echo \"github-actions[bot]\")\n GIT_EMAIL=$(gh api user --jq '\"\\(.id)+\\(.login)@users.noreply.github.com\"' 2>/dev/null || echo \"41898282+github-actions[bot]@users.noreply.github.com\")\n COMMIT_MSG=\"chore: sync from theholocron/holocron\"$'\\n\\n'\"Signed-off-by: $GIT_NAME <$GIT_EMAIL>\"\n node packages/cli/dist/cli.mjs sync-github \\\n --repo \"$PRIMARY_REPO\" \\\n --branch \"$SYNC_BRANCH\" \\\n --pr \\\n --message \"$COMMIT_MSG\"\n GH_TOKEN=\"$HOLOCRON_SYNC_TOKEN\" gh pr merge --auto --squash \\\n --repo \"$PRIMARY_REPO\" \"$SYNC_BRANCH\" 2>/dev/null || true\n env:\n GH_TOKEN: ${{ secrets.HOLOCRON_READ_TOKEN || secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN }}\n HOLOCRON_SYNC_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN }}\n PRIMARY_REPO: ${{ inputs.primary-repo }}\n SYNC_BRANCH: ${{ inputs.sync-branch }}\n\n - name: Sync secondary repos (PR)\n if: ${{ inputs.secondary-repos != '' }}\n run: |\n GIT_NAME=$(gh api user --jq .name 2>/dev/null || echo \"github-actions[bot]\")\n GIT_EMAIL=$(gh api user --jq '\"\\(.id)+\\(.login)@users.noreply.github.com\"' 2>/dev/null || echo \"41898282+github-actions[bot]@users.noreply.github.com\")\n COMMIT_MSG=\"chore: sync from theholocron/holocron\"$'\\n\\n'\"Signed-off-by: $GIT_NAME <$GIT_EMAIL>\"\n for repo in $SECONDARY_REPOS; do\n node packages/cli/dist/cli.mjs sync-github \\\n --repo \"$repo\" \\\n --branch \"$SYNC_BRANCH\" \\\n --pr \\\n --message \"$COMMIT_MSG\"\n GH_TOKEN=\"$HOLOCRON_SYNC_TOKEN\" gh pr merge --auto --squash \\\n --repo \"$repo\" \"$SYNC_BRANCH\" 2>/dev/null || true\n done\n env:\n GH_TOKEN: ${{ secrets.HOLOCRON_READ_TOKEN || secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN }}\n HOLOCRON_SYNC_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN }}\n SECONDARY_REPOS: ${{ inputs.secondary-repos }}\n SYNC_BRANCH: ${{ inputs.sync-branch }}\n";
5795
+ var sync_github_default = "name: Sync workflow templates\n\n# Builds the holocron CLI from source and pushes updated workflow templates\n# and composite actions to downstream .github repos. Runs whenever the\n# template source files change on main or alpha.\n#\n# Secrets required:\n# HOLOCRON_SYNC_TOKEN — fine-grained PAT (resource owner: org) with:\n# Actions: Read and write (dispatch workflow runs via gh workflow run)\n# Contents: Read and write (git trees, blobs, refs)\n# Pull requests: Read and write (open sync PR)\n# Workflows: Read and write (write .github/workflows/*.yml)\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n primary-repo:\n description: >\n Primary .github repo — receives composite actions, reusable workflows,\n and thin-caller templates. Requires a PR (branch protection assumed).\n type: string\n required: false\n default: theholocron/.github\n secondary-repos:\n description: >\n Space-separated list of secondary repos (reusable workflows + thin\n callers only, no composite actions). Changes are delivered via pull\n request, same as the primary repo.\n type: string\n required: false\n default: \"\"\n sync-branch:\n description: Branch name used for the primary and secondary repo PRs\n type: string\n required: false\n default: chore/sync-templates\n secrets:\n HOLOCRON_SYNC_TOKEN:\n required: true\n HOLOCRON_READ_TOKEN:\n description: >\n Fine-grained PAT for read-only GitHub API calls (e.g. resolving git\n committer identity via `gh api user`). Falls back to HOLOCRON_SYNC_TOKEN.\n required: false\n GH_TOKEN:\n description: >\n Generic GitHub token fallback for `gh` CLI calls. Used when neither\n HOLOCRON_READ_TOKEN nor HOLOCRON_SYNC_TOKEN is set.\n required: false\n HOLOCRON_AXIOM_TOKEN:\n description: >\n Axiom API token. When set alongside the HOLOCRON_AXIOM_DATASET\n repo/org variable, the CLI ships this run's structured logs to Axiom.\n Falls back to the vendor-native AXIOM_TOKEN secret.\n required: false\n AXIOM_TOKEN:\n description: Vendor-native fallback for HOLOCRON_AXIOM_TOKEN.\n required: false\n TURBO_TOKEN:\n required: false\n\njobs:\n sync:\n name: Sync templates\n runs-on: ubuntu-latest\n timeout-minutes: 15\n permissions:\n contents: read\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n HOLOCRON_AXIOM_TOKEN: ${{ secrets.HOLOCRON_AXIOM_TOKEN || secrets.AXIOM_TOKEN }}\n HOLOCRON_AXIOM_DATASET: ${{ vars.HOLOCRON_AXIOM_DATASET || vars.AXIOM_DATASET }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - run: pnpm build\n name: Build CLI\n\n - name: Cache actionlint\n id: cache-actionlint\n uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0\n with:\n path: /tmp/actionlint\n key: actionlint-v1.7.7-linux-amd64\n\n - name: Download actionlint\n if: steps.cache-actionlint.outputs.cache-hit != 'true'\n run: |\n curl -fsSL https://github.com/rhysd/actionlint/releases/download/v1.7.7/actionlint_1.7.7_linux_amd64.tar.gz \\\n | tar -xz -C /tmp actionlint\n\n - name: Validate generated workflows\n run: |\n node packages/cli/dist/cli.mjs sync-github \\\n --repo \"$PRIMARY_REPO\" \\\n --output-dir /tmp/sync-validate\n /tmp/actionlint /tmp/sync-validate/.github/workflows/*.yml\n env:\n PRIMARY_REPO: ${{ inputs.primary-repo }}\n\n - name: Sync primary repo (PR)\n run: |\n GIT_NAME=$(gh api user --jq .name 2>/dev/null || echo \"github-actions[bot]\")\n GIT_EMAIL=$(gh api user --jq '\"\\(.id)+\\(.login)@users.noreply.github.com\"' 2>/dev/null || echo \"41898282+github-actions[bot]@users.noreply.github.com\")\n COMMIT_MSG=\"chore: sync from theholocron/holocron\"$'\\n\\n'\"Signed-off-by: $GIT_NAME <$GIT_EMAIL>\"\n node packages/cli/dist/cli.mjs sync-github \\\n --repo \"$PRIMARY_REPO\" \\\n --branch \"$SYNC_BRANCH\" \\\n --pr \\\n --message \"$COMMIT_MSG\"\n GH_TOKEN=\"$HOLOCRON_SYNC_TOKEN\" gh pr merge --auto --squash \\\n --repo \"$PRIMARY_REPO\" \"$SYNC_BRANCH\" 2>/dev/null || true\n env:\n GH_TOKEN: ${{ secrets.HOLOCRON_READ_TOKEN || secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN }}\n HOLOCRON_SYNC_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN }}\n PRIMARY_REPO: ${{ inputs.primary-repo }}\n SYNC_BRANCH: ${{ inputs.sync-branch }}\n\n - name: Sync secondary repos (PR)\n if: ${{ inputs.secondary-repos != '' }}\n run: |\n GIT_NAME=$(gh api user --jq .name 2>/dev/null || echo \"github-actions[bot]\")\n GIT_EMAIL=$(gh api user --jq '\"\\(.id)+\\(.login)@users.noreply.github.com\"' 2>/dev/null || echo \"41898282+github-actions[bot]@users.noreply.github.com\")\n COMMIT_MSG=\"chore: sync from theholocron/holocron\"$'\\n\\n'\"Signed-off-by: $GIT_NAME <$GIT_EMAIL>\"\n for repo in $SECONDARY_REPOS; do\n node packages/cli/dist/cli.mjs sync-github \\\n --repo \"$repo\" \\\n --branch \"$SYNC_BRANCH\" \\\n --pr \\\n --message \"$COMMIT_MSG\"\n GH_TOKEN=\"$HOLOCRON_SYNC_TOKEN\" gh pr merge --auto --squash \\\n --repo \"$repo\" \"$SYNC_BRANCH\" 2>/dev/null || true\n done\n env:\n GH_TOKEN: ${{ secrets.HOLOCRON_READ_TOKEN || secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN }}\n HOLOCRON_SYNC_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN }}\n SECONDARY_REPOS: ${{ inputs.secondary-repos }}\n SYNC_BRANCH: ${{ inputs.sync-branch }}\n";
5720
5796
  //#endregion
5721
5797
  //#region src/templates/workflows/tag.yml
5722
5798
  var tag_default = "name: Tag\n\n# Release Please — fully automated tag and GitHub Release from Conventional Commits.\n# No package.json or npm publishing required. Operates in \"simple\" mode by default:\n# analyzes commits since the last tag, maintains a rolling Release PR, and creates\n# a tag + GitHub Release when that PR is merged.\n#\n# The calling repo must have two files at the root:\n# release-please-config.json — declares packages and release-type\n# .release-please-manifest.json — tracks the current version\n\non: # yamllint disable-line rule:truthy\n # Self-trigger: when this workflow lives in theholocron/.github itself,\n # push to main runs Release Please for that repo's own releases.\n push:\n branches:\n - main\n workflow_call:\n inputs:\n release-type:\n description: Release Please release type (simple, node, python, etc.)\n type: string\n required: false\n default: simple\n config-file:\n description: Path to release-please-config.json\n type: string\n required: false\n default: release-please-config.json\n manifest-file:\n description: Path to .release-please-manifest.json\n type: string\n required: false\n default: .release-please-manifest.json\n\njobs:\n tag:\n name: Tag release\n permissions:\n contents: write\n pull-requests: write\n runs-on: ubuntu-latest\n timeout-minutes: 10\n steps:\n - uses: google-github-actions/release-please-action@e4dc86ba9405554aeba3c6bb2d169500e7d3b4ee # v4.1.1\n name: Run Release Please\n with:\n release-type: ${{ inputs.release-type }}\n config-file: ${{ inputs.config-file }}\n manifest-file: ${{ inputs.manifest-file }}\n";
@@ -5821,6 +5897,7 @@ function gitBlobSha(content) {
5821
5897
  }
5822
5898
  async function runSyncGithub(input) {
5823
5899
  const print = input.print ?? ((line) => console.log(line));
5900
+ const logger = input.logger ?? getLogger();
5824
5901
  const repo = input.repo ?? DEFAULT_REPO;
5825
5902
  const { token, dryRun = false, branch, createPr = false } = input;
5826
5903
  const message = input.message ?? `chore: sync from theholocron/holocron`;
@@ -5832,6 +5909,25 @@ async function runSyncGithub(input) {
5832
5909
  print(` repo: ${repo}`);
5833
5910
  if (branch) print(` branch: ${branch}`);
5834
5911
  print("");
5912
+ logger.info({
5913
+ repo,
5914
+ branch,
5915
+ createPr: createPr || void 0,
5916
+ dryRun: dryRun || void 0
5917
+ }, "sync-github: start");
5918
+ /** Single exit point — one structured line per invocation, whatever the path. */
5919
+ const done = (r) => {
5920
+ logger[r.status === "fail" ? "warn" : "info"]({
5921
+ repo,
5922
+ status: r.status,
5923
+ created: r.created,
5924
+ updated: r.updated,
5925
+ unchanged: r.unchanged,
5926
+ ...r.message ? { reason: r.message } : {},
5927
+ ...r.prUrl ? { pr: r.prUrl } : {}
5928
+ }, "sync-github: done");
5929
+ return r;
5930
+ };
5835
5931
  if (input.outputDir) {
5836
5932
  const batch = buildBatch(repo);
5837
5933
  for (const file of batch) {
@@ -5840,12 +5936,12 @@ async function runSyncGithub(input) {
5840
5936
  writeFileSync(dest, file.content, "utf8");
5841
5937
  }
5842
5938
  print(` ${batch.length} files written to ${input.outputDir}`);
5843
- return {
5939
+ return done({
5844
5940
  status: "ok",
5845
5941
  created: batch.length,
5846
5942
  updated: 0,
5847
5943
  unchanged: 0
5848
- };
5944
+ });
5849
5945
  }
5850
5946
  let targetBranch = branch;
5851
5947
  let defaultBranch;
@@ -5855,13 +5951,13 @@ async function runSyncGithub(input) {
5855
5951
  } catch {
5856
5952
  const msg = "failed to fetch repo metadata";
5857
5953
  print(` ✗ ${msg}`);
5858
- return {
5954
+ return done({
5859
5955
  status: "fail",
5860
5956
  created: 0,
5861
5957
  updated: 0,
5862
5958
  unchanged: 0,
5863
5959
  message: msg
5864
- };
5960
+ });
5865
5961
  }
5866
5962
  const baseBranch = createPr && defaultBranch ? defaultBranch : targetBranch;
5867
5963
  let headSha;
@@ -5875,13 +5971,13 @@ async function runSyncGithub(input) {
5875
5971
  } catch (err) {
5876
5972
  const msg = err instanceof Error ? err.message : `Branch ${baseBranch} not found`;
5877
5973
  print(` ✗ ${msg}`);
5878
- return {
5974
+ return done({
5879
5975
  status: "fail",
5880
5976
  created: 0,
5881
5977
  updated: 0,
5882
5978
  unchanged: 0,
5883
5979
  message: msg
5884
- };
5980
+ });
5885
5981
  }
5886
5982
  const batch = buildBatch(repo);
5887
5983
  let created = 0;
@@ -5896,22 +5992,30 @@ async function runSyncGithub(input) {
5896
5992
  unchanged++;
5897
5993
  } else if (existingSha) {
5898
5994
  print(` ${dryRun ? "~" : "✓"} updated ${file.path}`);
5995
+ logger.debug({
5996
+ file: file.path,
5997
+ change: "updated"
5998
+ }, "sync-github: file");
5899
5999
  updated++;
5900
6000
  if (!dryRun) changedFiles.push(file);
5901
6001
  } else {
5902
6002
  print(` ${dryRun ? "~" : "✓"} created ${file.path}`);
6003
+ logger.debug({
6004
+ file: file.path,
6005
+ change: "created"
6006
+ }, "sync-github: file");
5903
6007
  created++;
5904
6008
  if (!dryRun) changedFiles.push(file);
5905
6009
  }
5906
6010
  }
5907
6011
  print("");
5908
6012
  print(` ${created} created, ${updated} updated, ${unchanged} unchanged`);
5909
- if (dryRun || changedFiles.length === 0) return {
6013
+ if (dryRun || changedFiles.length === 0) return done({
5910
6014
  status: dryRun ? "dry-run" : "ok",
5911
6015
  created,
5912
6016
  updated,
5913
6017
  unchanged
5914
- };
6018
+ });
5915
6019
  const treeEntries = [];
5916
6020
  for (const file of changedFiles) try {
5917
6021
  const blob = await client.git.createBlob(repo, file.content);
@@ -5924,13 +6028,13 @@ async function runSyncGithub(input) {
5924
6028
  } catch (err) {
5925
6029
  const msg = `failed to create blob for ${file.path}: ${err instanceof Error ? err.message : String(err)}`;
5926
6030
  print(` ✗ ${msg}`);
5927
- return {
6031
+ return done({
5928
6032
  status: "fail",
5929
6033
  created,
5930
6034
  updated,
5931
6035
  unchanged,
5932
6036
  message: msg
5933
- };
6037
+ });
5934
6038
  }
5935
6039
  let newTreeSha;
5936
6040
  try {
@@ -5938,13 +6042,13 @@ async function runSyncGithub(input) {
5938
6042
  } catch (err) {
5939
6043
  const msg = `failed to create tree: ${err instanceof Error ? err.message : String(err)}`;
5940
6044
  print(` ✗ ${msg}`);
5941
- return {
6045
+ return done({
5942
6046
  status: "fail",
5943
6047
  created,
5944
6048
  updated,
5945
6049
  unchanged,
5946
6050
  message: msg
5947
- };
6051
+ });
5948
6052
  }
5949
6053
  let newCommitSha;
5950
6054
  try {
@@ -5952,13 +6056,13 @@ async function runSyncGithub(input) {
5952
6056
  } catch (err) {
5953
6057
  const msg = `failed to create commit: ${err instanceof Error ? err.message : String(err)}`;
5954
6058
  print(` ✗ ${msg}`);
5955
- return {
6059
+ return done({
5956
6060
  status: "fail",
5957
6061
  created,
5958
6062
  updated,
5959
6063
  unchanged,
5960
6064
  message: msg
5961
- };
6065
+ });
5962
6066
  }
5963
6067
  try {
5964
6068
  if (createPr && branch) try {
@@ -5971,13 +6075,13 @@ async function runSyncGithub(input) {
5971
6075
  } catch (err) {
5972
6076
  const msg = `failed to update ref: ${err instanceof Error ? err.message : String(err)}`;
5973
6077
  print(` ✗ ${msg}`);
5974
- return {
6078
+ return done({
5975
6079
  status: "fail",
5976
6080
  created,
5977
6081
  updated,
5978
6082
  unchanged,
5979
6083
  message: msg
5980
- };
6084
+ });
5981
6085
  }
5982
6086
  let prUrl;
5983
6087
  if (branch && createPr && !dryRun) try {
@@ -5992,13 +6096,13 @@ async function runSyncGithub(input) {
5992
6096
  if (err instanceof ProviderApiError && err.status === 422 && String(err.details).includes("already exists")) print(` → PR already open for ${branch} — branch updated, ready to merge`);
5993
6097
  else print(` ⚠ PR creation failed: ${err instanceof Error ? err.message : String(err)}`);
5994
6098
  }
5995
- return {
6099
+ return done({
5996
6100
  status: "ok",
5997
6101
  created,
5998
6102
  updated,
5999
6103
  unchanged,
6000
6104
  prUrl
6001
- };
6105
+ });
6002
6106
  }
6003
6107
  //#endregion
6004
6108
  //#region src/commands/upgrade-node.ts