@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 +18 -0
- package/dist/config/index.d.mts +1 -1
- package/dist/index.d.mts +72 -10
- package/dist/index.mjs +224 -80
- package/dist/{schema-DbpiBZCP.d.mts → schema-uPSn69gZ.d.mts} +6 -4
- package/package.json +3 -3
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
|
|
package/dist/config/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as normalizeTaskEntry, n as TaskEntry, r as TasksConfig, t as TaskConfigItem } from "../schema-
|
|
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-
|
|
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.
|
|
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
|
|
170
|
+
* The GitHub status-check context a `required` task contributes to branch
|
|
171
|
+
* protection. Format: `"{workflow name} / {job name}"`.
|
|
139
172
|
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
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
|
|
391
|
-
*
|
|
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.
|
|
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 (
|
|
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
|
|
485
|
+
* The GitHub status-check context a `required` task contributes to branch
|
|
486
|
+
* protection. Format: `"{workflow name} / {job name}"`.
|
|
522
487
|
*
|
|
523
|
-
*
|
|
524
|
-
*
|
|
525
|
-
*
|
|
526
|
-
*
|
|
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 /
|
|
530
|
-
test: "Test /
|
|
531
|
-
typecheck: "Typecheck /
|
|
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
|
|
26
|
-
*
|
|
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 —
|
|
55
|
-
*
|
|
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.
|
|
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.
|
|
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.
|
|
56
|
+
"@theholocron/rollup-plugin-transform-template": "4.8.0"
|
|
57
57
|
},
|
|
58
58
|
"engines": {
|
|
59
59
|
"node": ">=22"
|