@theholocron/cli 3.58.0 → 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
@@ -549,24 +637,55 @@ var PluginLoader = class {
549
637
  context;
550
638
  importer;
551
639
  registry = /* @__PURE__ */ new Map();
640
+ failures = [];
552
641
  constructor(config, context, importer = defaultImporter) {
553
642
  this.config = config;
554
643
  this.context = context;
555
644
  this.importer = importer;
556
645
  }
557
- /** Imports every configured plugin and builds the capability registry. */
646
+ /**
647
+ * Imports every configured plugin and builds the capability registry.
648
+ *
649
+ * Never throws for a single plugin's failure — a missing vendor token,
650
+ * an uninstalled package, or an unimplemented capability records a
651
+ * {@link PluginLoadFailure} and the load continues. This is the
652
+ * "soft-skip over hard-fail" contract: a command that needs a
653
+ * capability learns it is absent via `has()` / `get()` (which
654
+ * re-surfaces the original error), and orchestrators report the skip
655
+ * in their summary. Inspect {@link loadFailures} for the full list.
656
+ */
558
657
  async load() {
559
658
  const entries = Object.entries(this.config.providers);
560
659
  for (const [key, entry] of entries) {
561
660
  if (!entry) continue;
562
- if (entry.cardinality === "single") this.registry.set(key, await this.loadOne(key, entry.tuple));
661
+ if (entry.cardinality === "single") try {
662
+ this.registry.set(key, await this.loadOne(key, entry.tuple));
663
+ } catch (err) {
664
+ this.recordFailure(key, entry.tuple, err);
665
+ }
563
666
  else {
564
667
  const impls = [];
565
- for (const tuple of entry.tuples) impls.push(await this.loadOne(key, tuple));
566
- this.registry.set(key, impls);
668
+ for (const tuple of entry.tuples) try {
669
+ impls.push(await this.loadOne(key, tuple));
670
+ } catch (err) {
671
+ this.recordFailure(key, tuple, err);
672
+ }
673
+ if (impls.length > 0) this.registry.set(key, impls);
567
674
  }
568
675
  }
569
676
  }
677
+ recordFailure(key, tuple, err) {
678
+ this.failures.push({
679
+ key,
680
+ provider: tuple.provider,
681
+ packageName: tuple.packageName,
682
+ error: err instanceof Error ? err : new Error(String(err))
683
+ });
684
+ }
685
+ /** Providers that failed to load during {@link load}. Empty on a clean load. */
686
+ loadFailures() {
687
+ return this.failures;
688
+ }
570
689
  /**
571
690
  * Type-safe lookup. Single-cardinality keys return one impl;
572
691
  * many-cardinality keys return an array. `ResolvedCapability<K>`
@@ -574,7 +693,11 @@ var PluginLoader = class {
574
693
  */
575
694
  get(key) {
576
695
  const impl = this.registry.get(key);
577
- if (impl === void 0) throw new LoaderError(`capability \`${key}\` is not loaded — is it declared in holocron.config.json?`);
696
+ if (impl === void 0) {
697
+ const failure = this.failures.find((f) => f.key === key);
698
+ if (failure) throw failure.error;
699
+ throw new LoaderError(`capability \`${key}\` is not loaded — is it declared in holocron.config.json?`);
700
+ }
578
701
  return impl;
579
702
  }
580
703
  /** Whether a capability has been loaded. */
@@ -649,9 +772,14 @@ function prStateLabel(pr) {
649
772
  }
650
773
  async function runCleanupPreview(input) {
651
774
  const print = input.print ?? ((line) => console.log(line));
775
+ const logger = input.logger ?? getLogger();
652
776
  // c8 ignore next -- real PluginLoader construction is integration-level; unit tests always supply loader
653
777
  const loader = input.loader ?? new PluginLoader(input.loaded.resolved, input.context);
654
778
  await loader.load();
779
+ logger.info({
780
+ pr: input.prNumber,
781
+ project: input.project
782
+ }, "cleanup-preview: start");
655
783
  if (!loader.has("source")) throw new Error("source capability is not configured — add a source provider to holocron.config.json");
656
784
  const source = loader.get("source");
657
785
  if (!source.getPullRequest) throw new Error(`${source.providerName} source provider does not support getPullRequest`);
@@ -681,6 +809,13 @@ async function runCleanupPreview(input) {
681
809
  }
682
810
  if (deployments.length === 0) {
683
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");
684
819
  return {
685
820
  pr,
686
821
  branch,
@@ -733,6 +868,13 @@ async function runCleanupPreview(input) {
733
868
  try {
734
869
  const count = await deploy.deletePreviewDeployments(input.project, selected);
735
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");
736
878
  return {
737
879
  pr,
738
880
  branch,
@@ -743,6 +885,13 @@ async function runCleanupPreview(input) {
743
885
  } catch (err) {
744
886
  const message = err instanceof Error ? err.message : String(err);
745
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");
746
895
  return {
747
896
  pr,
748
897
  branch,
@@ -859,9 +1008,16 @@ async function runClone(input) {
859
1008
  //#region src/commands/deploy.ts
860
1009
  async function runDeploy(input) {
861
1010
  const print = input.print ?? ((line) => console.log(line));
1011
+ const logger = input.logger ?? getLogger();
862
1012
  const loader = input.loader ?? new PluginLoader(input.loaded.resolved, input.context);
863
1013
  await loader.load();
864
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");
865
1021
  print(style.header(`Holocron deploy — branch=${input.branch}${input.target ? `, target=${input.target}` : " (preview)"}${dryRun ? " (dry-run)" : ""}`));
866
1022
  if (!loader.has("deployment")) throw new Error("deployment capability is not configured — add a `deployment` provider to holocron.config.json");
867
1023
  const deploy = loader.get("deployment");
@@ -881,6 +1037,11 @@ async function runDeploy(input) {
881
1037
  ...input.target ? { target: input.target } : {}
882
1038
  }));
883
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");
884
1045
  return {
885
1046
  deployment: record,
886
1047
  status: "ok"
@@ -888,6 +1049,10 @@ async function runDeploy(input) {
888
1049
  } catch (err) {
889
1050
  const message = err instanceof Error ? err.message : String(err);
890
1051
  print(` ${style.fail(message)}`);
1052
+ logger.warn({
1053
+ branch: input.branch,
1054
+ reason: message
1055
+ }, "deploy: failed");
891
1056
  return {
892
1057
  deployment: null,
893
1058
  status: "fail",
@@ -896,94 +1061,6 @@ async function runDeploy(input) {
896
1061
  }
897
1062
  }
898
1063
  //#endregion
899
- //#region src/logger.ts
900
- /**
901
- * CLI-side wiring for `@theholocron/logger`.
902
- *
903
- * `logger` is the operational-output channel — internal state, debug
904
- * traces, errors, structured context that routes to Axiom. It runs in
905
- * parallel to `print` (user-facing UX output) and does not replace it.
906
- */
907
- /**
908
- * Resolve the explicit level to hand to `createLogger`, in priority order:
909
- *
910
- * 1. `--verbose` → `"debug"` 2. `--quiet` → `"error"`
911
- * 3. `HOLOCRON_LOG_LEVEL` env var
912
- * 4. `holocron.config` `log.level` (`configLevel`)
913
- *
914
- * Returns `undefined` when nothing applies — `createLogger` then defaults
915
- * to `"info"`. Resolving the full chain here (rather than passing
916
- * `configLevel` straight through) keeps config below the env var.
917
- */
918
- function resolveLogLevel(argv, configLevel) {
919
- if (argv.verbose) return "debug";
920
- if (argv.quiet) return "error";
921
- return parseLogLevel(env.get("HOLOCRON_LOG_LEVEL")) ?? configLevel;
922
- }
923
- let root;
924
- let rootLevel;
925
- let rootCommand;
926
- let rootAxiomKey;
927
- /**
928
- * Resolve Axiom credentials for the CLI. Env vars win — same contract as
929
- * `@theholocron/logger`'s `resolveAxiomFromEnv`. Failing that, the CLI-only
930
- * bridge pairs the OS-keyring token (`axiom.<org>` then bare `axiom`) with a
931
- * dataset from `HOLOCRON_AXIOM_DATASET` / `AXIOM_DATASET` or
932
- * `holocron.config` `log.axiom.dataset`. Returns `undefined` unless both a
933
- * token and a dataset are found.
934
- */
935
- function resolveCliAxiom(opts) {
936
- const fromEnv = resolveAxiomFromEnv();
937
- if (fromEnv) return fromEnv;
938
- const dataset = env.get("HOLOCRON_AXIOM_DATASET") || env.get("AXIOM_DATASET") || opts.configAxiomDataset;
939
- if (!dataset) return void 0;
940
- const org = opts.org ?? env.get("HOLOCRON_ORG");
941
- const token = (org ? getToken(`axiom.${org}`) : null) ?? getToken("axiom");
942
- return token ? {
943
- dataset,
944
- token
945
- } : void 0;
946
- }
947
- /**
948
- * The process-wide root logger. Built once (from `cli.ts`'s middleware,
949
- * with the command name + flags + env). Rebuilt at most once more when a
950
- * command's handler supplies `holocron.config` context the flag/env-only
951
- * first pass could not have known — `log.level` (unless `--verbose` /
952
- * `--quiet` already fixed it) or `log.axiom.dataset` + the resolved org
953
- * for a keyring-backed Axiom transport. That rebuild generates a fresh
954
- * `runId`, which is harmless: nothing logs between the middleware and the
955
- * handler.
956
- */
957
- function buildCliLogger(argv, opts = {}) {
958
- const { command, configLevel } = opts;
959
- if (command) rootCommand = command;
960
- const level = resolveLogLevel(argv, configLevel);
961
- const axiom = resolveCliAxiom(opts);
962
- const axiomKey = axiom?.dataset;
963
- const rebuildForConfig = configLevel !== void 0 && level !== rootLevel && !argv.verbose && !argv.quiet;
964
- if (!root || rebuildForConfig || axiomKey !== void 0 && axiomKey !== rootAxiomKey) {
965
- const built = createLogger({
966
- ...level ? { level } : {},
967
- ...axiom ? { axiom } : {}
968
- });
969
- root = {
970
- logger: rootCommand ? built.logger.child({ command: rootCommand }) : built.logger,
971
- runId: built.runId
972
- };
973
- rootLevel = level;
974
- rootAxiomKey = axiomKey;
975
- }
976
- return root;
977
- }
978
- /** Lazily-memoized `Logger` for module-level call sites with no `argv` in scope. */
979
- function getLogger() {
980
- return (root ??= createLogger()).logger;
981
- }
982
- /** The current root logger's correlation id, if a root has been built. */
983
- function getRunId() {
984
- return root?.runId;
985
- }
986
- //#endregion
987
1064
  //#region src/commands/doctor.ts
988
1065
  async function runDoctor(input) {
989
1066
  const print = input.print ?? ((line) => console.log(line));
@@ -994,6 +1071,12 @@ async function runDoctor(input) {
994
1071
  print(style.header(`Holocron doctor — ${config.name}`));
995
1072
  print(style.dim(` config: ${input.loaded.filepath}`));
996
1073
  print("");
1074
+ for (const failure of loader.loadFailures()) rows.push({
1075
+ capability: failure.key,
1076
+ provider: failure.provider,
1077
+ status: "fail",
1078
+ message: failure.error.message
1079
+ });
997
1080
  for (const key of loader.loadedKeys()) {
998
1081
  const cardinality = CARDINALITY[key];
999
1082
  const entry = config.providers[key];
@@ -2830,8 +2913,13 @@ function describeScope(scope) {
2830
2913
  //#region src/commands/secrets-sync.ts
2831
2914
  async function runSecretsSync(input) {
2832
2915
  const print = input.print ?? ((line) => console.log(line));
2916
+ const logger = input.logger ?? getLogger();
2833
2917
  const loader = input.loader ?? new PluginLoader(input.loaded.resolved, input.context);
2834
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");
2835
2923
  const dryRun = input.context.dryRun ?? false;
2836
2924
  const targets = input.targets ?? ["production", "preview"];
2837
2925
  print(style.header(`Holocron secrets sync — environment ${input.environmentId}${dryRun ? " (dry-run)" : ""}`));
@@ -2877,6 +2965,13 @@ async function runSecretsSync(input) {
2877
2965
  }
2878
2966
  }
2879
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}`);
2880
2975
  const summary = rows.reduce((acc, r) => {
2881
2976
  if (r.status === "ok") acc.ok += 1;
2882
2977
  else if (r.status === "fail") acc.fail += 1;
@@ -2889,6 +2984,10 @@ async function runSecretsSync(input) {
2889
2984
  skip: 0,
2890
2985
  dryRun: 0
2891
2986
  });
2987
+ logger[summary.fail > 0 ? "warn" : "info"]({
2988
+ ...summary,
2989
+ keys: rows.length
2990
+ }, "secrets sync: done");
2892
2991
  print("");
2893
2992
  const summaryLine = `${summary.ok} ok, ${summary.fail} fail, ${summary.skip} skipped${dryRun ? `, ${summary.dryRun} would-do` : ""}`;
2894
2993
  print(summary.fail > 0 ? style.fail(summaryLine) : style.success(summaryLine));
@@ -5258,17 +5357,23 @@ const LOCAL_STEPS = /* @__PURE__ */ new Set([
5258
5357
  ]);
5259
5358
  async function runSync(input) {
5260
5359
  const print = input.print ?? ((line) => console.log(line));
5360
+ const logger = input.logger ?? getLogger();
5261
5361
  const loader = input.loader ?? new PluginLoader(input.loaded.resolved, input.context);
5262
5362
  const config = input.loaded.resolved;
5263
5363
  const dryRun = input.context.dryRun ?? false;
5264
5364
  const requestedSteps = input.steps;
5265
5365
  const steps = [];
5266
- if (!requestedSteps || requestedSteps.some((s) => !LOCAL_STEPS.has(s))) await loader.load();
5267
- else try {
5268
- await loader.load();
5269
- } catch (err) {
5270
- if (!(err instanceof AuthError) && !(err instanceof LoaderError)) throw err;
5271
- }
5366
+ logger.info({
5367
+ config: config.name,
5368
+ steps: requestedSteps ?? "all",
5369
+ dryRun: dryRun || void 0
5370
+ }, "sync: start");
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`);
5272
5377
  print(`Holocron sync — ${config.name}${dryRun ? " (dry-run)" : ""}`);
5273
5378
  print(` config: ${input.loaded.filepath}`);
5274
5379
  print("");
@@ -5536,6 +5641,12 @@ async function runSync(input) {
5536
5641
  print(formatSyncStep(result));
5537
5642
  }
5538
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}`);
5539
5650
  const summary = steps.reduce((acc, s) => {
5540
5651
  if (s.status === "ok") acc.ok += 1;
5541
5652
  else if (s.status === "fail") acc.fail += 1;
@@ -5548,6 +5659,7 @@ async function runSync(input) {
5548
5659
  skip: 0,
5549
5660
  dryRun: 0
5550
5661
  });
5662
+ logger[summary.fail > 0 ? "warn" : "info"]({ ...summary }, "sync: done");
5551
5663
  print("");
5552
5664
  print(` ${summary.ok} ok, ${summary.fail} fail, ${summary.skip} skipped${dryRun ? `, ${summary.dryRun} would-do` : ""}`);
5553
5665
  return {
@@ -5674,13 +5786,13 @@ var security_default = "name: Security\n\non: # yamllint disable-line rule:truth
5674
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";
5675
5787
  //#endregion
5676
5788
  //#region src/templates/workflows/sync.yml
5677
- 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 run: |\n if [ -n \"$STEPS\" ]; then\n # shellcheck disable=SC2086\n pnpm --workspace-root exec holocron sync --steps $STEPS\n else\n pnpm --workspace-root exec holocron 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";
5678
5790
  //#endregion
5679
5791
  //#region src/templates/workflows/sync-dispatch.yml
5680
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";
5681
5793
  //#endregion
5682
5794
  //#region src/templates/workflows/sync-github.yml
5683
- 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";
5684
5796
  //#endregion
5685
5797
  //#region src/templates/workflows/tag.yml
5686
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";
@@ -5785,6 +5897,7 @@ function gitBlobSha(content) {
5785
5897
  }
5786
5898
  async function runSyncGithub(input) {
5787
5899
  const print = input.print ?? ((line) => console.log(line));
5900
+ const logger = input.logger ?? getLogger();
5788
5901
  const repo = input.repo ?? DEFAULT_REPO;
5789
5902
  const { token, dryRun = false, branch, createPr = false } = input;
5790
5903
  const message = input.message ?? `chore: sync from theholocron/holocron`;
@@ -5796,6 +5909,25 @@ async function runSyncGithub(input) {
5796
5909
  print(` repo: ${repo}`);
5797
5910
  if (branch) print(` branch: ${branch}`);
5798
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
+ };
5799
5931
  if (input.outputDir) {
5800
5932
  const batch = buildBatch(repo);
5801
5933
  for (const file of batch) {
@@ -5804,12 +5936,12 @@ async function runSyncGithub(input) {
5804
5936
  writeFileSync(dest, file.content, "utf8");
5805
5937
  }
5806
5938
  print(` ${batch.length} files written to ${input.outputDir}`);
5807
- return {
5939
+ return done({
5808
5940
  status: "ok",
5809
5941
  created: batch.length,
5810
5942
  updated: 0,
5811
5943
  unchanged: 0
5812
- };
5944
+ });
5813
5945
  }
5814
5946
  let targetBranch = branch;
5815
5947
  let defaultBranch;
@@ -5819,13 +5951,13 @@ async function runSyncGithub(input) {
5819
5951
  } catch {
5820
5952
  const msg = "failed to fetch repo metadata";
5821
5953
  print(` ✗ ${msg}`);
5822
- return {
5954
+ return done({
5823
5955
  status: "fail",
5824
5956
  created: 0,
5825
5957
  updated: 0,
5826
5958
  unchanged: 0,
5827
5959
  message: msg
5828
- };
5960
+ });
5829
5961
  }
5830
5962
  const baseBranch = createPr && defaultBranch ? defaultBranch : targetBranch;
5831
5963
  let headSha;
@@ -5839,13 +5971,13 @@ async function runSyncGithub(input) {
5839
5971
  } catch (err) {
5840
5972
  const msg = err instanceof Error ? err.message : `Branch ${baseBranch} not found`;
5841
5973
  print(` ✗ ${msg}`);
5842
- return {
5974
+ return done({
5843
5975
  status: "fail",
5844
5976
  created: 0,
5845
5977
  updated: 0,
5846
5978
  unchanged: 0,
5847
5979
  message: msg
5848
- };
5980
+ });
5849
5981
  }
5850
5982
  const batch = buildBatch(repo);
5851
5983
  let created = 0;
@@ -5860,22 +5992,30 @@ async function runSyncGithub(input) {
5860
5992
  unchanged++;
5861
5993
  } else if (existingSha) {
5862
5994
  print(` ${dryRun ? "~" : "✓"} updated ${file.path}`);
5995
+ logger.debug({
5996
+ file: file.path,
5997
+ change: "updated"
5998
+ }, "sync-github: file");
5863
5999
  updated++;
5864
6000
  if (!dryRun) changedFiles.push(file);
5865
6001
  } else {
5866
6002
  print(` ${dryRun ? "~" : "✓"} created ${file.path}`);
6003
+ logger.debug({
6004
+ file: file.path,
6005
+ change: "created"
6006
+ }, "sync-github: file");
5867
6007
  created++;
5868
6008
  if (!dryRun) changedFiles.push(file);
5869
6009
  }
5870
6010
  }
5871
6011
  print("");
5872
6012
  print(` ${created} created, ${updated} updated, ${unchanged} unchanged`);
5873
- if (dryRun || changedFiles.length === 0) return {
6013
+ if (dryRun || changedFiles.length === 0) return done({
5874
6014
  status: dryRun ? "dry-run" : "ok",
5875
6015
  created,
5876
6016
  updated,
5877
6017
  unchanged
5878
- };
6018
+ });
5879
6019
  const treeEntries = [];
5880
6020
  for (const file of changedFiles) try {
5881
6021
  const blob = await client.git.createBlob(repo, file.content);
@@ -5888,13 +6028,13 @@ async function runSyncGithub(input) {
5888
6028
  } catch (err) {
5889
6029
  const msg = `failed to create blob for ${file.path}: ${err instanceof Error ? err.message : String(err)}`;
5890
6030
  print(` ✗ ${msg}`);
5891
- return {
6031
+ return done({
5892
6032
  status: "fail",
5893
6033
  created,
5894
6034
  updated,
5895
6035
  unchanged,
5896
6036
  message: msg
5897
- };
6037
+ });
5898
6038
  }
5899
6039
  let newTreeSha;
5900
6040
  try {
@@ -5902,13 +6042,13 @@ async function runSyncGithub(input) {
5902
6042
  } catch (err) {
5903
6043
  const msg = `failed to create tree: ${err instanceof Error ? err.message : String(err)}`;
5904
6044
  print(` ✗ ${msg}`);
5905
- return {
6045
+ return done({
5906
6046
  status: "fail",
5907
6047
  created,
5908
6048
  updated,
5909
6049
  unchanged,
5910
6050
  message: msg
5911
- };
6051
+ });
5912
6052
  }
5913
6053
  let newCommitSha;
5914
6054
  try {
@@ -5916,13 +6056,13 @@ async function runSyncGithub(input) {
5916
6056
  } catch (err) {
5917
6057
  const msg = `failed to create commit: ${err instanceof Error ? err.message : String(err)}`;
5918
6058
  print(` ✗ ${msg}`);
5919
- return {
6059
+ return done({
5920
6060
  status: "fail",
5921
6061
  created,
5922
6062
  updated,
5923
6063
  unchanged,
5924
6064
  message: msg
5925
- };
6065
+ });
5926
6066
  }
5927
6067
  try {
5928
6068
  if (createPr && branch) try {
@@ -5935,13 +6075,13 @@ async function runSyncGithub(input) {
5935
6075
  } catch (err) {
5936
6076
  const msg = `failed to update ref: ${err instanceof Error ? err.message : String(err)}`;
5937
6077
  print(` ✗ ${msg}`);
5938
- return {
6078
+ return done({
5939
6079
  status: "fail",
5940
6080
  created,
5941
6081
  updated,
5942
6082
  unchanged,
5943
6083
  message: msg
5944
- };
6084
+ });
5945
6085
  }
5946
6086
  let prUrl;
5947
6087
  if (branch && createPr && !dryRun) try {
@@ -5956,13 +6096,13 @@ async function runSyncGithub(input) {
5956
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`);
5957
6097
  else print(` ⚠ PR creation failed: ${err instanceof Error ? err.message : String(err)}`);
5958
6098
  }
5959
- return {
6099
+ return done({
5960
6100
  status: "ok",
5961
6101
  created,
5962
6102
  updated,
5963
6103
  unchanged,
5964
6104
  prUrl
5965
- };
6105
+ });
5966
6106
  }
5967
6107
  //#endregion
5968
6108
  //#region src/commands/upgrade-node.ts