@theholocron/astromech 5.0.0-alpha.147 → 5.0.0-alpha.148
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/config/index.d.mts +24 -8
- package/dist/config/index.mjs +1 -1
- package/dist/index.d.mts +140 -201
- package/dist/index.mjs +295 -239
- package/dist/{load-ytspboBj.mjs → load-BqpxDjMJ.mjs} +27 -14
- package/dist/{schema-BDld3xq1.d.mts → schema.type-BUiSrwS5.d.mts} +2 -12
- package/package.json +6 -4
package/dist/config/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { n as TaskEntry, r as TasksConfig, t as TaskConfigItem } from "../schema.type-BUiSrwS5.mjs";
|
|
2
2
|
//#region src/config/define.d.ts
|
|
3
3
|
/**
|
|
4
4
|
* Typed identity helper for `astromech.config.ts`:
|
|
@@ -14,13 +14,6 @@ import { i as normalizeTaskEntry, n as TaskEntry, r as TasksConfig, t as TaskCon
|
|
|
14
14
|
declare const defineConfig: <C extends TasksConfig>(config: C) => C;
|
|
15
15
|
//#endregion
|
|
16
16
|
//#region src/config/load.d.ts
|
|
17
|
-
/**
|
|
18
|
-
* The `tasks` value in each source. A dedicated `astromech.config.*`
|
|
19
|
-
* default-exports the full {@link TasksConfig} object; the `tasks` key of
|
|
20
|
-
* `holocron.config.*` is the bare item array (it mirrors the old
|
|
21
|
-
* `workflows` key). Either form is accepted from either source.
|
|
22
|
-
*/
|
|
23
|
-
type TasksInput = TasksConfig | TaskConfigItem[];
|
|
24
17
|
/**
|
|
25
18
|
* Resolve the task manifest for a repo: the `tasks` key of
|
|
26
19
|
* `holocron.config.*`, then a dedicated `astromech.config.*` merged on
|
|
@@ -32,6 +25,17 @@ type TasksInput = TasksConfig | TaskConfigItem[];
|
|
|
32
25
|
* boundary; see `@theholocron/datapad`'s own doc comment).
|
|
33
26
|
*/
|
|
34
27
|
declare function loadTasksConfig(cwd: string): Promise<TasksConfig>;
|
|
28
|
+
//#endregion
|
|
29
|
+
//#region src/config/load.type.d.ts
|
|
30
|
+
/**
|
|
31
|
+
* The `tasks` value in each source. A dedicated `astromech.config.*`
|
|
32
|
+
* default-exports the full {@link TasksConfig} object; the `tasks` key of
|
|
33
|
+
* `holocron.config.*` is the bare item array (it mirrors the old
|
|
34
|
+
* `workflows` key). Either form is accepted from either source.
|
|
35
|
+
*/
|
|
36
|
+
type TasksInput = TasksConfig | TaskConfigItem[];
|
|
37
|
+
//#endregion
|
|
38
|
+
//#region src/config/merge-tasks-layers.d.ts
|
|
35
39
|
/**
|
|
36
40
|
* The merge step of {@link loadTasksConfig}, for callers that load the two
|
|
37
41
|
* sources some other way (Sentinel reads them from the GitHub API, not
|
|
@@ -41,4 +45,16 @@ declare function loadTasksConfig(cwd: string): Promise<TasksConfig>;
|
|
|
41
45
|
*/
|
|
42
46
|
declare function mergeTasksLayers(parentTasks: TasksInput | undefined, dedicated: TasksInput | undefined): TasksConfig;
|
|
43
47
|
//#endregion
|
|
48
|
+
//#region src/config/schema.d.ts
|
|
49
|
+
/**
|
|
50
|
+
* The `tasks` config shape — the single manifest of what a repo runs.
|
|
51
|
+
* Read from a dedicated `astromech.config.*` file or the `tasks` key of
|
|
52
|
+
* `holocron.config.*` (see {@link loadTasksConfig}).
|
|
53
|
+
*
|
|
54
|
+
* Runtime validation is the loader's job, not this type's — the type only
|
|
55
|
+
* describes what an author writes.
|
|
56
|
+
*/
|
|
57
|
+
/** Normalise a `TaskConfigItem` to a full {@link TaskEntry} with defaults applied. */
|
|
58
|
+
declare function normalizeTaskEntry(item: TaskConfigItem): Required<Pick<TaskEntry, "name" | "ci" | "local">> & TaskEntry;
|
|
59
|
+
//#endregion
|
|
44
60
|
export { type TaskConfigItem, type TaskEntry, type TasksConfig, defineConfig, loadTasksConfig, mergeTasksLayers, normalizeTaskEntry };
|
package/dist/config/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as mergeTasksLayers, r as normalizeTaskEntry, t as loadTasksConfig } from "../load-
|
|
1
|
+
import { n as mergeTasksLayers, r as normalizeTaskEntry, t as loadTasksConfig } from "../load-BqpxDjMJ.mjs";
|
|
2
2
|
import { createDefineConfig } from "@theholocron/datapad";
|
|
3
3
|
//#region src/config/define.ts
|
|
4
4
|
/**
|
package/dist/index.d.mts
CHANGED
|
@@ -1,55 +1,5 @@
|
|
|
1
|
-
import { r as TasksConfig } from "./schema-
|
|
2
|
-
//#region src/generators/github-action-thin-callers.d.ts
|
|
3
|
-
/**
|
|
4
|
-
* Workflow templates + thin-caller generation.
|
|
5
|
-
*
|
|
6
|
-
* `WORKFLOW_TEMPLATES` holds each reusable `.github/workflows/<name>.yml`
|
|
7
|
-
* (synced to `theholocron/.github`); `generateThinCallerContent` wraps one
|
|
8
|
-
* into the thin caller `holocron repo setup` / `holocron repo sync` write locally.
|
|
9
|
-
*/
|
|
10
|
-
declare const WORKFLOW_TEMPLATES: Record<string, string>;
|
|
11
|
-
declare const KNOWN_WORKFLOWS: Set<string>;
|
|
12
|
-
/**
|
|
13
|
-
* The GitHub status-check context a `required` task contributes to branch
|
|
14
|
-
* protection. Format: `"{workflow name} / {job name}"`.
|
|
15
|
-
*
|
|
16
|
-
* Most of these are single-job workflows now (D3's decomposition split what
|
|
17
|
-
* used to be multi-job `lint`/`audit` into one task per concern) — the
|
|
18
|
-
* context names that job directly, no `Conclusion` aggregator needed; a
|
|
19
|
-
* single job's own conclusion already *is* the workflow's conclusion.
|
|
20
|
-
* `Conclusion` fan-in jobs are kept only where a task genuinely has several
|
|
21
|
-
* conditionally-run jobs feeding one required check:
|
|
22
|
-
* `verification.unitTests` (unit / Storybook / Chromatic / interaction /
|
|
23
|
-
* user-flow, each gated by its own `run-*` input) and
|
|
24
|
-
* `platform.repoValidation` (its three script jobs all must pass). Only
|
|
25
|
-
* merge-gating workflows are listed. `astromech.requiredChecks()` reads this
|
|
26
|
-
* for every `required` task.
|
|
27
|
-
*/
|
|
28
|
-
declare const WORKFLOW_CHECK_CONTEXTS: Partial<Record<string, string>>;
|
|
29
|
-
/**
|
|
30
|
-
* Generate the thin caller content for a workflow, optionally injecting or
|
|
31
|
-
* merging `with:` overrides into the jobs block.
|
|
32
|
-
*
|
|
33
|
-
* Two strategies are used depending on the template:
|
|
34
|
-
* - Templates that already have a `with:` block (e.g. lint):
|
|
35
|
-
* the override entries are merged in, replacing existing keys and appending
|
|
36
|
-
* new ones.
|
|
37
|
-
* - Templates that end with ` secrets: inherit`: a new `with:` block is
|
|
38
|
-
* injected immediately before `secrets: inherit`.
|
|
39
|
-
* If neither pattern matches the template, a warning is emitted and the
|
|
40
|
-
* base template is returned unchanged.
|
|
41
|
-
*/
|
|
42
|
-
declare function generateThinCallerContent(name: string, withOverrides?: Record<string, unknown>, additionalPaths?: string[],
|
|
43
|
-
/** Optional sink for the "could not inject `with:`" warning. */
|
|
44
|
-
logger?: {
|
|
45
|
-
warn(obj: Record<string, unknown>, msg: string): void;
|
|
46
|
-
},
|
|
47
|
-
/**
|
|
48
|
-
* `with:` key → a `# comment` line rendered immediately above that entry.
|
|
49
|
-
* Only honored when injecting a fresh `with:` block (no template today
|
|
50
|
-
* ships one), which is the path `lint` takes.
|
|
51
|
-
*/
|
|
52
|
-
comments?: Record<string, string>): string;
|
|
1
|
+
import { r as TasksConfig } from "./schema.type-BUiSrwS5.mjs";
|
|
2
|
+
//#region src/generators/github-action-thin-callers.type.d.ts
|
|
53
3
|
interface PreviewConfig {
|
|
54
4
|
/** Cloudflare Pages project name shared across all repos for previews. */
|
|
55
5
|
project: string;
|
|
@@ -72,46 +22,8 @@ interface OrgContext {
|
|
|
72
22
|
/** Project name — injected as the `name` input to the deploy reusable when `docs: true`. */
|
|
73
23
|
repoName?: string;
|
|
74
24
|
}
|
|
75
|
-
/**
|
|
76
|
-
* Extract the Cloudflare Pages preview config from a deploy workflow's `with:` object.
|
|
77
|
-
*
|
|
78
|
-
* Accepts three forms:
|
|
79
|
-
* - `preview: true` — derive both project and domain from org context
|
|
80
|
-
* - `preview: { project: "..." }` — explicit project; domain derived from context if omitted
|
|
81
|
-
* - `preview: { project: "...", domain: "..." }` — fully explicit
|
|
82
|
-
*
|
|
83
|
-
* Returns null when `preview:` is absent, false, or can't be resolved.
|
|
84
|
-
*/
|
|
85
|
-
declare function extractPreviewConfig(raw: Record<string, unknown>, ctx?: OrgContext): PreviewConfig | null;
|
|
86
|
-
/**
|
|
87
|
-
* Generate the full thin-caller YAML for a `deploy.yml` that handles both
|
|
88
|
-
* production (push to main → GitHub Pages) and preview (pull_request →
|
|
89
|
-
* Cloudflare Pages) in a single file.
|
|
90
|
-
*
|
|
91
|
-
* Both jobs receive the same docs/storybook `with:` inputs. If the per-repo
|
|
92
|
-
* config supplies `cloudflare-project` it is forwarded; otherwise the reusable
|
|
93
|
-
* falls back to the `CLOUDFLARE_PAGES_PROJECT` org variable — set that once and
|
|
94
|
-
* all repos with a `deploy` workflow get previews without per-repo config.
|
|
95
|
-
*/
|
|
96
|
-
declare function generateCombinedDeployContent(deployWith: Record<string, unknown>, paths: string[], preview: Pick<PreviewConfig, "project">): string;
|
|
97
|
-
/**
|
|
98
|
-
* Expand structured with-values to flat GitHub Actions inputs before
|
|
99
|
-
* generating the thin caller. Handles:
|
|
100
|
-
* - deploy shorthand: docs/storybook → type + storybook-projects
|
|
101
|
-
* - preview: stripped (handled separately via extractPreviewConfig)
|
|
102
|
-
* - run-chromatic object → run-chromatic: true + chromatic-projects
|
|
103
|
-
* - plain arrays → JSON-stringified for YAML scalar quoting
|
|
104
|
-
*
|
|
105
|
-
* Used by both `holocron repo setup` and `sync-workflow-templates`.
|
|
106
|
-
*/
|
|
107
|
-
declare function normalizeWorkflowWith(raw: Record<string, unknown>): Record<string, unknown>;
|
|
108
|
-
/**
|
|
109
|
-
* Derive on.push.paths entries from the deploy with: shorthand.
|
|
110
|
-
* Used by both `holocron repo setup` and `sync-workflow-templates`.
|
|
111
|
-
*/
|
|
112
|
-
declare function deriveDeployPaths(raw: Record<string, unknown>): string[];
|
|
113
25
|
//#endregion
|
|
114
|
-
//#region src/generators/turbo.d.ts
|
|
26
|
+
//#region src/generators/turbo.type.d.ts
|
|
115
27
|
interface EnsureRootWorkspaceMemberResult {
|
|
116
28
|
content: string;
|
|
117
29
|
changed: boolean;
|
|
@@ -121,36 +33,7 @@ interface EnsureTurboDependencyResult {
|
|
|
121
33
|
changed: boolean;
|
|
122
34
|
}
|
|
123
35
|
//#endregion
|
|
124
|
-
//#region src/tasks/run/run.d.ts
|
|
125
|
-
/**
|
|
126
|
-
* `holocron run <task> [job] [-- <passthrough>]` — run a registry task (or one
|
|
127
|
-
* of its sub-jobs) locally.
|
|
128
|
-
*
|
|
129
|
-
* A `job` argument only applies to tasks that declare `jobs`; for any other
|
|
130
|
-
* task it is folded back into the passthrough (`holocron run delivery.build
|
|
131
|
-
* src/`).
|
|
132
|
-
*
|
|
133
|
-
* Resolution:
|
|
134
|
-
*
|
|
135
|
-
* J. job given → TASKS[task].jobs[job].local (unknown job → exit 1)
|
|
136
|
-
* 1. turbo.json defines the task → `turbo run <task>`
|
|
137
|
-
* 2. package.json has a `<task>` script → `<pm> run <task>`
|
|
138
|
-
* (unless it's the `holocron run …` thin caller — that recurses)
|
|
139
|
-
* 3. TASKS[task].local resolves → `<tool> <args> <org-flags> <passthrough>`
|
|
140
|
-
* 3b. TASKS[task].linterGroup → run each resolved linter natively (see below)
|
|
141
|
-
* 4. TASKS[task].jobs has entries → run each job in declared order
|
|
142
|
-
* 5. TASKS[task].local === null → "enforced in CI" (skip, even with --required)
|
|
143
|
-
* 5b. known task, nothing resolved → "no <task> task" (exit 0, or 1 with --required)
|
|
144
|
-
* 6. unknown task → "unknown task" (exit 1)
|
|
145
|
-
*
|
|
146
|
-
* A `linterGroup` task (`sourceQuality.staticAnalysis`, etc.) runs its fixed
|
|
147
|
-
* set of `linters.ts` entries, each still gated by that linter's own
|
|
148
|
-
* `detect`/`always` rule — every entry runs its `localBin` natively when
|
|
149
|
-
* found on PATH. Missing tools are flagged; the exit code is the worst of
|
|
150
|
-
* the lot. Unlike the old single `lint` task, this check runs *after*
|
|
151
|
-
* turbo/`package.json` resolution (step 3b, not step 0) — a `linterGroup`
|
|
152
|
-
* task is a normal turbo-delegatable task like any other.
|
|
153
|
-
*/
|
|
36
|
+
//#region src/tasks/run/run.type.d.ts
|
|
154
37
|
/** Minimal structural logger — `@theholocron/observability`'s `Logger` satisfies it. */
|
|
155
38
|
interface RunLogger {
|
|
156
39
|
debug(obj: Record<string, unknown>, msg?: string): void;
|
|
@@ -202,9 +85,8 @@ interface RunTaskReport {
|
|
|
202
85
|
command?: string;
|
|
203
86
|
message?: string;
|
|
204
87
|
}
|
|
205
|
-
declare function runTask(input: RunTaskInput): RunTaskReport;
|
|
206
88
|
//#endregion
|
|
207
|
-
//#region src/tasks/ci.d.ts
|
|
89
|
+
//#region src/tasks/ci.type.d.ts
|
|
208
90
|
interface CiOptions {
|
|
209
91
|
/** Print the plan without running anything. */
|
|
210
92
|
dryRun?: boolean;
|
|
@@ -229,9 +111,8 @@ interface CiInput extends RunDeps, CiOptions {
|
|
|
229
111
|
cwd: string;
|
|
230
112
|
config: TasksConfig;
|
|
231
113
|
}
|
|
232
|
-
declare function runCi(input: CiInput): CiReport;
|
|
233
114
|
//#endregion
|
|
234
|
-
//#region src/tasks/group.d.ts
|
|
115
|
+
//#region src/tasks/group.type.d.ts
|
|
235
116
|
interface GroupOptions {
|
|
236
117
|
/** Print the resolved commands without running anything. */
|
|
237
118
|
dryRun?: boolean;
|
|
@@ -255,11 +136,8 @@ interface GroupInput extends RunDeps, GroupOptions {
|
|
|
255
136
|
/** The bare namespace, e.g. `"sourceQuality"` — matched against `"sourceQuality."`-prefixed task names. */
|
|
256
137
|
namespace: string;
|
|
257
138
|
}
|
|
258
|
-
/** Every configured task whose name starts with `"<namespace>."`, in manifest declaration order. */
|
|
259
|
-
declare function matchNamespace(config: TasksConfig, namespace: string): string[];
|
|
260
|
-
declare function runGroup(input: GroupInput): GroupReport;
|
|
261
139
|
//#endregion
|
|
262
|
-
//#region src/astromech.d.ts
|
|
140
|
+
//#region src/astromech.type.d.ts
|
|
263
141
|
interface AstromechOptions {
|
|
264
142
|
/** Repo root. */
|
|
265
143
|
cwd: string;
|
|
@@ -395,23 +273,19 @@ interface Astromech {
|
|
|
395
273
|
*/
|
|
396
274
|
ensureTurboDependency(packageJson: string, workspaceYaml: string): EnsureTurboDependencyResult;
|
|
397
275
|
}
|
|
276
|
+
//#endregion
|
|
277
|
+
//#region src/astromech.d.ts
|
|
398
278
|
declare function createAstromech(options: AstromechOptions): Astromech;
|
|
399
279
|
//#endregion
|
|
400
|
-
//#region src/generators/codecov.d.ts
|
|
401
|
-
/**
|
|
402
|
-
* `codecov.yml` generation — a manifest-derived artifact (component `paths`
|
|
403
|
-
* come from workspace packages; the status targets track the `test` task's
|
|
404
|
-
* coverage setup), the same category as the reusable `test.yml` this package
|
|
405
|
-
* also generates. Moved out of `@theholocron/cli` (theholocron/holocron#650)
|
|
406
|
-
* — `holocron repo setup`'s codecov step now calls {@link codecovConfig} instead
|
|
407
|
-
* of importing this logic directly.
|
|
408
|
-
*/
|
|
280
|
+
//#region src/generators/codecov.type.d.ts
|
|
409
281
|
interface WorkspacePackage {
|
|
410
282
|
slug: string;
|
|
411
283
|
name: string;
|
|
412
284
|
/** Which workspace root the package was discovered under. Defaults to `"packages"`. */
|
|
413
285
|
dir?: "packages" | "apps" | "plugins";
|
|
414
286
|
}
|
|
287
|
+
//#endregion
|
|
288
|
+
//#region src/generators/codecov.d.ts
|
|
415
289
|
declare function codecovComponentBlock(packages: WorkspacePackage[]): string;
|
|
416
290
|
/**
|
|
417
291
|
* Ensure every codecov status default carries `if_not_found: success`, so an
|
|
@@ -470,52 +344,94 @@ declare const WORKFLOW_TEMPLATE_PROPERTIES: Record<string, string>;
|
|
|
470
344
|
*/
|
|
471
345
|
declare function reusableTemplates(): Map<string, string>;
|
|
472
346
|
//#endregion
|
|
473
|
-
//#region src/generators/
|
|
474
|
-
|
|
475
|
-
declare
|
|
476
|
-
//#endregion
|
|
477
|
-
//#region src/generators/tsconfig.d.ts
|
|
347
|
+
//#region src/generators/github-action-thin-callers.d.ts
|
|
348
|
+
declare const WORKFLOW_TEMPLATES: Record<string, string>;
|
|
349
|
+
declare const KNOWN_WORKFLOWS: Set<string>;
|
|
478
350
|
/**
|
|
479
|
-
*
|
|
480
|
-
*
|
|
481
|
-
* directly from disk, no `--config` override mechanism to hand it a path
|
|
482
|
-
* instead), but its content is fully derivable, so it's generated rather
|
|
483
|
-
* than hand-authored — the same category as `.editorconfig`
|
|
484
|
-
* ({@link "./templates/configs/editorconfig/create-config.js" createConfig})
|
|
485
|
-
* and {@link codecovConfig}.
|
|
351
|
+
* The GitHub status-check context a `required` task contributes to branch
|
|
352
|
+
* protection. Format: `"{workflow name} / {job name}"`.
|
|
486
353
|
*
|
|
487
|
-
*
|
|
488
|
-
*
|
|
489
|
-
*
|
|
490
|
-
*
|
|
491
|
-
*
|
|
492
|
-
*
|
|
493
|
-
*
|
|
354
|
+
* Most of these are single-job workflows now (D3's decomposition split what
|
|
355
|
+
* used to be multi-job `lint`/`audit` into one task per concern) — the
|
|
356
|
+
* context names that job directly, no `Conclusion` aggregator needed; a
|
|
357
|
+
* single job's own conclusion already *is* the workflow's conclusion.
|
|
358
|
+
* `Conclusion` fan-in jobs are kept only where a task genuinely has several
|
|
359
|
+
* conditionally-run jobs feeding one required check:
|
|
360
|
+
* `verification.unitTests` (unit / Storybook / Chromatic / interaction /
|
|
361
|
+
* user-flow, each gated by its own `run-*` input) and
|
|
362
|
+
* `platform.repoValidation` (its three script jobs all must pass). Only
|
|
363
|
+
* merge-gating workflows are listed. `astromech.requiredChecks()` reads this
|
|
364
|
+
* for every `required` task.
|
|
365
|
+
*/
|
|
366
|
+
declare const WORKFLOW_CHECK_CONTEXTS: Partial<Record<string, string>>;
|
|
367
|
+
/**
|
|
368
|
+
* Generate the thin caller content for a workflow, optionally injecting or
|
|
369
|
+
* merging `with:` overrides into the jobs block.
|
|
370
|
+
*
|
|
371
|
+
* Two strategies are used depending on the template:
|
|
372
|
+
* - Templates that already have a `with:` block (e.g. lint):
|
|
373
|
+
* the override entries are merged in, replacing existing keys and appending
|
|
374
|
+
* new ones.
|
|
375
|
+
* - Templates that end with ` secrets: inherit`: a new `with:` block is
|
|
376
|
+
* injected immediately before `secrets: inherit`.
|
|
377
|
+
* If neither pattern matches the template, a warning is emitted and the
|
|
378
|
+
* base template is returned unchanged.
|
|
379
|
+
*/
|
|
380
|
+
declare function generateThinCallerContent(name: string, withOverrides?: Record<string, unknown>, additionalPaths?: string[],
|
|
381
|
+
/** Optional sink for the "could not inject `with:`" warning. */
|
|
382
|
+
logger?: {
|
|
383
|
+
warn(obj: Record<string, unknown>, msg: string): void;
|
|
384
|
+
},
|
|
385
|
+
/**
|
|
386
|
+
* `with:` key → a `# comment` line rendered immediately above that entry.
|
|
387
|
+
* Only honored when injecting a fresh `with:` block (no template today
|
|
388
|
+
* ships one), which is the path `lint` takes.
|
|
389
|
+
*/
|
|
390
|
+
comments?: Record<string, string>): string;
|
|
391
|
+
/**
|
|
392
|
+
* Extract the Cloudflare Pages preview config from a deploy workflow's `with:` object.
|
|
393
|
+
*
|
|
394
|
+
* Accepts three forms:
|
|
395
|
+
* - `preview: true` — derive both project and domain from org context
|
|
396
|
+
* - `preview: { project: "..." }` — explicit project; domain derived from context if omitted
|
|
397
|
+
* - `preview: { project: "...", domain: "..." }` — fully explicit
|
|
494
398
|
*
|
|
495
|
-
*
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
*
|
|
500
|
-
*
|
|
501
|
-
*
|
|
502
|
-
* `compilerOptions.rootDir` (present in exactly two packages, always the
|
|
503
|
-
* same value `./src` that `include` already implies — an unnecessary
|
|
504
|
-
* override wherever it appears, not a pattern worth generalizing).
|
|
399
|
+
* Returns null when `preview:` is absent, false, or can't be resolved.
|
|
400
|
+
*/
|
|
401
|
+
declare function extractPreviewConfig(raw: Record<string, unknown>, ctx?: OrgContext): PreviewConfig | null;
|
|
402
|
+
/**
|
|
403
|
+
* Generate the full thin-caller YAML for a `deploy.yml` that handles both
|
|
404
|
+
* production (push to main → GitHub Pages) and preview (pull_request →
|
|
405
|
+
* Cloudflare Pages) in a single file.
|
|
505
406
|
*
|
|
506
|
-
*
|
|
507
|
-
*
|
|
508
|
-
*
|
|
509
|
-
*
|
|
407
|
+
* Both jobs receive the same docs/storybook `with:` inputs. If the per-repo
|
|
408
|
+
* config supplies `cloudflare-project` it is forwarded; otherwise the reusable
|
|
409
|
+
* falls back to the `CLOUDFLARE_PAGES_PROJECT` org variable — set that once and
|
|
410
|
+
* all repos with a `deploy` workflow get previews without per-repo config.
|
|
411
|
+
*/
|
|
412
|
+
declare function generateCombinedDeployContent(deployWith: Record<string, unknown>, paths: string[], preview: Pick<PreviewConfig, "project">): string;
|
|
413
|
+
/**
|
|
414
|
+
* Expand structured with-values to flat GitHub Actions inputs before
|
|
415
|
+
* generating the thin caller. Handles:
|
|
416
|
+
* - deploy shorthand: docs/storybook → type + storybook-projects
|
|
417
|
+
* - preview: stripped (handled separately via extractPreviewConfig)
|
|
418
|
+
* - run-chromatic object → run-chromatic: true + chromatic-projects
|
|
419
|
+
* - plain arrays → JSON-stringified for YAML scalar quoting
|
|
510
420
|
*
|
|
511
|
-
*
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
*
|
|
516
|
-
*
|
|
517
|
-
* the generator itself, ready for it to call.
|
|
421
|
+
* Used by both `holocron repo setup` and `sync-workflow-templates`.
|
|
422
|
+
*/
|
|
423
|
+
declare function normalizeWorkflowWith(raw: Record<string, unknown>): Record<string, unknown>;
|
|
424
|
+
/**
|
|
425
|
+
* Derive on.push.paths entries from the deploy with: shorthand.
|
|
426
|
+
* Used by both `holocron repo setup` and `sync-workflow-templates`.
|
|
518
427
|
*/
|
|
428
|
+
declare function deriveDeployPaths(raw: Record<string, unknown>): string[];
|
|
429
|
+
//#endregion
|
|
430
|
+
//#region src/generators/required-checks.d.ts
|
|
431
|
+
/** Ordered (task contexts in {@link CI_ORDER}, then extras), de-duplicated. */
|
|
432
|
+
declare function requiredChecks(config: TasksConfig): string[];
|
|
433
|
+
//#endregion
|
|
434
|
+
//#region src/generators/tsconfig.type.d.ts
|
|
519
435
|
interface TsconfigOptions {
|
|
520
436
|
/** Human-readable name for the `display` field — the package's own name is the usual choice. */
|
|
521
437
|
display: string;
|
|
@@ -532,6 +448,8 @@ interface TsconfigOptions {
|
|
|
532
448
|
*/
|
|
533
449
|
paths?: boolean;
|
|
534
450
|
}
|
|
451
|
+
//#endregion
|
|
452
|
+
//#region src/generators/tsconfig.d.ts
|
|
535
453
|
/**
|
|
536
454
|
* A package-level `tsconfig.json` — the uniform shape confirmed across
|
|
537
455
|
* every package checked, parameterized only by the two fields that
|
|
@@ -543,28 +461,7 @@ interface TsconfigOptions {
|
|
|
543
461
|
*/
|
|
544
462
|
declare function createTsconfig(options: TsconfigOptions): string;
|
|
545
463
|
//#endregion
|
|
546
|
-
//#region src/registry.d.ts
|
|
547
|
-
/**
|
|
548
|
-
* The task registry — how each task runs *locally*, without GitHub
|
|
549
|
-
* Actions. `holocron run <task>` and `holocron run ci` resolve against this;
|
|
550
|
-
* adding a task here gives every repo that task.
|
|
551
|
-
*
|
|
552
|
-
* Keyed identically to the workflow templates — a task IS a workflow. This
|
|
553
|
-
* is the canonical vocabulary table (epic #672, D11): every other artifact
|
|
554
|
-
* (`KNOWN_TASKS`, `thin-callers.ts`'s `KNOWN_WORKFLOWS`/`WORKFLOW_CHECK_CONTEXTS`,
|
|
555
|
-
* `CI_ORDER`) derives from these keys rather than hand-duplicating them —
|
|
556
|
-
* including the future GitHub App (#679), which imports this same table for
|
|
557
|
-
* config-schema validation instead of reimplementing its own copy.
|
|
558
|
-
*
|
|
559
|
-
* Task names are an intent-facing vocabulary (`verification.*`,
|
|
560
|
-
* `sourceQuality.*`, `security.*`, `delivery.*`, `platform.*`,
|
|
561
|
-
* `knowledge.*`), not tool names — `eslint`/`vitest`/`tsdown`/… stay
|
|
562
|
-
* internal to this file and `theholocron/configs`. See
|
|
563
|
-
* `docs/wiki/specifications/tech-vocabulary-rename.spec.md` (#675) for the full mapping and
|
|
564
|
-
* the reasoning behind each namespace and decomposition.
|
|
565
|
-
*
|
|
566
|
-
* Spec: `docs/wiki/specifications/tech-astromech-task-runner.spec.md` (epic #581).
|
|
567
|
-
*/
|
|
464
|
+
//#region src/registry.type.d.ts
|
|
568
465
|
/** How to run one task (or job) locally. */
|
|
569
466
|
interface LocalRunner {
|
|
570
467
|
/** Binary to invoke — resolved from `node_modules/.bin` then PATH. */
|
|
@@ -640,6 +537,29 @@ interface TaskDef {
|
|
|
640
537
|
/** Turborepo fan-out config for this task — see {@link TurboTaskConfig}. Omitted for whole-repo, non-fan-out tasks. */
|
|
641
538
|
turbo?: TurboTaskConfig;
|
|
642
539
|
}
|
|
540
|
+
//#endregion
|
|
541
|
+
//#region src/registry.d.ts
|
|
542
|
+
/**
|
|
543
|
+
* The task registry — how each task runs *locally*, without GitHub
|
|
544
|
+
* Actions. `holocron run <task>` and `holocron run ci` resolve against this;
|
|
545
|
+
* adding a task here gives every repo that task.
|
|
546
|
+
*
|
|
547
|
+
* Keyed identically to the workflow templates — a task IS a workflow. This
|
|
548
|
+
* is the canonical vocabulary table (epic #672, D11): every other artifact
|
|
549
|
+
* (`KNOWN_TASKS`, `thin-callers.ts`'s `KNOWN_WORKFLOWS`/`WORKFLOW_CHECK_CONTEXTS`,
|
|
550
|
+
* `CI_ORDER`) derives from these keys rather than hand-duplicating them —
|
|
551
|
+
* including the future GitHub App (#679), which imports this same table for
|
|
552
|
+
* config-schema validation instead of reimplementing its own copy.
|
|
553
|
+
*
|
|
554
|
+
* Task names are an intent-facing vocabulary (`verification.*`,
|
|
555
|
+
* `sourceQuality.*`, `security.*`, `delivery.*`, `platform.*`,
|
|
556
|
+
* `knowledge.*`), not tool names — `eslint`/`vitest`/`tsdown`/… stay
|
|
557
|
+
* internal to this file and `theholocron/configs`. See
|
|
558
|
+
* `docs/wiki/specifications/tech-vocabulary-rename.spec.md` (#675) for the full mapping and
|
|
559
|
+
* the reasoning behind each namespace and decomposition.
|
|
560
|
+
*
|
|
561
|
+
* Spec: `docs/wiki/specifications/tech-astromech-task-runner.spec.md` (epic #581).
|
|
562
|
+
*/
|
|
643
563
|
declare const TASKS: Record<string, TaskDef>;
|
|
644
564
|
/** Every task name the registry knows. */
|
|
645
565
|
declare const KNOWN_TASKS: Set<string>;
|
|
@@ -651,17 +571,24 @@ declare const KNOWN_TASKS: Set<string>;
|
|
|
651
571
|
*/
|
|
652
572
|
declare const CI_ORDER: string[];
|
|
653
573
|
//#endregion
|
|
654
|
-
//#region src/tasks/
|
|
574
|
+
//#region src/tasks/ci.d.ts
|
|
575
|
+
declare function runCi(input: CiInput): CiReport;
|
|
576
|
+
//#endregion
|
|
577
|
+
//#region src/utils/default-run.type.d.ts
|
|
655
578
|
interface SpawnResult {
|
|
656
579
|
status: number | null;
|
|
657
580
|
stdout: string;
|
|
658
581
|
stderr: string;
|
|
659
582
|
}
|
|
660
583
|
type Run = (command: string, args: string[], options: Record<string, unknown>) => SpawnResult;
|
|
584
|
+
//#endregion
|
|
585
|
+
//#region src/tasks/deploy-on-release/workspace-packages.type.d.ts
|
|
661
586
|
interface Tree {
|
|
662
587
|
readJson: (path: string) => unknown;
|
|
663
588
|
listDirs: (path: string) => string[];
|
|
664
589
|
}
|
|
590
|
+
//#endregion
|
|
591
|
+
//#region src/tasks/deploy-on-release/deploy-on-release.type.d.ts
|
|
665
592
|
interface DeployOnReleaseOptions {
|
|
666
593
|
cwd: string;
|
|
667
594
|
/** The release's channel: its prerelease identifier (`alpha`); empty or `main` for a stable release. */
|
|
@@ -691,6 +618,8 @@ interface DeployOnReleaseReport {
|
|
|
691
618
|
status: "ok" | "fail" | "skip";
|
|
692
619
|
results: DeployOnReleaseResult[];
|
|
693
620
|
}
|
|
621
|
+
//#endregion
|
|
622
|
+
//#region src/tasks/deploy-on-release/deploy-on-release.d.ts
|
|
694
623
|
/**
|
|
695
624
|
* Deploys every package whose manifest declares a release-time deploy that
|
|
696
625
|
* applies to this release. Never throws for a deploy problem: each package's
|
|
@@ -698,4 +627,14 @@ interface DeployOnReleaseReport {
|
|
|
698
627
|
*/
|
|
699
628
|
declare function deployOnRelease(options: DeployOnReleaseOptions, { run, readJson, listDirs, sleep, now, loadTasks }?: DeployOnReleaseDeps): Promise<DeployOnReleaseReport>;
|
|
700
629
|
//#endregion
|
|
630
|
+
//#region src/tasks/group.d.ts
|
|
631
|
+
declare function runGroup(input: GroupInput): GroupReport;
|
|
632
|
+
//#endregion
|
|
633
|
+
//#region src/tasks/match-namespace.d.ts
|
|
634
|
+
/** Every configured task whose name starts with `"<namespace>."`, in manifest declaration order. */
|
|
635
|
+
declare function matchNamespace(config: TasksConfig, namespace: string): string[];
|
|
636
|
+
//#endregion
|
|
637
|
+
//#region src/tasks/run/run.d.ts
|
|
638
|
+
declare function runTask(input: RunTaskInput): RunTaskReport;
|
|
639
|
+
//#endregion
|
|
701
640
|
export { type Astromech, type AstromechOptions, CI_ORDER, type CiJobReport, type CiOptions, type CiReport, type DeployOnReleaseOptions, type DeployOnReleaseReport, type DeployOnReleaseResult, type ExecFn, type GroupJobReport, type GroupOptions, type GroupReport, type JobDef, KNOWN_TASKS, KNOWN_WORKFLOWS, type LocalRunner, type OrgContext, type PreviewConfig, REUSABLE_ACTIONS, REUSABLE_WORKFLOWS, type RunLogger, type RunOptions, type RunTaskInput, type RunTaskReport, TASKS, type TaskDef, type TsconfigOptions, WORKFLOW_CHECK_CONTEXTS, WORKFLOW_TEMPLATES, WORKFLOW_TEMPLATE_PROPERTIES, type WorkspacePackage, codecovComponentBlock, codecovConfig, createAstromech, createCodecovConfig, createTsconfig, deployOnRelease, deriveDeployPaths, ensureIfNotFound, extractPreviewConfig, generateCombinedDeployContent, generateThinCallerContent, matchNamespace, mergeCodecovComponents, normalizeWorkflowWith, readWorkspacePackages, requiredChecks, reusableTemplates, runCi, runGroup, runTask };
|
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { r as normalizeTaskEntry, t as loadTasksConfig } from "./load-
|
|
2
|
-
import { spawnSync } from "node:child_process";
|
|
1
|
+
import { r as normalizeTaskEntry, t as loadTasksConfig } from "./load-BqpxDjMJ.mjs";
|
|
3
2
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
4
3
|
import { join, relative } from "node:path";
|
|
5
4
|
import { stringifyJson } from "@theholocron/json-utils";
|
|
5
|
+
import { dependencyNames, detectPackageManager, readPackageJsonSync } from "@theholocron/pkg-utils";
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
import { findExecutableSync } from "@theholocron/fs-utils";
|
|
6
8
|
//#region src/templates/codecov/codecov.yml
|
|
7
9
|
var codecov_default = "codecov:\n require_ci_to_pass: true\n\ncoverage:\n precision: 2\n round: down\n status:\n project:\n default:\n target: auto\n threshold: 2%\n # Infra-only PRs (dep bumps, config, lockfile) touch no coverable code,\n # so codecov produces no report to compare. Pass instead of blocking\n # forever on a required check that will never post.\n if_not_found: success\n patch:\n default:\n target: 80%\n if_not_found: success\n\ncomment:\n layout: \"reach,diff,flags,components\"\n behavior: default\n require_changes: true\n\ncomponent_management:\n default_rules:\n statuses:\n - type: patch\n target: 80%\n if_not_found: success\n individual_components:\n";
|
|
8
10
|
//#endregion
|
|
@@ -608,6 +610,27 @@ function reusableTemplates() {
|
|
|
608
610
|
}
|
|
609
611
|
//#endregion
|
|
610
612
|
//#region src/registry.ts
|
|
613
|
+
/**
|
|
614
|
+
* The task registry — how each task runs *locally*, without GitHub
|
|
615
|
+
* Actions. `holocron run <task>` and `holocron run ci` resolve against this;
|
|
616
|
+
* adding a task here gives every repo that task.
|
|
617
|
+
*
|
|
618
|
+
* Keyed identically to the workflow templates — a task IS a workflow. This
|
|
619
|
+
* is the canonical vocabulary table (epic #672, D11): every other artifact
|
|
620
|
+
* (`KNOWN_TASKS`, `thin-callers.ts`'s `KNOWN_WORKFLOWS`/`WORKFLOW_CHECK_CONTEXTS`,
|
|
621
|
+
* `CI_ORDER`) derives from these keys rather than hand-duplicating them —
|
|
622
|
+
* including the future GitHub App (#679), which imports this same table for
|
|
623
|
+
* config-schema validation instead of reimplementing its own copy.
|
|
624
|
+
*
|
|
625
|
+
* Task names are an intent-facing vocabulary (`verification.*`,
|
|
626
|
+
* `sourceQuality.*`, `security.*`, `delivery.*`, `platform.*`,
|
|
627
|
+
* `knowledge.*`), not tool names — `eslint`/`vitest`/`tsdown`/… stay
|
|
628
|
+
* internal to this file and `theholocron/configs`. See
|
|
629
|
+
* `docs/wiki/specifications/tech-vocabulary-rename.spec.md` (#675) for the full mapping and
|
|
630
|
+
* the reasoning behind each namespace and decomposition.
|
|
631
|
+
*
|
|
632
|
+
* Spec: `docs/wiki/specifications/tech-astromech-task-runner.spec.md` (epic #581).
|
|
633
|
+
*/
|
|
611
634
|
const TASKS = {
|
|
612
635
|
"verification.unitTests": {
|
|
613
636
|
local: {
|
|
@@ -1022,6 +1045,15 @@ function ensureTurboDependency(packageJson, workspaceYaml) {
|
|
|
1022
1045
|
//#endregion
|
|
1023
1046
|
//#region src/tasks/run/linters.ts
|
|
1024
1047
|
/**
|
|
1048
|
+
* The linter registry — one list of linter names (from `config.tasks`'
|
|
1049
|
+
* `{ name: "lint", linters: [...] }`, or auto-detected) maps to both the
|
|
1050
|
+
* CI super-linter `VALIDATE_*` env and the native local run. This table is
|
|
1051
|
+
* the single source of truth for that mapping; {@link superLinterConfig}
|
|
1052
|
+
* (CI) and the `holocron run lint` aggregate (local) both read it.
|
|
1053
|
+
*
|
|
1054
|
+
* Spec: `docs/wiki/specifications/tech-astromech-task-runner.spec.md` "Lint parity".
|
|
1055
|
+
*/
|
|
1056
|
+
/**
|
|
1025
1057
|
* Known linters, in execution order. `always` entries are the current
|
|
1026
1058
|
* hard-coded super-linter baseline; `prettier` is always-on because the org
|
|
1027
1059
|
* applies it universally (super-linter only lints files that exist).
|
|
@@ -1165,7 +1197,79 @@ function resolveLinters(opts) {
|
|
|
1165
1197
|
}));
|
|
1166
1198
|
}
|
|
1167
1199
|
//#endregion
|
|
1168
|
-
//#region src/tasks/run/
|
|
1200
|
+
//#region src/tasks/run/resolve-bin.ts
|
|
1201
|
+
/** `<cwd>/node_modules/.bin/<tool>` when it exists, else the bare `tool` name (left to `PATH`). */
|
|
1202
|
+
function resolveBin(cwd, tool, fileExists) {
|
|
1203
|
+
const local = join(cwd, "node_modules", ".bin", tool);
|
|
1204
|
+
return fileExists(local) ? local : tool;
|
|
1205
|
+
}
|
|
1206
|
+
//#endregion
|
|
1207
|
+
//#region src/tasks/run/resolve-runner.ts
|
|
1208
|
+
const mkRunner = (tool, args = []) => ({
|
|
1209
|
+
tool,
|
|
1210
|
+
args
|
|
1211
|
+
});
|
|
1212
|
+
/**
|
|
1213
|
+
* Resolve `{ tool, args }` for a runner, applying `detect[]` against repo
|
|
1214
|
+
* files, falling back (#750) to each candidate's `tool` name against
|
|
1215
|
+
* `package.json`'s own declared dependencies when neither matches by
|
|
1216
|
+
* filename. Only called for `tool` / `detect` runners — the caller handles
|
|
1217
|
+
* `command` runners itself, so `local.detect` is present whenever
|
|
1218
|
+
* `local.tool` is not.
|
|
1219
|
+
*
|
|
1220
|
+
* The dependency fallback matters once a Bucket A migration (#680) deletes
|
|
1221
|
+
* a build tool's own config file (`tsdown.config.ts`, `vite.config.ts`, …):
|
|
1222
|
+
* that file was the *only* signal `detect[]` had to offer, so removing it
|
|
1223
|
+
* used to make `delivery.build` report "no task for this repo" and silently
|
|
1224
|
+
* skip the build, even though the tool is still very much in use — a
|
|
1225
|
+
* package can't actually build with `tsdown` without it in
|
|
1226
|
+
* `dependencies`/`devDependencies`, so that's a signal at least as reliable
|
|
1227
|
+
* as the config file, and one deleting the file doesn't remove.
|
|
1228
|
+
*
|
|
1229
|
+
* **Per-candidate, not two full passes** (#750 follow-up — the first
|
|
1230
|
+
* shipped version got this wrong): `delivery.build`'s own `detect[]` ends
|
|
1231
|
+
* with `{ when: /^tsconfig\.json$/, tool: "tsc" }` as its last-resort
|
|
1232
|
+
* fallback, and `tsconfig.json` exists in essentially every package
|
|
1233
|
+
* regardless of build tool. A "check every candidate's filename, *then*
|
|
1234
|
+
* check every candidate's dependency" two-pass design would let that
|
|
1235
|
+
* always-present fallback file win over a real `tsdown`/`vite`/`rollup`
|
|
1236
|
+
* dependency match, the moment the earlier candidates' own config files are
|
|
1237
|
+
* gone — exactly backwards. Checking `(file match) || (dependency match)`
|
|
1238
|
+
* together for each candidate before moving to the next preserves the
|
|
1239
|
+
* intended precedence: a real signal for an *earlier* candidate, by either
|
|
1240
|
+
* means, still wins over a later candidate's mere filename presence.
|
|
1241
|
+
*/
|
|
1242
|
+
function resolveRunner(local, cwd, listDir, readFile) {
|
|
1243
|
+
if (local.tool) return mkRunner(local.tool, local.args);
|
|
1244
|
+
let files;
|
|
1245
|
+
try {
|
|
1246
|
+
files = listDir(cwd);
|
|
1247
|
+
} catch {
|
|
1248
|
+
files = [];
|
|
1249
|
+
}
|
|
1250
|
+
const deps = dependencyNames(readPackageJsonSync(cwd, { readFile }));
|
|
1251
|
+
for (const candidate of local.detect) if (files.some((f) => candidate.when.test(f)) || deps.has(candidate.tool)) return mkRunner(candidate.tool, candidate.args);
|
|
1252
|
+
}
|
|
1253
|
+
//#endregion
|
|
1254
|
+
//#region src/tasks/run/resolve-export-entry.ts
|
|
1255
|
+
/**
|
|
1256
|
+
* Unwrap a `package.json#exports` subpath entry down to its file path —
|
|
1257
|
+
* either a bare string, or an object with condition keys (`import`,
|
|
1258
|
+
* `default`, `require`, …) that may themselves nest one level (`{ import: {
|
|
1259
|
+
* types, default } }`, the shape every `@theholocron/*-config` package
|
|
1260
|
+
* actually uses). Prefers `import` (this org ships ESM), then `default`,
|
|
1261
|
+
* then `require` as a last resort.
|
|
1262
|
+
*/
|
|
1263
|
+
function resolveExportEntry(entry) {
|
|
1264
|
+
if (typeof entry === "string") return entry;
|
|
1265
|
+
if (entry === null || typeof entry !== "object") return void 0;
|
|
1266
|
+
const conditions = entry;
|
|
1267
|
+
const value = conditions.import ?? conditions.default ?? conditions.require;
|
|
1268
|
+
if (typeof value === "string") return value;
|
|
1269
|
+
if (typeof value === "object" && value !== null) return resolveExportEntry(value);
|
|
1270
|
+
}
|
|
1271
|
+
//#endregion
|
|
1272
|
+
//#region src/tasks/run/tool-configs.ts
|
|
1169
1273
|
/**
|
|
1170
1274
|
* `library` is every mapped tool's org-wide default variant today — every
|
|
1171
1275
|
* currently-migrated repo (`holocron`, `clients`, `utils`, `configs`,
|
|
@@ -1242,7 +1346,42 @@ const LOCAL_CONFIG_FILES = {
|
|
|
1242
1346
|
"commitlint.config.cjs"
|
|
1243
1347
|
]
|
|
1244
1348
|
};
|
|
1245
|
-
|
|
1349
|
+
//#endregion
|
|
1350
|
+
//#region src/tasks/run/resolve-tool-config.ts
|
|
1351
|
+
/**
|
|
1352
|
+
* Resolves the absolute path to a Bucket A tool's shared `@theholocron/*-config`
|
|
1353
|
+
* entry point, installed in the consuming repo's own `node_modules` — and the
|
|
1354
|
+
* CLI flag(s) to hand it via (`--config`, `--extends`, …). `run.ts` splices the
|
|
1355
|
+
* result into the command it builds for a tool/detect runner or a linter-group
|
|
1356
|
+
* entry, right after the tool's own args.
|
|
1357
|
+
*
|
|
1358
|
+
* Every mapped package's resolved entry point already carries a ready-to-use
|
|
1359
|
+
* default export (a plain re-export for `prettier`/`commitlint`; an invoked,
|
|
1360
|
+
* zero-arg preset for `eslint`/`vitest`/`tsdown` — verified end-to-end against
|
|
1361
|
+
* each tool's real `--config`/`--extends` loader, not just the file's shape).
|
|
1362
|
+
* A repo doesn't need this package installed at all — every lookup degrades to
|
|
1363
|
+
* `[]` (no flag added, the tool falls back to its own auto-discovery of a
|
|
1364
|
+
* local file, unchanged from today) rather than throwing.
|
|
1365
|
+
*
|
|
1366
|
+
* **Local file wins.** A `<tool>.config.*` sitting in `cwd` (whether it's
|
|
1367
|
+
* pure boilerplate or has real per-package customization on top of the
|
|
1368
|
+
* shared bundle, e.g. `mergeConfig(base, { test: { coverage: { exclude }
|
|
1369
|
+
* } })`) always skips the splice, even when the shared package is also
|
|
1370
|
+
* installed — an explicit `--config <shared path>` flag beats a tool's own
|
|
1371
|
+
* auto-discovery every time, so splicing it unconditionally would silently
|
|
1372
|
+
* discard that customization the file is still sitting right there on disk.
|
|
1373
|
+
* The shared bundle is only ever the fallback for a package with *no* local
|
|
1374
|
+
* file at all — that's still the "delete the file to go fully shared" path
|
|
1375
|
+
* this was designed for (#676); it just isn't unconditional (#749).
|
|
1376
|
+
*
|
|
1377
|
+
* `semantic-release`, `devmoji`, `editorconfig-checker`, and `knip` are
|
|
1378
|
+
* deliberately absent — `semantic-release-config`'s `defineConfig()` needs
|
|
1379
|
+
* real per-repo data (branches, npm options) a static `--extends <path>` can't
|
|
1380
|
+
* carry; `devmoji` runs through a git hook template, not `holocron run
|
|
1381
|
+
* <task>`; `editorconfig-checker` has no shared-config package yet; `knip` is
|
|
1382
|
+
* repo-specific by nature, never a shared-config candidate. See
|
|
1383
|
+
* `.notes/tech-config-resolution.spec.md` (#676).
|
|
1384
|
+
*/
|
|
1246
1385
|
/**
|
|
1247
1386
|
* `<flag> <absolute path>` for `tool`, resolved against `cwd`'s
|
|
1248
1387
|
* `node_modules` — or `[]` when the tool isn't mapped, `cwd` already has its
|
|
@@ -1255,9 +1394,9 @@ new Set(Object.keys(TOOL_CONFIGS));
|
|
|
1255
1394
|
function resolveToolConfig(tool, cwd, deps) {
|
|
1256
1395
|
const spec = TOOL_CONFIGS[tool];
|
|
1257
1396
|
if (!spec) return [];
|
|
1258
|
-
if ((LOCAL_CONFIG_FILES[tool] ?? []).some((name) => deps.fileExists(
|
|
1259
|
-
const pkgDir =
|
|
1260
|
-
const pkgJsonPath =
|
|
1397
|
+
if ((LOCAL_CONFIG_FILES[tool] ?? []).some((name) => deps.fileExists(join(cwd, name)))) return [];
|
|
1398
|
+
const pkgDir = join(cwd, "node_modules", ...spec.package.split("/"));
|
|
1399
|
+
const pkgJsonPath = join(pkgDir, "package.json");
|
|
1261
1400
|
if (!deps.fileExists(pkgJsonPath)) return [];
|
|
1262
1401
|
let pkg;
|
|
1263
1402
|
try {
|
|
@@ -1267,36 +1406,21 @@ function resolveToolConfig(tool, cwd, deps) {
|
|
|
1267
1406
|
}
|
|
1268
1407
|
const relative = resolveExportEntry(pkg.exports?.[spec.exportPath]);
|
|
1269
1408
|
if (!relative) return [];
|
|
1270
|
-
const absolute =
|
|
1409
|
+
const absolute = join(pkgDir, relative);
|
|
1271
1410
|
if (!deps.fileExists(absolute)) return [];
|
|
1272
1411
|
return [...spec.flag, absolute];
|
|
1273
1412
|
}
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
const conditions = entry;
|
|
1286
|
-
const value = conditions.import ?? conditions.default ?? conditions.require;
|
|
1287
|
-
if (typeof value === "string") return value;
|
|
1288
|
-
if (typeof value === "object" && value !== null) return resolveExportEntry(value);
|
|
1289
|
-
}
|
|
1290
|
-
/**
|
|
1291
|
-
* A minimal, POSIX-safe `path.join` — avoids importing `node:path` just for
|
|
1292
|
-
* this. Every path in play here is either already POSIX (a `package.json`
|
|
1293
|
-
* `exports` value, always forward-slashed regardless of platform) or built
|
|
1294
|
-
* from platform-neutral segments (`cwd`, `"node_modules"`, a scoped package
|
|
1295
|
-
* name split on `/`), so a manual join is safe and keeps this module
|
|
1296
|
-
* dependency-free like the rest of `run.ts`'s helpers.
|
|
1297
|
-
*/
|
|
1298
|
-
function joinPath(...segments) {
|
|
1299
|
-
return segments.map((s, i) => i === 0 ? s.replace(/\/+$/, "") : s.replace(/^\/+|\/+$/g, "")).filter(Boolean).join("/");
|
|
1413
|
+
//#endregion
|
|
1414
|
+
//#region src/tasks/run/turbo-defines-task.ts
|
|
1415
|
+
/** `true` when `<cwd>/turbo.json` declares `task` (under `tasks`, or turbo 1.x's `pipeline`). */
|
|
1416
|
+
function turboDefinesTask(cwd, task, readFile, fileExists) {
|
|
1417
|
+
if (!fileExists(join(cwd, "turbo.json"))) return false;
|
|
1418
|
+
try {
|
|
1419
|
+
const turbo = JSON.parse(readFile(join(cwd, "turbo.json")));
|
|
1420
|
+
return Boolean(turbo.tasks?.[task] ?? turbo.pipeline?.[task]);
|
|
1421
|
+
} catch {
|
|
1422
|
+
return false;
|
|
1423
|
+
}
|
|
1300
1424
|
}
|
|
1301
1425
|
//#endregion
|
|
1302
1426
|
//#region src/tasks/run/run.ts
|
|
@@ -1405,8 +1529,11 @@ function runTask(input) {
|
|
|
1405
1529
|
];
|
|
1406
1530
|
return run(resolveBin(cwd, "turbo", fileExists), args);
|
|
1407
1531
|
}
|
|
1408
|
-
const script =
|
|
1409
|
-
if (script && !/^holocron run\b/.test(script.trim())) return run(
|
|
1532
|
+
const script = readPackageJsonSync(cwd, { readFile })?.scripts?.[task];
|
|
1533
|
+
if (script && !/^holocron run\b/.test(script.trim())) return run(detectPackageManager(cwd, {
|
|
1534
|
+
readFile,
|
|
1535
|
+
fileExists
|
|
1536
|
+
}), [
|
|
1410
1537
|
"run",
|
|
1411
1538
|
task,
|
|
1412
1539
|
...passthrough.length ? ["--", ...passthrough] : []
|
|
@@ -1628,92 +1755,6 @@ function runLinterGroup(input, names, passthrough) {
|
|
|
1628
1755
|
command
|
|
1629
1756
|
};
|
|
1630
1757
|
}
|
|
1631
|
-
const mkRunner = (tool, args = []) => ({
|
|
1632
|
-
tool,
|
|
1633
|
-
args
|
|
1634
|
-
});
|
|
1635
|
-
/**
|
|
1636
|
-
* Resolve `{ tool, args }` for a runner, applying `detect[]` against repo
|
|
1637
|
-
* files, falling back (#750) to each candidate's `tool` name against
|
|
1638
|
-
* `package.json`'s own declared dependencies when neither matches by
|
|
1639
|
-
* filename. Only called for `tool` / `detect` runners — the caller handles
|
|
1640
|
-
* `command` runners itself, so `local.detect` is present whenever
|
|
1641
|
-
* `local.tool` is not.
|
|
1642
|
-
*
|
|
1643
|
-
* The dependency fallback matters once a Bucket A migration (#680) deletes
|
|
1644
|
-
* a build tool's own config file (`tsdown.config.ts`, `vite.config.ts`, …):
|
|
1645
|
-
* that file was the *only* signal `detect[]` had to offer, so removing it
|
|
1646
|
-
* used to make `delivery.build` report "no task for this repo" and silently
|
|
1647
|
-
* skip the build, even though the tool is still very much in use — a
|
|
1648
|
-
* package can't actually build with `tsdown` without it in
|
|
1649
|
-
* `dependencies`/`devDependencies`, so that's a signal at least as reliable
|
|
1650
|
-
* as the config file, and one deleting the file doesn't remove.
|
|
1651
|
-
*
|
|
1652
|
-
* **Per-candidate, not two full passes** (#750 follow-up — the first
|
|
1653
|
-
* shipped version got this wrong): `delivery.build`'s own `detect[]` ends
|
|
1654
|
-
* with `{ when: /^tsconfig\.json$/, tool: "tsc" }` as its last-resort
|
|
1655
|
-
* fallback, and `tsconfig.json` exists in essentially every package
|
|
1656
|
-
* regardless of build tool. A "check every candidate's filename, *then*
|
|
1657
|
-
* check every candidate's dependency" two-pass design would let that
|
|
1658
|
-
* always-present fallback file win over a real `tsdown`/`vite`/`rollup`
|
|
1659
|
-
* dependency match, the moment the earlier candidates' own config files are
|
|
1660
|
-
* gone — exactly backwards. Checking `(file match) || (dependency match)`
|
|
1661
|
-
* together for each candidate before moving to the next preserves the
|
|
1662
|
-
* intended precedence: a real signal for an *earlier* candidate, by either
|
|
1663
|
-
* means, still wins over a later candidate's mere filename presence.
|
|
1664
|
-
*/
|
|
1665
|
-
function resolveRunner(local, cwd, listDir, readFile) {
|
|
1666
|
-
if (local.tool) return mkRunner(local.tool, local.args);
|
|
1667
|
-
let files;
|
|
1668
|
-
try {
|
|
1669
|
-
files = listDir(cwd);
|
|
1670
|
-
} catch {
|
|
1671
|
-
files = [];
|
|
1672
|
-
}
|
|
1673
|
-
const deps = packageJsonDependencies(cwd, readFile);
|
|
1674
|
-
for (const candidate of local.detect) if (files.some((f) => candidate.when.test(f)) || deps.has(candidate.tool)) return mkRunner(candidate.tool, candidate.args);
|
|
1675
|
-
}
|
|
1676
|
-
/** Every key from `package.json`'s `dependencies` + `devDependencies`, or an empty set on any read/parse failure. */
|
|
1677
|
-
function packageJsonDependencies(cwd, readFile) {
|
|
1678
|
-
try {
|
|
1679
|
-
const pkg = JSON.parse(readFile(join(cwd, "package.json")));
|
|
1680
|
-
return /* @__PURE__ */ new Set([...Object.keys(pkg.dependencies ?? {}), ...Object.keys(pkg.devDependencies ?? {})]);
|
|
1681
|
-
} catch {
|
|
1682
|
-
return /* @__PURE__ */ new Set();
|
|
1683
|
-
}
|
|
1684
|
-
}
|
|
1685
|
-
function resolveBin(cwd, tool, fileExists) {
|
|
1686
|
-
const local = join(cwd, "node_modules", ".bin", tool);
|
|
1687
|
-
return fileExists(local) ? local : tool;
|
|
1688
|
-
}
|
|
1689
|
-
function packageManager(cwd, readFile, fileExists) {
|
|
1690
|
-
try {
|
|
1691
|
-
const pkg = JSON.parse(readFile(join(cwd, "package.json")));
|
|
1692
|
-
if (typeof pkg.packageManager === "string") return pkg.packageManager.split("@")[0];
|
|
1693
|
-
} catch {}
|
|
1694
|
-
if (fileExists(join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
1695
|
-
if (fileExists(join(cwd, "bun.lockb"))) return "bun";
|
|
1696
|
-
if (fileExists(join(cwd, "yarn.lock"))) return "yarn";
|
|
1697
|
-
if (fileExists(join(cwd, "package-lock.json"))) return "npm";
|
|
1698
|
-
return "pnpm";
|
|
1699
|
-
}
|
|
1700
|
-
function turboDefinesTask(cwd, task, readFile, fileExists) {
|
|
1701
|
-
if (!fileExists(join(cwd, "turbo.json"))) return false;
|
|
1702
|
-
try {
|
|
1703
|
-
const turbo = JSON.parse(readFile(join(cwd, "turbo.json")));
|
|
1704
|
-
return Boolean(turbo.tasks?.[task] ?? turbo.pipeline?.[task]);
|
|
1705
|
-
} catch {
|
|
1706
|
-
return false;
|
|
1707
|
-
}
|
|
1708
|
-
}
|
|
1709
|
-
function packageJsonScript(cwd, task, readFile, fileExists) {
|
|
1710
|
-
if (!fileExists(join(cwd, "package.json"))) return void 0;
|
|
1711
|
-
try {
|
|
1712
|
-
return JSON.parse(readFile(join(cwd, "package.json"))).scripts?.[task];
|
|
1713
|
-
} catch {
|
|
1714
|
-
return;
|
|
1715
|
-
}
|
|
1716
|
-
}
|
|
1717
1758
|
//#endregion
|
|
1718
1759
|
//#region src/tasks/ci.ts
|
|
1719
1760
|
/**
|
|
@@ -1785,19 +1826,7 @@ function taskName(item) {
|
|
|
1785
1826
|
return typeof item === "string" ? item : item.name;
|
|
1786
1827
|
}
|
|
1787
1828
|
//#endregion
|
|
1788
|
-
//#region src/tasks/
|
|
1789
|
-
/**
|
|
1790
|
-
* `holocron run <namespace>` — run every configured task under one
|
|
1791
|
-
* namespace prefix (`sourceQuality.*`, `verification.*`, …) in sequence.
|
|
1792
|
-
* `Astromech.run()` falls back here whenever the given name isn't an exact
|
|
1793
|
-
* `TASKS` entry but matches the prefix of one or more configured tasks —
|
|
1794
|
-
* same "aggregate several real tasks into one report" shape `runCi` already
|
|
1795
|
-
* established, scoped to one namespace instead of the merge-gating set.
|
|
1796
|
-
*
|
|
1797
|
-
* Every task in the namespace always runs — one task's failure never skips
|
|
1798
|
-
* the rest, same as `runCi`'s own loop — so a single invocation surfaces
|
|
1799
|
-
* every problem in the namespace at once instead of stopping at the first.
|
|
1800
|
-
*/
|
|
1829
|
+
//#region src/tasks/match-namespace.ts
|
|
1801
1830
|
/** Every configured task whose name starts with `"<namespace>."`, in manifest declaration order. */
|
|
1802
1831
|
function matchNamespace(config, namespace) {
|
|
1803
1832
|
const prefix = `${namespace}.`;
|
|
@@ -1806,6 +1835,8 @@ function matchNamespace(config, namespace) {
|
|
|
1806
1835
|
return names;
|
|
1807
1836
|
}, []);
|
|
1808
1837
|
}
|
|
1838
|
+
//#endregion
|
|
1839
|
+
//#region src/tasks/group.ts
|
|
1809
1840
|
function runGroup(input) {
|
|
1810
1841
|
const { print, namespace } = input;
|
|
1811
1842
|
const names = matchNamespace(input.config, namespace);
|
|
@@ -1844,29 +1875,47 @@ function runGroup(input) {
|
|
|
1844
1875
|
};
|
|
1845
1876
|
}
|
|
1846
1877
|
//#endregion
|
|
1847
|
-
//#region src/
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1878
|
+
//#region src/utils/default-list-dirs.ts
|
|
1879
|
+
const defaultListDirs = (path) => readdirSync(path, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
1880
|
+
//#endregion
|
|
1881
|
+
//#region src/utils/default-read-json.ts
|
|
1882
|
+
const defaultReadJson = (path) => JSON.parse(readFileSync(path, "utf8"));
|
|
1883
|
+
//#endregion
|
|
1884
|
+
//#region src/utils/default-run.ts
|
|
1885
|
+
const defaultRun = (command, args, opts) => spawnSync(command, args, {
|
|
1886
|
+
encoding: "utf8",
|
|
1887
|
+
...opts
|
|
1888
|
+
});
|
|
1889
|
+
//#endregion
|
|
1890
|
+
//#region src/utils/default-sleep.ts
|
|
1891
|
+
const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
1892
|
+
//#endregion
|
|
1893
|
+
//#region src/utils/exec.ts
|
|
1894
|
+
/** Runs `cmd` with the parent's stdio; a signal-killed or unspawnable process reports `-1`. */
|
|
1857
1895
|
const realExec = (cmd, args, opts) => {
|
|
1858
1896
|
return { exitCode: spawnSync(cmd, args, {
|
|
1859
1897
|
cwd: opts.cwd,
|
|
1860
1898
|
stdio: "inherit"
|
|
1861
1899
|
}).status ?? -1 };
|
|
1862
1900
|
};
|
|
1901
|
+
//#endregion
|
|
1902
|
+
//#region src/utils/look-path.ts
|
|
1863
1903
|
/** `node_modules/.bin/<bin>`, else the first `PATH` entry that has it, else `null`. */
|
|
1864
|
-
const realLookPath = (cwd, bin) => {
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1904
|
+
const realLookPath = (cwd, bin) => findExecutableSync(bin, { path: join(cwd, "node_modules", ".bin") }) ?? findExecutableSync(bin) ?? null;
|
|
1905
|
+
//#endregion
|
|
1906
|
+
//#region src/utils/noop-logger.ts
|
|
1907
|
+
/** The `RunLogger` used when the caller injects none: drops every record. */
|
|
1908
|
+
const noopLogger = {
|
|
1909
|
+
debug() {},
|
|
1910
|
+
warn() {}
|
|
1869
1911
|
};
|
|
1912
|
+
//#endregion
|
|
1913
|
+
//#region src/astromech.ts
|
|
1914
|
+
/**
|
|
1915
|
+
* `createAstromech(options)` — the self-contained task runner.
|
|
1916
|
+
* `@theholocron/cli` instantiates it once and delegates the `run` /
|
|
1917
|
+
* `ci` / workflow-generation commands to it (like `@theholocron/observability`).
|
|
1918
|
+
*/
|
|
1870
1919
|
function createAstromech(options) {
|
|
1871
1920
|
const deps = {
|
|
1872
1921
|
print: options.print ?? ((line) => console.log(line)),
|
|
@@ -2047,78 +2096,22 @@ function createTsconfig(options) {
|
|
|
2047
2096
|
return stringifyJson(config);
|
|
2048
2097
|
}
|
|
2049
2098
|
//#endregion
|
|
2050
|
-
//#region src/tasks/deploy-on-release.ts
|
|
2051
|
-
/**
|
|
2052
|
-
* Deploy workspace packages once a release has published (holocron#930).
|
|
2053
|
-
*
|
|
2054
|
-
* A package opts in with a `delivery.deploy` task in its own `holocron.config`
|
|
2055
|
-
* whose `with.on` is `"release"`:
|
|
2056
|
-
*
|
|
2057
|
-
* ```ts
|
|
2058
|
-
* // packages/<app>/holocron.config.ts
|
|
2059
|
-
* tasks: [{ name: "delivery.deploy", with: { on: "release", channel: "alpha" } }];
|
|
2060
|
-
* ```
|
|
2061
|
-
*
|
|
2062
|
-
* `channel` defaults to `main` (a stable release); set a prerelease identifier
|
|
2063
|
-
* such as `alpha` to deploy those releases instead.
|
|
2064
|
-
*
|
|
2065
|
-
* The shared `delivery.publish` workflow runs this (`holocron deploy on-release`)
|
|
2066
|
-
* as its own `deploy` job after the release job, so a deploy is a separate,
|
|
2067
|
-
* re-runnable check rather than a step buried in the release log. Each package
|
|
2068
|
-
* with such a task deploys when the release is on its `channel` and changes a file under its paths: the package itself plus its
|
|
2069
|
-
* `workspace:*` dependencies ({@link defaultPaths}), or an explicit
|
|
2070
|
-
* `with.paths`. It first waits for npm to serve the release's new versions of
|
|
2071
|
-
* those dependencies ({@link waitForPublished}) — the registry can lag a version
|
|
2072
|
-
* it has just accepted, and the deploy installs them — then runs
|
|
2073
|
-
* `pnpm --filter <package> delivery.deploy`.
|
|
2074
|
-
*
|
|
2075
|
-
* The release is already published by then, so a failed deploy can't undo it;
|
|
2076
|
-
* it fails this job instead, loudly and re-runnable on its own. A task is
|
|
2077
|
-
* `skip`ped (not failed) only when it doesn't apply to this release.
|
|
2078
|
-
*
|
|
2079
|
-
* A `delivery.deploy` task with `on: "release"` generates no CI workflow.
|
|
2080
|
-
*/
|
|
2099
|
+
//#region src/tasks/deploy-on-release/constants.ts
|
|
2081
2100
|
/** A stable release (no prerelease identifier) is on `main`, the default channel for a task. */
|
|
2082
2101
|
const MAIN_CHANNEL = "main";
|
|
2083
2102
|
const DEPLOY_TASK = "delivery.deploy";
|
|
2084
2103
|
/** How long to wait for npm to serve the release's versions, and how often to ask. */
|
|
2085
2104
|
const WAIT_TIMEOUT_MS = 3e5;
|
|
2086
2105
|
const WAIT_INTERVAL_MS = 1e4;
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
try {
|
|
2090
|
-
folders = listDirs(dirPath);
|
|
2091
|
-
} catch {
|
|
2092
|
-
return [];
|
|
2093
|
-
}
|
|
2094
|
-
return folders.flatMap((folder) => {
|
|
2095
|
-
try {
|
|
2096
|
-
const manifest = readJson(join(dirPath, folder, "package.json"));
|
|
2097
|
-
if (!manifest.name) return [];
|
|
2098
|
-
return [{
|
|
2099
|
-
name: manifest.name,
|
|
2100
|
-
dir: join(dirPath, folder),
|
|
2101
|
-
version: manifest.version,
|
|
2102
|
-
dependencies: manifest.dependencies ?? {}
|
|
2103
|
-
}];
|
|
2104
|
-
} catch {
|
|
2105
|
-
return [];
|
|
2106
|
-
}
|
|
2107
|
-
});
|
|
2108
|
-
}
|
|
2109
|
-
/**
|
|
2110
|
-
* Every package under `<repoRoot>/packages/*` and `<repoRoot>/plugins/*`
|
|
2111
|
-
* with a readable, named `package.json` — `holocron`'s own layout splits
|
|
2112
|
-
* vendor plugin packages into `plugins/*`, sibling to `packages/*`.
|
|
2113
|
-
*/
|
|
2114
|
-
function workspacePackages(repoRoot, tree) {
|
|
2115
|
-
return [...scanWorkspaceDir(join(repoRoot, "packages"), tree), ...scanWorkspaceDir(join(repoRoot, "plugins"), tree)];
|
|
2116
|
-
}
|
|
2106
|
+
//#endregion
|
|
2107
|
+
//#region src/tasks/deploy-on-release/workspace-dependencies.ts
|
|
2117
2108
|
/** The `workspace:*` dependencies of `pkg`, as workspace packages (with their current version). */
|
|
2118
2109
|
function workspaceDependencies(pkg, all) {
|
|
2119
2110
|
const names = new Set(Object.entries(pkg.dependencies).filter(([, spec]) => String(spec).startsWith("workspace:")).map(([name]) => name));
|
|
2120
2111
|
return all.filter((candidate) => names.has(candidate.name));
|
|
2121
2112
|
}
|
|
2113
|
+
//#endregion
|
|
2114
|
+
//#region src/tasks/deploy-on-release/default-paths.ts
|
|
2122
2115
|
/**
|
|
2123
2116
|
* The paths a release must touch to redeploy `pkg` when its manifest doesn't
|
|
2124
2117
|
* list them: its own folder plus the folder of every
|
|
@@ -2130,6 +2123,8 @@ function workspaceDependencies(pkg, all) {
|
|
|
2130
2123
|
function defaultPaths(repoRoot, pkg, all) {
|
|
2131
2124
|
return [pkg, ...workspaceDependencies(pkg, all)].map(({ dir }) => `${relative(repoRoot, dir)}/`);
|
|
2132
2125
|
}
|
|
2126
|
+
//#endregion
|
|
2127
|
+
//#region src/tasks/deploy-on-release/deploy-targets.ts
|
|
2133
2128
|
/**
|
|
2134
2129
|
* The packages whose manifest declares a release-time `delivery.deploy`
|
|
2135
2130
|
* (`with.on === "release"`), with the task's options resolved.
|
|
@@ -2137,7 +2132,7 @@ function defaultPaths(repoRoot, pkg, all) {
|
|
|
2137
2132
|
function deployTargets(packages, manifests) {
|
|
2138
2133
|
return packages.flatMap((pkg) => (manifests.get(pkg.dir)?.tasks ?? []).flatMap((item) => {
|
|
2139
2134
|
const entry = normalizeTaskEntry(item);
|
|
2140
|
-
if (entry.name !==
|
|
2135
|
+
if (entry.name !== "delivery.deploy" || entry.with?.["on"] !== "release") return [];
|
|
2141
2136
|
const { channel, paths } = entry.with;
|
|
2142
2137
|
return [{
|
|
2143
2138
|
pkg,
|
|
@@ -2146,6 +2141,32 @@ function deployTargets(packages, manifests) {
|
|
|
2146
2141
|
}];
|
|
2147
2142
|
}));
|
|
2148
2143
|
}
|
|
2144
|
+
//#endregion
|
|
2145
|
+
//#region src/tasks/deploy-on-release/should-deploy.ts
|
|
2146
|
+
/**
|
|
2147
|
+
* @param channel The release's channel: its prerelease identifier (`alpha`), or `main` for a stable release.
|
|
2148
|
+
* @param changedFiles `undefined` when there's no previous release to diff against.
|
|
2149
|
+
*/
|
|
2150
|
+
function shouldDeploy({ channel, configuredChannel, changedFiles, paths }) {
|
|
2151
|
+
if (channel !== configuredChannel) return {
|
|
2152
|
+
deploy: false,
|
|
2153
|
+
reason: `channel "${channel}" isn't "${configuredChannel}"`
|
|
2154
|
+
};
|
|
2155
|
+
if (changedFiles === void 0) return {
|
|
2156
|
+
deploy: true,
|
|
2157
|
+
reason: "no previous release to diff against"
|
|
2158
|
+
};
|
|
2159
|
+
const touched = changedFiles.find((file) => paths.some((path) => file.startsWith(path)));
|
|
2160
|
+
return touched ? {
|
|
2161
|
+
deploy: true,
|
|
2162
|
+
reason: `the release changes ${touched}`
|
|
2163
|
+
} : {
|
|
2164
|
+
deploy: false,
|
|
2165
|
+
reason: `the release changes nothing under ${paths.join(", ")}`
|
|
2166
|
+
};
|
|
2167
|
+
}
|
|
2168
|
+
//#endregion
|
|
2169
|
+
//#region src/tasks/deploy-on-release/wait-for-published.ts
|
|
2149
2170
|
/**
|
|
2150
2171
|
* Waits until npm serves every `name@version` in `packages`, asking with
|
|
2151
2172
|
* `npm view --prefer-online` (so npm's local cache can't answer for the
|
|
@@ -2171,35 +2192,70 @@ async function waitForPublished({ packages, run, sleep, now, cwd, timeoutMs = WA
|
|
|
2171
2192
|
await sleep(intervalMs);
|
|
2172
2193
|
}
|
|
2173
2194
|
}
|
|
2195
|
+
//#endregion
|
|
2196
|
+
//#region src/tasks/deploy-on-release/workspace-packages.ts
|
|
2197
|
+
function scanWorkspaceDir(dirPath, { readJson, listDirs }) {
|
|
2198
|
+
let folders;
|
|
2199
|
+
try {
|
|
2200
|
+
folders = listDirs(dirPath);
|
|
2201
|
+
} catch {
|
|
2202
|
+
return [];
|
|
2203
|
+
}
|
|
2204
|
+
return folders.flatMap((folder) => {
|
|
2205
|
+
try {
|
|
2206
|
+
const manifest = readJson(join(dirPath, folder, "package.json"));
|
|
2207
|
+
if (!manifest.name) return [];
|
|
2208
|
+
return [{
|
|
2209
|
+
name: manifest.name,
|
|
2210
|
+
dir: join(dirPath, folder),
|
|
2211
|
+
version: manifest.version,
|
|
2212
|
+
dependencies: manifest.dependencies ?? {}
|
|
2213
|
+
}];
|
|
2214
|
+
} catch {
|
|
2215
|
+
return [];
|
|
2216
|
+
}
|
|
2217
|
+
});
|
|
2218
|
+
}
|
|
2174
2219
|
/**
|
|
2175
|
-
*
|
|
2176
|
-
*
|
|
2220
|
+
* Every package under `<repoRoot>/packages/*` and `<repoRoot>/plugins/*`
|
|
2221
|
+
* with a readable, named `package.json` — `holocron`'s own layout splits
|
|
2222
|
+
* vendor plugin packages into `plugins/*`, sibling to `packages/*`.
|
|
2177
2223
|
*/
|
|
2178
|
-
function
|
|
2179
|
-
|
|
2180
|
-
deploy: false,
|
|
2181
|
-
reason: `channel "${channel}" isn't "${configuredChannel}"`
|
|
2182
|
-
};
|
|
2183
|
-
if (changedFiles === void 0) return {
|
|
2184
|
-
deploy: true,
|
|
2185
|
-
reason: "no previous release to diff against"
|
|
2186
|
-
};
|
|
2187
|
-
const touched = changedFiles.find((file) => paths.some((path) => file.startsWith(path)));
|
|
2188
|
-
return touched ? {
|
|
2189
|
-
deploy: true,
|
|
2190
|
-
reason: `the release changes ${touched}`
|
|
2191
|
-
} : {
|
|
2192
|
-
deploy: false,
|
|
2193
|
-
reason: `the release changes nothing under ${paths.join(", ")}`
|
|
2194
|
-
};
|
|
2224
|
+
function workspacePackages(repoRoot, tree) {
|
|
2225
|
+
return [...scanWorkspaceDir(join(repoRoot, "packages"), tree), ...scanWorkspaceDir(join(repoRoot, "plugins"), tree)];
|
|
2195
2226
|
}
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2227
|
+
//#endregion
|
|
2228
|
+
//#region src/tasks/deploy-on-release/deploy-on-release.ts
|
|
2229
|
+
/**
|
|
2230
|
+
* Deploy workspace packages once a release has published (holocron#930).
|
|
2231
|
+
*
|
|
2232
|
+
* A package opts in with a `delivery.deploy` task in its own `holocron.config`
|
|
2233
|
+
* whose `with.on` is `"release"`:
|
|
2234
|
+
*
|
|
2235
|
+
* ```ts
|
|
2236
|
+
* // packages/<app>/holocron.config.ts
|
|
2237
|
+
* tasks: [{ name: "delivery.deploy", with: { on: "release", channel: "alpha" } }];
|
|
2238
|
+
* ```
|
|
2239
|
+
*
|
|
2240
|
+
* `channel` defaults to `main` (a stable release); set a prerelease identifier
|
|
2241
|
+
* such as `alpha` to deploy those releases instead.
|
|
2242
|
+
*
|
|
2243
|
+
* The shared `delivery.publish` workflow runs this (`holocron deploy on-release`)
|
|
2244
|
+
* as its own `deploy` job after the release job, so a deploy is a separate,
|
|
2245
|
+
* re-runnable check rather than a step buried in the release log. Each package
|
|
2246
|
+
* with such a task deploys when the release is on its `channel` and changes a file under its paths: the package itself plus its
|
|
2247
|
+
* `workspace:*` dependencies ({@link defaultPaths}), or an explicit
|
|
2248
|
+
* `with.paths`. It first waits for npm to serve the release's new versions of
|
|
2249
|
+
* those dependencies ({@link waitForPublished}) — the registry can lag a version
|
|
2250
|
+
* it has just accepted, and the deploy installs them — then runs
|
|
2251
|
+
* `pnpm --filter <package> delivery.deploy`.
|
|
2252
|
+
*
|
|
2253
|
+
* The release is already published by then, so a failed deploy can't undo it;
|
|
2254
|
+
* it fails this job instead, loudly and re-runnable on its own. A task is
|
|
2255
|
+
* `skip`ped (not failed) only when it doesn't apply to this release.
|
|
2256
|
+
*
|
|
2257
|
+
* A `delivery.deploy` task with `on: "release"` generates no CI workflow.
|
|
2258
|
+
*/
|
|
2203
2259
|
/**
|
|
2204
2260
|
* Deploys every package whose manifest declares a release-time deploy that
|
|
2205
2261
|
* applies to this release. Never throws for a deploy problem: each package's
|
|
@@ -2207,7 +2263,7 @@ const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
2207
2263
|
*/
|
|
2208
2264
|
async function deployOnRelease(options, { run = defaultRun, readJson = defaultReadJson, listDirs = defaultListDirs, sleep = defaultSleep, now = Date.now, loadTasks = loadTasksConfig } = {}) {
|
|
2209
2265
|
const { cwd, from, print } = options;
|
|
2210
|
-
const channel = options.channel ||
|
|
2266
|
+
const channel = options.channel || "main";
|
|
2211
2267
|
const to = options.to ?? "HEAD";
|
|
2212
2268
|
const packages = workspacePackages(cwd, {
|
|
2213
2269
|
readJson,
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { loadConfigFile, mergeConfig } from "@theholocron/datapad";
|
|
2
2
|
//#region src/config/schema.ts
|
|
3
|
+
/**
|
|
4
|
+
* The `tasks` config shape — the single manifest of what a repo runs.
|
|
5
|
+
* Read from a dedicated `astromech.config.*` file or the `tasks` key of
|
|
6
|
+
* `holocron.config.*` (see {@link loadTasksConfig}).
|
|
7
|
+
*
|
|
8
|
+
* Runtime validation is the loader's job, not this type's — the type only
|
|
9
|
+
* describes what an author writes.
|
|
10
|
+
*/
|
|
3
11
|
/** Normalise a `TaskConfigItem` to a full {@link TaskEntry} with defaults applied. */
|
|
4
12
|
function normalizeTaskEntry(item) {
|
|
5
13
|
return {
|
|
@@ -9,6 +17,25 @@ function normalizeTaskEntry(item) {
|
|
|
9
17
|
};
|
|
10
18
|
}
|
|
11
19
|
//#endregion
|
|
20
|
+
//#region src/config/coerce-tasks.ts
|
|
21
|
+
/** A bare item array is shorthand for `{ tasks: [...] }`. */
|
|
22
|
+
function coerceTasks(value) {
|
|
23
|
+
if (value == null) return void 0;
|
|
24
|
+
return Array.isArray(value) ? { tasks: value } : value;
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
//#region src/config/merge-tasks-layers.ts
|
|
28
|
+
/**
|
|
29
|
+
* The merge step of {@link loadTasksConfig}, for callers that load the two
|
|
30
|
+
* sources some other way (Sentinel reads them from the GitHub API, not
|
|
31
|
+
* from disk, holocron#916): `holocron.config.*`'s `tasks` value first,
|
|
32
|
+
* then a dedicated `astromech.config.*`'s default export merged on top.
|
|
33
|
+
* Either may be `undefined` (source absent); returns `{}` when both are.
|
|
34
|
+
*/
|
|
35
|
+
function mergeTasksLayers(parentTasks, dedicated) {
|
|
36
|
+
return [coerceTasks(parentTasks), coerceTasks(dedicated)].filter((layer) => layer !== void 0).reduce((acc, layer) => mergeConfig(acc, layer), {});
|
|
37
|
+
}
|
|
38
|
+
//#endregion
|
|
12
39
|
//#region src/config/load.ts
|
|
13
40
|
/**
|
|
14
41
|
* Resolve the task manifest for a repo: the `tasks` key of
|
|
@@ -32,19 +59,5 @@ async function loadTasksConfig(cwd) {
|
|
|
32
59
|
walkUp: true
|
|
33
60
|
}))?.config.tasks, dedicated?.config);
|
|
34
61
|
}
|
|
35
|
-
/**
|
|
36
|
-
* The merge step of {@link loadTasksConfig}, for callers that load the two
|
|
37
|
-
* sources some other way (Sentinel reads them from the GitHub API, not
|
|
38
|
-
* from disk, holocron#916): `holocron.config.*`'s `tasks` value first,
|
|
39
|
-
* then a dedicated `astromech.config.*`'s default export merged on top.
|
|
40
|
-
* Either may be `undefined` (source absent); returns `{}` when both are.
|
|
41
|
-
*/
|
|
42
|
-
function mergeTasksLayers(parentTasks, dedicated) {
|
|
43
|
-
return [coerce(parentTasks), coerce(dedicated)].filter((layer) => layer !== void 0).reduce((acc, layer) => mergeConfig(acc, layer), {});
|
|
44
|
-
}
|
|
45
|
-
function coerce(value) {
|
|
46
|
-
if (value == null) return void 0;
|
|
47
|
-
return Array.isArray(value) ? { tasks: value } : value;
|
|
48
|
-
}
|
|
49
62
|
//#endregion
|
|
50
63
|
export { mergeTasksLayers as n, normalizeTaskEntry as r, loadTasksConfig as t };
|
|
@@ -1,12 +1,4 @@
|
|
|
1
|
-
//#region src/config/schema.d.ts
|
|
2
|
-
/**
|
|
3
|
-
* The `tasks` config shape — the single manifest of what a repo runs.
|
|
4
|
-
* Read from a dedicated `astromech.config.*` file or the `tasks` key of
|
|
5
|
-
* `holocron.config.*` (see {@link loadTasksConfig}).
|
|
6
|
-
*
|
|
7
|
-
* Runtime validation is the loader's job, not this type's — the type only
|
|
8
|
-
* describes what an author writes.
|
|
9
|
-
*/
|
|
1
|
+
//#region src/config/schema.type.d.ts
|
|
10
2
|
interface TaskEntry {
|
|
11
3
|
/** Registry task name — `verification.unitTests`, `sourceQuality.staticAnalysis`, `delivery.build`, … */
|
|
12
4
|
name: string;
|
|
@@ -71,7 +63,5 @@ interface TasksConfig {
|
|
|
71
63
|
prePush?: boolean;
|
|
72
64
|
};
|
|
73
65
|
}
|
|
74
|
-
/** Normalise a `TaskConfigItem` to a full {@link TaskEntry} with defaults applied. */
|
|
75
|
-
declare function normalizeTaskEntry(item: TaskConfigItem): Required<Pick<TaskEntry, "name" | "ci" | "local">> & TaskEntry;
|
|
76
66
|
//#endregion
|
|
77
|
-
export {
|
|
67
|
+
export { TaskEntry as n, TasksConfig as r, TaskConfigItem as t };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@theholocron/astromech",
|
|
3
|
-
"version": "5.0.0-alpha.
|
|
3
|
+
"version": "5.0.0-alpha.148",
|
|
4
4
|
"description": "The Holocron task runner — one task manifest drives `holocron run`, `holocron run ci`, the CI workflows, package.json scripts, linters, and required checks.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ci",
|
|
@@ -37,8 +37,10 @@
|
|
|
37
37
|
"dist"
|
|
38
38
|
],
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@theholocron/
|
|
41
|
-
"@theholocron/
|
|
40
|
+
"@theholocron/fs-utils": "^1.12.0",
|
|
41
|
+
"@theholocron/json-utils": "^1.12.0",
|
|
42
|
+
"@theholocron/pkg-utils": "^1.12.0",
|
|
43
|
+
"@theholocron/datapad": "5.0.0-alpha.148"
|
|
42
44
|
},
|
|
43
45
|
"devDependencies": {
|
|
44
46
|
"@theholocron/eslint-config": "^8.7.1",
|
|
@@ -54,7 +56,7 @@
|
|
|
54
56
|
"tsdown": "^0.22.14",
|
|
55
57
|
"typescript": "^5.9.3",
|
|
56
58
|
"vitest": "^4.1.11",
|
|
57
|
-
"@theholocron/rollup-plugin-transform-template": "5.0.0-alpha.
|
|
59
|
+
"@theholocron/rollup-plugin-transform-template": "5.0.0-alpha.148"
|
|
58
60
|
},
|
|
59
61
|
"engines": {
|
|
60
62
|
"node": ">=22"
|