@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 +260 -120
- package/dist/cli.mjs.map +1 -1
- package/package.json +3 -3
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
|
-
/**
|
|
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")
|
|
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)
|
|
566
|
-
|
|
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)
|
|
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
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
|
|
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
|
|
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
|