@theholocron/astromech 4.6.0 → 4.8.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/README.md CHANGED
@@ -90,8 +90,16 @@ const astromech = createAstromech({ cwd, config, orgContext: { org, domain } });
90
90
 
91
91
  astromech.thinCallers(); // Map<"<name>.yml", yaml> — one per templated, ci-enabled task
92
92
  astromech.packageScripts(); // { holocron: "holocron", lint: "holocron run lint", … }
93
+ astromech.requiredChecks(); // ["Lint / Conclusion", "codecov/patch", …] — branch-protection contexts
94
+ astromech.ci({ scope: "required" }); // CiReport — run the gating checks locally, in CI order
93
95
  ```
94
96
 
97
+ `ci()` runs every `required: true` task (else every `ci: true` task) through the
98
+ same resolution as `run()`, in `CI_ORDER`, and returns `{ status, jobs }`. A
99
+ `required` task whose local runner can't run is a failure; `local: null` tasks
100
+ (`audit` / `codeql` / `deploy`) are reported skipped. `holocron ci` sets the
101
+ process exit code from `status`.
102
+
95
103
  `thinCallers()` returns the raw `.github/workflows/*.yml` content (no
96
104
  generated-by header — the caller prefixes its own). `deploy` with
97
105
  `preview:` shorthand produces the combined push-to-Pages / PR-to-preview
@@ -101,6 +109,16 @@ per runnable task; it skips `local: false` entries and tasks with no local
101
109
  runner (`codeql`, `deploy`), and returns `{}` when `syncScripts: false` or
102
110
  there is no config.
103
111
 
112
+ ### Required checks
113
+
114
+ `requiredChecks()` derives the branch-protection required-status-check list
115
+ from the manifest: every `{ required: true }` task's check context (the
116
+ `… / Conclusion` aggregate job, from `WORKFLOW_CHECK_CONTEXTS`), ordered by
117
+ `CI_ORDER`, then `config.extraRequiredChecks` (codecov gates, the
118
+ bundle-build check, …), de-duplicated. `holocron setup` prepends `"DCO"` and
119
+ applies the list for `protection: "strict"` repos. Policy-free — manifest
120
+ only.
121
+
104
122
  `holocron run` itself does not read the config yet — that (and
105
123
  `holocron ci`) come in later phases (epic #581).
106
124
 
@@ -1,4 +1,4 @@
1
- import { i as normalizeTaskEntry, n as TaskEntry, r as TasksConfig, t as TaskConfigItem } from "../schema-DbpiBZCP.mjs";
1
+ import { i as normalizeTaskEntry, n as TaskEntry, r as TasksConfig, t as TaskConfigItem } from "../schema-uPSn69gZ.mjs";
2
2
  //#region src/config/define.d.ts
3
3
  /**
4
4
  * Typed identity helper for `astromech.config.ts`:
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { r as TasksConfig } from "./schema-DbpiBZCP.mjs";
1
+ import { r as TasksConfig } from "./schema-uPSn69gZ.mjs";
2
2
  //#region src/run.d.ts
3
3
  /**
4
4
  * `holocron run <task> [-- <passthrough>]` — run a registry task locally.
@@ -10,7 +10,8 @@ import { r as TasksConfig } from "./schema-DbpiBZCP.mjs";
10
10
  * 2. package.json has a `<task>` script → `<pm> run <task>`
11
11
  * (unless it's the `holocron run …` thin caller — that recurses)
12
12
  * 3. TASKS[task].local resolves → `<tool> <args> <org-flags> <passthrough>`
13
- * 4. known task, nothing to run → "no <task> task" (exit 0, or 1 with --required)
13
+ * 4. TASKS[task].local === null → "enforced in CI" (skip, even with --required)
14
+ * 4b. known task, nothing resolved → "no <task> task" (exit 0, or 1 with --required)
14
15
  * 5. unknown task → "unknown task" (exit 1)
15
16
  *
16
17
  * `lint` runs the resolved linter set (`config.tasks` `linters`, else
@@ -58,6 +59,8 @@ interface RunTaskInput extends RunDeps {
58
59
  required?: boolean;
59
60
  /** The `lint` task's explicit linter list from `config.tasks`, if any. */
60
61
  linters?: string[];
62
+ /** `turbo --filter=<pkg>` passthrough (monorepo). Ignored when the repo has no `turbo.json`. */
63
+ filter?: string;
61
64
  }
62
65
  interface RunTaskReport {
63
66
  status: "ok" | "fail" | "skip" | "dry-run" | "unknown";
@@ -67,6 +70,35 @@ interface RunTaskReport {
67
70
  }
68
71
  declare function runTask(input: RunTaskInput): RunTaskReport;
69
72
  //#endregion
73
+ //#region src/ci.d.ts
74
+ interface CiOptions {
75
+ /** Print the plan without running anything. */
76
+ dryRun?: boolean;
77
+ /** `turbo --filter=<pkg>` passthrough (monorepo). */
78
+ filter?: string;
79
+ /** `"required"` (default) or `"all"` (every `ci: true` task). */
80
+ scope?: "required" | "all";
81
+ }
82
+ interface CiJobReport {
83
+ task: string;
84
+ /** The branch-protection check context, or `null` for a non-gating task. */
85
+ checkContext: string | null;
86
+ status: "ok" | "fail" | "skip" | "dry-run";
87
+ command?: string;
88
+ message?: string;
89
+ }
90
+ interface CiReport {
91
+ status: "ok" | "fail";
92
+ jobs: CiJobReport[];
93
+ }
94
+ interface CiInput extends RunDeps, CiOptions {
95
+ cwd: string;
96
+ config: TasksConfig;
97
+ /** Explicit linter list for the `lint` task (the config's lint entry `linters`). */
98
+ linters?: string[];
99
+ }
100
+ declare function runCi(input: CiInput): CiReport;
101
+ //#endregion
70
102
  //#region src/super-linter.d.ts
71
103
  /**
72
104
  * `superLinterConfig()` — turn the resolved linter set into the exact
@@ -135,12 +167,13 @@ declare function lintThinCallerWith(opts: {
135
167
  declare const WORKFLOW_TEMPLATES: Record<string, string>;
136
168
  declare const KNOWN_WORKFLOWS: Set<string>;
137
169
  /**
138
- * GitHub check context name each CI workflow produces on a PR.
170
+ * The GitHub status-check context a `required` task contributes to branch
171
+ * protection. Format: `"{workflow name} / {job name}"`.
139
172
  *
140
- * The format is "{caller-workflow-name} / {reusable-job-name}". The caller
141
- * job's own `name:` field does NOT appear in the external check name — only
142
- * the calling workflow's top-level `name:` and the inner reusable-workflow
143
- * job name matter. Only workflows that gate merges are listed here.
173
+ * These name the **aggregate `Conclusion` job** (fan-in, `if: always()`), not
174
+ * an individual inner job — `test` has several conditionally-run sub-jobs, so
175
+ * `"Test / Conclusion"` is the only stable gate. Only merge-gating workflows
176
+ * are listed. `astro.requiredChecks()` reads this for every `required` task.
144
177
  */
145
178
  declare const WORKFLOW_CHECK_CONTEXTS: Partial<Record<string, string>>;
146
179
  /**
@@ -263,10 +296,18 @@ interface RunOptions {
263
296
  dryRun?: boolean;
264
297
  /** Fail (exit 1) instead of skipping when the repo has no such task. */
265
298
  required?: boolean;
299
+ /** `turbo --filter=<pkg>` passthrough (monorepo). */
300
+ filter?: string;
266
301
  }
267
302
  interface Astromech {
268
303
  /** Run one task locally. */
269
304
  run(task: string, opts?: RunOptions): RunTaskReport;
305
+ /**
306
+ * Run the merge-gating checks locally, in CI order — "will CI pass?".
307
+ * Default scope: `required: true` tasks (falls back to every `ci: true`
308
+ * task when nothing is marked required). Exit non-zero on any failure.
309
+ */
310
+ ci(opts?: CiOptions): CiReport;
270
311
  /**
271
312
  * The `.github/workflows/*.yml` thin callers for this repo's manifest —
272
313
  * `filename` → YAML content (no generated-by header; the caller adds it).
@@ -290,6 +331,13 @@ interface Astromech {
290
331
  * `linters` list, else auto-detection from the repo's config files.
291
332
  */
292
333
  superLinterConfig(): SuperLinterConfig;
334
+ /**
335
+ * The branch-protection required-status-check contexts for this repo —
336
+ * every `required: true` task's check context plus `extraRequiredChecks`,
337
+ * ordered and de-duplicated. `holocron setup` prepends `"DCO"` and applies
338
+ * the list; this method is policy-free (manifest only).
339
+ */
340
+ requiredChecks(): string[];
293
341
  }
294
342
  declare function createAstromech(options: AstromechOptions): Astromech;
295
343
  //#endregion
@@ -387,8 +435,11 @@ interface LocalRunner {
387
435
  }
388
436
  interface TaskDef {
389
437
  /**
390
- * `null` — no local equivalent (CodeQL, deploys). `holocron ci` reports
391
- * it as skipped; `holocron run` treats it as "nothing to do".
438
+ * `null` — the registry has no built-in runner (CodeQL, deploys, audit's
439
+ * server / baseline jobs). An explicit turbo task or `package.json` script
440
+ * still runs (resolution steps 1–2); with neither, `holocron run` does
441
+ * nothing and `holocron ci` skips it — never a failure, even when the task
442
+ * is `required` (a CI-only check isn't a local one).
392
443
  */
393
444
  local: LocalRunner | null;
394
445
  /** Sub-jobs, keyed by slug — `holocron run audit performance`. */
@@ -407,5 +458,16 @@ interface TaskDef {
407
458
  declare const TASKS: Record<string, TaskDef>;
408
459
  /** Every task name the registry knows. */
409
460
  declare const KNOWN_TASKS: Set<string>;
461
+ /**
462
+ * The order `holocron ci` runs tasks in — cheapest / fastest signal first, so
463
+ * an agent or a `pre-push` hook fails early. Tasks not listed here run last, in
464
+ * manifest order. (The generated thin callers carry no `needs:` — cross-workflow
465
+ * ordering lives in `theholocron/.github` — so `holocron ci` declares its own.)
466
+ */
467
+ declare const CI_ORDER: string[];
468
+ //#endregion
469
+ //#region src/required-checks.d.ts
470
+ /** Ordered (task contexts in {@link CI_ORDER}, then extras), de-duplicated. */
471
+ declare function requiredChecks(config: TasksConfig): string[];
410
472
  //#endregion
411
- export { type Astromech, type AstromechOptions, type ExecFn, KNOWN_TASKS, KNOWN_WORKFLOWS, LINTERS, LINTER_NAMES, type LinterDef, type LocalRunner, type OrgContext, type PreviewConfig, type RunLogger, type RunOptions, type RunTaskInput, type RunTaskReport, type SuperLinterConfig, TASKS, type TaskDef, WORKFLOW_CHECK_CONTEXTS, WORKFLOW_TEMPLATES, baselineSuperLinterEnv, createAstromech, deriveDeployPaths, extractPreviewConfig, generateCombinedDeployContent, generateThinCallerContent, lintThinCallerWith, normalizeWorkflowWith, resolveLinters, runTask, superLinterConfig };
473
+ export { type Astromech, type AstromechOptions, CI_ORDER, type CiJobReport, type CiOptions, type CiReport, type ExecFn, KNOWN_TASKS, KNOWN_WORKFLOWS, LINTERS, LINTER_NAMES, type LinterDef, type LocalRunner, type OrgContext, type PreviewConfig, type RunLogger, type RunOptions, type RunTaskInput, type RunTaskReport, type SuperLinterConfig, TASKS, type TaskDef, WORKFLOW_CHECK_CONTEXTS, WORKFLOW_TEMPLATES, baselineSuperLinterEnv, createAstromech, deriveDeployPaths, extractPreviewConfig, generateCombinedDeployContent, generateThinCallerContent, lintThinCallerWith, normalizeWorkflowWith, requiredChecks, resolveLinters, runCi, runTask, superLinterConfig };
package/dist/index.mjs CHANGED
@@ -45,11 +45,27 @@ const TASKS = {
45
45
  ] } },
46
46
  sync: { local: { command: "sync" } },
47
47
  wiki: { local: { command: "sync-wiki" } },
48
+ audit: { local: null },
48
49
  codeql: { local: null },
49
50
  deploy: { local: null }
50
51
  };
51
52
  /** Every task name the registry knows. */
52
53
  const KNOWN_TASKS = new Set(Object.keys(TASKS));
54
+ /**
55
+ * The order `holocron ci` runs tasks in — cheapest / fastest signal first, so
56
+ * an agent or a `pre-push` hook fails early. Tasks not listed here run last, in
57
+ * manifest order. (The generated thin callers carry no `needs:` — cross-workflow
58
+ * ordering lives in `theholocron/.github` — so `holocron ci` declares its own.)
59
+ */
60
+ const CI_ORDER = [
61
+ "typecheck",
62
+ "lint",
63
+ "test",
64
+ "build",
65
+ "audit",
66
+ "codeql",
67
+ "deploy"
68
+ ];
53
69
  //#endregion
54
70
  //#region src/linters.ts
55
71
  /**
@@ -190,7 +206,8 @@ function resolveLinters(opts) {
190
206
  * 2. package.json has a `<task>` script → `<pm> run <task>`
191
207
  * (unless it's the `holocron run …` thin caller — that recurses)
192
208
  * 3. TASKS[task].local resolves → `<tool> <args> <org-flags> <passthrough>`
193
- * 4. known task, nothing to run → "no <task> task" (exit 0, or 1 with --required)
209
+ * 4. TASKS[task].local === null → "enforced in CI" (skip, even with --required)
210
+ * 4b. known task, nothing resolved → "no <task> task" (exit 0, or 1 with --required)
194
211
  * 5. unknown task → "unknown task" (exit 1)
195
212
  *
196
213
  * `lint` runs the resolved linter set (`config.tasks` `linters`, else
@@ -242,6 +259,7 @@ function runTask(input) {
242
259
  const args = [
243
260
  "run",
244
261
  task,
262
+ ...input.filter ? [`--filter=${input.filter}`] : [],
245
263
  ...passthrough.length ? ["--", ...passthrough] : []
246
264
  ];
247
265
  return run(resolveBin(cwd, "turbo", fileExists), args);
@@ -269,7 +287,19 @@ function runTask(input) {
269
287
  ]);
270
288
  }
271
289
  }
272
- if (KNOWN_TASKS.has(task) || def?.local === null) {
290
+ if (def?.local === null) {
291
+ const msg = `no local equivalent for ${task} — enforced in CI`;
292
+ print(`· ${msg}`);
293
+ logger.debug({
294
+ task,
295
+ status: "skip"
296
+ }, `run: ${task}`);
297
+ return {
298
+ status: "skip",
299
+ message: msg
300
+ };
301
+ }
302
+ if (KNOWN_TASKS.has(task)) {
273
303
  const msg = `no ${task} task for this repo`;
274
304
  print(input.required ? `✗ ${msg} (required)` : `· ${msg}`);
275
305
  logger[input.required ? "warn" : "debug"]({
@@ -328,9 +358,11 @@ function runLintAggregate(input) {
328
358
  };
329
359
  if (resolved.some((r) => r.name === "eslint")) {
330
360
  const script = packageJsonScript(cwd, "lint", readFile, fileExists);
361
+ const filterArg = input.filter ? [`--filter=${input.filter}`] : [];
331
362
  if (turboDefinesTask(cwd, "lint", readFile, fileExists)) reports.push(runOne(resolveBin(cwd, "turbo", fileExists), [
332
363
  "run",
333
364
  "lint",
365
+ ...filterArg,
334
366
  ...pass
335
367
  ]));
336
368
  else if (script && !/^holocron run\b/.test(script.trim())) reports.push(runOne(packageManager(cwd, readFile, fileExists), [
@@ -423,74 +455,6 @@ function packageJsonScript(cwd, task, readFile, fileExists) {
423
455
  }
424
456
  }
425
457
  //#endregion
426
- //#region src/super-linter.ts
427
- /**
428
- * `superLinterConfig()` — turn the resolved linter set into the exact
429
- * super-linter `VALIDATE_*` / `FIX_*` env the CI `lint` job needs. The CLI
430
- * serializes {@link SuperLinterConfig.env} as the `super-linter-env` input
431
- * on each repo's generated `lint` thin caller; the reusable workflow
432
- * expands it verbatim. This is the CI half of "lint parity" — the local
433
- * half is the `holocron run lint` aggregate, driven by the same
434
- * {@link resolveLinters}.
435
- */
436
- /**
437
- * Resolve the super-linter env for a repo's `lint` task.
438
- *
439
- * @param opts.explicit the task's `linters` list, if any (else auto-detect)
440
- * @param opts.rootFiles repo-root filenames (from `listDir(cwd)`)
441
- * @param opts.includeFix emit `FIX_*` keys too (default `true`)
442
- */
443
- function superLinterConfig(opts) {
444
- const includeFix = opts.includeFix ?? true;
445
- const resolved = resolveLinters({
446
- explicit: opts.explicit,
447
- rootFiles: opts.rootFiles
448
- });
449
- const env = {};
450
- const configInputs = {};
451
- for (const { def } of resolved) {
452
- for (const key of def.validate) env[key] = "true";
453
- if (includeFix) for (const key of def.fix ?? []) env[key] = "true";
454
- if (def.configInput) configInputs[def.configInput] = true;
455
- }
456
- return {
457
- env,
458
- linters: resolved.map((r) => r.name),
459
- configInputs
460
- };
461
- }
462
- /**
463
- * The always-on baseline env — every `always` linter, no detection. This is
464
- * what the reusable `lint.yml`'s `super-linter-env` input defaults to, so a
465
- * repo whose thin caller has not been re-synced yet behaves exactly as before.
466
- */
467
- function baselineSuperLinterEnv() {
468
- return superLinterConfig({ rootFiles: [] }).env;
469
- }
470
- /**
471
- * The `lint` thin caller's `with:` overrides + the `# linters: …` comment,
472
- * from the resolved linter set. Shared by `createAstromech().thinCallers()`
473
- * and the CLI's `sync` / `setup` workflow writers so the three stay in step.
474
- *
475
- * @param opts.explicit the `lint` task's `linters` list, if any
476
- * @param opts.rootFiles repo-root filenames (auto-detect fallback)
477
- * @param opts.extra per-repo `with:` overrides that win over the defaults
478
- */
479
- function lintThinCallerWith(opts) {
480
- const sl = superLinterConfig({
481
- explicit: opts.explicit,
482
- rootFiles: opts.rootFiles
483
- });
484
- return {
485
- withOverrides: {
486
- "enable-auto-commit": true,
487
- "super-linter-env": JSON.stringify(sl.env),
488
- ...opts.extra ?? {}
489
- },
490
- comments: { "super-linter-env": `linters: ${sl.linters.join(", ")}` }
491
- };
492
- }
493
- //#endregion
494
458
  //#region src/thin-callers.ts
495
459
  /**
496
460
  * Workflow templates + thin-caller generation.
@@ -518,17 +482,19 @@ const WORKFLOW_TEMPLATES = {
518
482
  };
519
483
  const KNOWN_WORKFLOWS = new Set(Object.keys(WORKFLOW_TEMPLATES));
520
484
  /**
521
- * GitHub check context name each CI workflow produces on a PR.
485
+ * The GitHub status-check context a `required` task contributes to branch
486
+ * protection. Format: `"{workflow name} / {job name}"`.
522
487
  *
523
- * The format is "{caller-workflow-name} / {reusable-job-name}". The caller
524
- * job's own `name:` field does NOT appear in the external check name — only
525
- * the calling workflow's top-level `name:` and the inner reusable-workflow
526
- * job name matter. Only workflows that gate merges are listed here.
488
+ * These name the **aggregate `Conclusion` job** (fan-in, `if: always()`), not
489
+ * an individual inner job — `test` has several conditionally-run sub-jobs, so
490
+ * `"Test / Conclusion"` is the only stable gate. Only merge-gating workflows
491
+ * are listed. `astro.requiredChecks()` reads this for every `required` task.
527
492
  */
528
493
  const WORKFLOW_CHECK_CONTEXTS = {
529
- lint: "Lint / Lint entire codebase",
530
- test: "Test / Run tests and collect coverage",
531
- typecheck: "Typecheck / tsc --noEmit"
494
+ lint: "Lint / Conclusion",
495
+ test: "Test / Conclusion",
496
+ typecheck: "Typecheck / Conclusion",
497
+ audit: "audit / Conclusion"
532
498
  };
533
499
  /**
534
500
  * Generate the thin caller content for a workflow, optionally injecting or
@@ -750,6 +716,175 @@ function deriveDeployPaths(raw) {
750
716
  return paths;
751
717
  }
752
718
  //#endregion
719
+ //#region src/ci.ts
720
+ /**
721
+ * `holocron ci` / `astro.ci()` — run the merge-gating checks locally, in CI
722
+ * order, and exit non-zero on the first failure. The "will CI pass?" command a
723
+ * `pre-push` hook and the agent skills point at.
724
+ *
725
+ * Default scope: every `required: true` task. If the manifest marks nothing
726
+ * required (an un-migrated repo), fall back to every `ci: true` task. `--all`
727
+ * forces the full set. Each task runs through {@link runTask}: a `required`
728
+ * task whose local runner can't run is a failure; a `local: null` task with
729
+ * no turbo task / `package.json` script is skipped, never failed — even when
730
+ * `required` (it's a CI-only check, not a local one).
731
+ */
732
+ function runCi(input) {
733
+ const { print } = input;
734
+ const byName = /* @__PURE__ */ new Map();
735
+ for (const item of input.config?.tasks ?? []) byName.set(taskName(item), normalizeTaskEntry(item));
736
+ const gating = [...byName.values()].filter((e) => CI_ORDER.includes(e.name));
737
+ const all = gating.filter((e) => e.ci !== false);
738
+ const required = gating.filter((e) => e.required === true);
739
+ let selected = input.scope === "all" ? all : required;
740
+ const fellBack = input.scope !== "all" && selected.length === 0 && all.length > 0;
741
+ if (fellBack) selected = all;
742
+ const ordered = [...selected].sort((a, b) => CI_ORDER.indexOf(a.name) - CI_ORDER.indexOf(b.name));
743
+ print(`holocron ci — ${input.scope === "all" ? "all CI checks" : "required checks"} (${ordered.length})`);
744
+ if (fellBack) print(" (no required tasks in the manifest — running every CI task)");
745
+ print("");
746
+ const jobs = [];
747
+ for (const entry of ordered) {
748
+ const context = WORKFLOW_CHECK_CONTEXTS[entry.name] ?? null;
749
+ print(`▶ ${context ?? entry.name}`);
750
+ const r = runTask({
751
+ print: (line) => print(` ${line}`),
752
+ logger: input.logger,
753
+ exec: input.exec,
754
+ readFile: input.readFile,
755
+ fileExists: input.fileExists,
756
+ listDir: input.listDir,
757
+ lookPath: input.lookPath,
758
+ task: entry.name,
759
+ cwd: input.cwd,
760
+ dryRun: input.dryRun ?? false,
761
+ required: entry.required === true,
762
+ ...input.filter ? { filter: input.filter } : {},
763
+ ...entry.name === "lint" ? { linters: input.linters } : {}
764
+ });
765
+ const status = r.status;
766
+ jobs.push({
767
+ task: entry.name,
768
+ checkContext: context,
769
+ status,
770
+ command: r.command,
771
+ message: r.message
772
+ });
773
+ }
774
+ const failed = jobs.filter((j) => j.status === "fail").length;
775
+ const passed = jobs.filter((j) => j.status === "ok" || j.status === "dry-run").length;
776
+ const skipped = jobs.filter((j) => j.status === "skip").length;
777
+ print("");
778
+ if (ordered.length === 0) print("holocron ci — nothing to run");
779
+ else if (failed > 0) print(`✗ ${failed} failed, ${passed} passed${skipped ? `, ${skipped} skipped` : ""}`);
780
+ else print(`✓ ${passed} passed${skipped ? `, ${skipped} skipped` : ""}${input.dryRun ? " (plan only)" : ""}`);
781
+ return {
782
+ status: failed > 0 ? "fail" : "ok",
783
+ jobs
784
+ };
785
+ }
786
+ function taskName(item) {
787
+ return typeof item === "string" ? item : item.name;
788
+ }
789
+ //#endregion
790
+ //#region src/required-checks.ts
791
+ /**
792
+ * `requiredChecks(config)` — the branch-protection required-status-check list,
793
+ * derived from the task manifest. Every `required: true` task contributes its
794
+ * {@link WORKFLOW_CHECK_CONTEXTS} entry; `config.extraRequiredChecks` adds
795
+ * contexts not backed by a task (codecov, DCO is prepended by the caller).
796
+ *
797
+ * `@theholocron/cli`'s `holocron setup` calls this instead of the old
798
+ * hand-maintained `repo.requiredChecks` array. Policy-free — it only knows the
799
+ * manifest.
800
+ */
801
+ /** Ordered (task contexts in {@link CI_ORDER}, then extras), de-duplicated. */
802
+ function requiredChecks(config) {
803
+ const entries = (config.tasks ?? []).map(normalizeTaskEntry);
804
+ const seen = /* @__PURE__ */ new Set();
805
+ const out = [];
806
+ for (const name of CI_ORDER) {
807
+ const context = entries.some((e) => e.name === name && e.required === true) ? WORKFLOW_CHECK_CONTEXTS[name] : void 0;
808
+ if (context && !seen.has(context)) {
809
+ seen.add(context);
810
+ out.push(context);
811
+ }
812
+ }
813
+ for (const context of config.extraRequiredChecks ?? []) if (!seen.has(context)) {
814
+ seen.add(context);
815
+ out.push(context);
816
+ }
817
+ return out;
818
+ }
819
+ //#endregion
820
+ //#region src/super-linter.ts
821
+ /**
822
+ * `superLinterConfig()` — turn the resolved linter set into the exact
823
+ * super-linter `VALIDATE_*` / `FIX_*` env the CI `lint` job needs. The CLI
824
+ * serializes {@link SuperLinterConfig.env} as the `super-linter-env` input
825
+ * on each repo's generated `lint` thin caller; the reusable workflow
826
+ * expands it verbatim. This is the CI half of "lint parity" — the local
827
+ * half is the `holocron run lint` aggregate, driven by the same
828
+ * {@link resolveLinters}.
829
+ */
830
+ /**
831
+ * Resolve the super-linter env for a repo's `lint` task.
832
+ *
833
+ * @param opts.explicit the task's `linters` list, if any (else auto-detect)
834
+ * @param opts.rootFiles repo-root filenames (from `listDir(cwd)`)
835
+ * @param opts.includeFix emit `FIX_*` keys too (default `true`)
836
+ */
837
+ function superLinterConfig(opts) {
838
+ const includeFix = opts.includeFix ?? true;
839
+ const resolved = resolveLinters({
840
+ explicit: opts.explicit,
841
+ rootFiles: opts.rootFiles
842
+ });
843
+ const env = {};
844
+ const configInputs = {};
845
+ for (const { def } of resolved) {
846
+ for (const key of def.validate) env[key] = "true";
847
+ if (includeFix) for (const key of def.fix ?? []) env[key] = "true";
848
+ if (def.configInput) configInputs[def.configInput] = true;
849
+ }
850
+ return {
851
+ env,
852
+ linters: resolved.map((r) => r.name),
853
+ configInputs
854
+ };
855
+ }
856
+ /**
857
+ * The always-on baseline env — every `always` linter, no detection. This is
858
+ * what the reusable `lint.yml`'s `super-linter-env` input defaults to, so a
859
+ * repo whose thin caller has not been re-synced yet behaves exactly as before.
860
+ */
861
+ function baselineSuperLinterEnv() {
862
+ return superLinterConfig({ rootFiles: [] }).env;
863
+ }
864
+ /**
865
+ * The `lint` thin caller's `with:` overrides + the `# linters: …` comment,
866
+ * from the resolved linter set. Shared by `createAstromech().thinCallers()`
867
+ * and the CLI's `sync` / `setup` workflow writers so the three stay in step.
868
+ *
869
+ * @param opts.explicit the `lint` task's `linters` list, if any
870
+ * @param opts.rootFiles repo-root filenames (auto-detect fallback)
871
+ * @param opts.extra per-repo `with:` overrides that win over the defaults
872
+ */
873
+ function lintThinCallerWith(opts) {
874
+ const sl = superLinterConfig({
875
+ explicit: opts.explicit,
876
+ rootFiles: opts.rootFiles
877
+ });
878
+ return {
879
+ withOverrides: {
880
+ "enable-auto-commit": true,
881
+ "super-linter-env": JSON.stringify(sl.env),
882
+ ...opts.extra ?? {}
883
+ },
884
+ comments: { "super-linter-env": `linters: ${sl.linters.join(", ")}` }
885
+ };
886
+ }
887
+ //#endregion
753
888
  //#region src/astromech.ts
754
889
  /**
755
890
  * `createAstromech(options)` — the self-contained task runner.
@@ -800,8 +935,16 @@ function createAstromech(options) {
800
935
  passthrough: opts.passthrough ?? [],
801
936
  dryRun: opts.dryRun ?? false,
802
937
  required: opts.required ?? false,
938
+ ...opts.filter ? { filter: opts.filter } : {},
803
939
  ...task === "lint" ? { linters: lintEntry()?.linters } : {}
804
940
  }),
941
+ ci: (opts = {}) => runCi({
942
+ ...deps,
943
+ cwd: options.cwd,
944
+ config: options.config ?? {},
945
+ linters: lintEntry()?.linters,
946
+ ...opts
947
+ }),
805
948
  thinCallers: () => {
806
949
  const orgCtx = options.orgContext ?? {};
807
950
  const out = /* @__PURE__ */ new Map();
@@ -847,8 +990,9 @@ function createAstromech(options) {
847
990
  superLinterConfig: () => superLinterConfig({
848
991
  explicit: lintEntry()?.linters,
849
992
  rootFiles: rootFiles()
850
- })
993
+ }),
994
+ requiredChecks: () => requiredChecks(options.config ?? {})
851
995
  };
852
996
  }
853
997
  //#endregion
854
- export { KNOWN_TASKS, KNOWN_WORKFLOWS, LINTERS, LINTER_NAMES, TASKS, WORKFLOW_CHECK_CONTEXTS, WORKFLOW_TEMPLATES, baselineSuperLinterEnv, createAstromech, deriveDeployPaths, extractPreviewConfig, generateCombinedDeployContent, generateThinCallerContent, lintThinCallerWith, normalizeWorkflowWith, resolveLinters, runTask, superLinterConfig };
998
+ export { CI_ORDER, KNOWN_TASKS, KNOWN_WORKFLOWS, LINTERS, LINTER_NAMES, TASKS, WORKFLOW_CHECK_CONTEXTS, WORKFLOW_TEMPLATES, baselineSuperLinterEnv, createAstromech, deriveDeployPaths, extractPreviewConfig, generateCombinedDeployContent, generateThinCallerContent, lintThinCallerWith, normalizeWorkflowWith, requiredChecks, resolveLinters, runCi, runTask, superLinterConfig };
@@ -22,8 +22,9 @@ interface TaskEntry {
22
22
  */
23
23
  local?: boolean;
24
24
  /**
25
- * The task's CI check context is a required status check (branch
26
- * protection) and part of `holocron ci`'s default run.
25
+ * The task's CI check context (from `WORKFLOW_CHECK_CONTEXTS`) is a
26
+ * required status check in branch protection (`astro.requiredChecks()`)
27
+ * and part of `holocron ci`'s default run.
27
28
  */
28
29
  required?: boolean;
29
30
  /** Per-repo overrides on the same channel the reusable workflow reads. */
@@ -51,8 +52,9 @@ interface TasksConfig {
51
52
  */
52
53
  holocronScript?: string;
53
54
  /**
54
- * Required status-check contexts not backed by a task — DCO, semantic
55
- * PR title, …
55
+ * Required status-check contexts not backed by a task — codecov gates,
56
+ * a bundle-build check, … Appended to `astro.requiredChecks()` after the
57
+ * `required`-task contexts. (`DCO` is prepended by `holocron setup` itself.)
56
58
  */
57
59
  extraRequiredChecks?: string[];
58
60
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theholocron/astromech",
3
- "version": "4.6.0",
3
+ "version": "4.8.0",
4
4
  "description": "The Holocron task runner — one task manifest drives `holocron run`, `holocron ci`, the CI workflows, package.json scripts, linters, and required checks.",
5
5
  "keywords": [
6
6
  "ci",
@@ -37,7 +37,7 @@
37
37
  "dist"
38
38
  ],
39
39
  "dependencies": {
40
- "@theholocron/datapad": "4.6.0"
40
+ "@theholocron/datapad": "4.8.0"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@theholocron/eslint-config": "^8.0.0",
@@ -53,7 +53,7 @@
53
53
  "tsdown": "^0.22.14",
54
54
  "typescript": "^5.9.3",
55
55
  "vitest": "^4.1.11",
56
- "@theholocron/rollup-plugin-transform-template": "4.6.0"
56
+ "@theholocron/rollup-plugin-transform-template": "4.8.0"
57
57
  },
58
58
  "engines": {
59
59
  "node": ">=22"