@theholocron/astromech 5.0.0-alpha.13 → 5.0.0-alpha.133
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 +57 -31
- package/dist/config/index.d.mts +21 -2
- package/dist/config/index.mjs +3 -25
- package/dist/index.d.mts +335 -336
- package/dist/index.mjs +2004 -1400
- package/dist/load-ytspboBj.mjs +50 -0
- package/dist/{schema-D01VYg43.d.mts → schema-BDld3xq1.d.mts} +5 -5
- package/package.json +8 -8
- package/dist/schema-Cf5dfaDO.mjs +0 -11
package/dist/index.mjs
CHANGED
|
@@ -1,1513 +1,1827 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { r as normalizeTaskEntry, t as loadTasksConfig } from "./load-ytspboBj.mjs";
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
3
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
//#region src/
|
|
6
|
-
|
|
7
|
-
"verification.unitTests": {
|
|
8
|
-
local: {
|
|
9
|
-
tool: "vitest",
|
|
10
|
-
args: ["run"]
|
|
11
|
-
},
|
|
12
|
-
flags: { vitest: ["--coverage"] },
|
|
13
|
-
turbo: {
|
|
14
|
-
inputs: [
|
|
15
|
-
"src/**",
|
|
16
|
-
"vitest.config.ts",
|
|
17
|
-
"vitest.config.js",
|
|
18
|
-
"vitest.config.mjs",
|
|
19
|
-
"package.json"
|
|
20
|
-
],
|
|
21
|
-
outputs: ["test-report.junit.xml", "coverage/**"],
|
|
22
|
-
dependsOn: ["delivery.build", "^delivery.build"]
|
|
23
|
-
}
|
|
24
|
-
},
|
|
25
|
-
"verification.typeSafety": {
|
|
26
|
-
local: {
|
|
27
|
-
tool: "tsc",
|
|
28
|
-
args: ["--noEmit"]
|
|
29
|
-
},
|
|
30
|
-
turbo: {
|
|
31
|
-
inputs: ["src/**", "tsconfig.json"],
|
|
32
|
-
outputs: [],
|
|
33
|
-
dependsOn: ["^delivery.build"]
|
|
34
|
-
}
|
|
35
|
-
},
|
|
36
|
-
"verification.performance": { local: { detect: [{
|
|
37
|
-
when: /^lighthouse\.config\.(cjs|js|mjs|ts)$/,
|
|
38
|
-
tool: "lhci",
|
|
39
|
-
args: ["autorun"]
|
|
40
|
-
}, {
|
|
41
|
-
when: /^lighthouserc\.(cjs|js|mjs|json|yml|yaml)$/,
|
|
42
|
-
tool: "lhci",
|
|
43
|
-
args: ["autorun"]
|
|
44
|
-
}] } },
|
|
45
|
-
"sourceQuality.staticAnalysis": {
|
|
46
|
-
local: null,
|
|
47
|
-
linterGroup: [
|
|
48
|
-
"eslint",
|
|
49
|
-
"actionlint",
|
|
50
|
-
"git-merge-conflict-markers"
|
|
51
|
-
],
|
|
52
|
-
turbo: {
|
|
53
|
-
inputs: [
|
|
54
|
-
"src/**",
|
|
55
|
-
"eslint.config.ts",
|
|
56
|
-
"eslint.config.js",
|
|
57
|
-
"eslint.config.mjs",
|
|
58
|
-
"eslint.config.cjs"
|
|
59
|
-
],
|
|
60
|
-
outputs: [],
|
|
61
|
-
dependsOn: ["^delivery.build"]
|
|
62
|
-
}
|
|
63
|
-
},
|
|
64
|
-
"sourceQuality.formatting": {
|
|
65
|
-
local: null,
|
|
66
|
-
linterGroup: [
|
|
67
|
-
"prettier",
|
|
68
|
-
"editorconfig",
|
|
69
|
-
"markdownlint"
|
|
70
|
-
]
|
|
71
|
-
},
|
|
72
|
-
"sourceQuality.structuredDataValidation": {
|
|
73
|
-
local: null,
|
|
74
|
-
linterGroup: ["yamllint"]
|
|
75
|
-
},
|
|
76
|
-
"sourceQuality.deadCodeAnalysis": { local: { tool: "knip" } },
|
|
77
|
-
"security.secretDetection": {
|
|
78
|
-
local: null,
|
|
79
|
-
linterGroup: ["gitleaks"]
|
|
80
|
-
},
|
|
81
|
-
"security.codeScanning": { local: null },
|
|
82
|
-
"delivery.build": {
|
|
83
|
-
local: { detect: [
|
|
84
|
-
{
|
|
85
|
-
when: /^tsdown\.config\.(ts|js|mjs|cjs)$/,
|
|
86
|
-
tool: "tsdown"
|
|
87
|
-
},
|
|
88
|
-
{
|
|
89
|
-
when: /^vite\.config\.(ts|js|mjs|cjs)$/,
|
|
90
|
-
tool: "vite",
|
|
91
|
-
args: ["build"]
|
|
92
|
-
},
|
|
93
|
-
{
|
|
94
|
-
when: /^rollup\.config\.(ts|js|mjs|cjs)$/,
|
|
95
|
-
tool: "rollup",
|
|
96
|
-
args: ["-c"]
|
|
97
|
-
},
|
|
98
|
-
{
|
|
99
|
-
when: /^tsconfig\.json$/,
|
|
100
|
-
tool: "tsc",
|
|
101
|
-
args: ["-b"]
|
|
102
|
-
}
|
|
103
|
-
] },
|
|
104
|
-
turbo: {
|
|
105
|
-
inputs: [
|
|
106
|
-
"src/**",
|
|
107
|
-
"package.json",
|
|
108
|
-
"tsdown.config.ts",
|
|
109
|
-
"vite.config.ts",
|
|
110
|
-
"rollup.config.ts",
|
|
111
|
-
"tsconfig.json"
|
|
112
|
-
],
|
|
113
|
-
outputs: ["dist/**"],
|
|
114
|
-
dependsOn: ["^delivery.build"]
|
|
115
|
-
}
|
|
116
|
-
},
|
|
117
|
-
"delivery.publish": {
|
|
118
|
-
local: null,
|
|
119
|
-
preview: true
|
|
120
|
-
},
|
|
121
|
-
"delivery.deploy": {
|
|
122
|
-
local: null,
|
|
123
|
-
preview: true
|
|
124
|
-
},
|
|
125
|
-
"delivery.bundleSize": { local: null },
|
|
126
|
-
"platform.repoSync": { local: { command: "sync" } },
|
|
127
|
-
"platform.commitStandards": {
|
|
128
|
-
local: null,
|
|
129
|
-
linterGroup: ["commitlint"]
|
|
130
|
-
},
|
|
131
|
-
"platform.repoValidation": {
|
|
132
|
-
local: null,
|
|
133
|
-
jobs: {
|
|
134
|
-
adrs: {
|
|
135
|
-
local: {
|
|
136
|
-
tool: "node",
|
|
137
|
-
args: ["scripts/validate-adrs.mjs"]
|
|
138
|
-
},
|
|
139
|
-
checkContext: "platform.repoValidation / Validate ADRs and specs"
|
|
140
|
-
},
|
|
141
|
-
registry: {
|
|
142
|
-
local: {
|
|
143
|
-
tool: "node",
|
|
144
|
-
args: ["scripts/validate-registry.mjs"]
|
|
145
|
-
},
|
|
146
|
-
checkContext: "platform.repoValidation / Validate registry consistency"
|
|
147
|
-
},
|
|
148
|
-
docsPresence: {
|
|
149
|
-
local: {
|
|
150
|
-
tool: "node",
|
|
151
|
-
args: ["scripts/validate-docs-presence.mjs"]
|
|
152
|
-
},
|
|
153
|
-
checkContext: "platform.repoValidation / Validate docs presence"
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
},
|
|
157
|
-
"knowledge.wiki": {
|
|
158
|
-
local: { command: "sync-wiki" },
|
|
159
|
-
preview: true
|
|
160
|
-
},
|
|
161
|
-
"knowledge.docs": {
|
|
162
|
-
local: null,
|
|
163
|
-
preview: true
|
|
164
|
-
},
|
|
165
|
-
"knowledge.components": {
|
|
166
|
-
local: null,
|
|
167
|
-
preview: true
|
|
168
|
-
}
|
|
169
|
-
};
|
|
170
|
-
/** Every task name the registry knows. */
|
|
171
|
-
const KNOWN_TASKS = new Set(Object.keys(TASKS));
|
|
172
|
-
/**
|
|
173
|
-
* The order `holocron ci` runs tasks in — cheapest / fastest signal first, so
|
|
174
|
-
* an agent or a `pre-push` hook fails early. Tasks not listed here run last, in
|
|
175
|
-
* manifest order. (The generated thin callers carry no `needs:` — cross-workflow
|
|
176
|
-
* ordering lives in `theholocron/.github` — so `holocron ci` declares its own.)
|
|
177
|
-
*/
|
|
178
|
-
const CI_ORDER = [
|
|
179
|
-
"verification.typeSafety",
|
|
180
|
-
"sourceQuality.staticAnalysis",
|
|
181
|
-
"sourceQuality.formatting",
|
|
182
|
-
"sourceQuality.structuredDataValidation",
|
|
183
|
-
"security.secretDetection",
|
|
184
|
-
"platform.commitStandards",
|
|
185
|
-
"platform.repoValidation",
|
|
186
|
-
"verification.unitTests",
|
|
187
|
-
"delivery.build",
|
|
188
|
-
"sourceQuality.deadCodeAnalysis",
|
|
189
|
-
"delivery.bundleSize",
|
|
190
|
-
"verification.performance",
|
|
191
|
-
"security.codeScanning",
|
|
192
|
-
"delivery.deploy"
|
|
193
|
-
];
|
|
4
|
+
import { join, relative } from "node:path";
|
|
5
|
+
//#region src/templates/codecov/codecov.yml
|
|
6
|
+
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";
|
|
194
7
|
//#endregion
|
|
195
|
-
//#region src/
|
|
8
|
+
//#region src/generators/codecov.ts
|
|
196
9
|
/**
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
10
|
+
* `codecov.yml` generation — a manifest-derived artifact (component `paths`
|
|
11
|
+
* come from workspace packages; the status targets track the `test` task's
|
|
12
|
+
* coverage setup), the same category as the reusable `test.yml` this package
|
|
13
|
+
* also generates. Moved out of `@theholocron/cli` (theholocron/holocron#650)
|
|
14
|
+
* — `holocron repo setup`'s codecov step now calls {@link codecovConfig} instead
|
|
15
|
+
* of importing this logic directly.
|
|
200
16
|
*/
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
prettier: {
|
|
220
|
-
validate: [
|
|
221
|
-
"VALIDATE_JAVASCRIPT_PRETTIER",
|
|
222
|
-
"VALIDATE_JSX_PRETTIER",
|
|
223
|
-
"VALIDATE_TYPESCRIPT_PRETTIER",
|
|
224
|
-
"VALIDATE_TSX",
|
|
225
|
-
"VALIDATE_MARKDOWN_PRETTIER"
|
|
226
|
-
],
|
|
227
|
-
fix: [
|
|
228
|
-
"FIX_JAVASCRIPT_PRETTIER",
|
|
229
|
-
"FIX_JSX_PRETTIER",
|
|
230
|
-
"FIX_TYPESCRIPT_PRETTIER",
|
|
231
|
-
"FIX_TSX",
|
|
232
|
-
"FIX_MARKDOWN_PRETTIER"
|
|
233
|
-
],
|
|
234
|
-
always: true,
|
|
235
|
-
localBin: "prettier",
|
|
236
|
-
localArgs: ["--check", "."],
|
|
237
|
-
configInput: "prettier-config"
|
|
238
|
-
},
|
|
239
|
-
yamllint: {
|
|
240
|
-
validate: ["VALIDATE_YAML"],
|
|
241
|
-
always: true,
|
|
242
|
-
localBin: "yamllint",
|
|
243
|
-
localArgs: ["."],
|
|
244
|
-
installHint: "brew install yamllint",
|
|
245
|
-
configInput: "yaml-config"
|
|
246
|
-
},
|
|
247
|
-
actionlint: {
|
|
248
|
-
validate: ["VALIDATE_GITHUB_ACTIONS"],
|
|
249
|
-
always: true,
|
|
250
|
-
localBin: "actionlint",
|
|
251
|
-
localArgs: [],
|
|
252
|
-
installHint: "brew install actionlint"
|
|
253
|
-
},
|
|
254
|
-
gitleaks: {
|
|
255
|
-
validate: ["VALIDATE_GITLEAKS"],
|
|
256
|
-
always: true,
|
|
257
|
-
localBin: "gitleaks",
|
|
258
|
-
localArgs: [
|
|
259
|
-
"dir",
|
|
260
|
-
"--no-banner",
|
|
261
|
-
"--config",
|
|
262
|
-
".gitleaks.toml"
|
|
263
|
-
],
|
|
264
|
-
installHint: "brew install gitleaks"
|
|
265
|
-
},
|
|
266
|
-
editorconfig: {
|
|
267
|
-
validate: ["VALIDATE_EDITORCONFIG"],
|
|
268
|
-
always: true,
|
|
269
|
-
localBin: "editorconfig-checker",
|
|
270
|
-
localArgs: [],
|
|
271
|
-
installHint: "brew install editorconfig-checker"
|
|
272
|
-
},
|
|
273
|
-
commitlint: {
|
|
274
|
-
validate: ["VALIDATE_GIT_COMMITLINT"],
|
|
275
|
-
always: true
|
|
276
|
-
},
|
|
277
|
-
"git-merge-conflict-markers": {
|
|
278
|
-
validate: ["VALIDATE_GIT_MERGE_CONFLICT_MARKERS"],
|
|
279
|
-
always: true
|
|
280
|
-
},
|
|
281
|
-
markdownlint: {
|
|
282
|
-
validate: ["VALIDATE_MARKDOWN"],
|
|
283
|
-
localBin: "markdownlint-cli2",
|
|
284
|
-
localArgs: ["**/*.md"],
|
|
285
|
-
detect: [
|
|
286
|
-
".markdownlint.json",
|
|
287
|
-
".markdownlint.jsonc",
|
|
288
|
-
".markdownlint.yaml",
|
|
289
|
-
".markdownlint.yml",
|
|
290
|
-
".markdownlint-cli2.jsonc",
|
|
291
|
-
".markdownlint-cli2.yaml",
|
|
292
|
-
".markdownlint-cli2.mjs"
|
|
293
|
-
]
|
|
294
|
-
}
|
|
295
|
-
};
|
|
296
|
-
/** Every linter name the registry knows. */
|
|
297
|
-
const LINTER_NAMES = new Set(Object.keys(LINTERS));
|
|
17
|
+
const INDIVIDUAL_COMPONENTS_MARKER = " individual_components:";
|
|
18
|
+
function scaffoldHeader() {
|
|
19
|
+
return [
|
|
20
|
+
`# Scaffolded by holocron repo setup — edit this file freely.`,
|
|
21
|
+
`# Source: theholocron/holocron · packages/astromech/src/generators/codecov.ts`,
|
|
22
|
+
``
|
|
23
|
+
].join("\n");
|
|
24
|
+
}
|
|
25
|
+
function codecovComponentBlock(packages) {
|
|
26
|
+
if (packages.length === 0) return "\n []\n";
|
|
27
|
+
return "\n" + packages.flatMap(({ slug, dir }) => [
|
|
28
|
+
` - component_id: ${slug}`,
|
|
29
|
+
` name: "${slug}"`,
|
|
30
|
+
` paths:`,
|
|
31
|
+
` - ${dir ?? "packages"}/${slug}/**`,
|
|
32
|
+
``
|
|
33
|
+
]).join("\n");
|
|
34
|
+
}
|
|
298
35
|
/**
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
*
|
|
302
|
-
* root. Either way, a `detect`-gated linter (eslint, markdownlint) only makes
|
|
303
|
-
* the final set when its config file is actually present — a repo can't run
|
|
304
|
-
* eslint without an `eslint.config.*`, and super-linter FATALs if you ask it
|
|
305
|
-
* to (theholocron/holocron#654). `always` linters are unconditional. Result
|
|
306
|
-
* is ordered by {@link LINTERS} declaration order.
|
|
36
|
+
* Ensure every codecov status default carries `if_not_found: success`, so an
|
|
37
|
+
* infra-only PR (dep bump, config, lockfile — no coverable change) passes
|
|
38
|
+
* instead of hanging forever on a required check codecov never posts.
|
|
307
39
|
*
|
|
308
|
-
*
|
|
309
|
-
*
|
|
40
|
+
* Injects the key on the line after each `target:` that sits inside a status
|
|
41
|
+
* block (`coverage.status.*.default` or a `statuses:` list item — indent ≥ 8,
|
|
42
|
+
* or ≥ 8 for a `- type:` list entry), when the immediate sibling lines do not
|
|
43
|
+
* already declare it.
|
|
310
44
|
*/
|
|
311
|
-
function
|
|
312
|
-
const
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
45
|
+
function ensureIfNotFound(yaml) {
|
|
46
|
+
const lines = yaml.split("\n");
|
|
47
|
+
const out = [];
|
|
48
|
+
for (let i = 0; i < lines.length; i++) {
|
|
49
|
+
const line = lines[i];
|
|
50
|
+
out.push(line);
|
|
51
|
+
const m = /^(\s{8,})target:\s+\S+\s*$/.exec(line);
|
|
52
|
+
if (!m) continue;
|
|
53
|
+
const indent = m[1];
|
|
54
|
+
let alreadySet = false;
|
|
55
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
56
|
+
const sib = lines[j];
|
|
57
|
+
if (sib.trim() === "") continue;
|
|
58
|
+
const sibIndent = sib.match(/^\s*/)[0].length;
|
|
59
|
+
if (sibIndent < indent.length) break;
|
|
60
|
+
if (sibIndent === indent.length && /^\s*if_not_found\s*:/.test(sib)) {
|
|
61
|
+
alreadySet = true;
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
if (sibIndent === indent.length && /^\s*(- |\w)/.test(sib) && !/^\s*(threshold|flags|paths|branches)\s*:/.test(sib)) break;
|
|
65
|
+
}
|
|
66
|
+
if (!alreadySet) out.push(`${indent}if_not_found: success`);
|
|
323
67
|
}
|
|
324
|
-
return
|
|
325
|
-
name,
|
|
326
|
-
def: LINTERS[name]
|
|
327
|
-
}));
|
|
68
|
+
return out.join("\n");
|
|
328
69
|
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
flag: ["--config"]
|
|
350
|
-
},
|
|
351
|
-
vitest: {
|
|
352
|
-
package: "@theholocron/vitest-config",
|
|
353
|
-
exportPath: "./bundles/library",
|
|
354
|
-
flag: ["--config"]
|
|
355
|
-
},
|
|
356
|
-
tsdown: {
|
|
357
|
-
package: "@theholocron/tsdown-config",
|
|
358
|
-
exportPath: "./presets/library",
|
|
359
|
-
flag: ["--config"]
|
|
360
|
-
},
|
|
361
|
-
commitlint: {
|
|
362
|
-
package: "@theholocron/commitlint-config",
|
|
363
|
-
exportPath: ".",
|
|
364
|
-
flag: ["--config"]
|
|
365
|
-
}
|
|
366
|
-
};
|
|
367
|
-
/** Every tool name the resolver knows how to point at a shared config. */
|
|
368
|
-
const RESOLVABLE_TOOLS = new Set(Object.keys(TOOL_CONFIGS));
|
|
369
|
-
/**
|
|
370
|
-
* `<flag> <absolute path>` for `tool`, resolved against `cwd`'s
|
|
371
|
-
* `node_modules` — or `[]` when the tool isn't mapped, the package isn't
|
|
372
|
-
* installed, its `package.json` doesn't declare the export subpath this
|
|
373
|
-
* needs, or the resolved file doesn't actually exist on disk (a stale
|
|
374
|
-
* install, or a package version that predates the export existing).
|
|
375
|
-
*/
|
|
376
|
-
function resolveToolConfig(tool, cwd, deps) {
|
|
377
|
-
const spec = TOOL_CONFIGS[tool];
|
|
378
|
-
if (!spec) return [];
|
|
379
|
-
const pkgDir = joinPath(cwd, "node_modules", ...spec.package.split("/"));
|
|
380
|
-
const pkgJsonPath = joinPath(pkgDir, "package.json");
|
|
381
|
-
if (!deps.fileExists(pkgJsonPath)) return [];
|
|
382
|
-
let pkg;
|
|
383
|
-
try {
|
|
384
|
-
pkg = JSON.parse(deps.readFile(pkgJsonPath));
|
|
385
|
-
} catch {
|
|
386
|
-
return [];
|
|
70
|
+
function mergeCodecovComponents(existing, packages) {
|
|
71
|
+
const idx = existing.indexOf(INDIVIDUAL_COMPONENTS_MARKER);
|
|
72
|
+
if (idx === -1) return ensureIfNotFound(existing);
|
|
73
|
+
return ensureIfNotFound(existing.slice(0, idx + 24)) + codecovComponentBlock(packages);
|
|
74
|
+
}
|
|
75
|
+
function readWorkspaceDir(repoRoot, dir) {
|
|
76
|
+
const dirPath = join(repoRoot, dir);
|
|
77
|
+
if (!existsSync(dirPath)) return [];
|
|
78
|
+
const packages = [];
|
|
79
|
+
for (const entry of readdirSync(dirPath, { withFileTypes: true })) {
|
|
80
|
+
if (!entry.isDirectory()) continue;
|
|
81
|
+
try {
|
|
82
|
+
const raw = readFileSync(join(dirPath, entry.name, "package.json"), "utf8");
|
|
83
|
+
const pkg = JSON.parse(raw);
|
|
84
|
+
if (typeof pkg.name === "string") packages.push({
|
|
85
|
+
slug: entry.name,
|
|
86
|
+
name: pkg.name,
|
|
87
|
+
dir
|
|
88
|
+
});
|
|
89
|
+
} catch {}
|
|
387
90
|
}
|
|
388
|
-
|
|
389
|
-
if (!relative) return [];
|
|
390
|
-
const absolute = joinPath(pkgDir, relative.replace(/^\.\//, ""));
|
|
391
|
-
if (!deps.fileExists(absolute)) return [];
|
|
392
|
-
return [...spec.flag, absolute];
|
|
91
|
+
return packages;
|
|
393
92
|
}
|
|
394
93
|
/**
|
|
395
|
-
*
|
|
396
|
-
*
|
|
397
|
-
*
|
|
398
|
-
*
|
|
399
|
-
*
|
|
400
|
-
*
|
|
94
|
+
* Every public `packages/*`, `apps/*`, and `plugins/*` workspace, sorted by
|
|
95
|
+
* slug. `[]` when none of the three directories exist. Monorepo templates
|
|
96
|
+
* (Next.js, React, …) ship user-facing code under `apps/` alongside library
|
|
97
|
+
* code under `packages/`; `holocron` itself splits vendor plugin packages
|
|
98
|
+
* into their own `plugins/*` root — every recognized root needs a codecov
|
|
99
|
+
* component so `codecov/patch/<slug>` actually posts for each.
|
|
401
100
|
*/
|
|
402
|
-
function
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
if (typeof value === "object" && value !== null) return resolveExportEntry(value);
|
|
101
|
+
function readWorkspacePackages(repoRoot) {
|
|
102
|
+
return [
|
|
103
|
+
...readWorkspaceDir(repoRoot, "packages"),
|
|
104
|
+
...readWorkspaceDir(repoRoot, "apps"),
|
|
105
|
+
...readWorkspaceDir(repoRoot, "plugins")
|
|
106
|
+
].sort((a, b) => a.slug.localeCompare(b.slug));
|
|
409
107
|
}
|
|
410
|
-
/**
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
*
|
|
416
|
-
*
|
|
108
|
+
/** A brand-new `codecov.yml` — the scaffold header + the base template + this repo's component list. */
|
|
109
|
+
function createCodecovConfig(packages) {
|
|
110
|
+
return `${scaffoldHeader()}${codecov_default.trimEnd()}${codecovComponentBlock(packages)}`;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* The full pipeline `holocron repo setup` needs: read this repo's public
|
|
114
|
+
* `packages/*`, then either merge them into an `existing` `codecov.yml` or
|
|
115
|
+
* scaffold a new one.
|
|
417
116
|
*/
|
|
418
|
-
function
|
|
419
|
-
|
|
117
|
+
function codecovConfig(repoRoot, existing) {
|
|
118
|
+
const packages = readWorkspacePackages(repoRoot);
|
|
119
|
+
return existing != null ? mergeCodecovComponents(existing, packages) : createCodecovConfig(packages);
|
|
420
120
|
}
|
|
421
121
|
//#endregion
|
|
422
|
-
//#region src/
|
|
122
|
+
//#region src/templates/reusable/actions/auto-commit.yml
|
|
123
|
+
var auto_commit_default = "name: Auto Commit\ndescription: >\n Commit and push any file changes using stefanzweifel/git-auto-commit-action.\n If a branch name is provided and differs from the current branch, a new branch\n is created automatically.\n\ninputs:\n token:\n description: >\n GitHub token with push access to the target branch. Used to authenticate\n the git remote so the commit can be pushed.\n required: true\n branch:\n description: >\n Branch to commit to. Leave empty to commit to the current branch (useful\n in PR contexts). Provide an explicit name (e.g. chore/auto-sync) to push\n to a separate branch.\n required: false\n default: \"\"\n commit-message:\n description: Commit message.\n required: true\n commit-options:\n description: >\n Additional options passed to git commit (e.g. --no-verify).\n required: false\n default: \"\"\n commit-user-name:\n description: Git user.name for the commit.\n required: false\n default: \"github-actions[bot]\"\n commit-user-email:\n description: Git user.email for the commit.\n required: false\n default: \"41898282+github-actions[bot]@users.noreply.github.com\"\n commit-author:\n description: >\n Git author string in \"Name <email>\" format.\n required: false\n default: \"github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>\"\n\noutputs:\n changes-detected:\n description: \"'true' if any files were committed, 'false' otherwise.\"\n value: ${{ steps.commit.outputs.changes_detected }}\n\nruns:\n using: composite\n steps:\n - name: Configure git remote with token\n shell: bash\n run: |\n REMOTE_URL=$(git remote get-url origin)\n AUTHED_URL=$(echo \"$REMOTE_URL\" | sed \"s|https://|https://x-access-token:${TOKEN}@|\")\n git remote set-url origin \"$AUTHED_URL\"\n env:\n TOKEN: ${{ inputs.token }}\n\n - name: Reset branch to current HEAD\n if: ${{ inputs.branch != '' }}\n shell: bash\n run: git checkout -B \"$BRANCH\"\n env:\n BRANCH: ${{ inputs.branch }}\n\n - name: Build signed commit message\n id: msg\n shell: bash\n run: |\n TRAILER=\"Signed-off-by: ${COMMIT_USER_NAME} <${COMMIT_USER_EMAIL}>\"\n if echo \"$COMMIT_MESSAGE\" | grep -qF \"$TRAILER\"; then\n FINAL=\"$COMMIT_MESSAGE\"\n else\n FINAL=\"${COMMIT_MESSAGE}\"$'\\n\\n'\"${TRAILER}\"\n fi\n {\n echo \"value<<EOF_TRAILER\"\n printf '%s' \"$FINAL\"\n echo \"\"\n echo \"EOF_TRAILER\"\n } >> \"$GITHUB_OUTPUT\"\n env:\n COMMIT_MESSAGE: ${{ inputs.commit-message }}\n COMMIT_USER_NAME: ${{ inputs.commit-user-name }}\n COMMIT_USER_EMAIL: ${{ inputs.commit-user-email }}\n\n - uses: stefanzweifel/git-auto-commit-action@4a55954c782fc1ea30b9056cd3e7a2b40ca8887d # v7.2.0\n id: commit\n with:\n branch: ${{ inputs.branch }}\n commit_message: ${{ steps.msg.outputs.value }}\n commit_options: ${{ inputs.commit-options }}\n commit_user_name: ${{ inputs.commit-user-name }}\n commit_user_email: ${{ inputs.commit-user-email }}\n commit_author: ${{ inputs.commit-author }}\n";
|
|
124
|
+
//#endregion
|
|
125
|
+
//#region src/templates/reusable/actions/holocron.yml
|
|
126
|
+
var holocron_default = "name: Holocron\ndescription: >\n Run the Holocron CLI — `holocron run <task> [job]` by default, or any other\n subcommand via `command:` (`repo sync`, `sync github`, …). Resolves\n `@theholocron/cli` from `node_modules` (published tarball) or builds it from\n source when it resolves to an unbuilt workspace checkout (the holocron repo\n itself). Requires dependencies to be installed already (run the setup action\n first).\n\ninputs:\n command:\n description: >\n Holocron subcommand. Default `run` — then `task` (and optional `job`)\n name the manifest task. Set to `repo sync` / `sync github` / … to run that\n subcommand directly; `args` becomes everything after it.\n required: false\n default: run\n task:\n description: >\n Manifest task to run when command is run — typecheck, test, build, audit.\n required: false\n default: \"\"\n job:\n description: Optional sub-job within the task (e.g. knip, performance for audit).\n required: false\n default: \"\"\n args:\n description: >\n For `command: run`, extra arguments forwarded to the tool after `--`.\n For any other command, the arguments passed straight to it.\n required: false\n default: \"\"\n\noutputs:\n log-file:\n description: >\n Path to a file holding the full captured stdout+stderr of this\n invocation — the same text the live Actions log shows, saved for a\n caller that wants to ship it elsewhere (e.g. Sentinel's dispatched\n checks shipping it to Axiom, holocron#769/#794).\n value: ${{ steps.run.outputs.log-file }}\n\nruns:\n using: composite\n\n steps:\n - name: Build the workspace when the CLI resolves to an unbuilt checkout\n shell: bash\n # The holocron repo carries @theholocron/cli as `workspace:*`; its bin\n # (dist/cli.mjs) does not exist on a fresh checkout. `pnpm build` is a\n # turbo run — cached and cheap once warm. A no-op everywhere else, where\n # the published tarball already ships dist/.\n #\n # pnpm creates every workspace package's node_modules/.bin/holocron shim\n # once, during the setup action's earlier `pnpm install` — before this\n # build has run, so every one of them is silently skipped (\"Failed to\n # create bin ... ENOENT\"), not just @theholocron/cli's own. That's a\n # one-time decision: a later build doesn't retroactively fix a shim that\n # was never created. Re-running `pnpm install` here (fast — the lockfile\n # hasn't changed, so it only re-links `.bin`) is what actually fixes it,\n # confirmed against a real cold install: `holocron` stays unresolvable\n # from inside any package until this second install runs.\n run: |\n if [ ! -f node_modules/@theholocron/cli/dist/cli.mjs ]; then\n echo \"→ @theholocron/cli dist/ missing — building the workspace from source\" >&2\n pnpm build\n echo \"→ re-linking node_modules/.bin — pnpm skipped it pre-build\" >&2\n pnpm install --frozen-lockfile\n fi\n\n - name: holocron ${{ inputs.command }} ${{ inputs.task }}\n id: run\n shell: bash\n env:\n HOLOCRON_COMMAND: ${{ inputs.command }}\n HOLOCRON_TASK: ${{ inputs.task }}\n HOLOCRON_JOB: ${{ inputs.job }}\n HOLOCRON_ARGS: ${{ inputs.args }}\n run: |\n if [ \"$HOLOCRON_COMMAND\" = \"run\" ]; then\n set -- run \"$HOLOCRON_TASK\"\n [ -n \"$HOLOCRON_JOB\" ] && set -- \"$@\" \"$HOLOCRON_JOB\"\n # shellcheck disable=SC2086\n [ -n \"$HOLOCRON_ARGS\" ] && set -- \"$@\" -- $HOLOCRON_ARGS\n else\n # `command` may be a multi-word path (`repo sync`, `deploy on-release`):\n # word-split it so each token is its own argument.\n # shellcheck disable=SC2086\n set -- $HOLOCRON_COMMAND $HOLOCRON_ARGS\n fi\n # Invoke the built entry directly. `pnpm exec holocron` relies on a\n # node_modules/.bin/holocron shim that pnpm does not create for a\n # workspace package whose dist/ was absent at install time (the holocron\n # repo itself). Resolving the package's main export gives dist/index.mjs;\n # the bin sits next to it as dist/cli.mjs. This is `require.resolve` from\n # node_modules — it works in the holocron monorepo AND in every consumer\n # repo (theholocron/holocron#655), unlike a hard-coded `packages/cli/…`.\n cli=\"$(dirname \"$(node -e 'process.stdout.write(require.resolve(\"@theholocron/cli\"))')\")/cli.mjs\"\n if [ ! -f \"$cli\" ]; then\n echo \"::error::@theholocron/cli entry not found at $cli — is the workspace built?\" >&2\n exit 1\n fi\n echo \"→ $cli $*\" >&2\n # `tee` keeps the live Actions log streaming exactly as before while\n # also capturing it to a file callers can ship elsewhere (log-file\n # output, above). PIPESTATUS[0] -- not $? -- is `node`'s own exit\n # code; $? after a pipe is tee's, which succeeds even when node fails.\n log_file=\"${RUNNER_TEMP}/holocron-output.log\"\n node \"$cli\" \"$@\" 2>&1 | tee \"$log_file\"\n exit_code=${PIPESTATUS[0]}\n echo \"log-file=$log_file\" >> \"$GITHUB_OUTPUT\"\n exit \"$exit_code\"\n";
|
|
127
|
+
//#endregion
|
|
128
|
+
//#region src/templates/reusable/actions/install.yml
|
|
129
|
+
var install_default = "name: Install dependencies\ndescription: Install project dependencies with pnpm frozen lockfile.\n\nruns:\n using: composite\n\n steps:\n - name: Install dependencies\n if: ${{ hashFiles('pnpm-lock.yaml') != '' }}\n shell: bash\n run: pnpm install --frozen-lockfile\n";
|
|
130
|
+
//#endregion
|
|
131
|
+
//#region src/templates/reusable/actions/setup.yml
|
|
132
|
+
var setup_default = "name: Setup\ndescription: Prepare the environment and install project dependencies.\n\ninputs:\n node-version:\n description: Node.js version\n required: false\n default: \"22.x\"\n\nruns:\n using: composite\n\n steps:\n - uses: theholocron/.github/.github/actions/setup-node@main\n with:\n node-version: ${{ inputs.node-version }}\n\n - uses: theholocron/.github/.github/actions/install@main\n\n # `@theholocron/cli`'s `dist/cli.mjs` doesn't exist yet at this point, so\n # pnpm's workspace bin-linking can't create `node_modules/.bin/holocron`\n # (silent `WARN ... ENOENT`, not a hard failure). Every package whose\n # scripts delegate via `holocron run <task> --` then fails at run time\n # with `holocron: not found` (#698). Same fix the `holocron` composite\n # action already applies for its own direct invocation (see its \"Build\n # the workspace when the CLI resolves to an unbuilt checkout\" step) —\n # mirrored here so it also covers *nested* `holocron run <task> --`\n # calls from inside package.json scripts, which that action's own guard\n # doesn't reach. `pnpm build` (not a scoped `--filter`) so the CLI's own\n # workspace dependencies (e.g. rollup-plugin-transform-template) build\n # in topological order too — a scoped build skips them and fails.\n # No-op in every consumer repo, where the published tarball already\n # ships `dist/`.\n - name: Build the Holocron CLI\n shell: bash\n run: |\n if [ ! -f node_modules/@theholocron/cli/dist/cli.mjs ]; then\n echo \"→ @theholocron/cli dist/ missing — building the workspace from source\" >&2\n pnpm build\n echo \"→ re-linking node_modules/.bin — pnpm skipped it pre-build\" >&2\n pnpm install --frozen-lockfile\n fi\n\n - name: Cache Turbo artifacts\n if: ${{ hashFiles('turbo.json') != '' }}\n uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0\n with:\n path: .turbo\n key: turbo-${{ runner.os }}-${{ github.sha }}\n restore-keys: |\n turbo-${{ runner.os }}-\n";
|
|
133
|
+
//#endregion
|
|
134
|
+
//#region src/templates/reusable/actions/setup-node.yml
|
|
135
|
+
var setup_node_default = "name: Setup Node\ndescription: Install pnpm and Node.js with pnpm dependency caching.\n\ninputs:\n node-version:\n description: Node.js version\n required: false\n default: \"22.x\"\nruns:\n using: composite\n\n steps:\n - name: Setup pnpm\n if: ${{ hashFiles('pnpm-lock.yaml') != '' }}\n uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4\n\n - name: Setup Node.js\n uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0\n with:\n node-version: ${{ hashFiles('.node-version') != '' && '' || inputs.node-version }}\n node-version-file: ${{ hashFiles('.node-version') != '' && '.node-version' || '' }}\n cache: ${{ hashFiles('pnpm-lock.yaml') != '' && 'pnpm' || '' }}\n # Do NOT add registry-url here. setup-node writes .npmrc with\n # _authToken=${NODE_AUTH_TOKEN} and sets NPM_CONFIG_USERCONFIG to it.\n # pnpm reads that file, fails env-var substitution when the token is\n # empty, and loses auth entirely. Without registry-url, pnpm 10.15+\n # handles Trusted Publishing via its own native OIDC exchange.\n\n - name: Add node_modules/.bin to PATH\n shell: bash\n run: echo \"$GITHUB_WORKSPACE/node_modules/.bin\" >> $GITHUB_PATH\n";
|
|
136
|
+
//#endregion
|
|
137
|
+
//#region src/templates/reusable/bookkeeping.yml
|
|
138
|
+
var bookkeeping_default$1 = "name: Bookkeeping\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n configuration-path:\n description: Path to the labeler configuration file in the calling repo\n type: string\n required: false\n default: .github/labeler.yml\n\njobs:\n label:\n name: Apply Labels\n permissions:\n contents: read\n issues: write\n pull-requests: write\n runs-on: ubuntu-latest\n timeout-minutes: 5\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n with:\n sparse-checkout: ${{ inputs.configuration-path || '.github/labeler.yml' }}\n sparse-checkout-cone-mode: false\n\n - uses: github/issue-labeler@c1b0f9f52a63158c4adc09425e858e87b32e9685 # v3.4\n if: ${{ github.event_name == 'pull_request' && hashFiles(inputs.configuration-path || '.github/labeler.yml') != '' }}\n # v3.4 bundles Node 20; allow it to run under Actions' current default.\n env:\n ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true\n with:\n # Fall back to default path when triggered directly (not via workflow_call)\n # because inputs.* defaults only apply on workflow_call events.\n configuration-path: ${{ inputs.configuration-path || '.github/labeler.yml' }}\n include-title: 1\n include-body: 0\n sync-labels: 1\n enable-versioned-regex: 0\n repo-token: ${{ github.token }}\n";
|
|
139
|
+
//#endregion
|
|
140
|
+
//#region src/templates/reusable/delivery.build.yml
|
|
141
|
+
var delivery_build_default$1 = "name: Build\n\n# Sanity check that every workspace actually compiles to dist/ — doesn't\n# upload or publish anything. delivery.publish builds again before\n# publishing; this just answers \"does it build?\" on every push/PR.\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n secrets:\n TURBO_TOKEN:\n required: false\n\njobs:\n build:\n name: Build every workspace\n permissions:\n contents: read\n runs-on: ubuntu-latest\n timeout-minutes: 10\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - uses: theholocron/.github/.github/actions/holocron@main\n name: Build\n with:\n task: delivery.build\n";
|
|
142
|
+
//#endregion
|
|
143
|
+
//#region src/templates/reusable/delivery.bundleSize.yml
|
|
144
|
+
var delivery_bundleSize_default$1 = "name: Audit the Bundle Size\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n secrets:\n CODECOV_TOKEN:\n required: false\n TURBO_TOKEN:\n required: false\n\njobs:\n bundle-size:\n name: Upload bundle stats to Codecov\n permissions:\n contents: read\n runs-on: ubuntu-latest\n timeout-minutes: 15\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n # Read by the bundle-stats uploader inside `holocron run delivery.build`.\n CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - uses: theholocron/.github/.github/actions/holocron@main\n name: Build and upload bundle stats\n with:\n task: delivery.build\n";
|
|
145
|
+
//#endregion
|
|
146
|
+
//#region src/templates/reusable/delivery.deploy.yml
|
|
147
|
+
var delivery_deploy_default$1 = "name: Deploy\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n type:\n description: \"Type of deployment: docs or storybook\"\n required: true\n type: string\n storybook-projects:\n description: >\n JSON array of { \"name\"?, \"workingDir\", \"outputDir\"? } objects for storybook deploys.\n Each is built via `pnpm -C <workingDir> build:storybook`. If \"name\" is provided the\n output is placed under `sandbox/<name>/`; omit \"name\" for single-repo deploys and the\n output lands directly in `sandbox/`.\n type: string\n required: false\n default: \"[]\"\n build-script:\n description: pnpm script that builds the Storybook static output (single storybook, type:storybook only)\n type: string\n required: false\n default: build:storybook\n output-dir:\n description: Directory where Storybook writes its static output (single storybook, type:storybook only)\n type: string\n required: false\n default: storybook-static\n\njobs:\n deploy:\n name: Deploy\n runs-on: ubuntu-latest\n permissions:\n contents: read\n pages: write\n id-token: write\n environment:\n name: github-pages\n url: ${{ steps.deployment.outputs.page_url }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - name: Build docs site\n if: ${{ inputs.type == 'docs' }}\n run: pnpm -C docs build\n\n - name: Build Storybook projects\n if: ${{ inputs.storybook-projects != '[]' }}\n env:\n PROJECTS: ${{ inputs.storybook-projects }}\n run: |\n echo \"$PROJECTS\" | jq -c '.[]' | while IFS= read -r project; do\n workingDir=$(echo \"$project\" | jq -r '.workingDir')\n pnpm -C \"$workingDir\" build:storybook\n done\n\n - name: Build Storybook\n if: ${{ inputs.type == 'storybook' && inputs.storybook-projects == '[]' }}\n env:\n BUILD_SCRIPT: ${{ inputs.build-script }}\n run: pnpm run \"$BUILD_SCRIPT\"\n\n - name: Assemble site\n env:\n DEPLOY_TYPE: ${{ inputs.type }}\n PROJECTS: ${{ inputs.storybook-projects }}\n STORYBOOK_OUTPUT_DIR: ${{ inputs.output-dir }}\n run: |\n mkdir -p _site\n if [ \"$DEPLOY_TYPE\" = \"docs\" ]; then\n cp -r docs/dist/. _site/\n fi\n if [ \"$PROJECTS\" != \"[]\" ]; then\n echo \"$PROJECTS\" | jq -c '.[]' | while IFS= read -r project; do\n name=$(echo \"$project\" | jq -r '.name // \"\"')\n workingDir=$(echo \"$project\" | jq -r '.workingDir')\n outputDir=$(echo \"$project\" | jq -r '.outputDir // \"storybook-static\"')\n if [ -n \"$name\" ]; then\n target=\"_site/sandbox/${name}\"\n else\n target=\"_site/sandbox\"\n fi\n mkdir -p \"$target\"\n cp -r \"${workingDir}/${outputDir}/.\" \"$target/\"\n done\n elif [ \"$DEPLOY_TYPE\" = \"storybook\" ]; then\n mkdir -p _site/sandbox\n cp -r \"${STORYBOOK_OUTPUT_DIR}/.\" _site/sandbox/\n fi\n\n - uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3.0.1\n name: Upload pages artifact\n with:\n path: _site\n\n - uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5\n id: deployment\n name: Deploy to GitHub Pages\n";
|
|
148
|
+
//#endregion
|
|
149
|
+
//#region src/templates/reusable/delivery.publish.yml
|
|
150
|
+
var delivery_publish_default$1 = "name: Release\n\n# Semantic-release with OIDC Trusted Publishing.\n# actions/setup-node writes a default NODE_AUTH_TOKEN=${{ github.token }}\n# which shadows OIDC auth. We explicitly clear it so npm falls through to\n# the Trusted Publisher OIDC exchange.\n# The calling repo must have a .releaserc.json that configures branches,\n# plugins, and any publish options. npm@11+ is installed to support OIDC.\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n dry-run:\n description: >\n When true, runs semantic-release --dry-run: analyzes commits and\n previews the next release without git writes or publish. Only\n meaningful for workflow_dispatch triggers; push-triggered runs\n always run fully.\n type: boolean\n required: false\n default: false\n post-release:\n description: >\n When true, dispatches a readme sync broadcast after a successful\n release. Enable for packages consumed by other repos via the\n registry-doc system so downstream README installation blocks\n stay current.\n type: boolean\n required: false\n default: false\n run-build:\n description: Run `pnpm build` before releasing\n type: boolean\n required: false\n default: true\n deploy-on-release:\n description: >\n When true, runs a separate `deploy` job after a successful release\n (holocron#930): `holocron deploy on-release` deploys every workspace\n package whose `delivery.deploy` task is `with: { on: release }`, when\n the release is on its channel and changes its paths. The job is its\n own check, re-runnable on its own, and gets VERCEL_TOKEN; the release\n job never sees it. Off by default, so a repo that doesn't deploy from\n its release never exposes the token.\n type: boolean\n required: false\n default: false\n sentry-project:\n description: >\n Sentry project slug for sourcemap upload and release creation after\n publishing. Omit to skip the Sentry release step entirely.\n type: string\n required: false\n default: \"\"\n secrets:\n HOLOCRON_RELEASE_TOKEN:\n description: >\n Fine-grained PAT (Contents + Issues + Pull requests: write) owned by\n an admin. Required when the default branch is protected by a ruleset —\n github.token cannot push through rulesets, but an admin PAT can.\n Takes priority over HOLOCRON_SYNC_TOKEN. Falls back to github.token.\n required: false\n HOLOCRON_SYNC_TOKEN:\n description: >\n Legacy alias for HOLOCRON_RELEASE_TOKEN — kept for backward compatibility.\n Prefer HOLOCRON_RELEASE_TOKEN for new repos.\n required: false\n HOLOCRON_READ_TOKEN:\n description: >\n Fine-grained PAT for read-only GitHub API calls (e.g. resolving git\n committer identity via `gh api user`). Falls back to github.token.\n required: false\n GH_TOKEN:\n description: >\n Generic GitHub token fallback for `gh` CLI calls. Used when\n HOLOCRON_READ_TOKEN is not set.\n required: false\n SENTRY_AUTH_TOKEN:\n description: >\n Sentry auth token for sourcemap upload and release creation.\n Required when sentry-project is set. Use the org-level secret.\n required: false\n TURBO_TOKEN:\n required: false\n VERCEL_TOKEN:\n description: >\n Vercel token for the deploy job. Only used when deploy-on-release is\n true, and only by that job.\n required: false\n\njobs:\n release:\n name: Semantic release\n permissions:\n contents: write\n id-token: write\n issues: write\n pull-requests: write\n runs-on: ubuntu-latest\n timeout-minutes: 30\n outputs:\n # Read by the deploy job (holocron#930). Empty unless semantic-release\n # published: `commit` is the release commit, `previous` the commit of\n # the release before it (to diff against), `channel` the new version's\n # prerelease identifier (`alpha`), empty for a stable release (`main`).\n commit: ${{ steps.release.outputs.commit }}\n previous: ${{ steps.release.outputs.previous }}\n channel: ${{ steps.release.outputs.channel }}\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n # Do not cancel in-progress releases — a partial release is worse than a slow one.\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n with:\n fetch-depth: 0\n persist-credentials: false\n # Use HOLOCRON_RELEASE_TOKEN when available — git push (tags, release commits)\n # uses the checkout credential, not GITHUB_TOKEN env var. The\n # built-in github.token cannot push through branch protection rulesets.\n token: ${{ secrets.HOLOCRON_RELEASE_TOKEN || secrets.HOLOCRON_SYNC_TOKEN || github.token }}\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - name: Configure git identity\n run: |\n GIT_NAME=$(gh api user --jq .name 2>/dev/null || echo \"github-actions[bot]\")\n GIT_EMAIL=$(gh api user --jq '\"\\(.id)+\\(.login)@users.noreply.github.com\"' 2>/dev/null || echo \"41898282+github-actions[bot]@users.noreply.github.com\")\n git config --global user.name \"$GIT_NAME\"\n git config --global user.email \"$GIT_EMAIL\"\n git config --global format.signoff true\n env:\n GH_TOKEN: ${{ secrets.HOLOCRON_READ_TOKEN || secrets.GH_TOKEN || github.token }}\n\n - name: Upgrade npm for OIDC support\n run: npm install -g npm@11 sigstore\n # sigstore is required by libnpmpublish/provenance.js at module parse\n # time — before any config takes effect. Some npm 11.x builds stopped\n # bundling it; installing it globally into the same prefix ensures it\n # resolves regardless of npm version. (Discovered 2026-07-09.)\n\n - run: pnpm build\n name: Build\n if: ${{ inputs.run-build == true }}\n\n - name: Release\n id: release\n run: |\n BEFORE=$(git rev-parse HEAD)\n PREVIOUS_TAG=$(git describe --tags --abbrev=0 2>/dev/null || true)\n PREVIOUS=\"\"\n if [ -n \"$PREVIOUS_TAG\" ]; then PREVIOUS=$(git rev-list -n 1 \"$PREVIOUS_TAG\"); fi\n if [ \"$DRY_RUN\" = \"true\" ]; then\n npx semantic-release --dry-run\n else\n npx semantic-release\n fi\n AFTER=$(git rev-parse HEAD)\n # semantic-release only commits (`chore(release): … [skip ci]`) when it\n # actually publishes. That commit gets no CI (and so no coverage\n # upload), which makes the next PR's `codecov/project` compare a full\n # report against a stale ancestor — a phantom drop (holocron#644).\n # Emit the new SHA so the steps below upload coverage for it.\n if [ \"$BEFORE\" != \"$AFTER\" ]; then\n echo \"commit=$AFTER\" >> \"$GITHUB_OUTPUT\"\n echo \"previous=$PREVIOUS\" >> \"$GITHUB_OUTPUT\"\n # The new version's channel: the prerelease identifier of its tag\n # (v5.0.0-alpha.108 -> alpha), empty for a stable release.\n VERSION=$(git describe --tags --abbrev=0)\n VERSION=${VERSION#v}\n CHANNEL=\"\"\n case \"$VERSION\" in *-*) PRE=${VERSION#*-}; CHANNEL=${PRE%%.*} ;; esac\n echo \"channel=$CHANNEL\" >> \"$GITHUB_OUTPUT\"\n fi\n env:\n DRY_RUN: ${{ inputs.dry-run }}\n # Prefer HOLOCRON_RELEASE_TOKEN (fine-grained PAT, Contents+Issues+PRs write,\n # owned by an admin with ruleset bypass) so @semantic-release/git can\n # push the version-bump commit through branch protection. Falls back to\n # HOLOCRON_SYNC_TOKEN (legacy) then github.token for unprotected repos.\n GITHUB_TOKEN: ${{ secrets.HOLOCRON_RELEASE_TOKEN || secrets.HOLOCRON_SYNC_TOKEN || github.token }}\n HUSKY: \"0\"\n NPM_CONFIG_PROVENANCE: true\n\n - uses: theholocron/.github/.github/actions/holocron@main\n name: Coverage for the release commit\n if: ${{ steps.release.outputs.commit != '' }}\n with:\n task: verification.unitTests\n\n - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0\n name: Upload release-commit coverage to Codecov\n if: ${{ steps.release.outputs.commit != '' }}\n # fail_ci_if_error makes a failed upload fail this step, so it is visible, but\n # continue-on-error keeps it from failing the job: the release has already\n # published, and the Sentry release steps below must still run (holocron#934).\n continue-on-error: true\n with:\n use_oidc: true\n fail_ci_if_error: true\n override_commit: ${{ steps.release.outputs.commit }}\n override_branch: ${{ github.ref_name }}\n\n - name: Get release version\n id: release_version\n if: ${{ inputs.sentry-project != '' && inputs.dry-run != true }}\n env:\n SENTRY_PROJECT: ${{ inputs.sentry-project }}\n run: |\n TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo \"\")\n if [ -n \"$TAG\" ]; then\n echo \"release=${SENTRY_PROJECT}@${TAG#v}\" >> \"$GITHUB_OUTPUT\"\n fi\n\n - name: Create Sentry release\n if: ${{ inputs.sentry-project != '' && steps.release_version.outputs.release != '' && inputs.dry-run != true }}\n uses: getsentry/action-release@ff07929a6537bac57790c3451cf4d364aca38528 # v3\n env:\n SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}\n SENTRY_ORG: theholocron\n SENTRY_PROJECT: ${{ inputs.sentry-project }}\n with:\n environment: production\n version: ${{ steps.release_version.outputs.release }}\n sourcemaps: \"**/dist\"\n\n deploy:\n name: Deploy\n needs: release\n # Only after a release that actually published (`commit` is empty on a dry\n # run or when nothing was released). A failed deploy fails only this job:\n # the release is already on npm, and this job re-runs on its own.\n if: ${{ inputs.deploy-on-release == true && inputs.dry-run != true && needs.release.outputs.commit != '' }}\n permissions:\n contents: read\n runs-on: ubuntu-latest\n timeout-minutes: 30\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout the release commit\n with:\n ref: ${{ needs.release.outputs.commit }}\n fetch-depth: 0\n persist-credentials: false\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - uses: theholocron/.github/.github/actions/holocron@main\n name: Deploy packages that deploy on release\n env:\n VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}\n with:\n command: deploy on-release\n args: >-\n --channel=${{ needs.release.outputs.channel }}\n --from=${{ needs.release.outputs.previous }}\n --to=${{ needs.release.outputs.commit }}\n\n sync-readme:\n name: Post-release readme sync\n needs: release\n if: ${{ inputs.post-release == true && inputs.dry-run != true }}\n runs-on: ubuntu-latest\n timeout-minutes: 5\n steps:\n - name: Trigger broadcast readme sync\n run: |\n gh workflow run sync-dispatch.yml \\\n --repo theholocron/.github \\\n --field \"steps=readme\"\n env:\n GH_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN }}\n";
|
|
151
|
+
//#endregion
|
|
152
|
+
//#region src/templates/reusable/dependencies.yml
|
|
153
|
+
var dependencies_default$1 = "name: Dependencies\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n secrets:\n merge-token:\n description: >\n Optional privileged token for auto-merge. Falls back to GITHUB_TOKEN.\n Required when branch protection enforces required reviews — GITHUB_TOKEN\n cannot approve its own PRs.\n required: false\n\njobs:\n dependabot:\n name: Update the dependencies\n permissions:\n contents: write\n pull-requests: write\n runs-on: ubuntu-latest\n timeout-minutes: 5\n if: github.event.pull_request.user.login == 'dependabot[bot]'\n steps:\n - uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3.1.0\n name: Fetch Dependabot metadata\n id: metadata\n\n - run: gh pr merge --auto --squash \"$PR_URL\"\n # --squash is intentional: repo protection sets allow_merge_commit: false,\n # so --merge would fail on any repo using the standard preset.\n name: Enable auto-merge for Dependabot PRs\n if: steps.metadata.outputs.update-type == 'version-update:semver-patch'\n env:\n PR_URL: ${{ github.event.pull_request.html_url }}\n GH_TOKEN: ${{ secrets.merge-token || github.token }}\n";
|
|
154
|
+
//#endregion
|
|
155
|
+
//#region src/templates/reusable/greetings.yml
|
|
156
|
+
var greetings_default$1 = "name: Greetings\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n\njobs:\n greeting:\n name: Greet first-time contributors\n permissions:\n issues: write\n pull-requests: write\n runs-on: ubuntu-latest\n timeout-minutes: 5\n # Group by the issue/PR number so duplicate events don't race each other.\n steps:\n - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0\n name: Greet on first contribution\n with:\n script: |\n // Only greet on the initial open — ignore synchronize, reopened, etc.\n if (context.payload.action !== 'opened') return;\n\n const actor = context.actor;\n const { owner, repo } = context.repo;\n\n // Payload inspection is more reliable than context.eventName for detecting\n // whether this is an issue vs. PR event — works regardless of how GitHub\n // propagates event names through workflow_call chains.\n const isIssue = !!context.payload.issue && !context.payload.pull_request;\n // listForRepo returns both issues and PRs (GitHub treats PRs as issues),\n // sorted newest-first. Filter by type to track first-issue and first-PR\n // independently, and avoid search-index eventual-consistency lag.\n const { data: recent } = await github.rest.issues.listForRepo({\n owner, repo,\n creator: actor,\n state: 'all',\n per_page: 100\n });\n\n const sameType = recent.filter(item =>\n isIssue ? !item.pull_request : !!item.pull_request\n );\n\n if (sameType.length !== 1) return;\n const body = isIssue\n ? `Hey @${actor}!\\n\\nWe really appreciate you taking the time to report an issue. The collaborators on this project attempt to help as many people as possible, but we are a limited number of volunteers, so it is possible that this will not be addressed as swiftly.\\n\\nYour patience is much appreciated and we will get back to you as quickly as possible.`\n : `Hey @${actor}!\\n\\nWe really appreciate you taking the time to help out with this PR. The collaborators on this project attempt to help as many people as possible, but we are a limited number of volunteers, so it is possible that this will not be addressed as swiftly.\\n\\nYour patience is much appreciated and we will get back to you as quickly as possible.`;\n\n await github.rest.issues.createComment({\n owner,\n repo,\n issue_number: context.issue.number,\n body\n });\n";
|
|
157
|
+
//#endregion
|
|
158
|
+
//#region src/templates/reusable/knowledge.wiki.yml
|
|
159
|
+
var knowledge_wiki_default$1 = "name: Wiki\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n fern-version:\n description: >\n Fern CLI version to install. Pin this to avoid breaking changes when\n Fern updates their config schema.\n type: string\n required: false\n default: \"5.114.1\"\n preview:\n description: >\n When true, publishes a preview instead of production.\n Requires preview-id to be set.\n type: boolean\n required: false\n default: false\n preview-id:\n description: >\n Stable ID for the preview build. Use the PR number (e.g. \"pr-123\")\n so the same preview URL is reused on every push to the branch.\n type: string\n required: false\n default: \"\"\n fern-org:\n description: >\n DEPRECATED — the preview URL is now read from Fern's own output.\n Kept as a fallback for building the URL when that parse fails, and so\n existing callers do not error.\n type: string\n required: false\n default: \"\"\n base-path:\n description: >\n DEPRECATED — see fern-org. Basepath appended to the fallback preview\n URL (e.g. \"holocron\" for multi-source routing).\n type: string\n required: false\n default: \"\"\n secrets:\n HOLOCRON_FERN_TOKEN:\n required: false\n FERN_TOKEN:\n required: false\n\njobs:\n publish:\n name: Publish to Fern\n runs-on: ubuntu-latest\n timeout-minutes: 10\n permissions:\n contents: read\n deployments: write\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - name: Install Fern CLI\n run: npm install -g \"fern-api@$FERN_VERSION\"\n env:\n FERN_VERSION: ${{ inputs.fern-version }}\n\n - name: Publish docs\n if: ${{ !inputs.preview }}\n run: fern generate --docs\n env:\n FERN_TOKEN: ${{ secrets.HOLOCRON_FERN_TOKEN || secrets.FERN_TOKEN }}\n\n - name: Preview docs\n id: preview\n if: ${{ inputs.preview && inputs.preview-id != '' }}\n env:\n FERN_TOKEN: ${{ secrets.HOLOCRON_FERN_TOKEN || secrets.FERN_TOKEN }}\n PREVIEW_ID: ${{ inputs.preview-id }}\n run: |\n out=$(fern generate --docs --preview --id \"$PREVIEW_ID\" 2>&1) || { printf '%s\\n' \"$out\"; exit 1; }\n printf '%s\\n' \"$out\"\n # Fern prints \"Published docs to https://<org>-preview-<id>.docs.buildwithfern.com/<path>\"\n url=$(printf '%s\\n' \"$out\" | grep -oiE 'https://[a-z0-9.-]+\\.docs\\.buildwithfern\\.com[^ )]*' | head -n1)\n echo \"url=$url\" >> \"$GITHUB_OUTPUT\"\n\n - name: Report preview URL\n if: ${{ inputs.preview && inputs.preview-id != '' }}\n env:\n GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n CAPTURED_URL: ${{ steps.preview.outputs.url }}\n FERN_ORG: ${{ inputs.fern-org }}\n PREVIEW_ID: ${{ inputs.preview-id }}\n BASE_PATH: ${{ inputs.base-path }}\n run: |\n PREVIEW_URL=\"$CAPTURED_URL\"\n if [ -z \"$PREVIEW_URL\" ] && [ -n \"$FERN_ORG\" ]; then\n PREVIEW_URL=\"https://${FERN_ORG}-preview-${PREVIEW_ID}.docs.buildwithfern.com${BASE_PATH:+/${BASE_PATH}}\"\n fi\n if [ -z \"$PREVIEW_URL\" ]; then\n echo \"::warning::no preview URL parsed from Fern output and no fern-org fallback — skipping the deployment widget\"\n exit 0\n fi\n PAYLOAD=$(printf '{\"ref\":\"%s\",\"environment\":\"wiki (Preview)\",\"description\":\"Wiki\",\"production_environment\":false,\"auto_merge\":false,\"required_contexts\":[]}' \\\n \"$GITHUB_HEAD_REF\")\n DEPLOY_ID=$(printf '%s\\n' \"$PAYLOAD\" | gh api \"repos/${GITHUB_REPOSITORY}/deployments\" \\\n --method POST --input - | jq -r '.id')\n gh api \"repos/${GITHUB_REPOSITORY}/deployments/${DEPLOY_ID}/statuses\" \\\n --method POST --field state=success --field environment_url=\"$PREVIEW_URL\"\n";
|
|
160
|
+
//#endregion
|
|
161
|
+
//#region src/templates/reusable/platform.commitStandards.yml
|
|
162
|
+
var platform_commitStandards_default$1 = "name: Commit Standards\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n secrets:\n TURBO_TOKEN:\n required: false\n\njobs:\n commit-standards:\n name: Run commitlint\n permissions:\n contents: read\n runs-on: ubuntu-latest\n timeout-minutes: 10\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n with:\n fetch-depth: 0\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n # No local equivalent (linters.ts: commitlint has no localBin — enforced\n # only at commit time via the commit-msg hook, and here in CI over the\n # PR's commit range). Not run through the holocron composite action.\n #\n # --config <resolved shared path> (config-resolution workstream, #676)\n # — falls back to commitlint's own auto-discovery of a local\n # commitlint.config.ts when the shared package's built dist/ isn't\n # present, same guard as the commit-msg hook.\n - name: Run commitlint over the PR range\n if: github.event_name == 'pull_request'\n run: |\n COMMITLINT_CONFIG=\"node_modules/@theholocron/commitlint-config/dist/index.js\"\n if [ -f \"$COMMITLINT_CONFIG\" ]; then\n pnpm exec commitlint --config \"$COMMITLINT_CONFIG\" --from \"${{ github.event.pull_request.base.sha }}\" --to \"${{ github.sha }}\"\n else\n pnpm exec commitlint --from \"${{ github.event.pull_request.base.sha }}\" --to \"${{ github.sha }}\"\n fi\n";
|
|
163
|
+
//#endregion
|
|
164
|
+
//#region src/templates/reusable/platform.dispatchedCheck.yml
|
|
165
|
+
var platform_dispatchedCheck_default = "name: Dispatched Check\n\non: # yamllint disable-line rule:truthy\n workflow_dispatch:\n inputs:\n repo:\n description: Target repo, \"owner/name\", to check out and run the task against.\n type: string\n required: true\n ref:\n description: Git ref (branch or commit SHA) to check out.\n type: string\n required: true\n task:\n description: The holocron task name to run (e.g. \"verification.typeSafety\").\n type: string\n required: true\n check-run-id:\n description: >\n ID of the check run already posted as \"queued\" on the target repo —\n patched to \"completed\" once the task finishes.\n type: string\n required: true\n\npermissions:\n contents: read\n\njobs:\n run-task:\n name: Run a Sentinel-dispatched task against another repo\n runs-on: ubuntu-latest\n timeout-minutes: 20\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n steps:\n # actions/create-github-app-token defaults to scoping its token to the\n # *current* repo (.github, where this workflow runs) when owner/\n # repositories are omitted — useless here, since every real use of this\n # token targets inputs.repo instead. Split \"owner/name\" up front so the\n # token step below can scope explicitly to the actual target.\n - name: Parse target repo\n id: target\n run: |\n echo \"owner=${INPUT_REPO%%/*}\" >> \"$GITHUB_OUTPUT\"\n echo \"name=${INPUT_REPO##*/}\" >> \"$GITHUB_OUTPUT\"\n env:\n INPUT_REPO: ${{ inputs.repo }}\n\n # Sentinel's own dispatch call only carries plain, non-secret inputs\n # (repo/ref/task/check-run-id — all visible in the Actions UI/logs).\n # This step mints a fresh, short-lived installation token here instead,\n # scoped by the same App's stored credentials — never a raw token\n # passed through a dispatch input.\n - name: Generate installation token\n id: app-token\n uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0\n with:\n app-id: ${{ secrets.SENTINEL_APP_ID }}\n private-key: ${{ secrets.SENTINEL_APP_PRIVATE_KEY }}\n owner: ${{ steps.target.outputs.owner }}\n repositories: ${{ steps.target.outputs.name }}\n\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout target repository\n with:\n repository: ${{ inputs.repo }}\n ref: ${{ inputs.ref }}\n token: ${{ steps.app-token.outputs.token }}\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - name: Run the task\n id: run-task\n continue-on-error: true\n uses: theholocron/.github/.github/actions/holocron@main\n with:\n task: ${{ inputs.task }}\n\n # A bare .../actions/runs/<id> link lands on the run summary, one click\n # short of the actual step-by-step log a normal check's details_url\n # points at. This job is always alone in its run, so the first (only)\n # job in the run's own job list is unambiguously this one.\n - name: Resolve this job's numeric ID\n id: job-info\n if: always()\n run: |\n job_id=$(gh api \"repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/jobs\" --jq '.jobs[0].id')\n echo \"job_id=$job_id\" >> \"$GITHUB_OUTPUT\"\n env:\n GH_TOKEN: ${{ steps.app-token.outputs.token }}\n\n # Ships the task's real captured output (tsc/vitest/etc — whatever\n # `holocron run` actually printed) to the same Axiom dataset Sentinel's\n # own structured logs already flow into, tagged so it's queryable by\n # this exact run. Reuses the existing dataset rather than provisioning\n # a new one — a prototype-stage call, revisit if CI-log volume ever\n # warrants a dedicated dataset. Builds a permalink filtered to this\n # run's own logs (Axiom's own \"copy query link\" URL shape:\n # /<org>/query?initForm={\"apl\":\"...\"} ) so a viewer lands directly on\n # the relevant lines, not a bare dataset view.\n - name: Ship task output to Axiom\n id: axiom\n if: always()\n run: |\n message=$(tail -c 40000 \"$LOG_FILE\" 2>/dev/null || echo \"(no output captured)\")\n # \"level\" is one of the field names Axiom's Stream/Query tabs\n # recognize for severity-color highlighting (docs: level, @level,\n # severity, @severity, status.code) -- conclusion alone isn't.\n level=$([ \"$CONCLUSION\" = \"success\" ] && echo \"info\" || echo \"error\")\n event=$(jq -cn \\\n --arg time \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\" \\\n --arg repo \"$REPO\" --arg task \"$TASK\" --arg run_id \"$GITHUB_RUN_ID\" \\\n --arg job_id \"$JOB_ID\" --arg check_run_id \"$CHECK_RUN_ID\" \\\n --arg conclusion \"$CONCLUSION\" --arg level \"$level\" --arg message \"$message\" \\\n '{_time: $time, source: \"dispatched-check\", repo: $repo, task: $task,\n run_id: $run_id, job_id: $job_id, check_run_id: $check_run_id,\n conclusion: $conclusion, level: $level, message: $message}')\n curl -sf -X POST \"https://api.axiom.co/v1/datasets/${AXIOM_DATASET}/ingest\" \\\n -H \"Authorization: Bearer ${AXIOM_TOKEN}\" \\\n -H \"Content-Type: application/json\" \\\n -d \"[${event}]\" || echo \"::warning::Axiom ingest failed — continuing, the check itself still reports the real result\"\n\n # The ['...'] is Axiom's own APL array-dataset-reference syntax, not\n # a shell quote boundary — $AXIOM_DATASET/$CHECK_RUN_ID both expand\n # fine here; shellcheck's single-quote heuristic just can't tell.\n # shellcheck disable=SC2016\n apl=\"['${AXIOM_DATASET}'] | where check_run_id == \\\"${CHECK_RUN_ID}\\\"\"\n init_form=$(jq -cn --arg apl \"$apl\" '{apl: $apl}')\n encoded=$(jq -rn --arg s \"$init_form\" '$s | @uri')\n echo \"query-url=https://app.axiom.co/${AXIOM_ORG}/query?initForm=${encoded}\" >> \"$GITHUB_OUTPUT\"\n env:\n LOG_FILE: ${{ steps.run-task.outputs.log-file }}\n REPO: ${{ inputs.repo }}\n TASK: ${{ inputs.task }}\n JOB_ID: ${{ steps.job-info.outputs.job_id }}\n CHECK_RUN_ID: ${{ inputs.check-run-id }}\n CONCLUSION: ${{ steps.run-task.outcome == 'success' && 'success' || 'failure' }}\n AXIOM_ORG: the-holocron-7bbe\n AXIOM_DATASET: holocron-sentinel\n AXIOM_TOKEN: ${{ secrets.SENTINEL_AXIOM_INGEST_TOKEN }}\n\n # Always runs, success or failure — the queued check run on the target\n # repo must be resolved either way, not left hanging. Full JSON body via\n # jq + --input, not -f flags: output.text needs real nesting (gh api -f\n # has no dot-notation for that), and the captured task output can\n # contain characters (quotes, $, backticks) that would break a\n # hand-built -f string.\n - name: Report result to the target repo's check run\n if: always()\n run: |\n excerpt=$(tail -c 3000 \"$LOG_FILE\" 2>/dev/null || echo \"(no output captured)\")\n # The single-quoted format string is intentional — every %s fills\n # via printf's own args below, not shell expansion; nothing in it\n # needs to expand.\n # shellcheck disable=SC2016\n text=$(printf '**Task:** `%s`\\n\\n**Result:** %s\\n\\n[Full logs on Axiom](%s)\\n\\n### Last output\\n\\n```\\n%s\\n```\\n' \\\n \"$TASK\" \"$CONCLUSION\" \"$QUERY_URL\" \"$excerpt\")\n body=$(jq -n \\\n --arg conclusion \"$CONCLUSION\" \\\n --arg details_url \"$RUN_URL\" \\\n --arg title \"$TASK ($CONCLUSION)\" \\\n --arg summary \"Dispatched via theholocron/.github — see the log excerpt below, or the full run/Axiom links.\" \\\n --arg text \"$text\" \\\n '{status: \"completed\", conclusion: $conclusion, details_url: $details_url,\n output: {title: $title, summary: $summary, text: $text}}')\n echo \"$body\" | gh api --method PATCH \"/repos/${REPO}/check-runs/${CHECK_RUN_ID}\" --input -\n env:\n # `GH_TOKEN` is the literal name `gh api` requires to auto-authenticate\n # -- not a stand-in for a shared/blanket credential. Its value is the\n # short-lived, Sentinel-App-scoped token minted above, same as the\n # checkout step uses; never secrets.HOLOCRON_SYNC_TOKEN (the one\n # genuinely shared PAT in this org, used by sync-dispatch.yml).\n GH_TOKEN: ${{ steps.app-token.outputs.token }}\n REPO: ${{ inputs.repo }}\n TASK: ${{ inputs.task }}\n CHECK_RUN_ID: ${{ inputs.check-run-id }}\n CONCLUSION: ${{ steps.run-task.outcome == 'success' && 'success' || 'failure' }}\n LOG_FILE: ${{ steps.run-task.outputs.log-file }}\n QUERY_URL: ${{ steps.axiom.outputs.query-url }}\n RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}/job/${{ steps.job-info.outputs.job_id }}\n";
|
|
166
|
+
//#endregion
|
|
167
|
+
//#region src/templates/reusable/platform.repoSync.yml
|
|
168
|
+
var platform_repoSync_default$1 = "name: Sync\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n steps:\n description: >\n Sync steps to run (default: all). Valid values:\n labels, properties, teams, topics, keywords, description, homepage, readme, workflows, wiki.\n Pass a space-separated list to run a subset, e.g. \"readme\" or \"readme wiki\".\n type: string\n required: false\n secrets:\n HOLOCRON_ADMIN_TOKEN:\n description: Fine-grained PAT with admin scopes (labels, properties, teams).\n required: false\n HOLOCRON_AXIOM_TOKEN:\n description: >\n Axiom API token. When set alongside the HOLOCRON_AXIOM_DATASET\n repo/org variable, the CLI ships this run's structured logs to Axiom.\n Falls back to the vendor-native AXIOM_TOKEN secret.\n required: false\n AXIOM_TOKEN:\n description: Vendor-native fallback for HOLOCRON_AXIOM_TOKEN.\n required: false\n HOLOCRON_DEPLOY_TOKEN:\n description: Fine-grained PAT for GitHub Pages configuration.\n required: false\n HOLOCRON_ISSUES_TOKEN:\n description: Fine-grained PAT for issue management.\n required: false\n HOLOCRON_ORG_TOKEN:\n description: Org-scoped fine-grained PAT for team sync and org properties.\n required: false\n HOLOCRON_READ_TOKEN:\n description: Fine-grained PAT for read-only GitHub API calls.\n required: false\n HOLOCRON_SYNC_TOKEN:\n required: false\n GH_TOKEN:\n description: >\n Generic GitHub token fallback for gh CLI calls. Used when\n HOLOCRON_SYNC_TOKEN is not set.\n required: false\n\njobs:\n sync:\n name: Run holocron repo sync\n runs-on: ubuntu-latest\n timeout-minutes: 10\n permissions:\n contents: write\n pull-requests: write\n # Job-level so the `holocron` composite action's steps inherit them.\n env:\n HOLOCRON_ADMIN_TOKEN: ${{ secrets.HOLOCRON_ADMIN_TOKEN }}\n HOLOCRON_AXIOM_TOKEN: ${{ secrets.HOLOCRON_AXIOM_TOKEN || secrets.AXIOM_TOKEN }}\n HOLOCRON_AXIOM_DATASET: ${{ vars.HOLOCRON_AXIOM_DATASET || vars.AXIOM_DATASET }}\n POSTHOG_PROJECT_TOKEN: ${{ vars.POSTHOG_PROJECT_TOKEN }}\n HOLOCRON_DEPLOY_TOKEN: ${{ secrets.HOLOCRON_DEPLOY_TOKEN }}\n HOLOCRON_ISSUES_TOKEN: ${{ secrets.HOLOCRON_ISSUES_TOKEN }}\n HOLOCRON_ORG_TOKEN: ${{ secrets.HOLOCRON_ORG_TOKEN }}\n HOLOCRON_READ_TOKEN: ${{ secrets.HOLOCRON_READ_TOKEN }}\n HOLOCRON_SYNC_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n with:\n token: ${{ secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN || github.token }}\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - name: Run holocron repo sync\n # The `holocron` action resolves @theholocron/cli from node_modules (or\n # builds the workspace when it's an unbuilt checkout — the holocron repo\n # itself). `inputs.steps` is a space-separated list or empty; the\n # `--steps <list>` arg is omitted when empty.\n uses: theholocron/.github/.github/actions/holocron@main\n with:\n command: repo sync\n args: ${{ inputs.steps != '' && format('--steps {0}', inputs.steps) || '' }}\n\n - name: Format generated files\n run: |\n pnpm exec prettier --write README.md docs/src/content/docs/index.mdx 2>/dev/null || true\n # `holocron repo sync` writes package.json fields (keywords, description,\n # homepage) with a plain assignment, which appends new keys at the\n # end — re-apply the canonical order the lint config enforces.\n pnpm exec eslint --fix --no-warn-ignored package.json 2>/dev/null || true\n\n - uses: theholocron/.github/.github/actions/auto-commit@main\n id: auto-commit\n name: Commit sync changes\n with:\n token: ${{ secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN || github.token }}\n branch: chore/auto-sync\n commit-message: \"chore: sync from holocron.config\"\n commit-options: \"--no-verify\"\n\n - name: Open PR if changes were committed\n if: steps.auto-commit.outputs.changes-detected == 'true'\n run: |\n gh pr create \\\n --title \"chore: sync README and repo metadata\" \\\n --body \"Automated sync triggered by changes to config or package files. Merge to apply.\" \\\n --base main \\\n --head chore/auto-sync \\\n || echo \"PR already open — branch updated.\"\n env:\n GH_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN || github.token }}\n\n - name: Broadcast wiki sync if navbar changed\n if: >-\n steps.auto-commit.outputs.changes-detected == 'true' &&\n github.event_name == 'push' &&\n (inputs.steps == '' || contains(inputs.steps, 'wiki'))\n run: |\n if git diff HEAD~1 --name-only | grep -q 'fern/docs.yml'; then\n gh workflow run sync-dispatch.yml \\\n --repo theholocron/.github \\\n --field \"steps=wiki\" \\\n || echo \"skipping broadcast — insufficient permissions\"\n fi\n env:\n GH_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN || github.token }}\n";
|
|
169
|
+
//#endregion
|
|
170
|
+
//#region src/templates/reusable/platform.repoValidation.yml
|
|
171
|
+
var platform_repoValidation_default$1 = "name: Repo Validation\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n secrets:\n TURBO_TOKEN:\n required: false\n\njobs:\n adrs:\n name: Validate ADRs and specs\n permissions:\n contents: read\n runs-on: ubuntu-latest\n timeout-minutes: 10\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n with:\n fetch-depth: 0\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - uses: theholocron/.github/.github/actions/holocron@main\n name: Validate ADR and spec frontmatter\n with:\n task: platform.repoValidation\n job: adrs\n\n registry:\n name: Validate registry consistency\n permissions:\n contents: read\n runs-on: ubuntu-latest\n timeout-minutes: 10\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - uses: theholocron/.github/.github/actions/holocron@main\n name: Validate registry consistency\n with:\n task: platform.repoValidation\n job: registry\n\n docs-presence:\n name: Validate docs presence\n permissions:\n contents: read\n runs-on: ubuntu-latest\n timeout-minutes: 10\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n with:\n fetch-depth: 0\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - uses: theholocron/.github/.github/actions/holocron@main\n name: Validate docs presence for new packages\n with:\n task: platform.repoValidation\n job: docsPresence\n env:\n BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}\n\n conclusion:\n name: Conclusion\n runs-on: ubuntu-latest\n if: always()\n needs: [adrs, registry, docs-presence]\n steps:\n - name: Check job statuses\n run: |\n if [[ \"$RESULTS\" == *\"failure\"* ]] || [[ \"$RESULTS\" == *\"cancelled\"* ]]; then\n exit 1\n fi\n env:\n RESULTS: ${{ join(needs.*.result, ',') }}\n";
|
|
172
|
+
//#endregion
|
|
173
|
+
//#region src/templates/reusable/preview.yml
|
|
174
|
+
var preview_default$1 = "name: Preview\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n type:\n description: \"Type of deployment: docs or storybook\"\n required: true\n type: string\n name:\n description: Repo name prefix used to filter the docs site package (<name>-site); omit to run pnpm -C docs build\n required: false\n type: string\n default: \"\"\n storybook-projects:\n description: >\n JSON array of { \"name\"?, \"workingDir\", \"outputDir\"? } objects for storybook deploys.\n Each is built via `pnpm -C <workingDir> build:storybook`. If \"name\" is provided the\n output is placed under `sandbox/<name>/`; omit \"name\" for single-repo deploys and the\n output lands directly in `sandbox/`.\n type: string\n required: false\n default: \"[]\"\n build-script:\n description: pnpm script that builds the Storybook static output (single storybook, type:storybook only)\n type: string\n required: false\n default: build:storybook\n output-dir:\n description: Directory where Storybook writes its static output (single storybook, type:storybook only)\n type: string\n required: false\n default: storybook-static\n cloudflare-project:\n description: >\n Cloudflare Pages project name. Falls back to the CLOUDFLARE_PAGES_PROJECT\n org variable when omitted — set that variable once and all repos get previews\n without per-repo config.\n required: false\n type: string\n default: \"\"\n\njobs:\n deploy-preview:\n name: Preview\n if: ${{ github.event.action != 'closed' }}\n runs-on: ubuntu-latest\n permissions:\n contents: read\n deployments: write\n pull-requests: write\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - name: Build workspace dependencies\n # The docs site imports workspace packages (e.g. a shared theme) via\n # `workspace:*` — its exports map points at dist/, which doesn't\n # exist until something builds it. Nothing else in this job does,\n # so without this step Vite fails to resolve the import entirely.\n if: ${{ inputs.type == 'docs' }}\n run: pnpm run build\n\n - name: Build docs site\n if: ${{ inputs.type == 'docs' && inputs.name != '' }}\n env:\n SITE_NAME: ${{ inputs.name }}\n run: pnpm --filter @theholocron/\"$SITE_NAME\"-site build\n\n - name: Build docs site\n if: ${{ inputs.type == 'docs' && inputs.name == '' }}\n run: pnpm -C docs build\n\n - name: Build Storybook projects\n if: ${{ inputs.storybook-projects != '[]' }}\n env:\n PROJECTS: ${{ inputs.storybook-projects }}\n run: |\n echo \"$PROJECTS\" | jq -c '.[]' | while IFS= read -r project; do\n workingDir=$(echo \"$project\" | jq -r '.workingDir')\n pnpm -C \"$workingDir\" build:storybook\n done\n\n - name: Build Storybook\n if: ${{ inputs.type == 'storybook' && inputs.storybook-projects == '[]' }}\n env:\n BUILD_SCRIPT: ${{ inputs.build-script }}\n run: pnpm run \"$BUILD_SCRIPT\"\n\n - name: Assemble site\n env:\n DEPLOY_TYPE: ${{ inputs.type }}\n SITE_NAME: ${{ inputs.name }}\n PROJECTS: ${{ inputs.storybook-projects }}\n STORYBOOK_OUTPUT_DIR: ${{ inputs.output-dir }}\n run: |\n mkdir -p _site\n if [ \"$DEPLOY_TYPE\" = \"docs\" ]; then\n if [ -n \"$SITE_NAME\" ]; then\n # Site built with base: /<name>/ — nest under _site/<name>/ so asset\n # paths match (/name/_astro/... → served at /name/_astro/...).\n mkdir -p \"_site/${SITE_NAME}\"\n cp -r docs/dist/. \"_site/${SITE_NAME}/\"\n # Redirect root to the base path so the preview URL lands correctly.\n printf \"/ /%s/ 301\\n\" \"$SITE_NAME\" > _site/_redirects\n else\n cp -r docs/dist/. _site/\n fi\n fi\n if [ \"$PROJECTS\" != \"[]\" ]; then\n echo \"$PROJECTS\" | jq -c '.[]' | while IFS= read -r project; do\n name=$(echo \"$project\" | jq -r '.name // \"\"')\n workingDir=$(echo \"$project\" | jq -r '.workingDir')\n outputDir=$(echo \"$project\" | jq -r '.outputDir // \"storybook-static\"')\n if [ -n \"$name\" ]; then\n target=\"_site/sandbox/${name}\"\n else\n target=\"_site/sandbox\"\n fi\n mkdir -p \"$target\"\n cp -r \"${workingDir}/${outputDir}/.\" \"$target/\"\n done\n elif [ \"$DEPLOY_TYPE\" = \"storybook\" ]; then\n mkdir -p _site/sandbox\n cp -r \"${STORYBOOK_OUTPUT_DIR}/.\" _site/sandbox/\n fi\n\n - name: Install Wrangler\n # The action's install step runs npm/pnpm in the repo root; pnpm fails\n # because it rejects adding to a workspace root without -w, and npm fails\n # because package.json contains pnpm catalog: references it cannot parse.\n # Pre-installing from /tmp (no package.json) lets the action find wrangler\n # already on PATH and skip its own install entirely.\n working-directory: /tmp\n run: npm install -g wrangler@4\n\n - uses: cloudflare/wrangler-action@ebbaa1584979971c8614a24965b4405ff95890e0 # v4.0.0\n id: deploy\n name: Deploy to Cloudflare Pages\n # Skip when neither the per-repo input nor the org variable is set.\n if: ${{ inputs.cloudflare-project != '' || vars.CLOUDFLARE_PAGES_PROJECT != '' }}\n with:\n apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}\n accountId: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}\n packageManager: npm\n command: >-\n pages deploy _site\n --project-name ${{ inputs.cloudflare-project || vars.CLOUDFLARE_PAGES_PROJECT }}\n --branch ${{ github.event.repository.name }}-pr-${{ github.event.pull_request.number }}\n --commit-dirty=true\n\n - name: Report preview URL\n if: ${{ steps.deploy.outputs.pages-deployment-alias-url != '' }}\n env:\n GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n ALIAS: ${{ steps.deploy.outputs.pages-deployment-alias-url }}\n SITE_NAME: ${{ inputs.name }}\n PROJECT: ${{ inputs.cloudflare-project || vars.CLOUDFLARE_PAGES_PROJECT }}\n run: |\n PREVIEW_URL=\"${ALIAS%/}${SITE_NAME:+/${SITE_NAME}}/\"\n # GITHUB_HEAD_REF is the PR branch name — links the deployment to this PR's sidebar widget.\n PAYLOAD=$(printf '{\"ref\":\"%s\",\"environment\":\"%s\",\"description\":\"Cloudflare Pages\",\"production_environment\":false,\"auto_merge\":false,\"required_contexts\":[]}' \\\n \"$GITHUB_HEAD_REF\" \"${PROJECT} (Preview)\")\n DEPLOY_ID=$(echo \"$PAYLOAD\" | gh api \"repos/${GITHUB_REPOSITORY}/deployments\" \\\n --method POST --input - | jq -r '.id')\n gh api \"repos/${GITHUB_REPOSITORY}/deployments/${DEPLOY_ID}/statuses\" \\\n --method POST --field state=success --field environment_url=\"${PREVIEW_URL}\"\n\n cleanup:\n name: Clean up Preview\n if: ${{ github.event.action == 'closed' }}\n runs-on: ubuntu-latest\n permissions:\n contents: read\n deployments: write\n pull-requests: write\n steps:\n - name: Delete Cloudflare Pages deployments for branch\n if: ${{ inputs.cloudflare-project != '' || vars.CLOUDFLARE_PAGES_PROJECT != '' }}\n env:\n CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}\n CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}\n PROJECT: ${{ inputs.cloudflare-project || vars.CLOUDFLARE_PAGES_PROJECT }}\n BRANCH: ${{ github.event.repository.name }}-pr-${{ github.event.pull_request.number }}\n GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n run: |\n DEPLOYMENTS=$(curl -s \\\n \"https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/pages/projects/${PROJECT}/deployments\" \\\n -H \"Authorization: Bearer ${CLOUDFLARE_API_TOKEN}\" \\\n | jq -r --arg b \"$BRANCH\" \\\n '.result[] | select(.deployment_trigger.metadata.branch == $b) | .id')\n\n if [ -z \"$DEPLOYMENTS\" ]; then\n echo \"No deployments found for branch ${BRANCH} — nothing to clean up.\"\n exit 0\n fi\n\n for id in $DEPLOYMENTS; do\n echo \"Deleting CF Pages deployment: $id\"\n curl -s -X DELETE \\\n \"https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/pages/projects/${PROJECT}/deployments/${id}?force=true\" \\\n -H \"Authorization: Bearer ${CLOUDFLARE_API_TOKEN}\" | jq -r 'if .success then \" ✓ deleted\" else \" ✗ \\(.errors[0].message)\" end'\n done\n\n # Mark the GitHub Deployment environment as inactive.\n ENV_NAME=\"${PROJECT} (Preview)\"\n gh api \"repos/${GITHUB_REPOSITORY}/deployments\" \\\n | jq -r \".[] | select(.environment == \\\"${ENV_NAME}\\\") | .id\" \\\n | while read -r deploy_id; do\n gh api \"repos/${GITHUB_REPOSITORY}/deployments/${deploy_id}/statuses\" \\\n --method POST --field state=inactive 2>/dev/null || true\n done\n";
|
|
175
|
+
//#endregion
|
|
176
|
+
//#region src/templates/reusable/review.yml
|
|
177
|
+
var review_default$1 = "name: Review\n\n# ReviewDog is the annotation layer — posts inline PR diff annotations.\n# Runs on pull_request only: inline annotations require PR context,\n# and branch protection ensures all changes go through PRs anyway.\n# security.secretDetection.yml / sourceQuality.structuredDataValidation.yml\n# are the CI gates covering push + PR events. Gitleaks and YAML are\n# intentionally duplicated: those gate merges; ReviewDog surfaces exact\n# line annotations in the PR diff.\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n\njobs:\n reviewdog:\n name: Post inline lint annotations\n runs-on: ubuntu-latest\n timeout-minutes: 20\n permissions:\n contents: read\n pull-requests: write\n\n steps:\n - name: Checkout repository\n uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n with:\n fetch-depth: 0\n\n - name: Setup\n if: ${{ hashFiles('pnpm-lock.yaml') != '' }}\n uses: theholocron/.github/.github/actions/setup@main\n\n - name: Install ReviewDog\n uses: reviewdog/action-setup@d8a7baabd7f3e8544ee4dbde3ee41d0011c3a93f # v1\n with:\n reviewdog_version: latest\n\n # Detect which tools are relevant for this repo, excluding node_modules.\n # hashFiles('**/*') recurses into node_modules/.pnpm and produces false\n # positives for repos that don't own those file types.\n # -print -quit stops find after the first match without a pipe, avoiding\n # the SIGPIPE/pipefail exit-141 that find|head-1 triggers under\n # GitHub Actions' default bash --noprofile --norc -e -o pipefail mode.\n - name: Detect project features\n id: detect\n shell: bash\n run: |\n has() { find . -not -path '*/node_modules/*' -name \"$1\" -print -quit 2>/dev/null | grep -q .; }\n has_ext() { find . -not -path '*/node_modules/*' -name \"$1\" -print -quit 2>/dev/null | grep -q .; }\n { { has 'eslint.config.js' || has 'eslint.config.mjs' || has 'eslint.config.cjs' || \\\n has 'eslint.config.ts' || has '.eslintrc' || has '.eslintrc.js' || \\\n has '.eslintrc.cjs' || has '.eslintrc.json' || has '.eslintrc.yaml' || \\\n has '.eslintrc.yml'; } && grep -qF '\"eslint\":' package.json 2>/dev/null; } && echo \"eslint=true\" >> \"$GITHUB_OUTPUT\" || echo \"eslint=false\" >> \"$GITHUB_OUTPUT\"\n { has 'tsconfig.json' && grep -qF '\"typescript\":' package.json 2>/dev/null; } && echo \"tsconfig=true\" >> \"$GITHUB_OUTPUT\" || echo \"tsconfig=false\" >> \"$GITHUB_OUTPUT\"\n has_ext '*.sh' && echo \"shell=true\" >> \"$GITHUB_OUTPUT\" || echo \"shell=false\" >> \"$GITHUB_OUTPUT\"\n has 'Dockerfile' || has_ext '*.Dockerfile' || has 'Containerfile' && \\\n echo \"docker=true\" >> \"$GITHUB_OUTPUT\" || echo \"docker=false\" >> \"$GITHUB_OUTPUT\"\n has_ext '.env*' && echo \"dotenv=true\" >> \"$GITHUB_OUTPUT\" || echo \"dotenv=false\" >> \"$GITHUB_OUTPUT\"\n has_ext '*.md' && echo \"markdown=true\" >> \"$GITHUB_OUTPUT\" || echo \"markdown=false\" >> \"$GITHUB_OUTPUT\"\n\n #\n # Always applicable\n #\n\n - name: Gitleaks (secrets)\n uses: reviewdog/action-gitleaks@2b7b5685e3e3eecddab5d30cfa04f18123031421 # v1\n with:\n reporter: github-pr-annotations\n tool_name: \"Review / gitleaks\"\n fail_level: error\n gitleaks_flags: --log-opts=${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}\n\n - name: YamlLint\n if: ${{ hashFiles('.yamllint.yml') != '' }}\n uses: reviewdog/action-yamllint@b5f7217d8c815ae374d1d55840d5e569d82f01f0 # v1\n with:\n reporter: github-pr-annotations\n tool_name: \"Review / yamllint\"\n fail_level: error\n yamllint_flags: -c ${{ github.workspace }}/.yamllint.yml ${{ github.workspace }}\n\n - name: ActionLint (GitHub Actions)\n if: ${{ hashFiles('.github/workflows/*.yml', '.github/workflows/*.yaml') != '' }}\n uses: reviewdog/action-actionlint@6fb7acc99f4a1008869fa8a0f09cfca740837d9d # v1\n with:\n reporter: github-pr-annotations\n tool_name: \"Review / actionlint\"\n fail_level: error\n\n #\n # TypeScript / JavaScript\n #\n\n - name: ESLint\n if: steps.detect.outputs.eslint == 'true'\n uses: reviewdog/action-eslint@556a3fdaf8b4201d4d74d406013386aa4f7dab96 # v1.34.0\n with:\n reporter: github-pr-annotations\n tool_name: \"Review / eslint\"\n fail_level: error\n eslint_flags: .\n\n - name: TypeScript\n if: steps.detect.outputs.tsconfig == 'true'\n uses: EPMatt/reviewdog-action-tsc@63d923a3c5b4497671940b8874f58a404e2351b5 # v1\n with:\n reporter: github-pr-annotations\n tool_name: \"Review / tsc\"\n fail_level: error\n\n #\n # Shell\n #\n\n - name: ShellCheck\n if: steps.detect.outputs.shell == 'true'\n uses: reviewdog/action-shellcheck@4c07458293ac342d477251099501a718ae5ef86e # v1\n with:\n reporter: github-pr-annotations\n tool_name: \"Review / shellcheck\"\n fail_level: none\n\n #\n # Docker\n #\n\n - name: Hadolint\n if: steps.detect.outputs.docker == 'true'\n uses: reviewdog/action-hadolint@1b2cfa6ba72072ad35158d7ff3aa49bbdc03506d # v1\n with:\n reporter: github-pr-annotations\n tool_name: \"Review / hadolint\"\n fail_level: none\n\n #\n # Environment files\n #\n\n - name: dotenv-linter\n if: steps.detect.outputs.dotenv == 'true'\n uses: dotenv-linter/action-dotenv-linter@afde61cfda2ecffe7bea35837b6f20b956c88689 # v3.0.0\n with:\n reporter: github-code-suggestions\n\n #\n # Documentation\n #\n\n - name: Alex (inclusive language)\n if: steps.detect.outputs.markdown == 'true'\n uses: reviewdog/action-alex@347481655add010a2ae302df34b57c9bcfa0d6e4 # v1\n with:\n reporter: github-pr-annotations\n tool_name: \"Review / alex\"\n";
|
|
178
|
+
//#endregion
|
|
179
|
+
//#region src/templates/reusable/security.codeScanning.yml
|
|
180
|
+
var security_codeScanning_default$1 = "name: Security\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n language:\n description: CodeQL language to analyze\n type: string\n required: false\n default: javascript-typescript\n\njobs:\n analyze:\n name: Analyze (${{ inputs.language }})\n permissions:\n actions: read\n contents: read\n security-events: write\n runs-on: ubuntu-latest\n timeout-minutes: 45\n # Do not cancel in-progress security scans.\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0\n name: Initialize CodeQL\n with:\n languages: ${{ inputs.language }}\n\n - uses: github/codeql-action/autobuild@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0\n name: Autobuild\n\n - uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0\n name: Analyze\n with:\n category: /language:${{ inputs.language }}\n";
|
|
181
|
+
//#endregion
|
|
182
|
+
//#region src/templates/reusable/security.secretDetection.yml
|
|
183
|
+
var security_secretDetection_default$1 = "name: Secret Detection\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n secrets:\n TURBO_TOKEN:\n required: false\n\njobs:\n secret-detection:\n name: Run gitleaks\n permissions:\n contents: read\n runs-on: ubuntu-latest\n timeout-minutes: 15\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n with:\n fetch-depth: 0\n\n # gitleaks has no npm distribution — install the prebuilt binary via\n # the maintainer's own documented install script.\n # NOTE: unverified against a live CI run at authoring time (D12) — flag\n # here if this needs adjustment.\n - name: Install gitleaks\n run: |\n curl -sSfL https://raw.githubusercontent.com/gitleaks/gitleaks/master/install.sh | sh -s -- -b /usr/local/bin\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - uses: theholocron/.github/.github/actions/holocron@main\n name: Run gitleaks\n with:\n task: security.secretDetection\n";
|
|
184
|
+
//#endregion
|
|
185
|
+
//#region src/templates/reusable/sourceQuality.deadCodeAnalysis.yml
|
|
186
|
+
var sourceQuality_deadCodeAnalysis_default$1 = "name: Dead Code Analysis\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n secrets:\n TURBO_TOKEN:\n required: false\n\njobs:\n dead-code-analysis:\n name: Run knip\n permissions:\n contents: read\n runs-on: ubuntu-latest\n timeout-minutes: 10\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n with:\n persist-credentials: false\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - uses: theholocron/.github/.github/actions/holocron@main\n name: Run knip\n with:\n task: sourceQuality.deadCodeAnalysis\n";
|
|
187
|
+
//#endregion
|
|
188
|
+
//#region src/templates/reusable/sourceQuality.formatting.yml
|
|
189
|
+
var sourceQuality_formatting_default$1 = "name: Formatting\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n enable-auto-commit:\n description: >\n Auto-commit prettier/editorconfig/markdownlint --fix results as a\n verified commit via a GitHub App (same App used across every\n formatting-capable task — D12).\n type: boolean\n required: false\n default: false\n secrets:\n SUPER_LINTER_APP_ID:\n required: false\n SUPER_LINTER_PRIVATE_KEY:\n required: false\n TURBO_TOKEN:\n required: false\n\njobs:\n formatting:\n name: Run prettier, editorconfig-checker, markdownlint\n permissions:\n contents: write\n statuses: write\n runs-on: ubuntu-latest\n timeout-minutes: 15\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n APP_ID_SET: ${{ secrets.SUPER_LINTER_APP_ID != '' }}\n steps:\n - name: Generate GitHub App token\n id: app-token\n if: >\n inputs.enable-auto-commit == true &&\n github.event.pull_request != null &&\n github.event.pull_request.head.repo.full_name == github.repository &&\n github.ref_name != github.event.repository.default_branch &&\n env.APP_ID_SET == 'true'\n uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0\n with:\n app-id: ${{ secrets.SUPER_LINTER_APP_ID }}\n private-key: ${{ secrets.SUPER_LINTER_PRIVATE_KEY }}\n\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n with:\n fetch-depth: 0\n token: ${{ steps.app-token.outputs.token || github.token }}\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n # editorconfig-checker has no npm distribution — install the prebuilt\n # binary from its release tarball (v4.0.1+ ships the binary at the\n # tarball root as `editorconfig-checker`, not `bin/ec-<os>-<arch>` —\n # confirmed against a live CI run; the old asset name/path 404s now).\n - name: Install editorconfig-checker\n run: |\n curl -sSL https://github.com/editorconfig-checker/editorconfig-checker/releases/latest/download/editorconfig-checker-linux-amd64.tar.gz \\\n | tar xz -C /usr/local/bin editorconfig-checker\n\n - uses: theholocron/.github/.github/actions/holocron@main\n name: Run prettier, editorconfig-checker, markdownlint\n with:\n task: sourceQuality.formatting\n\n - uses: theholocron/.github/.github/actions/auto-commit@main\n name: Commit and push formatting fixes\n if: >\n inputs.enable-auto-commit == true &&\n github.event.pull_request != null &&\n github.event.pull_request.head.repo.full_name == github.repository &&\n github.ref_name != github.event.repository.default_branch &&\n env.APP_ID_SET == 'true' &&\n steps.app-token.conclusion == 'success'\n with:\n token: ${{ steps.app-token.outputs.token }}\n branch: ${{ github.event.pull_request.head.ref || github.head_ref || github.ref }}\n commit-message: \"chore: fix formatting issues\"\n commit-options: \"--no-verify\"\n";
|
|
190
|
+
//#endregion
|
|
191
|
+
//#region src/templates/reusable/sourceQuality.staticAnalysis.yml
|
|
192
|
+
var sourceQuality_staticAnalysis_default$1 = "name: Static Analysis\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n enable-auto-commit:\n description: >\n Auto-commit eslint --fix results as a verified commit via a GitHub App.\n Requires SUPER_LINTER_APP_ID and SUPER_LINTER_PRIVATE_KEY secrets\n (same App used across every formatting-capable task — D12).\n type: boolean\n required: false\n default: false\n secrets:\n SUPER_LINTER_APP_ID:\n required: false\n SUPER_LINTER_PRIVATE_KEY:\n required: false\n TURBO_TOKEN:\n required: false\n\njobs:\n static-analysis:\n name: Run eslint and actionlint\n permissions:\n contents: write\n statuses: write\n runs-on: ubuntu-latest\n timeout-minutes: 15\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n APP_ID_SET: ${{ secrets.SUPER_LINTER_APP_ID != '' }}\n steps:\n - name: Generate GitHub App token\n id: app-token\n if: >\n inputs.enable-auto-commit == true &&\n github.event.pull_request != null &&\n github.event.pull_request.head.repo.full_name == github.repository &&\n github.ref_name != github.event.repository.default_branch &&\n env.APP_ID_SET == 'true'\n uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0\n with:\n app-id: ${{ secrets.SUPER_LINTER_APP_ID }}\n private-key: ${{ secrets.SUPER_LINTER_PRIVATE_KEY }}\n\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n with:\n fetch-depth: 0\n token: ${{ steps.app-token.outputs.token || github.token }}\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n # actionlint has no npm distribution — install the prebuilt binary via\n # the maintainer's own documented download script.\n # NOTE: unverified against a live CI run at authoring time (D12) — flag\n # here if this needs adjustment.\n - name: Install actionlint\n run: |\n bash <(curl -sSf https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash)\n sudo mv ./actionlint /usr/local/bin/actionlint\n\n - name: Check for merge-conflict markers\n # No package distributes this as a standalone tool — a plain grep for\n # the marker lines (the same check the old bundled linter ran, D12).\n run: |\n if grep -rlnE '^(<{7}|={7}|>{7})( |$)' \\\n --exclude-dir=.git --exclude-dir=node_modules .; then\n echo \"::error::merge-conflict markers found\"\n exit 1\n fi\n\n - uses: theholocron/.github/.github/actions/holocron@main\n name: Run eslint and actionlint\n with:\n task: sourceQuality.staticAnalysis\n\n - uses: theholocron/.github/.github/actions/auto-commit@main\n name: Commit and push eslint --fix\n if: >\n inputs.enable-auto-commit == true &&\n github.event.pull_request != null &&\n github.event.pull_request.head.repo.full_name == github.repository &&\n github.ref_name != github.event.repository.default_branch &&\n env.APP_ID_SET == 'true' &&\n steps.app-token.conclusion == 'success'\n with:\n token: ${{ steps.app-token.outputs.token }}\n branch: ${{ github.event.pull_request.head.ref || github.head_ref || github.ref }}\n commit-message: \"chore: fix static-analysis issues\"\n commit-options: \"--no-verify\"\n";
|
|
193
|
+
//#endregion
|
|
194
|
+
//#region src/templates/reusable/sourceQuality.structuredDataValidation.yml
|
|
195
|
+
var sourceQuality_structuredDataValidation_default$1 = "name: Structured Data Validation\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n secrets:\n TURBO_TOKEN:\n required: false\n\njobs:\n structured-data-validation:\n name: Run yamllint\n permissions:\n contents: read\n runs-on: ubuntu-latest\n timeout-minutes: 10\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n # yamllint has no npm distribution — Python ships preinstalled on\n # ubuntu-latest runners.\n - name: Install yamllint\n run: pip install --user yamllint\n\n - uses: theholocron/.github/.github/actions/holocron@main\n name: Run yamllint\n with:\n task: sourceQuality.structuredDataValidation\n";
|
|
196
|
+
//#endregion
|
|
197
|
+
//#region src/templates/reusable/stale.yml
|
|
198
|
+
var stale_default$1 = "name: Stale\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n days-before-stale:\n description: Days of inactivity before an issue or PR is marked stale (applies to both unless type-specific value is set)\n type: number\n required: false\n default: 30\n days-before-close:\n description: Days after stale label before closing (applies to both unless type-specific value is set)\n type: number\n required: false\n default: 5\n days-before-issue-stale:\n description: Days of inactivity before an issue is marked stale; overrides days-before-stale when set to a non-negative value\n type: number\n required: false\n default: -1\n days-before-issue-close:\n description: Days after stale label before closing an issue; overrides days-before-close when set to a non-negative value\n type: number\n required: false\n default: -1\n days-before-pr-stale:\n description: Days of inactivity before a PR is marked stale; overrides days-before-stale when set to a non-negative value\n type: number\n required: false\n default: -1\n days-before-pr-close:\n description: Days after stale label before closing a PR; overrides days-before-close when set to a non-negative value\n type: number\n required: false\n default: -1\n exempt-issue-labels:\n description: Comma-separated labels that exempt an issue from being marked stale\n type: string\n required: false\n default: \"in-progress,wip\"\n exempt-pr-labels:\n description: Comma-separated labels that exempt a PR from being marked stale\n type: string\n required: false\n default: \"\"\n exempt-all-issue-milestones:\n description: Issues assigned to any milestone are never marked stale\n type: boolean\n required: false\n default: true\n exempt-all-pr-milestones:\n description: PRs assigned to any milestone are never marked stale\n type: boolean\n required: false\n default: true\n exempt-all-issue-projects:\n description: Issues assigned to any project are never marked stale\n type: boolean\n required: false\n default: true\n exempt-all-pr-projects:\n description: PRs assigned to any project are never marked stale\n type: boolean\n required: false\n default: true\n\njobs:\n stale:\n name: Mark stale issues and pull requests\n permissions:\n contents: write\n issues: write\n pull-requests: write\n runs-on: ubuntu-latest\n timeout-minutes: 10\n steps:\n - uses: actions/stale@1e223db275d687790206a7acac4d1a11bd6fe629 # v10.4.0\n name: Run Stale\n with:\n close-issue-message: >\n This issue was closed because it has been stalled for\n ${{ inputs.days-before-close }} days with no activity.\n days-before-close: ${{ inputs.days-before-close }}\n days-before-issue-close: ${{ inputs.days-before-issue-close }}\n days-before-issue-stale: ${{ inputs.days-before-issue-stale }}\n days-before-pr-close: ${{ inputs.days-before-pr-close }}\n days-before-pr-stale: ${{ inputs.days-before-pr-stale }}\n days-before-stale: ${{ inputs.days-before-stale }}\n exempt-all-issue-milestones: ${{ inputs.exempt-all-issue-milestones }}\n exempt-all-issue-projects: ${{ inputs.exempt-all-issue-projects }}\n exempt-all-pr-milestones: ${{ inputs.exempt-all-pr-milestones }}\n exempt-all-pr-projects: ${{ inputs.exempt-all-pr-projects }}\n exempt-issue-labels: ${{ inputs.exempt-issue-labels }}\n exempt-pr-labels: ${{ inputs.exempt-pr-labels }}\n stale-issue-label: wontfix\n stale-issue-message: >\n This issue is stale because it has been open ${{ inputs.days-before-stale }}\n days with no activity. Remove the stale label or comment, or this will be\n closed in ${{ inputs.days-before-close }} days.\n stale-pr-label: wontfix\n stale-pr-message: >\n This PR is stale because it has been open ${{ inputs.days-before-stale }}\n days with no activity. Remove the stale label or comment, or this will be\n closed in ${{ inputs.days-before-close }} days.\n";
|
|
199
|
+
//#endregion
|
|
200
|
+
//#region src/templates/reusable/sync-dispatch.yml
|
|
201
|
+
var sync_dispatch_default = "name: Sync Dispatch\n\non: # yamllint disable-line rule:truthy\n workflow_dispatch:\n inputs:\n steps:\n description: >\n Sync steps to pass to each repo's sync.yml. Default is \"readme\"\n (only README marker blocks are updated).\n type: string\n required: false\n default: readme\n\npermissions:\n contents: read\n\njobs:\n broadcast:\n name: Broadcast sync to all repos\n runs-on: ubuntu-latest\n timeout-minutes: 15\n steps:\n - name: Dispatch sync to all repos with sync.yml\n run: |\n gh api /orgs/theholocron/repos --paginate --jq '.[].name' \\\n | while IFS= read -r repo; do\n gh api \"/repos/theholocron/$repo/contents/.github/workflows/sync.yml\" --silent 2>/dev/null || continue\n echo \"Dispatching sync to theholocron/$repo\"\n gh workflow run sync.yml \\\n --repo \"theholocron/$repo\" \\\n --field \"steps=$STEPS\" \\\n || echo \"Warning: could not dispatch to theholocron/$repo — skipping\"\n done\n env:\n GH_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN }}\n STEPS: ${{ inputs.steps }}\n";
|
|
202
|
+
//#endregion
|
|
203
|
+
//#region src/templates/reusable/sync-github.yml
|
|
204
|
+
var sync_github_default = "name: Sync workflow templates\n\n# Builds the holocron CLI from source and pushes updated workflow templates\n# and composite actions to downstream .github repos. Runs whenever the\n# template source files change on main or alpha.\n#\n# Secrets required:\n# HOLOCRON_SYNC_TOKEN — fine-grained PAT (resource owner: org) with:\n# Actions: Read and write (dispatch workflow runs via gh workflow run)\n# Contents: Read and write (git trees, blobs, refs)\n# Pull requests: Read and write (open sync PR)\n# Workflows: Read and write (write .github/workflows/*.yml)\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n primary-repo:\n description: >\n Primary .github repo — receives composite actions, reusable workflows,\n and thin-caller templates. Requires a PR (branch protection assumed).\n type: string\n required: false\n default: theholocron/.github\n secondary-repos:\n description: >\n Space-separated list of secondary repos (reusable workflows + thin\n callers only, no composite actions). Changes are delivered via pull\n request, same as the primary repo.\n type: string\n required: false\n default: \"\"\n sync-branch:\n description: Branch name used for the primary and secondary repo PRs\n type: string\n required: false\n default: chore/sync-templates\n secrets:\n HOLOCRON_SYNC_TOKEN:\n required: true\n HOLOCRON_READ_TOKEN:\n description: >\n Fine-grained PAT for read-only GitHub API calls (e.g. resolving git\n committer identity via `gh api user`). Falls back to HOLOCRON_SYNC_TOKEN.\n required: false\n GH_TOKEN:\n description: >\n Generic GitHub token fallback for `gh` CLI calls. Used when neither\n HOLOCRON_READ_TOKEN nor HOLOCRON_SYNC_TOKEN is set.\n required: false\n HOLOCRON_AXIOM_TOKEN:\n description: >\n Axiom API token. When set alongside the HOLOCRON_AXIOM_DATASET\n repo/org variable, the CLI ships this run's structured logs to Axiom.\n Falls back to the vendor-native AXIOM_TOKEN secret.\n required: false\n AXIOM_TOKEN:\n description: Vendor-native fallback for HOLOCRON_AXIOM_TOKEN.\n required: false\n TURBO_TOKEN:\n required: false\n\njobs:\n sync:\n name: Sync templates\n runs-on: ubuntu-latest\n timeout-minutes: 15\n permissions:\n contents: read\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n HOLOCRON_AXIOM_TOKEN: ${{ secrets.HOLOCRON_AXIOM_TOKEN || secrets.AXIOM_TOKEN }}\n HOLOCRON_AXIOM_DATASET: ${{ vars.HOLOCRON_AXIOM_DATASET || vars.AXIOM_DATASET }}\n POSTHOG_PROJECT_TOKEN: ${{ vars.POSTHOG_PROJECT_TOKEN }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - run: pnpm build\n name: Build CLI\n\n - name: Cache actionlint\n id: cache-actionlint\n uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0\n with:\n path: /tmp/actionlint\n key: actionlint-v1.7.7-linux-amd64\n\n - name: Download actionlint\n if: steps.cache-actionlint.outputs.cache-hit != 'true'\n run: |\n curl -fsSL https://github.com/rhysd/actionlint/releases/download/v1.7.7/actionlint_1.7.7_linux_amd64.tar.gz \\\n | tar -xz -C /tmp actionlint\n\n - name: Validate generated workflows\n run: |\n node packages/cli/dist/cli.mjs sync github \\\n --repo \"$PRIMARY_REPO\" \\\n --output-dir /tmp/sync-validate\n /tmp/actionlint /tmp/sync-validate/.github/workflows/*.yml\n env:\n PRIMARY_REPO: ${{ inputs.primary-repo }}\n\n - name: Sync primary repo (PR)\n run: |\n GIT_NAME=$(gh api user --jq .name 2>/dev/null || echo \"github-actions[bot]\")\n GIT_EMAIL=$(gh api user --jq '\"\\(.id)+\\(.login)@users.noreply.github.com\"' 2>/dev/null || echo \"41898282+github-actions[bot]@users.noreply.github.com\")\n COMMIT_MSG=\"chore: sync from theholocron/holocron\"$'\\n\\n'\"Signed-off-by: $GIT_NAME <$GIT_EMAIL>\"\n node packages/cli/dist/cli.mjs sync github \\\n --repo \"$PRIMARY_REPO\" \\\n --branch \"$SYNC_BRANCH\" \\\n --pr \\\n --message \"$COMMIT_MSG\"\n GH_TOKEN=\"$HOLOCRON_SYNC_TOKEN\" gh pr merge --auto --squash \\\n --repo \"$PRIMARY_REPO\" \"$SYNC_BRANCH\" 2>/dev/null || true\n env:\n GH_TOKEN: ${{ secrets.HOLOCRON_READ_TOKEN || secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN }}\n HOLOCRON_SYNC_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN }}\n PRIMARY_REPO: ${{ inputs.primary-repo }}\n SYNC_BRANCH: ${{ inputs.sync-branch }}\n\n - name: Sync secondary repos (PR)\n if: ${{ inputs.secondary-repos != '' }}\n run: |\n GIT_NAME=$(gh api user --jq .name 2>/dev/null || echo \"github-actions[bot]\")\n GIT_EMAIL=$(gh api user --jq '\"\\(.id)+\\(.login)@users.noreply.github.com\"' 2>/dev/null || echo \"41898282+github-actions[bot]@users.noreply.github.com\")\n COMMIT_MSG=\"chore: sync from theholocron/holocron\"$'\\n\\n'\"Signed-off-by: $GIT_NAME <$GIT_EMAIL>\"\n for repo in $SECONDARY_REPOS; do\n node packages/cli/dist/cli.mjs sync github \\\n --repo \"$repo\" \\\n --branch \"$SYNC_BRANCH\" \\\n --pr \\\n --message \"$COMMIT_MSG\"\n GH_TOKEN=\"$HOLOCRON_SYNC_TOKEN\" gh pr merge --auto --squash \\\n --repo \"$repo\" \"$SYNC_BRANCH\" 2>/dev/null || true\n done\n env:\n GH_TOKEN: ${{ secrets.HOLOCRON_READ_TOKEN || secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN }}\n HOLOCRON_SYNC_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN }}\n SECONDARY_REPOS: ${{ inputs.secondary-repos }}\n SYNC_BRANCH: ${{ inputs.sync-branch }}\n";
|
|
205
|
+
//#endregion
|
|
206
|
+
//#region src/templates/reusable/tag.yml
|
|
207
|
+
var tag_default = "name: Tag\n\n# Release Please — fully automated tag and GitHub Release from Conventional Commits.\n# No package.json or npm publishing required. Operates in \"simple\" mode by default:\n# analyzes commits since the last tag, maintains a rolling Release PR, and creates\n# a tag + GitHub Release when that PR is merged.\n#\n# The calling repo must have two files at the root:\n# release-please-config.json — declares packages and release-type\n# .release-please-manifest.json — tracks the current version\n\non: # yamllint disable-line rule:truthy\n # Self-trigger: when this workflow lives in theholocron/.github itself,\n # push to main runs Release Please for that repo's own releases.\n push:\n branches:\n - main\n workflow_call:\n inputs:\n release-type:\n description: Release Please release type (simple, node, python, etc.)\n type: string\n required: false\n default: simple\n config-file:\n description: Path to release-please-config.json\n type: string\n required: false\n default: release-please-config.json\n manifest-file:\n description: Path to .release-please-manifest.json\n type: string\n required: false\n default: .release-please-manifest.json\n\njobs:\n tag:\n name: Tag release\n permissions:\n contents: write\n pull-requests: write\n runs-on: ubuntu-latest\n timeout-minutes: 10\n steps:\n - uses: google-github-actions/release-please-action@e4dc86ba9405554aeba3c6bb2d169500e7d3b4ee # v4.1.1\n name: Run Release Please\n with:\n release-type: ${{ inputs.release-type }}\n config-file: ${{ inputs.config-file }}\n manifest-file: ${{ inputs.manifest-file }}\n";
|
|
208
|
+
//#endregion
|
|
209
|
+
//#region src/templates/reusable/verification.performance.yml
|
|
210
|
+
var verification_performance_default$1 = "name: Audit the Performance\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n lighthouse-config:\n description: >\n Path to the Lighthouse CI config file, forwarded to `holocron run\n verification.performance -- --config`. Defaults to\n lighthouse.config.cjs (the org standard).\n type: string\n required: false\n default: lighthouse.config.cjs\n secrets:\n LHCI_GITHUB_APP_TOKEN:\n required: false\n TURBO_TOKEN:\n required: false\n\njobs:\n performance:\n name: Run Lighthouse CI\n permissions:\n contents: read\n runs-on: ubuntu-latest\n timeout-minutes: 15\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n # Read by `lhci autorun` inside `holocron run verification.performance`.\n LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - run: npm install -g @lhci/cli@0.14.x\n name: Install Lighthouse CLI\n\n - uses: theholocron/.github/.github/actions/holocron@main\n name: Run Lighthouse CI\n with:\n task: verification.performance\n args: --config=${{ inputs.lighthouse-config }}\n";
|
|
211
|
+
//#endregion
|
|
212
|
+
//#region src/templates/reusable/verification.typeSafety.yml
|
|
213
|
+
var verification_typeSafety_default$1 = "name: Typecheck\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n secrets:\n TURBO_TOKEN:\n required: false\n\njobs:\n typecheck:\n name: Run tsc --noEmit\n permissions:\n contents: read\n runs-on: ubuntu-latest\n timeout-minutes: 10\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - uses: theholocron/.github/.github/actions/holocron@main\n name: Type check\n with:\n task: verification.typeSafety\n";
|
|
214
|
+
//#endregion
|
|
215
|
+
//#region src/templates/reusable/verification.unitTests.yml
|
|
216
|
+
var verification_unitTests_default$1 = "name: Test\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n run-unit:\n description: Run unit tests with coverage (disable for UI-only repos that use Storybook testing exclusively)\n type: boolean\n required: false\n default: true\n run-storybook:\n description: Run Storybook vitest interaction tests (requires .storybook/ setup)\n type: boolean\n required: false\n default: false\n run-interaction:\n description: Run Storybook interaction and accessibility tests with Playwright\n type: boolean\n required: false\n default: false\n run-chromatic:\n description: Publish Storybook to Chromatic for visual regression testing\n type: boolean\n required: false\n default: false\n chromatic-projects:\n description: >\n JSON array of Chromatic projects to build, one matrix job per entry.\n Each entry: { \"tokenName\": \"WEB\", \"workingDir\": \"apps/web\", \"buildScript\": \"build:storybook\" }.\n tokenName maps to secret CHROMATIC_PROJECT_TOKEN_<TOKENNAME>; use \"default\"\n (or omit for the legacy empty-string form) to use the bare CHROMATIC_PROJECT_TOKEN\n secret for single-project repos.\n buildScript defaults to \"build:storybook\" when omitted.\n Default runs a single job from the repo root using CHROMATIC_PROJECT_TOKEN.\n type: string\n required: false\n default: '[{\"tokenName\":\"default\",\"workingDir\":\".\",\"buildScript\":\"build:storybook\"}]'\n run-user-flow:\n description: Run Cypress E2E user-flow tests (requires cypress.config.*)\n type: boolean\n required: false\n default: false\n wait-on-url:\n description: URL to wait for before running Cypress tests (default is Vite dev server; override for non-Vite stacks e.g. http://localhost:3000 for Next.js)\n type: string\n required: false\n default: \"http://localhost:5173\"\n secrets:\n CHROMATIC_PROJECT_TOKEN:\n required: false\n CYPRESS_RECORD_KEY:\n required: false\n TURBO_TOKEN:\n required: false\n\njobs:\n unit:\n name: Run unit tests and collect coverage\n if: ${{ inputs.run-unit }}\n permissions:\n contents: read\n id-token: write\n runs-on: ubuntu-latest\n timeout-minutes: 15\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - uses: theholocron/.github/.github/actions/holocron@main\n name: Run tests with coverage\n with:\n task: verification.unitTests\n\n # A Codecov upload must not fail silently. fail_ci_if_error makes a failed\n # upload fail this step (the action's default is to log the error and pass),\n # which fails the job and so the `Test / Conclusion` check. Whether that\n # blocks a merge is up to whether the check is required, not this workflow\n # (holocron#934).\n - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0\n name: Upload coverage to Codecov\n with:\n use_oidc: true\n fail_ci_if_error: true\n\n - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0\n name: Upload test results to Codecov\n if: ${{ !cancelled() }}\n with:\n use_oidc: true\n fail_ci_if_error: true\n report_type: test_results\n files: \"**/test-report.junit.xml\"\n\n storybook:\n name: Run Storybook interaction tests\n if: ${{ inputs.run-storybook }}\n permissions:\n contents: read\n id-token: write\n runs-on: ubuntu-latest\n timeout-minutes: 15\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - run: pnpm exec playwright install chromium --with-deps\n name: Install Playwright\n\n - run: pnpm test:storybook\n name: Run Storybook tests\n\n # A Codecov upload must not fail silently. fail_ci_if_error makes a failed\n # upload fail this step (the action's default is to log the error and pass),\n # which fails the job and so the `Test / Conclusion` check. Whether that\n # blocks a merge is up to whether the check is required, not this workflow\n # (holocron#934).\n - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0\n name: Upload coverage to Codecov\n with:\n use_oidc: true\n fail_ci_if_error: true\n\n - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0\n name: Upload test results to Codecov\n if: ${{ !cancelled() }}\n with:\n use_oidc: true\n fail_ci_if_error: true\n report_type: test_results\n files: \"**/test-report.junit.xml\"\n\n visual-and-composition:\n # No explicit matrix reference here — a skipped job (run-chromatic:\n # false, the common case) never expands its matrix, so a `${{\n # matrix.project.tokenName }}` in name: has nothing to resolve and\n # shows as raw template text in the checks UI. GitHub auto-appends\n # the matrix values in parens when the matrix *does* expand, so this\n # loses no distinctness across multiple Chromatic projects.\n name: Run visual and composition tests\n if: ${{ inputs.run-chromatic }}\n strategy:\n fail-fast: false\n matrix:\n project: ${{ fromJSON(inputs.chromatic-projects) }}\n permissions:\n contents: read\n statuses: write\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n with:\n fetch-depth: 0\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - uses: chromaui/action@14cfaef73576e69f95f47f60058063f46ca38719 # v18\n name: Publish to Chromatic\n with:\n projectToken: ${{ (matrix.project.tokenName == 'default' || matrix.project.tokenName == '') && secrets.CHROMATIC_PROJECT_TOKEN || secrets[format('CHROMATIC_PROJECT_TOKEN_{0}', matrix.project.tokenName)] }}\n token: ${{ github.token }}\n buildScriptName: ${{ matrix.project.buildScript || 'build:storybook' }}\n workingDir: ${{ matrix.project.workingDir || '.' }}\n storybookBaseDir: ${{ matrix.project.storybookBaseDir || '' }}\n untraced: ${{ matrix.project.untraced || '' }}\n onlyStoryFiles: ${{ matrix.project.onlyStoryFiles || '' }}\n exitZeroOnChanges: ${{ matrix.project.exitZeroOnChanges || false }}\n\n interaction-and-accessibility:\n name: Run interactions and accessibility tests\n if: ${{ inputs.run-interaction }}\n permissions:\n contents: read\n runs-on: ubuntu-latest\n timeout-minutes: 15\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - run: pnpm exec playwright install --with-deps\n name: Install Playwright\n\n - run: pnpm test:storybook\n name: Run interaction and accessibility tests\n\n user-flow:\n name: Run user flow tests\n if: ${{ inputs.run-user-flow }}\n permissions:\n contents: read\n runs-on: ubuntu-latest\n timeout-minutes: 15\n strategy:\n fail-fast: false\n matrix:\n containers: [1, 2]\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - run: pnpm exec cypress install\n name: Install Cypress binary\n\n - uses: cypress-io/github-action@1052aa98bbbe4f55210f844878213c07d9c8c399 # v6.7.13\n name: Cypress run\n with:\n start: pnpm dev\n wait-on: ${{ inputs.wait-on-url }}\n record: true\n parallel: true\n env:\n CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}\n GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n conclusion:\n name: Conclusion\n runs-on: ubuntu-latest\n if: always()\n needs: [unit, storybook, visual-and-composition, interaction-and-accessibility, user-flow]\n steps:\n - name: Check job statuses\n run: |\n if [[ \"$RESULTS\" == *\"failure\"* ]] || [[ \"$RESULTS\" == *\"cancelled\"* ]]; then\n exit 1\n fi\n env:\n RESULTS: ${{ join(needs.*.result, ',') }}\n";
|
|
217
|
+
//#endregion
|
|
218
|
+
//#region src/generators/github-action-thin-callers.ts
|
|
423
219
|
/**
|
|
424
|
-
*
|
|
425
|
-
* of its sub-jobs) locally.
|
|
426
|
-
*
|
|
427
|
-
* A `job` argument only applies to tasks that declare `jobs`; for any other
|
|
428
|
-
* task it is folded back into the passthrough (`holocron run delivery.build
|
|
429
|
-
* src/`).
|
|
220
|
+
* Workflow templates + thin-caller generation.
|
|
430
221
|
*
|
|
431
|
-
*
|
|
222
|
+
* `WORKFLOW_TEMPLATES` holds each reusable `.github/workflows/<name>.yml`
|
|
223
|
+
* (synced to `theholocron/.github`); `generateThinCallerContent` wraps one
|
|
224
|
+
* into the thin caller `holocron repo setup` / `holocron repo sync` write locally.
|
|
225
|
+
*/
|
|
226
|
+
const WORKFLOW_TEMPLATES = {
|
|
227
|
+
"verification.unitTests": "name: Verification\n\non: # yamllint disable-line rule:truthy\n push:\n branches: [main, alpha]\n pull_request:\n\nconcurrency:\n group: test-${{ github.ref }}\n cancel-in-progress: true\n\npermissions:\n contents: read\n id-token: write\n statuses: write\n\njobs:\n test:\n name: Test\n uses: theholocron/.github/.github/workflows/verification.unitTests.yml@main\n with:\n run-unit: true\n secrets: inherit\n",
|
|
228
|
+
"verification.typeSafety": "name: Verification\n\non: # yamllint disable-line rule:truthy\n push:\n branches: [main, alpha]\n pull_request:\n\nconcurrency:\n group: typecheck-${{ github.ref }}\n cancel-in-progress: true\n\npermissions:\n contents: read\n\njobs:\n typecheck:\n name: Typecheck\n uses: theholocron/.github/.github/workflows/verification.typeSafety.yml@main\n secrets: inherit\n",
|
|
229
|
+
"verification.performance": "name: Verification\n\non: # yamllint disable-line rule:truthy\n push:\n branches: [main, alpha]\n pull_request:\n\npermissions:\n contents: read\n\njobs:\n performance:\n name: Audit the Performance\n uses: theholocron/.github/.github/workflows/verification.performance.yml@main\n secrets: inherit\n",
|
|
230
|
+
"sourceQuality.staticAnalysis": "name: Source Quality\n\non: # yamllint disable-line rule:truthy\n push:\n branches: [main, alpha]\n pull_request:\n\nconcurrency:\n group: source-quality-static-analysis-${{ github.ref }}\n cancel-in-progress: true\n\npermissions:\n contents: write\n statuses: write\n\njobs:\n static-analysis:\n name: Static Analysis\n uses: theholocron/.github/.github/workflows/sourceQuality.staticAnalysis.yml@main\n secrets: inherit\n",
|
|
231
|
+
"sourceQuality.formatting": "name: Source Quality\n\non: # yamllint disable-line rule:truthy\n push:\n branches: [main, alpha]\n pull_request:\n\nconcurrency:\n group: source-quality-formatting-${{ github.ref }}\n cancel-in-progress: true\n\npermissions:\n contents: write\n statuses: write\n\njobs:\n formatting:\n name: Formatting\n uses: theholocron/.github/.github/workflows/sourceQuality.formatting.yml@main\n secrets: inherit\n",
|
|
232
|
+
"sourceQuality.structuredDataValidation": "name: Source Quality\n\non: # yamllint disable-line rule:truthy\n push:\n branches: [main, alpha]\n pull_request:\n\nconcurrency:\n group: source-quality-structured-data-validation-${{ github.ref }}\n cancel-in-progress: true\n\npermissions:\n contents: read\n\njobs:\n structured-data-validation:\n name: Structured Data Validation\n uses: theholocron/.github/.github/workflows/sourceQuality.structuredDataValidation.yml@main\n secrets: inherit\n",
|
|
233
|
+
"sourceQuality.deadCodeAnalysis": "name: Source Quality\n\non: # yamllint disable-line rule:truthy\n push:\n branches: [main, alpha]\n pull_request:\n\npermissions:\n contents: read\n\njobs:\n dead-code-analysis:\n name: Dead Code Analysis\n uses: theholocron/.github/.github/workflows/sourceQuality.deadCodeAnalysis.yml@main\n secrets: inherit\n",
|
|
234
|
+
"security.secretDetection": "name: Security\n\non: # yamllint disable-line rule:truthy\n push:\n branches: [main, alpha]\n pull_request:\n\nconcurrency:\n group: security-secret-detection-${{ github.ref }}\n cancel-in-progress: true\n\npermissions:\n contents: read\n\njobs:\n secret-detection:\n name: Secret Detection\n uses: theholocron/.github/.github/workflows/security.secretDetection.yml@main\n secrets: inherit\n",
|
|
235
|
+
"security.codeScanning": "name: Security\n\non: # yamllint disable-line rule:truthy\n push:\n branches:\n - main\n pull_request:\n branches:\n - main\n schedule:\n - cron: \"0 0 * * 1\"\n\npermissions:\n actions: read\n contents: read\n security-events: write\n\njobs:\n security:\n name: Code Scanning\n uses: theholocron/.github/.github/workflows/security.codeScanning.yml@main\n secrets: inherit\n",
|
|
236
|
+
"delivery.build": "name: Delivery\n\non: # yamllint disable-line rule:truthy\n push:\n branches-ignore:\n - main # delivery.publish builds before publishing\n pull_request:\n branches:\n - main\n\nconcurrency:\n group: delivery.build-${{ github.ref }}\n cancel-in-progress: true\n\npermissions:\n contents: read\n\njobs:\n build:\n name: Build\n uses: theholocron/.github/.github/workflows/delivery.build.yml@main\n secrets: inherit\n",
|
|
237
|
+
"delivery.publish": "name: Delivery\n\non: # yamllint disable-line rule:truthy\n push:\n branches:\n - main\n - alpha\n workflow_dispatch:\n inputs:\n dry_run:\n description: >\n Dry run — analyze commits and preview the release without git writes\n or publish. Push-triggered runs always run fully; this only applies\n to manual workflow_dispatch triggers.\n required: false\n default: true\n type: boolean\n\npermissions:\n contents: write\n id-token: write\n issues: write\n pull-requests: write\n\nconcurrency:\n group: ${{ github.workflow }}-${{ github.ref }}\n cancel-in-progress: false\n\njobs:\n release:\n name: Release\n uses: theholocron/.github/.github/workflows/delivery.publish.yml@main\n with:\n dry-run: ${{ inputs.dry_run == true }}\n secrets: inherit\n",
|
|
238
|
+
"delivery.deploy": "name: Delivery\n\non: # yamllint disable-line rule:truthy\n push:\n branches: [main]\n workflow_dispatch:\n\nconcurrency:\n group: pages\n cancel-in-progress: false\n\npermissions:\n contents: read\n pages: write\n id-token: write\n\njobs:\n deploy:\n name: Deploy\n uses: theholocron/.github/.github/workflows/delivery.deploy.yml@main\n secrets: inherit\n",
|
|
239
|
+
"delivery.bundleSize": "name: Delivery\n\non: # yamllint disable-line rule:truthy\n push:\n branches: [main, alpha]\n pull_request:\n\npermissions:\n contents: read\n\njobs:\n bundle-size:\n name: Audit the Bundle Size\n uses: theholocron/.github/.github/workflows/delivery.bundleSize.yml@main\n secrets: inherit\n",
|
|
240
|
+
"platform.repoSync": "name: Platform\n\non: # yamllint disable-line rule:truthy\n push:\n branches: [main]\n paths:\n - holocron.config.ts\n - package.json\n - pnpm-workspace.yaml\n workflow_dispatch:\n inputs:\n steps:\n description: \"Sync steps to run (default: all)\"\n type: string\n required: false\n\nconcurrency:\n group: sync-${{ github.ref }}\n cancel-in-progress: true\n\npermissions:\n contents: write\n pull-requests: write\n\njobs:\n sync:\n name: Sync\n uses: theholocron/.github/.github/workflows/platform.repoSync.yml@main\n with:\n steps: ${{ inputs.steps }}\n secrets: inherit\n",
|
|
241
|
+
"platform.commitStandards": "name: Platform\n\non: # yamllint disable-line rule:truthy\n pull_request:\n\nconcurrency:\n group: platform-commit-standards-${{ github.ref }}\n cancel-in-progress: true\n\npermissions:\n contents: read\n\njobs:\n commit-standards:\n name: Commit Standards\n uses: theholocron/.github/.github/workflows/platform.commitStandards.yml@main\n secrets: inherit\n",
|
|
242
|
+
"platform.repoValidation": "name: Platform\n\non: # yamllint disable-line rule:truthy\n push:\n branches: [main, alpha]\n pull_request:\n\npermissions:\n contents: read\n\njobs:\n repo-validation:\n name: Repo Validation\n uses: theholocron/.github/.github/workflows/platform.repoValidation.yml@main\n secrets: inherit\n",
|
|
243
|
+
"knowledge.wiki": "name: Knowledge\n\non: # yamllint disable-line rule:truthy\n push:\n branches: [main]\n pull_request:\n branches: [main]\n\nconcurrency:\n group: ${{ github.event_name == 'pull_request' && format('wiki-preview-{0}', github.event.pull_request.number) || 'wiki' }}\n cancel-in-progress: ${{ github.event_name == 'pull_request' }}\n\npermissions:\n contents: read\n deployments: write\n\njobs:\n publish:\n name: Publish\n if: ${{ github.event_name != 'pull_request' }}\n uses: theholocron/.github/.github/workflows/knowledge.wiki.yml@main\n secrets: inherit\n\n preview:\n name: Preview\n if: ${{ github.event_name == 'pull_request' }}\n uses: theholocron/.github/.github/workflows/knowledge.wiki.yml@main\n with:\n preview: true\n preview-id: pr-${{ github.event.pull_request.number }}\n secrets: inherit\n",
|
|
244
|
+
preview: "name: Preview\n\non: # yamllint disable-line rule:truthy\n pull_request:\n branches: [main]\n\nconcurrency:\n group: preview-${{ github.event.pull_request.number }}\n cancel-in-progress: true\n\npermissions:\n contents: read\n deployments: write\n pull-requests: write\n\njobs:\n preview:\n name: Preview\n uses: theholocron/.github/.github/workflows/preview.yml@main\n secrets: inherit\n",
|
|
245
|
+
review: "name: Source Quality\n\non: # yamllint disable-line rule:truthy\n pull_request:\n\nconcurrency:\n group: review-${{ github.ref }}\n cancel-in-progress: true\n\npermissions:\n contents: read\n checks: write\n pull-requests: write\n\njobs:\n review:\n name: Annotations\n uses: theholocron/.github/.github/workflows/review.yml@main\n secrets: inherit\n",
|
|
246
|
+
stale: "name: Stale\n\non: # yamllint disable-line rule:truthy\n schedule:\n - cron: \"30 1 * * *\"\n\npermissions:\n contents: write\n issues: write\n pull-requests: write\n\njobs:\n stale:\n uses: theholocron/.github/.github/workflows/stale.yml@main\n with:\n exempt-issue-labels: \"in-progress,wip\"\n exempt-all-issue-milestones: true\n exempt-all-issue-projects: true\n exempt-all-pr-projects: true\n secrets: inherit\n",
|
|
247
|
+
greetings: "name: Platform\n\non: # yamllint disable-line rule:truthy\n pull_request:\n issues:\n\npermissions:\n issues: write\n pull-requests: write\n\njobs:\n greetings:\n name: Greetings\n uses: theholocron/.github/.github/workflows/greetings.yml@main\n secrets: inherit\n",
|
|
248
|
+
dependencies: "name: Platform\n\non: # yamllint disable-line rule:truthy\n pull_request:\n\npermissions:\n contents: write\n pull-requests: write\n\njobs:\n dependencies:\n name: Dependencies\n uses: theholocron/.github/.github/workflows/dependencies.yml@main\n secrets: inherit\n",
|
|
249
|
+
bookkeeping: "name: Platform\n\non: # yamllint disable-line rule:truthy\n pull_request:\n types:\n - opened\n - edited\n\npermissions:\n contents: read\n issues: write\n pull-requests: write\n\njobs:\n bookkeeping:\n name: Bookkeeping\n uses: theholocron/.github/.github/workflows/bookkeeping.yml@main\n secrets: inherit\n"
|
|
250
|
+
};
|
|
251
|
+
const KNOWN_WORKFLOWS = new Set(Object.keys(WORKFLOW_TEMPLATES));
|
|
252
|
+
/**
|
|
253
|
+
* The GitHub status-check context a `required` task contributes to branch
|
|
254
|
+
* protection. Format: `"{workflow name} / {job name}"`.
|
|
432
255
|
*
|
|
433
|
-
*
|
|
434
|
-
*
|
|
435
|
-
*
|
|
436
|
-
*
|
|
437
|
-
*
|
|
438
|
-
*
|
|
439
|
-
*
|
|
440
|
-
*
|
|
441
|
-
*
|
|
442
|
-
*
|
|
256
|
+
* Most of these are single-job workflows now (D3's decomposition split what
|
|
257
|
+
* used to be multi-job `lint`/`audit` into one task per concern) — the
|
|
258
|
+
* context names that job directly, no `Conclusion` aggregator needed; a
|
|
259
|
+
* single job's own conclusion already *is* the workflow's conclusion.
|
|
260
|
+
* `Conclusion` fan-in jobs are kept only where a task genuinely has several
|
|
261
|
+
* conditionally-run jobs feeding one required check:
|
|
262
|
+
* `verification.unitTests` (unit / Storybook / Chromatic / interaction /
|
|
263
|
+
* user-flow, each gated by its own `run-*` input) and
|
|
264
|
+
* `platform.repoValidation` (its three script jobs all must pass). Only
|
|
265
|
+
* merge-gating workflows are listed. `astromech.requiredChecks()` reads this
|
|
266
|
+
* for every `required` task.
|
|
267
|
+
*/
|
|
268
|
+
const WORKFLOW_CHECK_CONTEXTS = {
|
|
269
|
+
"delivery.build": "Build / Build every workspace",
|
|
270
|
+
"verification.unitTests": "Test / Conclusion",
|
|
271
|
+
"verification.typeSafety": "Typecheck / Run tsc --noEmit",
|
|
272
|
+
"verification.performance": "Audit the Performance / Run Lighthouse CI",
|
|
273
|
+
"sourceQuality.staticAnalysis": "Static Analysis / Run eslint and actionlint",
|
|
274
|
+
"sourceQuality.formatting": "Formatting / Run prettier, editorconfig-checker, markdownlint",
|
|
275
|
+
"sourceQuality.structuredDataValidation": "Structured Data Validation / Run yamllint",
|
|
276
|
+
"sourceQuality.deadCodeAnalysis": "Dead Code Analysis / Run knip",
|
|
277
|
+
"security.secretDetection": "Secret Detection / Run gitleaks",
|
|
278
|
+
"platform.commitStandards": "Commit Standards / Run commitlint",
|
|
279
|
+
"platform.repoValidation": "Repo Validation / Conclusion",
|
|
280
|
+
"delivery.bundleSize": "Audit the Bundle Size / Upload bundle stats to Codecov"
|
|
281
|
+
};
|
|
282
|
+
/**
|
|
283
|
+
* Generate the thin caller content for a workflow, optionally injecting or
|
|
284
|
+
* merging `with:` overrides into the jobs block.
|
|
443
285
|
*
|
|
444
|
-
*
|
|
445
|
-
*
|
|
446
|
-
*
|
|
447
|
-
*
|
|
448
|
-
*
|
|
449
|
-
*
|
|
450
|
-
*
|
|
286
|
+
* Two strategies are used depending on the template:
|
|
287
|
+
* - Templates that already have a `with:` block (e.g. lint):
|
|
288
|
+
* the override entries are merged in, replacing existing keys and appending
|
|
289
|
+
* new ones.
|
|
290
|
+
* - Templates that end with ` secrets: inherit`: a new `with:` block is
|
|
291
|
+
* injected immediately before `secrets: inherit`.
|
|
292
|
+
* If neither pattern matches the template, a warning is emitted and the
|
|
293
|
+
* base template is returned unchanged.
|
|
451
294
|
*/
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
logger.debug({
|
|
461
|
-
task,
|
|
462
|
-
command,
|
|
463
|
-
status: "dry-run"
|
|
464
|
-
}, `run: ${task}`);
|
|
465
|
-
return {
|
|
466
|
-
status: "dry-run",
|
|
467
|
-
command
|
|
468
|
-
};
|
|
469
|
-
}
|
|
470
|
-
print(`→ ${command}`);
|
|
471
|
-
const { exitCode } = exec(cmd, args, { cwd });
|
|
472
|
-
const status = exitCode === 0 ? "ok" : "fail";
|
|
473
|
-
logger[status === "fail" ? "warn" : "debug"]({
|
|
474
|
-
task,
|
|
475
|
-
command,
|
|
476
|
-
exitCode,
|
|
477
|
-
status
|
|
478
|
-
}, `run: ${task}`);
|
|
479
|
-
return {
|
|
480
|
-
status,
|
|
481
|
-
command,
|
|
482
|
-
...status === "fail" ? { message: `\`${command}\` exited ${exitCode}` } : {}
|
|
483
|
-
};
|
|
295
|
+
function generateThinCallerContent(name, withOverrides, additionalPaths, logger, comments) {
|
|
296
|
+
const base = WORKFLOW_TEMPLATES[name];
|
|
297
|
+
if (!base) return "";
|
|
298
|
+
const yamlScalar = (v) => {
|
|
299
|
+
if (v === true) return "true";
|
|
300
|
+
if (v === false) return "false";
|
|
301
|
+
const s = String(v);
|
|
302
|
+
return s.startsWith("[") || s.startsWith("{") ? `'${s}'` : s;
|
|
484
303
|
};
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
let
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
const
|
|
499
|
-
|
|
500
|
-
logger.warn({
|
|
501
|
-
task,
|
|
502
|
-
job,
|
|
503
|
-
status: "unknown"
|
|
504
|
-
}, `run: ${task} ${job}`);
|
|
505
|
-
return {
|
|
506
|
-
status: "unknown",
|
|
507
|
-
message: `unknown job "${task} ${job}"`
|
|
508
|
-
};
|
|
509
|
-
}
|
|
510
|
-
const label = `${task} ${job}`;
|
|
511
|
-
return runUnit(input, label, jobDef.local, makeRunOne({
|
|
512
|
-
...input,
|
|
513
|
-
task: label
|
|
514
|
-
}), passthrough);
|
|
515
|
-
}
|
|
516
|
-
const run = makeRunOne(input);
|
|
517
|
-
if (turboDefinesTask(cwd, task, readFile, fileExists)) {
|
|
518
|
-
const filterArg = input.filter ? [`--filter=${input.filter}`] : [];
|
|
519
|
-
const forwarded = [...def?.local && !def.local.command && def.local.tool ? def.flags?.[def.local.tool] ?? [] : [], ...passthrough];
|
|
520
|
-
const args = [
|
|
521
|
-
"run",
|
|
522
|
-
task,
|
|
523
|
-
...filterArg,
|
|
524
|
-
...forwarded.length ? ["--", ...forwarded] : []
|
|
525
|
-
];
|
|
526
|
-
return run(resolveBin(cwd, "turbo", fileExists), args);
|
|
527
|
-
}
|
|
528
|
-
const script = packageJsonScript(cwd, task, readFile, fileExists);
|
|
529
|
-
if (script && !/^holocron run\b/.test(script.trim())) return run(packageManager(cwd, readFile, fileExists), [
|
|
530
|
-
"run",
|
|
531
|
-
task,
|
|
532
|
-
...passthrough.length ? ["--", ...passthrough] : []
|
|
533
|
-
]);
|
|
534
|
-
if (def?.local) {
|
|
535
|
-
if (def.local.command) return run(process.execPath, [
|
|
536
|
-
process.argv[1],
|
|
537
|
-
def.local.command,
|
|
538
|
-
...passthrough
|
|
539
|
-
]);
|
|
540
|
-
const runner = resolveRunner(def.local, cwd, listDir);
|
|
541
|
-
if (runner) {
|
|
542
|
-
const flags = (def.flags?.[runner.tool] ?? []).filter((f) => !passthrough.includes(f));
|
|
543
|
-
const configFlags = resolveToolConfig(runner.tool, cwd, {
|
|
544
|
-
readFile,
|
|
545
|
-
fileExists
|
|
546
|
-
});
|
|
547
|
-
return run(resolveBin(cwd, runner.tool, fileExists), [
|
|
548
|
-
...runner.args,
|
|
549
|
-
...configFlags,
|
|
550
|
-
...flags,
|
|
551
|
-
...passthrough
|
|
552
|
-
]);
|
|
304
|
+
const fmt = (k, v) => {
|
|
305
|
+
const line = ` ${k}: ${yamlScalar(v)}`;
|
|
306
|
+
return comments?.[k] ? ` # ${comments[k]}\n${line}` : line;
|
|
307
|
+
};
|
|
308
|
+
let result = base;
|
|
309
|
+
if (additionalPaths && additionalPaths.length > 0) {
|
|
310
|
+
const pathsBlockRe = /( {4}paths:\n)((?:[ ]{6}- [^\n]+\n)+)/;
|
|
311
|
+
if (pathsBlockRe.test(result)) result = result.replace(pathsBlockRe, (_, header, existing) => {
|
|
312
|
+
const existingPaths = new Set([...existing.matchAll(/- (.+)/g)].map((m) => m[1]));
|
|
313
|
+
const newEntries = additionalPaths.filter((p) => !existingPaths.has(p)).map((p) => ` - ${p}\n`).join("");
|
|
314
|
+
return header + existing + newEntries;
|
|
315
|
+
});
|
|
316
|
+
else {
|
|
317
|
+
const pathsBlock = ` paths:\n${additionalPaths.map((p) => ` - ${p}\n`).join("")}`;
|
|
318
|
+
result = result.replace(/( {4}branches: \[main\]\n)/, `$1${pathsBlock}`);
|
|
553
319
|
}
|
|
554
320
|
}
|
|
555
|
-
if (
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
message: msg
|
|
567
|
-
};
|
|
321
|
+
if (!withOverrides || Object.keys(withOverrides).length === 0) return result;
|
|
322
|
+
const withBlockRe = /( {4}with:\n)((?:[ ]{6}[^\n]+\n)*)/;
|
|
323
|
+
const existingMatch = result.match(withBlockRe);
|
|
324
|
+
if (existingMatch) {
|
|
325
|
+
const existingEntries = new Map(existingMatch[2].split("\n").filter(Boolean).filter((line) => !line.trim().startsWith("#")).map((line) => {
|
|
326
|
+
const m = line.match(/^ {6}([^:]+):\s*(.*)/);
|
|
327
|
+
return m ? [m[1].trim(), m[2].trim()] : null;
|
|
328
|
+
}).filter((e) => e !== null));
|
|
329
|
+
for (const [k, v] of Object.entries(withOverrides)) existingEntries.set(k, yamlScalar(v));
|
|
330
|
+
const merged = [...existingEntries.entries()].map(([k, v]) => ` ${k}: ${v}`).join("\n");
|
|
331
|
+
return result.replace(withBlockRe, ` with:\n${merged}\n`);
|
|
568
332
|
}
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
333
|
+
const withBlock = Object.entries(withOverrides).map(([k, v]) => fmt(k, v)).join("\n");
|
|
334
|
+
const injected = result.replace(/ {4}secrets: inherit\n$/, ` with:\n${withBlock}\n secrets: inherit\n`);
|
|
335
|
+
if (injected === result) logger?.warn({ template: name }, "generateThinCallerContent: could not inject `with:` overrides");
|
|
336
|
+
return injected;
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Extract the Cloudflare Pages preview config from a deploy workflow's `with:` object.
|
|
340
|
+
*
|
|
341
|
+
* Accepts three forms:
|
|
342
|
+
* - `preview: true` — derive both project and domain from org context
|
|
343
|
+
* - `preview: { project: "..." }` — explicit project; domain derived from context if omitted
|
|
344
|
+
* - `preview: { project: "...", domain: "..." }` — fully explicit
|
|
345
|
+
*
|
|
346
|
+
* Returns null when `preview:` is absent, false, or can't be resolved.
|
|
347
|
+
*/
|
|
348
|
+
function extractPreviewConfig(raw, ctx = {}) {
|
|
349
|
+
const preview = raw["preview"];
|
|
350
|
+
if (!preview) return null;
|
|
351
|
+
if (preview === true) {
|
|
352
|
+
const project = ctx.org ? `${ctx.org}-preview` : null;
|
|
353
|
+
const domain = ctx.domain ? `preview.${ctx.domain}` : void 0;
|
|
354
|
+
if (!project) return null;
|
|
576
355
|
return {
|
|
577
|
-
|
|
578
|
-
|
|
356
|
+
project,
|
|
357
|
+
...domain ? { domain } : {}
|
|
579
358
|
};
|
|
580
359
|
}
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
360
|
+
if (typeof preview !== "object") return null;
|
|
361
|
+
const p = preview;
|
|
362
|
+
const project = typeof p["project"] === "string" && p["project"] ? p["project"] : ctx.org ? `${ctx.org}-preview` : null;
|
|
363
|
+
if (!project) return null;
|
|
364
|
+
const domain = typeof p["domain"] === "string" && p["domain"] ? p["domain"] : ctx.domain ? `preview.${ctx.domain}` : void 0;
|
|
586
365
|
return {
|
|
587
|
-
|
|
588
|
-
|
|
366
|
+
project,
|
|
367
|
+
...domain ? { domain } : {}
|
|
589
368
|
};
|
|
590
369
|
}
|
|
591
370
|
/**
|
|
592
|
-
*
|
|
593
|
-
*
|
|
594
|
-
*
|
|
371
|
+
* Generate the full thin-caller YAML for a `deploy.yml` that handles both
|
|
372
|
+
* production (push to main → GitHub Pages) and preview (pull_request →
|
|
373
|
+
* Cloudflare Pages) in a single file.
|
|
595
374
|
*
|
|
596
|
-
*
|
|
597
|
-
*
|
|
598
|
-
*
|
|
599
|
-
*
|
|
600
|
-
* here, flag the rest, let CI enforce.
|
|
375
|
+
* Both jobs receive the same docs/storybook `with:` inputs. If the per-repo
|
|
376
|
+
* config supplies `cloudflare-project` it is forwarded; otherwise the reusable
|
|
377
|
+
* falls back to the `CLOUDFLARE_PAGES_PROJECT` org variable — set that once and
|
|
378
|
+
* all repos with a `deploy` workflow get previews without per-repo config.
|
|
601
379
|
*/
|
|
602
|
-
function
|
|
603
|
-
const
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
status: input.required ? "fail" : "skip"
|
|
609
|
-
}, `run: ${label}`);
|
|
610
|
-
return {
|
|
611
|
-
status: input.required ? "fail" : "skip",
|
|
612
|
-
message: msg
|
|
613
|
-
};
|
|
380
|
+
function generateCombinedDeployContent(deployWith, paths, preview) {
|
|
381
|
+
const yamlScalar = (v) => {
|
|
382
|
+
if (v === true) return "true";
|
|
383
|
+
if (v === false) return "false";
|
|
384
|
+
const s = String(v);
|
|
385
|
+
return s.startsWith("[") || s.startsWith("{") ? `'${s}'` : s;
|
|
614
386
|
};
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
return run(found, [...runner.args, ...passthrough]);
|
|
621
|
-
}
|
|
622
|
-
const msg = `no local equivalent for ${label} — enforced in CI`;
|
|
623
|
-
print(`· ${msg}`);
|
|
624
|
-
logger.debug({
|
|
625
|
-
task: label,
|
|
626
|
-
status: "skip"
|
|
627
|
-
}, `run: ${label}`);
|
|
628
|
-
return {
|
|
629
|
-
status: "skip",
|
|
630
|
-
message: msg
|
|
387
|
+
const withLines = (entries) => Object.entries(entries).map(([k, v]) => ` ${k}: ${yamlScalar(v)}`).join("\n");
|
|
388
|
+
const pathsBlock = paths.length > 0 ? ` paths:\n${paths.map((p) => ` - ${p}\n`).join("")}` : "";
|
|
389
|
+
const previewWith = {
|
|
390
|
+
...deployWith,
|
|
391
|
+
"cloudflare-project": preview.project
|
|
631
392
|
};
|
|
393
|
+
const deployWithBlock = Object.keys(deployWith).length > 0 ? ` with:\n${withLines(deployWith)}\n` : "";
|
|
394
|
+
const previewWithBlock = ` with:\n${withLines(previewWith)}\n`;
|
|
395
|
+
return [
|
|
396
|
+
`name: Deploy`,
|
|
397
|
+
``,
|
|
398
|
+
`on: # yamllint disable-line rule:truthy`,
|
|
399
|
+
` push:`,
|
|
400
|
+
` branches: [main]`,
|
|
401
|
+
...pathsBlock ? [`${pathsBlock}`] : [],
|
|
402
|
+
` pull_request:`,
|
|
403
|
+
` branches: [main]`,
|
|
404
|
+
` types: [opened, synchronize, reopened, closed]`,
|
|
405
|
+
...pathsBlock ? [`${pathsBlock}`] : [],
|
|
406
|
+
` workflow_dispatch:`,
|
|
407
|
+
``,
|
|
408
|
+
`concurrency:`,
|
|
409
|
+
` group: $\{{ github.event_name == 'pull_request' && format('preview-{0}', github.event.pull_request.number) || 'pages' }}`,
|
|
410
|
+
` cancel-in-progress: $\{{ github.event_name == 'pull_request' && github.event.action != 'closed' }}`,
|
|
411
|
+
``,
|
|
412
|
+
`permissions:`,
|
|
413
|
+
` contents: read`,
|
|
414
|
+
` deployments: write`,
|
|
415
|
+
` pages: write`,
|
|
416
|
+
` id-token: write`,
|
|
417
|
+
` pull-requests: write`,
|
|
418
|
+
``,
|
|
419
|
+
`jobs:`,
|
|
420
|
+
` deploy:`,
|
|
421
|
+
` name: Deploy`,
|
|
422
|
+
` if: \${{ github.event_name != 'pull_request' }}`,
|
|
423
|
+
` uses: theholocron/.github/.github/workflows/delivery.deploy.yml@main`,
|
|
424
|
+
...deployWithBlock ? [deployWithBlock.trimEnd()] : [],
|
|
425
|
+
` secrets: inherit`,
|
|
426
|
+
``,
|
|
427
|
+
` preview:`,
|
|
428
|
+
` name: Preview`,
|
|
429
|
+
` if: \${{ github.event_name == 'pull_request' }}`,
|
|
430
|
+
` uses: theholocron/.github/.github/workflows/preview.yml@main`,
|
|
431
|
+
previewWithBlock.trimEnd(),
|
|
432
|
+
` secrets: inherit`,
|
|
433
|
+
``
|
|
434
|
+
].join("\n");
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Expand structured with-values to flat GitHub Actions inputs before
|
|
438
|
+
* generating the thin caller. Handles:
|
|
439
|
+
* - deploy shorthand: docs/storybook → type + storybook-projects
|
|
440
|
+
* - preview: stripped (handled separately via extractPreviewConfig)
|
|
441
|
+
* - run-chromatic object → run-chromatic: true + chromatic-projects
|
|
442
|
+
* - plain arrays → JSON-stringified for YAML scalar quoting
|
|
443
|
+
*
|
|
444
|
+
* Used by both `holocron repo setup` and `sync-workflow-templates`.
|
|
445
|
+
*/
|
|
446
|
+
function normalizeWorkflowWith(raw) {
|
|
447
|
+
const result = { ...raw };
|
|
448
|
+
delete result["preview"];
|
|
449
|
+
const hasDocs = raw["docs"] === true || raw["docs"] !== null && typeof raw["docs"] === "object";
|
|
450
|
+
const storybookProjects = raw["storybook"];
|
|
451
|
+
if (hasDocs) {
|
|
452
|
+
result["type"] = "docs";
|
|
453
|
+
delete result["docs"];
|
|
454
|
+
}
|
|
455
|
+
if (Array.isArray(storybookProjects)) {
|
|
456
|
+
if (!hasDocs) result["type"] = "storybook";
|
|
457
|
+
result["storybook-projects"] = JSON.stringify(storybookProjects.map(({ name, path = "." }) => ({
|
|
458
|
+
name,
|
|
459
|
+
workingDir: path
|
|
460
|
+
})));
|
|
461
|
+
delete result["storybook"];
|
|
462
|
+
}
|
|
463
|
+
const runChromatic = raw["run-chromatic"];
|
|
464
|
+
if (runChromatic !== null && typeof runChromatic === "object" && "projects" in runChromatic) {
|
|
465
|
+
result["run-chromatic"] = true;
|
|
466
|
+
const projects = runChromatic.projects.map((p) => ({
|
|
467
|
+
...p,
|
|
468
|
+
...Array.isArray(p.untraced) ? { untraced: p.untraced.join("\n") } : {}
|
|
469
|
+
}));
|
|
470
|
+
result["chromatic-projects"] = JSON.stringify(projects);
|
|
471
|
+
}
|
|
472
|
+
for (const [k, v] of Object.entries(result)) if (Array.isArray(v)) result[k] = JSON.stringify(v);
|
|
473
|
+
return result;
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Derive on.push.paths entries from the deploy with: shorthand.
|
|
477
|
+
* Used by both `holocron repo setup` and `sync-workflow-templates`.
|
|
478
|
+
*/
|
|
479
|
+
function deriveDeployPaths(raw) {
|
|
480
|
+
const paths = [];
|
|
481
|
+
const docs = raw["docs"];
|
|
482
|
+
if (docs === true) {
|
|
483
|
+
paths.push("docs/**");
|
|
484
|
+
paths.push("astro.config.ts");
|
|
485
|
+
paths.push("pnpm-workspace.yaml");
|
|
486
|
+
paths.push("pnpm-lock.yaml");
|
|
487
|
+
} else if (docs !== null && typeof docs === "object" && "path" in docs) {
|
|
488
|
+
const p = docs.path;
|
|
489
|
+
if (p && p !== ".") paths.push(`${p}/**`);
|
|
490
|
+
}
|
|
491
|
+
const storybookProjects = raw["storybook"];
|
|
492
|
+
if (Array.isArray(storybookProjects)) for (const s of storybookProjects) {
|
|
493
|
+
const p = s.path || ".";
|
|
494
|
+
if (p === ".") {
|
|
495
|
+
paths.push("src/**");
|
|
496
|
+
paths.push(".storybook/**");
|
|
497
|
+
} else paths.push(`${p}/**`);
|
|
498
|
+
}
|
|
499
|
+
return paths;
|
|
632
500
|
}
|
|
501
|
+
//#endregion
|
|
502
|
+
//#region src/generators/github-action-reusable-workflows.ts
|
|
503
|
+
/**
|
|
504
|
+
* The **reusable** GitHub Actions surface pushed to `theholocron/.github` by
|
|
505
|
+
* `holocron sync github` — the `workflow_call` implementations and composite
|
|
506
|
+
* actions that every repo's thin callers delegate to.
|
|
507
|
+
*
|
|
508
|
+
* `theholocron/.github` is a **pure sync target**: `.github/workflows/*`,
|
|
509
|
+
* `.github/actions/*` and `workflow-templates/*` there are generated from the
|
|
510
|
+
* `.yml` files in `src/templates/reusable/` and pushed verbatim (with a
|
|
511
|
+
* "do not edit" header). Never hand-edit them in `.github`.
|
|
512
|
+
*
|
|
513
|
+
* Not to be confused with `WORKFLOW_TEMPLATES` in `github-action-thin-callers.ts` — those are
|
|
514
|
+
* the *thin-caller bases* written into each consumer repo's `.github/workflows/`.
|
|
515
|
+
*/
|
|
516
|
+
/** `workflow_call` implementations → `.github/workflows/<name>.yml`. */
|
|
517
|
+
const REUSABLE_WORKFLOWS = {
|
|
518
|
+
"verification.unitTests": verification_unitTests_default$1,
|
|
519
|
+
"verification.typeSafety": verification_typeSafety_default$1,
|
|
520
|
+
"verification.performance": verification_performance_default$1,
|
|
521
|
+
"sourceQuality.staticAnalysis": sourceQuality_staticAnalysis_default$1,
|
|
522
|
+
"sourceQuality.formatting": sourceQuality_formatting_default$1,
|
|
523
|
+
"sourceQuality.structuredDataValidation": sourceQuality_structuredDataValidation_default$1,
|
|
524
|
+
"sourceQuality.deadCodeAnalysis": sourceQuality_deadCodeAnalysis_default$1,
|
|
525
|
+
"security.secretDetection": security_secretDetection_default$1,
|
|
526
|
+
"security.codeScanning": security_codeScanning_default$1,
|
|
527
|
+
"delivery.build": delivery_build_default$1,
|
|
528
|
+
"delivery.publish": delivery_publish_default$1,
|
|
529
|
+
"delivery.deploy": delivery_deploy_default$1,
|
|
530
|
+
"delivery.bundleSize": delivery_bundleSize_default$1,
|
|
531
|
+
"platform.repoSync": platform_repoSync_default$1,
|
|
532
|
+
"platform.commitStandards": platform_commitStandards_default$1,
|
|
533
|
+
"platform.repoValidation": platform_repoValidation_default$1,
|
|
534
|
+
"platform.dispatchedCheck": platform_dispatchedCheck_default,
|
|
535
|
+
"knowledge.wiki": knowledge_wiki_default$1,
|
|
536
|
+
bookkeeping: bookkeeping_default$1,
|
|
537
|
+
dependencies: dependencies_default$1,
|
|
538
|
+
preview: preview_default$1,
|
|
539
|
+
greetings: greetings_default$1,
|
|
540
|
+
review: review_default$1,
|
|
541
|
+
stale: stale_default$1,
|
|
542
|
+
"sync-dispatch": sync_dispatch_default,
|
|
543
|
+
tag: tag_default,
|
|
544
|
+
"sync-github": sync_github_default
|
|
545
|
+
};
|
|
546
|
+
/** Composite actions → `.github/actions/<name>/action.yml` (key includes `/action`). */
|
|
547
|
+
const REUSABLE_ACTIONS = {
|
|
548
|
+
"auto-commit/action": auto_commit_default,
|
|
549
|
+
"holocron/action": holocron_default,
|
|
550
|
+
"install/action": install_default,
|
|
551
|
+
"setup/action": setup_default,
|
|
552
|
+
"setup-node/action": setup_node_default
|
|
553
|
+
};
|
|
554
|
+
/** GitHub starter-workflow metadata → `workflow-templates/<name>.properties.json`. */
|
|
555
|
+
const WORKFLOW_TEMPLATE_PROPERTIES = { bookkeeping: JSON.stringify({
|
|
556
|
+
name: "Bookkeeping",
|
|
557
|
+
description: "Label and track issues and pull requests.",
|
|
558
|
+
iconName: "octicon tag"
|
|
559
|
+
}, null, 2) };
|
|
560
|
+
const SOURCE = "packages/astromech/src/generators/github-action-reusable-workflows.ts";
|
|
633
561
|
/**
|
|
634
|
-
*
|
|
635
|
-
*
|
|
636
|
-
*
|
|
562
|
+
* "AUTO-GENERATED — do not edit" header for the YAML pushed to
|
|
563
|
+
* `theholocron/.github`. Matches `createHeader().workflowHeader({ forPrimary: true })`
|
|
564
|
+
* field-for-field, minus any timestamp — a live `Synced:` line makes the content
|
|
565
|
+
* hash change on every run and defeats `sync github`'s unchanged-file skip.
|
|
637
566
|
*/
|
|
638
|
-
function
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
const label = `${task} ${name}`;
|
|
647
|
-
print(`▶ ${jobDef.checkContext}`);
|
|
648
|
-
reports.push(runUnit(perJob, label, jobDef.local, makeRunOne({
|
|
649
|
-
...perJob,
|
|
650
|
-
task: label
|
|
651
|
-
}), passthrough));
|
|
652
|
-
}
|
|
653
|
-
const command = reports.map((r) => r.command).filter((c) => Boolean(c)).join(" && ");
|
|
654
|
-
const statuses = new Set(reports.map((r) => r.status));
|
|
655
|
-
if (statuses.has("fail")) return {
|
|
656
|
-
status: "fail",
|
|
657
|
-
command,
|
|
658
|
-
message: `one or more ${task} jobs failed`
|
|
659
|
-
};
|
|
660
|
-
if (statuses.has("ok")) return {
|
|
661
|
-
status: "ok",
|
|
662
|
-
command
|
|
663
|
-
};
|
|
664
|
-
if (statuses.has("dry-run")) return {
|
|
665
|
-
status: "dry-run",
|
|
666
|
-
command
|
|
667
|
-
};
|
|
668
|
-
return {
|
|
669
|
-
status: "skip",
|
|
670
|
-
message: `no ${task} job has a local equivalent — enforced in CI`
|
|
671
|
-
};
|
|
567
|
+
function reusableHeader() {
|
|
568
|
+
return [
|
|
569
|
+
`# AUTO-GENERATED — do not edit in theholocron/.github directly.`,
|
|
570
|
+
`# Source: theholocron/holocron · ${SOURCE}`,
|
|
571
|
+
`# Tool: holocron sync github`,
|
|
572
|
+
`# Changes: edit source in theholocron/holocron`,
|
|
573
|
+
``
|
|
574
|
+
].join("\n");
|
|
672
575
|
}
|
|
673
576
|
/**
|
|
674
|
-
*
|
|
675
|
-
*
|
|
676
|
-
*
|
|
677
|
-
* resolution (step 3b) — no per-linter turbo passthrough here, unlike the
|
|
678
|
-
* old single `lint` task's eslint-only special case; the whole task is what
|
|
679
|
-
* turbo/a package script would have already caught in steps 1–2.
|
|
577
|
+
* The complete file batch `holocron sync github` pushes to `theholocron/.github`:
|
|
578
|
+
* path → content. YAML gets the "do not edit" header; the starter-workflow
|
|
579
|
+
* `.properties.json` files are left bare (native JSON, not a commented file).
|
|
680
580
|
*/
|
|
681
|
-
function
|
|
682
|
-
const
|
|
683
|
-
const
|
|
684
|
-
const
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
581
|
+
function reusableTemplates() {
|
|
582
|
+
const out = /* @__PURE__ */ new Map();
|
|
583
|
+
const header = reusableHeader();
|
|
584
|
+
for (const [name, body] of Object.entries(REUSABLE_ACTIONS)) out.set(`.github/actions/${name}.yml`, `${header}${body}`);
|
|
585
|
+
for (const [name, body] of Object.entries(REUSABLE_WORKFLOWS)) out.set(`.github/workflows/${name}.yml`, `${header}${body}`);
|
|
586
|
+
for (const [name, body] of Object.entries(WORKFLOW_TEMPLATES)) {
|
|
587
|
+
out.set(`workflow-templates/${name}.yml`, `${header}${body}`);
|
|
588
|
+
const props = WORKFLOW_TEMPLATE_PROPERTIES[name];
|
|
589
|
+
if (props) out.set(`workflow-templates/${name}.properties.json`, props);
|
|
690
590
|
}
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
591
|
+
return out;
|
|
592
|
+
}
|
|
593
|
+
//#endregion
|
|
594
|
+
//#region src/registry.ts
|
|
595
|
+
const TASKS = {
|
|
596
|
+
"verification.unitTests": {
|
|
597
|
+
local: {
|
|
598
|
+
tool: "vitest",
|
|
599
|
+
args: ["run"]
|
|
600
|
+
},
|
|
601
|
+
flags: { vitest: ["--coverage"] },
|
|
602
|
+
turbo: {
|
|
603
|
+
inputs: [
|
|
604
|
+
"src/**",
|
|
605
|
+
"vitest.config.ts",
|
|
606
|
+
"vitest.config.js",
|
|
607
|
+
"vitest.config.mjs",
|
|
608
|
+
"package.json"
|
|
609
|
+
],
|
|
610
|
+
outputs: ["test-report.junit.xml", "coverage/**"],
|
|
611
|
+
dependsOn: ["delivery.build", "^delivery.build"]
|
|
702
612
|
}
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
613
|
+
},
|
|
614
|
+
"verification.typeSafety": {
|
|
615
|
+
local: {
|
|
616
|
+
tool: "tsc",
|
|
617
|
+
args: ["--noEmit"]
|
|
618
|
+
},
|
|
619
|
+
turbo: {
|
|
620
|
+
inputs: ["src/**", "tsconfig.json"],
|
|
621
|
+
outputs: [],
|
|
622
|
+
dependsOn: ["^delivery.build"]
|
|
708
623
|
}
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
]
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
624
|
+
},
|
|
625
|
+
"verification.performance": { local: { detect: [{
|
|
626
|
+
when: /^lighthouse\.config\.(cjs|js|mjs|ts)$/,
|
|
627
|
+
tool: "lhci",
|
|
628
|
+
args: ["autorun"]
|
|
629
|
+
}, {
|
|
630
|
+
when: /^lighthouserc\.(cjs|js|mjs|json|yml|yaml)$/,
|
|
631
|
+
tool: "lhci",
|
|
632
|
+
args: ["autorun"]
|
|
633
|
+
}] } },
|
|
634
|
+
"sourceQuality.staticAnalysis": {
|
|
635
|
+
local: null,
|
|
636
|
+
linterGroup: [
|
|
637
|
+
"eslint",
|
|
638
|
+
"actionlint",
|
|
639
|
+
"git-merge-conflict-markers"
|
|
640
|
+
],
|
|
641
|
+
turbo: {
|
|
642
|
+
inputs: [
|
|
643
|
+
"src/**",
|
|
644
|
+
"eslint.config.ts",
|
|
645
|
+
"eslint.config.js",
|
|
646
|
+
"eslint.config.mjs",
|
|
647
|
+
"eslint.config.cjs"
|
|
648
|
+
],
|
|
649
|
+
outputs: [],
|
|
650
|
+
dependsOn: ["^delivery.build"]
|
|
651
|
+
}
|
|
652
|
+
},
|
|
653
|
+
"sourceQuality.formatting": {
|
|
654
|
+
local: null,
|
|
655
|
+
linterGroup: [
|
|
656
|
+
"prettier",
|
|
657
|
+
"editorconfig",
|
|
658
|
+
"markdownlint"
|
|
659
|
+
]
|
|
660
|
+
},
|
|
661
|
+
"sourceQuality.structuredDataValidation": {
|
|
662
|
+
local: null,
|
|
663
|
+
linterGroup: ["yamllint"]
|
|
664
|
+
},
|
|
665
|
+
"sourceQuality.deadCodeAnalysis": { local: { tool: "knip" } },
|
|
666
|
+
"security.secretDetection": {
|
|
667
|
+
local: null,
|
|
668
|
+
linterGroup: ["gitleaks"]
|
|
669
|
+
},
|
|
670
|
+
"security.codeScanning": { local: null },
|
|
671
|
+
"delivery.build": {
|
|
672
|
+
local: { detect: [
|
|
673
|
+
{
|
|
674
|
+
when: /^tsdown\.config\.(ts|js|mjs|cjs)$/,
|
|
675
|
+
tool: "tsdown"
|
|
676
|
+
},
|
|
677
|
+
{
|
|
678
|
+
when: /^vite\.config\.(ts|js|mjs|cjs)$/,
|
|
679
|
+
tool: "vite",
|
|
680
|
+
args: ["build"]
|
|
681
|
+
},
|
|
682
|
+
{
|
|
683
|
+
when: /^rollup\.config\.(ts|js|mjs|cjs)$/,
|
|
684
|
+
tool: "rollup",
|
|
685
|
+
args: ["-c"]
|
|
686
|
+
},
|
|
687
|
+
{
|
|
688
|
+
when: /^tsconfig\.json$/,
|
|
689
|
+
tool: "tsc",
|
|
690
|
+
args: ["-b"]
|
|
691
|
+
}
|
|
692
|
+
] },
|
|
693
|
+
turbo: {
|
|
694
|
+
inputs: [
|
|
695
|
+
"src/**",
|
|
696
|
+
"package.json",
|
|
697
|
+
"tsdown.config.ts",
|
|
698
|
+
"vite.config.ts",
|
|
699
|
+
"rollup.config.ts",
|
|
700
|
+
"tsconfig.json"
|
|
701
|
+
],
|
|
702
|
+
outputs: ["dist/**"],
|
|
703
|
+
dependsOn: ["^delivery.build"]
|
|
704
|
+
}
|
|
705
|
+
},
|
|
706
|
+
"delivery.publish": {
|
|
707
|
+
local: null,
|
|
708
|
+
preview: true
|
|
709
|
+
},
|
|
710
|
+
"delivery.deploy": {
|
|
711
|
+
local: null,
|
|
712
|
+
preview: true
|
|
713
|
+
},
|
|
714
|
+
"delivery.bundleSize": { local: null },
|
|
715
|
+
"platform.repoSync": { local: { command: "sync" } },
|
|
716
|
+
"platform.commitStandards": {
|
|
717
|
+
local: null,
|
|
718
|
+
linterGroup: ["commitlint"]
|
|
719
|
+
},
|
|
720
|
+
"platform.repoValidation": {
|
|
721
|
+
local: null,
|
|
722
|
+
jobs: {
|
|
723
|
+
adrs: {
|
|
724
|
+
local: {
|
|
725
|
+
tool: "node",
|
|
726
|
+
args: ["scripts/validate-adrs.mjs"]
|
|
727
|
+
},
|
|
728
|
+
checkContext: "platform.repoValidation / Validate ADRs and specs"
|
|
729
|
+
},
|
|
730
|
+
registry: {
|
|
731
|
+
local: {
|
|
732
|
+
tool: "node",
|
|
733
|
+
args: ["scripts/validate-registry.mjs"]
|
|
734
|
+
},
|
|
735
|
+
checkContext: "platform.repoValidation / Validate registry consistency"
|
|
736
|
+
},
|
|
737
|
+
docsPresence: {
|
|
738
|
+
local: {
|
|
739
|
+
tool: "node",
|
|
740
|
+
args: ["scripts/validate-docs-presence.mjs"]
|
|
741
|
+
},
|
|
742
|
+
checkContext: "platform.repoValidation / Validate docs presence"
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
},
|
|
746
|
+
"knowledge.wiki": {
|
|
747
|
+
local: { command: "sync-wiki" },
|
|
748
|
+
preview: true
|
|
749
|
+
},
|
|
750
|
+
"knowledge.docs": {
|
|
751
|
+
local: null,
|
|
752
|
+
preview: true
|
|
753
|
+
},
|
|
754
|
+
"knowledge.components": {
|
|
755
|
+
local: null,
|
|
756
|
+
preview: true
|
|
731
757
|
}
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
command
|
|
736
|
-
};
|
|
737
|
-
return reports.some((r) => r.status === "fail") ? {
|
|
738
|
-
status: "fail",
|
|
739
|
-
command,
|
|
740
|
-
message: "one or more linters failed"
|
|
741
|
-
} : {
|
|
742
|
-
status: "ok",
|
|
743
|
-
command
|
|
744
|
-
};
|
|
745
|
-
}
|
|
746
|
-
const mkRunner = (tool, args = []) => ({
|
|
747
|
-
tool,
|
|
748
|
-
args
|
|
749
|
-
});
|
|
758
|
+
};
|
|
759
|
+
/** Every task name the registry knows. */
|
|
760
|
+
const KNOWN_TASKS = new Set(Object.keys(TASKS));
|
|
750
761
|
/**
|
|
751
|
-
*
|
|
752
|
-
*
|
|
753
|
-
*
|
|
754
|
-
* `
|
|
762
|
+
* The order `holocron run ci` runs tasks in — cheapest / fastest signal first, so
|
|
763
|
+
* an agent or a `pre-push` hook fails early. Tasks not listed here run last, in
|
|
764
|
+
* manifest order. (The generated thin callers carry no `needs:` — cross-workflow
|
|
765
|
+
* ordering lives in `theholocron/.github` — so `holocron run ci` declares its own.)
|
|
755
766
|
*/
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
const pkg = JSON.parse(readFile(join(cwd, "package.json")));
|
|
773
|
-
if (typeof pkg.packageManager === "string") return pkg.packageManager.split("@")[0];
|
|
774
|
-
} catch {}
|
|
775
|
-
if (fileExists(join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
776
|
-
if (fileExists(join(cwd, "bun.lockb"))) return "bun";
|
|
777
|
-
if (fileExists(join(cwd, "yarn.lock"))) return "yarn";
|
|
778
|
-
if (fileExists(join(cwd, "package-lock.json"))) return "npm";
|
|
779
|
-
return "pnpm";
|
|
780
|
-
}
|
|
781
|
-
function turboDefinesTask(cwd, task, readFile, fileExists) {
|
|
782
|
-
if (!fileExists(join(cwd, "turbo.json"))) return false;
|
|
783
|
-
try {
|
|
784
|
-
const turbo = JSON.parse(readFile(join(cwd, "turbo.json")));
|
|
785
|
-
return Boolean(turbo.tasks?.[task] ?? turbo.pipeline?.[task]);
|
|
786
|
-
} catch {
|
|
787
|
-
return false;
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
function packageJsonScript(cwd, task, readFile, fileExists) {
|
|
791
|
-
if (!fileExists(join(cwd, "package.json"))) return void 0;
|
|
792
|
-
try {
|
|
793
|
-
return JSON.parse(readFile(join(cwd, "package.json"))).scripts?.[task];
|
|
794
|
-
} catch {
|
|
795
|
-
return;
|
|
796
|
-
}
|
|
797
|
-
}
|
|
767
|
+
const CI_ORDER = [
|
|
768
|
+
"verification.typeSafety",
|
|
769
|
+
"sourceQuality.staticAnalysis",
|
|
770
|
+
"sourceQuality.formatting",
|
|
771
|
+
"sourceQuality.structuredDataValidation",
|
|
772
|
+
"security.secretDetection",
|
|
773
|
+
"platform.commitStandards",
|
|
774
|
+
"platform.repoValidation",
|
|
775
|
+
"verification.unitTests",
|
|
776
|
+
"delivery.build",
|
|
777
|
+
"sourceQuality.deadCodeAnalysis",
|
|
778
|
+
"delivery.bundleSize",
|
|
779
|
+
"verification.performance",
|
|
780
|
+
"security.codeScanning",
|
|
781
|
+
"delivery.deploy"
|
|
782
|
+
];
|
|
798
783
|
//#endregion
|
|
799
|
-
//#region src/
|
|
784
|
+
//#region src/generators/required-checks.ts
|
|
800
785
|
/**
|
|
801
|
-
*
|
|
786
|
+
* `requiredChecks(config)` — the branch-protection required-status-check list,
|
|
787
|
+
* derived from the task manifest. Every `required: true` task contributes its
|
|
788
|
+
* {@link WORKFLOW_CHECK_CONTEXTS} entry; `config.extraRequiredChecks` adds
|
|
789
|
+
* contexts not backed by a task (codecov, DCO is prepended by the caller).
|
|
802
790
|
*
|
|
803
|
-
* `
|
|
804
|
-
*
|
|
805
|
-
*
|
|
791
|
+
* `@theholocron/cli`'s `holocron repo setup` calls this instead of the old
|
|
792
|
+
* hand-maintained `repo.requiredChecks` array. Policy-free — it only knows the
|
|
793
|
+
* manifest.
|
|
806
794
|
*/
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
stale: "name: Stale\n\non: # yamllint disable-line rule:truthy\n schedule:\n - cron: \"30 1 * * *\"\n\npermissions:\n contents: write\n issues: write\n pull-requests: write\n\njobs:\n stale:\n uses: theholocron/.github/.github/workflows/stale.yml@main\n with:\n exempt-issue-labels: \"in-progress,wip\"\n exempt-all-issue-milestones: true\n exempt-all-issue-projects: true\n exempt-all-pr-projects: true\n secrets: inherit\n",
|
|
828
|
-
greetings: "name: Platform\n\non: # yamllint disable-line rule:truthy\n pull_request:\n issues:\n\npermissions:\n issues: write\n pull-requests: write\n\njobs:\n greetings:\n name: Greetings\n uses: theholocron/.github/.github/workflows/greetings.yml@main\n secrets: inherit\n",
|
|
829
|
-
dependencies: "name: Platform\n\non: # yamllint disable-line rule:truthy\n pull_request:\n\npermissions:\n contents: write\n pull-requests: write\n\njobs:\n dependencies:\n name: Dependencies\n uses: theholocron/.github/.github/workflows/dependencies.yml@main\n secrets: inherit\n",
|
|
830
|
-
bookkeeping: "name: Platform\n\non: # yamllint disable-line rule:truthy\n pull_request:\n types:\n - opened\n - edited\n\npermissions:\n contents: read\n issues: write\n pull-requests: write\n\njobs:\n bookkeeping:\n name: Bookkeeping\n uses: theholocron/.github/.github/workflows/bookkeeping.yml@main\n secrets: inherit\n"
|
|
831
|
-
};
|
|
832
|
-
const KNOWN_WORKFLOWS = new Set(Object.keys(WORKFLOW_TEMPLATES));
|
|
795
|
+
/** Ordered (task contexts in {@link CI_ORDER}, then extras), de-duplicated. */
|
|
796
|
+
function requiredChecks(config) {
|
|
797
|
+
const entries = (config.tasks ?? []).map(normalizeTaskEntry);
|
|
798
|
+
const seen = /* @__PURE__ */ new Set();
|
|
799
|
+
const out = [];
|
|
800
|
+
for (const name of CI_ORDER) {
|
|
801
|
+
const context = entries.some((e) => e.name === name && e.required === true) ? WORKFLOW_CHECK_CONTEXTS[name] : void 0;
|
|
802
|
+
if (context && !seen.has(context)) {
|
|
803
|
+
seen.add(context);
|
|
804
|
+
out.push(context);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
for (const context of config.extraRequiredChecks ?? []) if (!seen.has(context)) {
|
|
808
|
+
seen.add(context);
|
|
809
|
+
out.push(context);
|
|
810
|
+
}
|
|
811
|
+
return out;
|
|
812
|
+
}
|
|
813
|
+
//#endregion
|
|
814
|
+
//#region src/generators/turbo.ts
|
|
833
815
|
/**
|
|
834
|
-
*
|
|
835
|
-
*
|
|
816
|
+
* `turboConfig(config)` — the generated `turbo.json` for a repo's task
|
|
817
|
+
* manifest, epic #672 D9 (#681). Every workspace, monorepo or single-package
|
|
818
|
+
* (Turborepo's single-package mode), gets this from the same `tasks` array
|
|
819
|
+
* that already drives thin callers / required checks / package scripts —
|
|
820
|
+
* never hand-authored.
|
|
836
821
|
*
|
|
837
|
-
*
|
|
838
|
-
*
|
|
839
|
-
*
|
|
840
|
-
*
|
|
841
|
-
* `Conclusion` fan-in jobs are kept only where a task genuinely has several
|
|
842
|
-
* conditionally-run jobs feeding one required check:
|
|
843
|
-
* `verification.unitTests` (unit / Storybook / Chromatic / interaction /
|
|
844
|
-
* user-flow, each gated by its own `run-*` input) and
|
|
845
|
-
* `platform.repoValidation` (its three script jobs all must pass). Only
|
|
846
|
-
* merge-gating workflows are listed. `astromech.requiredChecks()` reads this
|
|
847
|
-
* for every `required` task.
|
|
848
|
-
*/
|
|
849
|
-
const WORKFLOW_CHECK_CONTEXTS = {
|
|
850
|
-
"delivery.build": "Build / Build every workspace",
|
|
851
|
-
"verification.unitTests": "Test / Conclusion",
|
|
852
|
-
"verification.typeSafety": "Typecheck / Run tsc --noEmit",
|
|
853
|
-
"verification.performance": "Audit the Performance / Run Lighthouse CI",
|
|
854
|
-
"sourceQuality.staticAnalysis": "Static Analysis / Run eslint and actionlint",
|
|
855
|
-
"sourceQuality.formatting": "Formatting / Run prettier, editorconfig-checker, markdownlint",
|
|
856
|
-
"sourceQuality.structuredDataValidation": "Structured Data Validation / Run yamllint",
|
|
857
|
-
"sourceQuality.deadCodeAnalysis": "Dead Code Analysis / Run knip",
|
|
858
|
-
"security.secretDetection": "Secret Detection / Run gitleaks",
|
|
859
|
-
"platform.commitStandards": "Commit Standards / Run commitlint",
|
|
860
|
-
"platform.repoValidation": "Repo Validation / Conclusion",
|
|
861
|
-
"delivery.bundleSize": "Audit the Bundle Size / Upload bundle stats to Codecov"
|
|
862
|
-
};
|
|
863
|
-
/**
|
|
864
|
-
* Generate the thin caller content for a workflow, optionally injecting or
|
|
865
|
-
* merging `with:` overrides into the jobs block.
|
|
822
|
+
* Only tasks with a {@link TurboTaskConfig} in the registry (real per-
|
|
823
|
+
* workspace build/compile/test steps) get an entry. Whole-repo, single-run
|
|
824
|
+
* tools (prettier, gitleaks, commitlint, yamllint, …) have none — they stay
|
|
825
|
+
* off turbo.json entirely, same as today.
|
|
866
826
|
*
|
|
867
|
-
*
|
|
868
|
-
*
|
|
869
|
-
*
|
|
870
|
-
*
|
|
871
|
-
* - Templates that end with ` secrets: inherit`: a new `with:` block is
|
|
872
|
-
* injected immediately before `secrets: inherit`.
|
|
873
|
-
* If neither pattern matches the template, a warning is emitted and the
|
|
874
|
-
* base template is returned unchanged.
|
|
827
|
+
* A task entry's own `turbo.passThroughEnv` (schema.ts) appends extra env
|
|
828
|
+
* vars to the registry default's cache-key hashing — the one thing that's
|
|
829
|
+
* genuinely per-repo (e.g. a package tagging Sentry releases during its own
|
|
830
|
+
* build needs `SENTRY_AUTH_TOKEN` in its cache key; most repos don't).
|
|
875
831
|
*/
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
const
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
const
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
});
|
|
897
|
-
else {
|
|
898
|
-
const pathsBlock = ` paths:\n${additionalPaths.map((p) => ` - ${p}\n`).join("")}`;
|
|
899
|
-
result = result.replace(/( {4}branches: \[main\]\n)/, `$1${pathsBlock}`);
|
|
900
|
-
}
|
|
901
|
-
}
|
|
902
|
-
if (!withOverrides || Object.keys(withOverrides).length === 0) return result;
|
|
903
|
-
const withBlockRe = /( {4}with:\n)((?:[ ]{6}[^\n]+\n)*)/;
|
|
904
|
-
const existingMatch = result.match(withBlockRe);
|
|
905
|
-
if (existingMatch) {
|
|
906
|
-
const existingEntries = new Map(existingMatch[2].split("\n").filter(Boolean).filter((line) => !line.trim().startsWith("#")).map((line) => {
|
|
907
|
-
const m = line.match(/^ {6}([^:]+):\s*(.*)/);
|
|
908
|
-
return m ? [m[1].trim(), m[2].trim()] : null;
|
|
909
|
-
}).filter((e) => e !== null));
|
|
910
|
-
for (const [k, v] of Object.entries(withOverrides)) existingEntries.set(k, yamlScalar(v));
|
|
911
|
-
const merged = [...existingEntries.entries()].map(([k, v]) => ` ${k}: ${v}`).join("\n");
|
|
912
|
-
return result.replace(withBlockRe, ` with:\n${merged}\n`);
|
|
832
|
+
/** Filed once per repo, unconditionally — every generated turbo.json's root. */
|
|
833
|
+
const GLOBAL_DEPENDENCIES = ["pnpm-workspace.yaml", "tsconfig.json"];
|
|
834
|
+
/**
|
|
835
|
+
* `turbo.json` content for this manifest, pretty-printed — write it
|
|
836
|
+
* directly, no merge (matches `thinCallers()` / `reusableTemplates()`: this
|
|
837
|
+
* file is generated, not hand-edited). `null` when no task in the manifest
|
|
838
|
+
* has turbo fan-out config (nothing to write).
|
|
839
|
+
*/
|
|
840
|
+
function turboConfig(config) {
|
|
841
|
+
const entries = (config.tasks ?? []).map(normalizeTaskEntry);
|
|
842
|
+
const tasks = {};
|
|
843
|
+
for (const entry of entries) {
|
|
844
|
+
if (entry.local === false) continue;
|
|
845
|
+
const turbo = TASKS[entry.name]?.turbo;
|
|
846
|
+
if (!turbo) continue;
|
|
847
|
+
const extraEnv = entry.turbo?.passThroughEnv;
|
|
848
|
+
tasks[entry.name] = extraEnv && extraEnv.length > 0 ? {
|
|
849
|
+
...turbo,
|
|
850
|
+
passThroughEnv: extraEnv
|
|
851
|
+
} : turbo;
|
|
913
852
|
}
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
853
|
+
if (Object.keys(tasks).length === 0) return null;
|
|
854
|
+
return `${JSON.stringify({
|
|
855
|
+
$schema: "https://turborepo.org/schema.json",
|
|
856
|
+
globalDependencies: GLOBAL_DEPENDENCIES,
|
|
857
|
+
tasks
|
|
858
|
+
}, null, 2)}\n`;
|
|
918
859
|
}
|
|
919
860
|
/**
|
|
920
|
-
*
|
|
861
|
+
* Fixes #692: `turboConfig()` writes a `turbo.json` the moment *any* task in
|
|
862
|
+
* the manifest has fan-out config — with no check for whether the repo's own
|
|
863
|
+
* root package is actually covered by `pnpm-workspace.yaml`'s `packages:`
|
|
864
|
+
* list. A repo shaped like `observability` (the library lives at repo root;
|
|
865
|
+
* `packages:` lists only an unrelated `docs` site) silently breaks the
|
|
866
|
+
* instant `turbo.json` exists: `holocron run <task>` switches from running
|
|
867
|
+
* root's own script directly to `turbo run <task>`, which only sees declared
|
|
868
|
+
* workspace members — root vanishes, the task "succeeds" with zero real work
|
|
869
|
+
* done (confirmed empirically: `Packages in scope: docs`, root never
|
|
870
|
+
* mentioned, exit 0).
|
|
921
871
|
*
|
|
922
|
-
*
|
|
923
|
-
*
|
|
924
|
-
*
|
|
925
|
-
*
|
|
872
|
+
* Detection is narrow and specific: root only needs to be an explicit
|
|
873
|
+
* workspace member if its *own* `package.json` has a script literally named
|
|
874
|
+
* after one of the tasks turbo.json is about to fan out — a plain
|
|
875
|
+
* orchestrator root (`holocron`'s own `"build": "turbo run delivery.build"`,
|
|
876
|
+
* which has no `delivery.build` script itself) never trips this; only a root
|
|
877
|
+
* that's genuinely a directly-buildable package does. Confirmed the fix
|
|
878
|
+
* empirically too — adding `.` to `packages:` makes turbo pick root back up
|
|
879
|
+
* (`Packages in scope: docs, root-lib`) without disturbing the sibling
|
|
880
|
+
* package's own caching.
|
|
926
881
|
*
|
|
927
|
-
*
|
|
882
|
+
* A matching script key alone isn't enough — its *value* has to be a real
|
|
883
|
+
* command, not the generated `holocron run <task> --` wrapper (#846, the
|
|
884
|
+
* counterpart `mergePackageJsonScripts()` already had to learn for #747/
|
|
885
|
+
* #748). A repo whose root predates this tooling can already have that exact
|
|
886
|
+
* wrapper sitting in a task-name-matching script with nothing ever having
|
|
887
|
+
* generated it this session; adding `.` there fires #747's recursion
|
|
888
|
+
* immediately (`turbo run <task>` → root's own `<task>` script → the wrapper
|
|
889
|
+
* → `holocron run <task>` sees `turbo.json` still defines it → `turbo run
|
|
890
|
+
* <task>` again, forever). Only a matching script whose value is *not* that
|
|
891
|
+
* wrapper counts as "root is genuinely a directly-buildable package".
|
|
892
|
+
*
|
|
893
|
+
* A targeted line-based edit, not a full YAML parse/reserialize — same
|
|
894
|
+
* pattern `codecov.ts`'s `mergeCodecovComponents()` already uses for editing
|
|
895
|
+
* an existing generated file. `pnpm-workspace.yaml` routinely carries a
|
|
896
|
+
* `catalog:`/`catalogs:`/`overrides:` block after `packages:`; a real parser
|
|
897
|
+
* round-trip risks reformatting or reordering content nobody asked to touch.
|
|
898
|
+
* Only ever *adds* a line — never rewrites or reorders anything already
|
|
899
|
+
* there — and is a no-op (`changed: false`) whenever root doesn't need it,
|
|
900
|
+
* root is already listed, or `packages:` isn't in the plain block-list form
|
|
901
|
+
* every repo checked actually uses.
|
|
928
902
|
*/
|
|
929
|
-
function
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
903
|
+
function ensureRootWorkspaceMember(workspaceYaml, rootScripts, taskNames) {
|
|
904
|
+
if (!taskNames.some((t) => {
|
|
905
|
+
const script = rootScripts[t];
|
|
906
|
+
return script !== void 0 && !isGeneratedRunWrapper(script, t);
|
|
907
|
+
})) return {
|
|
908
|
+
content: workspaceYaml,
|
|
909
|
+
changed: false
|
|
910
|
+
};
|
|
911
|
+
const lines = workspaceYaml.split("\n");
|
|
912
|
+
const packagesLineIdx = lines.findIndex((l) => /^packages:\s*$/.test(l));
|
|
913
|
+
if (packagesLineIdx === -1) return {
|
|
914
|
+
content: workspaceYaml,
|
|
915
|
+
changed: false
|
|
916
|
+
};
|
|
917
|
+
const items = [];
|
|
918
|
+
let cursor = packagesLineIdx + 1;
|
|
919
|
+
while (cursor < lines.length && /^\s*-\s*/.test(lines[cursor])) {
|
|
920
|
+
items.push(lines[cursor]);
|
|
921
|
+
cursor++;
|
|
940
922
|
}
|
|
941
|
-
if (
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
923
|
+
if (items.some((item) => {
|
|
924
|
+
const value = item.replace(/^\s*-\s*/, "").replace(/^["']|["']$/g, "").trim();
|
|
925
|
+
return value === "." || value === "";
|
|
926
|
+
})) return {
|
|
927
|
+
content: workspaceYaml,
|
|
928
|
+
changed: false
|
|
929
|
+
};
|
|
930
|
+
const newLine = `${items[0]?.match(/^(\s*-\s*)/)?.[1] ?? " - "}"."`;
|
|
946
931
|
return {
|
|
947
|
-
|
|
948
|
-
|
|
932
|
+
content: [
|
|
933
|
+
...lines.slice(0, packagesLineIdx + 1),
|
|
934
|
+
newLine,
|
|
935
|
+
...lines.slice(packagesLineIdx + 1)
|
|
936
|
+
].join("\n"),
|
|
937
|
+
changed: true
|
|
949
938
|
};
|
|
950
939
|
}
|
|
951
940
|
/**
|
|
952
|
-
*
|
|
953
|
-
*
|
|
954
|
-
*
|
|
941
|
+
* Matches the exact shape `packageScripts()` generates for `taskName` —
|
|
942
|
+
* `<bin> run <taskName> --`, `<bin>` normally `holocron` but substitutable
|
|
943
|
+
* (`holocronScript`, itself allowed to be multi-word, e.g. `"node
|
|
944
|
+
* packages/cli/dist/cli.mjs"` — see its own test). Checking the *suffix*
|
|
945
|
+
* rather than the whole string means an old wrapper survives a
|
|
946
|
+
* `holocronScript` rename intact — it's still recognized as a wrapper (safe
|
|
947
|
+
* to update to the new one), not mistaken for a real command. Mirrors
|
|
948
|
+
* `sync.ts`'s private `isGeneratedRunWrapper()` — same predicate, needed on
|
|
949
|
+
* both sides of #692/#747: whether to *write* the wrapper, and here, whether
|
|
950
|
+
* an *existing* one should count as "root is directly buildable".
|
|
951
|
+
*/
|
|
952
|
+
function isGeneratedRunWrapper(command, taskName) {
|
|
953
|
+
return command.endsWith(` run ${taskName} --`);
|
|
954
|
+
}
|
|
955
|
+
/**
|
|
956
|
+
* Fallback pin when a repo's own `pnpm-workspace.yaml` has no `turbo`
|
|
957
|
+
* catalog entry yet — matches this repo's own catalog version (the
|
|
958
|
+
* canonical source-of-truth repo for the whole org's tooling).
|
|
959
|
+
*/
|
|
960
|
+
const DEFAULT_TURBO_VERSION = "^2.10.12";
|
|
961
|
+
/**
|
|
962
|
+
* Ensures `turbo` is declared in `package.json#devDependencies` — found
|
|
963
|
+
* the hard way rolling this out to `observability` (predates #691):
|
|
964
|
+
* `turboConfig()` writes a `turbo.json` the moment any task has fan-out
|
|
965
|
+
* config, but never touches `package.json`, so a repo that predates this
|
|
966
|
+
* feature (or never had `turbo` installed for any other reason) ends up
|
|
967
|
+
* with a `turbo.json` and nothing local to run it. `resolveBin()`
|
|
968
|
+
* (`run.ts`) falls back to a bare `turbo` on `PATH` when
|
|
969
|
+
* `node_modules/.bin/turbo` doesn't exist — on a machine with no global
|
|
970
|
+
* `turbo` at all that's a hard failure for every task; on one that
|
|
971
|
+
* happens to have an unrelated global install, silent version skew
|
|
972
|
+
* (confirmed empirically: a global 2.6.0 rejected this repo's generated
|
|
973
|
+
* `turbo.json` outright — `Found an unknown key "globalDependencies"`,
|
|
974
|
+
* a schema key from a newer version than the one actually resolved).
|
|
955
975
|
*
|
|
956
|
-
*
|
|
957
|
-
*
|
|
958
|
-
*
|
|
959
|
-
*
|
|
976
|
+
* Prefers `"catalog:"` when the repo's own `pnpm-workspace.yaml` already
|
|
977
|
+
* has a `turbo` catalog entry — matches every already-migrated repo
|
|
978
|
+
* (`holocron`, `clients`, …) — falling back to a real pinned version
|
|
979
|
+
* otherwise. Never touches an existing `devDependencies.turbo` entry, so
|
|
980
|
+
* a repo pinning its own version on purpose is left alone — idempotent,
|
|
981
|
+
* a no-op once set either way.
|
|
960
982
|
*/
|
|
961
|
-
function
|
|
962
|
-
const
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
return s.startsWith("[") || s.startsWith("{") ? `'${s}'` : s;
|
|
983
|
+
function ensureTurboDependency(packageJson, workspaceYaml) {
|
|
984
|
+
const pkg = JSON.parse(packageJson);
|
|
985
|
+
if (typeof pkg.devDependencies?.turbo === "string") return {
|
|
986
|
+
content: packageJson,
|
|
987
|
+
changed: false
|
|
967
988
|
};
|
|
968
|
-
const
|
|
969
|
-
const
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
989
|
+
const hasTurboCatalogEntry = /^\s*turbo:\s*\S/m.test(workspaceYaml);
|
|
990
|
+
const devDependencies = {
|
|
991
|
+
...pkg.devDependencies,
|
|
992
|
+
turbo: hasTurboCatalogEntry ? "catalog:" : DEFAULT_TURBO_VERSION
|
|
993
|
+
};
|
|
994
|
+
const updated = {
|
|
995
|
+
...pkg,
|
|
996
|
+
devDependencies
|
|
997
|
+
};
|
|
998
|
+
return {
|
|
999
|
+
content: `${JSON.stringify(updated, null, 2)}\n`,
|
|
1000
|
+
changed: true
|
|
973
1001
|
};
|
|
974
|
-
const deployWithBlock = Object.keys(deployWith).length > 0 ? ` with:\n${withLines(deployWith)}\n` : "";
|
|
975
|
-
const previewWithBlock = ` with:\n${withLines(previewWith)}\n`;
|
|
976
|
-
return [
|
|
977
|
-
`name: Deploy`,
|
|
978
|
-
``,
|
|
979
|
-
`on: # yamllint disable-line rule:truthy`,
|
|
980
|
-
` push:`,
|
|
981
|
-
` branches: [main]`,
|
|
982
|
-
...pathsBlock ? [`${pathsBlock}`] : [],
|
|
983
|
-
` pull_request:`,
|
|
984
|
-
` branches: [main]`,
|
|
985
|
-
` types: [opened, synchronize, reopened, closed]`,
|
|
986
|
-
...pathsBlock ? [`${pathsBlock}`] : [],
|
|
987
|
-
` workflow_dispatch:`,
|
|
988
|
-
``,
|
|
989
|
-
`concurrency:`,
|
|
990
|
-
` group: $\{{ github.event_name == 'pull_request' && format('preview-{0}', github.event.pull_request.number) || 'pages' }}`,
|
|
991
|
-
` cancel-in-progress: $\{{ github.event_name == 'pull_request' && github.event.action != 'closed' }}`,
|
|
992
|
-
``,
|
|
993
|
-
`permissions:`,
|
|
994
|
-
` contents: read`,
|
|
995
|
-
` deployments: write`,
|
|
996
|
-
` pages: write`,
|
|
997
|
-
` id-token: write`,
|
|
998
|
-
` pull-requests: write`,
|
|
999
|
-
``,
|
|
1000
|
-
`jobs:`,
|
|
1001
|
-
` deploy:`,
|
|
1002
|
-
` name: Deploy`,
|
|
1003
|
-
` if: \${{ github.event_name != 'pull_request' }}`,
|
|
1004
|
-
` uses: theholocron/.github/.github/workflows/delivery.deploy.yml@main`,
|
|
1005
|
-
...deployWithBlock ? [deployWithBlock.trimEnd()] : [],
|
|
1006
|
-
` secrets: inherit`,
|
|
1007
|
-
``,
|
|
1008
|
-
` preview:`,
|
|
1009
|
-
` name: Preview`,
|
|
1010
|
-
` if: \${{ github.event_name == 'pull_request' }}`,
|
|
1011
|
-
` uses: theholocron/.github/.github/workflows/preview.yml@main`,
|
|
1012
|
-
previewWithBlock.trimEnd(),
|
|
1013
|
-
` secrets: inherit`,
|
|
1014
|
-
``
|
|
1015
|
-
].join("\n");
|
|
1016
1002
|
}
|
|
1003
|
+
//#endregion
|
|
1004
|
+
//#region src/tasks/run/linters.ts
|
|
1005
|
+
/**
|
|
1006
|
+
* Known linters, in execution order. `always` entries are the current
|
|
1007
|
+
* hard-coded super-linter baseline; `prettier` is always-on because the org
|
|
1008
|
+
* applies it universally (super-linter only lints files that exist).
|
|
1009
|
+
*/
|
|
1010
|
+
const LINTERS = {
|
|
1011
|
+
eslint: {
|
|
1012
|
+
validate: ["VALIDATE_JAVASCRIPT_ES", "VALIDATE_TYPESCRIPT_ES"],
|
|
1013
|
+
localBin: "eslint",
|
|
1014
|
+
localArgs: ["."],
|
|
1015
|
+
detect: [
|
|
1016
|
+
"eslint.config.ts",
|
|
1017
|
+
"eslint.config.js",
|
|
1018
|
+
"eslint.config.mjs",
|
|
1019
|
+
"eslint.config.cjs",
|
|
1020
|
+
".eslintrc",
|
|
1021
|
+
".eslintrc.json",
|
|
1022
|
+
".eslintrc.yml",
|
|
1023
|
+
".eslintrc.yaml",
|
|
1024
|
+
".eslintrc.cjs"
|
|
1025
|
+
],
|
|
1026
|
+
configInput: "eslint-config"
|
|
1027
|
+
},
|
|
1028
|
+
prettier: {
|
|
1029
|
+
validate: [
|
|
1030
|
+
"VALIDATE_JAVASCRIPT_PRETTIER",
|
|
1031
|
+
"VALIDATE_JSX_PRETTIER",
|
|
1032
|
+
"VALIDATE_TYPESCRIPT_PRETTIER",
|
|
1033
|
+
"VALIDATE_TSX",
|
|
1034
|
+
"VALIDATE_MARKDOWN_PRETTIER"
|
|
1035
|
+
],
|
|
1036
|
+
fix: [
|
|
1037
|
+
"FIX_JAVASCRIPT_PRETTIER",
|
|
1038
|
+
"FIX_JSX_PRETTIER",
|
|
1039
|
+
"FIX_TYPESCRIPT_PRETTIER",
|
|
1040
|
+
"FIX_TSX",
|
|
1041
|
+
"FIX_MARKDOWN_PRETTIER"
|
|
1042
|
+
],
|
|
1043
|
+
always: true,
|
|
1044
|
+
localBin: "prettier",
|
|
1045
|
+
localArgs: ["--check", "."],
|
|
1046
|
+
configInput: "prettier-config"
|
|
1047
|
+
},
|
|
1048
|
+
yamllint: {
|
|
1049
|
+
validate: ["VALIDATE_YAML"],
|
|
1050
|
+
always: true,
|
|
1051
|
+
localBin: "yamllint",
|
|
1052
|
+
localArgs: ["."],
|
|
1053
|
+
installHint: "brew install yamllint",
|
|
1054
|
+
configInput: "yaml-config"
|
|
1055
|
+
},
|
|
1056
|
+
actionlint: {
|
|
1057
|
+
validate: ["VALIDATE_GITHUB_ACTIONS"],
|
|
1058
|
+
always: true,
|
|
1059
|
+
localBin: "actionlint",
|
|
1060
|
+
localArgs: [],
|
|
1061
|
+
installHint: "brew install actionlint"
|
|
1062
|
+
},
|
|
1063
|
+
gitleaks: {
|
|
1064
|
+
validate: ["VALIDATE_GITLEAKS"],
|
|
1065
|
+
always: true,
|
|
1066
|
+
localBin: "gitleaks",
|
|
1067
|
+
localArgs: [
|
|
1068
|
+
"dir",
|
|
1069
|
+
"--no-banner",
|
|
1070
|
+
"--config",
|
|
1071
|
+
".gitleaks.toml"
|
|
1072
|
+
],
|
|
1073
|
+
installHint: "brew install gitleaks"
|
|
1074
|
+
},
|
|
1075
|
+
editorconfig: {
|
|
1076
|
+
validate: ["VALIDATE_EDITORCONFIG"],
|
|
1077
|
+
always: true,
|
|
1078
|
+
localBin: "editorconfig-checker",
|
|
1079
|
+
localArgs: [],
|
|
1080
|
+
installHint: "brew install editorconfig-checker"
|
|
1081
|
+
},
|
|
1082
|
+
commitlint: {
|
|
1083
|
+
validate: ["VALIDATE_GIT_COMMITLINT"],
|
|
1084
|
+
always: true
|
|
1085
|
+
},
|
|
1086
|
+
"git-merge-conflict-markers": {
|
|
1087
|
+
validate: ["VALIDATE_GIT_MERGE_CONFLICT_MARKERS"],
|
|
1088
|
+
always: true
|
|
1089
|
+
},
|
|
1090
|
+
markdownlint: {
|
|
1091
|
+
validate: ["VALIDATE_MARKDOWN"],
|
|
1092
|
+
localBin: "markdownlint-cli2",
|
|
1093
|
+
localArgs: ["**/*.md"],
|
|
1094
|
+
detect: [
|
|
1095
|
+
".markdownlint.json",
|
|
1096
|
+
".markdownlint.jsonc",
|
|
1097
|
+
".markdownlint.yaml",
|
|
1098
|
+
".markdownlint.yml",
|
|
1099
|
+
".markdownlint-cli2.jsonc",
|
|
1100
|
+
".markdownlint-cli2.yaml",
|
|
1101
|
+
".markdownlint-cli2.mjs"
|
|
1102
|
+
]
|
|
1103
|
+
}
|
|
1104
|
+
};
|
|
1105
|
+
/** Every linter name the registry knows. */
|
|
1106
|
+
const LINTER_NAMES = new Set(Object.keys(LINTERS));
|
|
1017
1107
|
/**
|
|
1018
|
-
*
|
|
1019
|
-
*
|
|
1020
|
-
*
|
|
1021
|
-
*
|
|
1022
|
-
*
|
|
1023
|
-
*
|
|
1108
|
+
* Resolve the linter set for a repo. An `explicit` list (from `config.tasks`)
|
|
1109
|
+
* picks the candidate set; otherwise the candidates are every `always` linter
|
|
1110
|
+
* plus every `detect`-gated linter whose filenames are present at the repo
|
|
1111
|
+
* root, OR whose mapped `@theholocron/*-config` package resolves per
|
|
1112
|
+
* {@link sharedConfigAvailable} — a repo can't run eslint with genuinely no
|
|
1113
|
+
* config anywhere (super-linter FATALs if you ask it to,
|
|
1114
|
+
* theholocron/holocron#654), but a *shared* one it has installed counts
|
|
1115
|
+
* (holocron#795: proving `clients`' eslint.config.ts files reduce to zero
|
|
1116
|
+
* remaining content is only half the story — the file itself was still load-
|
|
1117
|
+
* bearing here as the sole "eslint is even configured" signal until this
|
|
1118
|
+
* fallback existed). `always` linters are unconditional. Result is ordered
|
|
1119
|
+
* by {@link LINTERS} declaration order.
|
|
1024
1120
|
*
|
|
1025
|
-
*
|
|
1121
|
+
* `sharedConfigAvailable` is caller-computed (`run.ts`'s `runLinterGroup`
|
|
1122
|
+
* already resolves each linter's `resolveToolConfig()` per member to build
|
|
1123
|
+
* its actual command — reusing that instead of duplicating the package-
|
|
1124
|
+
* resolution logic here keeps this function itself pure and file-I/O-free).
|
|
1125
|
+
*
|
|
1126
|
+
* @throws when an `explicit` name is not in the registry — a typo is a
|
|
1127
|
+
* config bug, not a linter to silently skip.
|
|
1026
1128
|
*/
|
|
1027
|
-
function
|
|
1028
|
-
const
|
|
1029
|
-
|
|
1030
|
-
const
|
|
1031
|
-
const
|
|
1032
|
-
if (
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
if (!hasDocs) result["type"] = "storybook";
|
|
1038
|
-
result["storybook-projects"] = JSON.stringify(storybookProjects.map(({ name, path = "." }) => ({
|
|
1129
|
+
function resolveLinters(opts) {
|
|
1130
|
+
const order = Object.keys(LINTERS);
|
|
1131
|
+
const present = new Set(opts.rootFiles);
|
|
1132
|
+
const shared = opts.sharedConfigAvailable ?? /* @__PURE__ */ new Set();
|
|
1133
|
+
const configPresent = (name, def) => def.always === true || (def.detect ?? []).some((f) => present.has(f)) || shared.has(name);
|
|
1134
|
+
if (opts.explicit && opts.explicit.length > 0) {
|
|
1135
|
+
const unknown = opts.explicit.filter((n) => !LINTER_NAMES.has(n));
|
|
1136
|
+
if (unknown.length > 0) throw new Error(`unknown linter${unknown.length > 1 ? "s" : ""} ${unknown.map((n) => `"${n}"`).join(", ")} — known: ${order.join(", ")}`);
|
|
1137
|
+
const wanted = new Set(opts.explicit);
|
|
1138
|
+
return order.filter((n) => wanted.has(n) && configPresent(n, LINTERS[n])).map((name) => ({
|
|
1039
1139
|
name,
|
|
1040
|
-
|
|
1041
|
-
})));
|
|
1042
|
-
delete result["storybook"];
|
|
1043
|
-
}
|
|
1044
|
-
const runChromatic = raw["run-chromatic"];
|
|
1045
|
-
if (runChromatic !== null && typeof runChromatic === "object" && "projects" in runChromatic) {
|
|
1046
|
-
result["run-chromatic"] = true;
|
|
1047
|
-
const projects = runChromatic.projects.map((p) => ({
|
|
1048
|
-
...p,
|
|
1049
|
-
...Array.isArray(p.untraced) ? { untraced: p.untraced.join("\n") } : {}
|
|
1140
|
+
def: LINTERS[name]
|
|
1050
1141
|
}));
|
|
1051
|
-
result["chromatic-projects"] = JSON.stringify(projects);
|
|
1052
1142
|
}
|
|
1053
|
-
|
|
1054
|
-
|
|
1143
|
+
return order.filter((name) => configPresent(name, LINTERS[name])).map((name) => ({
|
|
1144
|
+
name,
|
|
1145
|
+
def: LINTERS[name]
|
|
1146
|
+
}));
|
|
1055
1147
|
}
|
|
1148
|
+
//#endregion
|
|
1149
|
+
//#region src/tasks/run/resolver.ts
|
|
1056
1150
|
/**
|
|
1057
|
-
*
|
|
1058
|
-
*
|
|
1151
|
+
* `library` is every mapped tool's org-wide default variant today — every
|
|
1152
|
+
* currently-migrated repo (`holocron`, `clients`, `utils`, `configs`,
|
|
1153
|
+
* `themes`, `observability`, `skills`) is a publishable library package, and
|
|
1154
|
+
* `eslint-config`/`vitest-config`/`tsdown-config` all name their
|
|
1155
|
+
* general-purpose preset exactly that. Picking a different variant per repo
|
|
1156
|
+
* (an app-type template, say) needs new `holocron.config.ts` surface this
|
|
1157
|
+
* doesn't add yet — deliberately out of scope until a real consumer needs it.
|
|
1059
1158
|
*/
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1159
|
+
const TOOL_CONFIGS = {
|
|
1160
|
+
eslint: {
|
|
1161
|
+
package: "@theholocron/eslint-config",
|
|
1162
|
+
exportPath: "./bundles/library",
|
|
1163
|
+
flag: ["--config"]
|
|
1164
|
+
},
|
|
1165
|
+
prettier: {
|
|
1166
|
+
package: "@theholocron/prettier-config",
|
|
1167
|
+
exportPath: ".",
|
|
1168
|
+
flag: ["--config"]
|
|
1169
|
+
},
|
|
1170
|
+
vitest: {
|
|
1171
|
+
package: "@theholocron/vitest-config",
|
|
1172
|
+
exportPath: "./bundles/library",
|
|
1173
|
+
flag: ["--config"]
|
|
1174
|
+
},
|
|
1175
|
+
tsdown: {
|
|
1176
|
+
package: "@theholocron/tsdown-config",
|
|
1177
|
+
exportPath: "./presets/library",
|
|
1178
|
+
flag: ["--config"]
|
|
1179
|
+
},
|
|
1180
|
+
commitlint: {
|
|
1181
|
+
package: "@theholocron/commitlint-config",
|
|
1182
|
+
exportPath: ".",
|
|
1183
|
+
flag: ["--config"]
|
|
1079
1184
|
}
|
|
1080
|
-
|
|
1081
|
-
}
|
|
1082
|
-
//#endregion
|
|
1083
|
-
//#region src/ci.ts
|
|
1185
|
+
};
|
|
1084
1186
|
/**
|
|
1085
|
-
*
|
|
1086
|
-
*
|
|
1087
|
-
*
|
|
1088
|
-
*
|
|
1089
|
-
*
|
|
1090
|
-
*
|
|
1091
|
-
*
|
|
1092
|
-
* task whose local runner can't run is a failure; a `local: null` task with
|
|
1093
|
-
* no turbo task / `package.json` script is skipped, never failed — even when
|
|
1094
|
-
* `required` (it's a CI-only check, not a local one).
|
|
1187
|
+
* Every filename a tool's own auto-discovery would pick up from `cwd` —
|
|
1188
|
+
* same extension set `registry.ts`'s `turbo.inputs`/`local.detect` entries
|
|
1189
|
+
* already use for these tools, kept in sync by hand (no shared source of
|
|
1190
|
+
* truth between the two today). Presence of any one of these means the
|
|
1191
|
+
* package has its own local config — customized or not, `resolveToolConfig`
|
|
1192
|
+
* can't tell the difference from the filename alone, so it defers to it
|
|
1193
|
+
* either way (#749).
|
|
1095
1194
|
*/
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1195
|
+
const LOCAL_CONFIG_FILES = {
|
|
1196
|
+
eslint: [
|
|
1197
|
+
"eslint.config.ts",
|
|
1198
|
+
"eslint.config.js",
|
|
1199
|
+
"eslint.config.mjs",
|
|
1200
|
+
"eslint.config.cjs"
|
|
1201
|
+
],
|
|
1202
|
+
prettier: [
|
|
1203
|
+
"prettier.config.ts",
|
|
1204
|
+
"prettier.config.js",
|
|
1205
|
+
"prettier.config.mjs",
|
|
1206
|
+
"prettier.config.cjs"
|
|
1207
|
+
],
|
|
1208
|
+
vitest: [
|
|
1209
|
+
"vitest.config.ts",
|
|
1210
|
+
"vitest.config.js",
|
|
1211
|
+
"vitest.config.mjs"
|
|
1212
|
+
],
|
|
1213
|
+
tsdown: [
|
|
1214
|
+
"tsdown.config.ts",
|
|
1215
|
+
"tsdown.config.js",
|
|
1216
|
+
"tsdown.config.mjs",
|
|
1217
|
+
"tsdown.config.cjs"
|
|
1218
|
+
],
|
|
1219
|
+
commitlint: [
|
|
1220
|
+
"commitlint.config.ts",
|
|
1221
|
+
"commitlint.config.js",
|
|
1222
|
+
"commitlint.config.mjs",
|
|
1223
|
+
"commitlint.config.cjs"
|
|
1224
|
+
]
|
|
1225
|
+
};
|
|
1226
|
+
new Set(Object.keys(TOOL_CONFIGS));
|
|
1227
|
+
/**
|
|
1228
|
+
* `<flag> <absolute path>` for `tool`, resolved against `cwd`'s
|
|
1229
|
+
* `node_modules` — or `[]` when the tool isn't mapped, `cwd` already has its
|
|
1230
|
+
* own local config file for it (#749 — local always wins, customized or
|
|
1231
|
+
* not), the shared package isn't installed, its `package.json` doesn't
|
|
1232
|
+
* declare the export subpath this needs, or the resolved file doesn't
|
|
1233
|
+
* actually exist on disk (a stale install, or a package version that
|
|
1234
|
+
* predates the export existing).
|
|
1235
|
+
*/
|
|
1236
|
+
function resolveToolConfig(tool, cwd, deps) {
|
|
1237
|
+
const spec = TOOL_CONFIGS[tool];
|
|
1238
|
+
if (!spec) return [];
|
|
1239
|
+
if ((LOCAL_CONFIG_FILES[tool] ?? []).some((name) => deps.fileExists(joinPath(cwd, name)))) return [];
|
|
1240
|
+
const pkgDir = joinPath(cwd, "node_modules", ...spec.package.split("/"));
|
|
1241
|
+
const pkgJsonPath = joinPath(pkgDir, "package.json");
|
|
1242
|
+
if (!deps.fileExists(pkgJsonPath)) return [];
|
|
1243
|
+
let pkg;
|
|
1244
|
+
try {
|
|
1245
|
+
pkg = JSON.parse(deps.readFile(pkgJsonPath));
|
|
1246
|
+
} catch {
|
|
1247
|
+
return [];
|
|
1136
1248
|
}
|
|
1137
|
-
const
|
|
1138
|
-
|
|
1139
|
-
const
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
else if (failed > 0) print(`✗ ${failed} failed, ${passed} passed${skipped ? `, ${skipped} skipped` : ""}`);
|
|
1143
|
-
else print(`✓ ${passed} passed${skipped ? `, ${skipped} skipped` : ""}${input.dryRun ? " (plan only)" : ""}`);
|
|
1144
|
-
return {
|
|
1145
|
-
status: failed > 0 ? "fail" : "ok",
|
|
1146
|
-
jobs
|
|
1147
|
-
};
|
|
1148
|
-
}
|
|
1149
|
-
function taskName(item) {
|
|
1150
|
-
return typeof item === "string" ? item : item.name;
|
|
1249
|
+
const relative = resolveExportEntry(pkg.exports?.[spec.exportPath]);
|
|
1250
|
+
if (!relative) return [];
|
|
1251
|
+
const absolute = joinPath(pkgDir, relative.replace(/^\.\//, ""));
|
|
1252
|
+
if (!deps.fileExists(absolute)) return [];
|
|
1253
|
+
return [...spec.flag, absolute];
|
|
1151
1254
|
}
|
|
1152
|
-
//#endregion
|
|
1153
|
-
//#region src/templates/codecov/codecov.yml
|
|
1154
|
-
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";
|
|
1155
|
-
//#endregion
|
|
1156
|
-
//#region src/codecov.ts
|
|
1157
1255
|
/**
|
|
1158
|
-
* `
|
|
1159
|
-
*
|
|
1160
|
-
*
|
|
1161
|
-
*
|
|
1162
|
-
*
|
|
1163
|
-
*
|
|
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.
|
|
1164
1262
|
*/
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
return
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
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);
|
|
1172
1270
|
}
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1271
|
+
/**
|
|
1272
|
+
* A minimal, POSIX-safe `path.join` — avoids importing `node:path` just for
|
|
1273
|
+
* this. Every path in play here is either already POSIX (a `package.json`
|
|
1274
|
+
* `exports` value, always forward-slashed regardless of platform) or built
|
|
1275
|
+
* from platform-neutral segments (`cwd`, `"node_modules"`, a scoped package
|
|
1276
|
+
* name split on `/`), so a manual join is safe and keeps this module
|
|
1277
|
+
* dependency-free like the rest of `run.ts`'s helpers.
|
|
1278
|
+
*/
|
|
1279
|
+
function joinPath(...segments) {
|
|
1280
|
+
return segments.map((s, i) => i === 0 ? s.replace(/\/+$/, "") : s.replace(/^\/+|\/+$/g, "")).filter(Boolean).join("/");
|
|
1182
1281
|
}
|
|
1282
|
+
//#endregion
|
|
1283
|
+
//#region src/tasks/run/run.ts
|
|
1183
1284
|
/**
|
|
1184
|
-
*
|
|
1185
|
-
*
|
|
1186
|
-
* instead of hanging forever on a required check codecov never posts.
|
|
1285
|
+
* `holocron run <task> [job] [-- <passthrough>]` — run a registry task (or one
|
|
1286
|
+
* of its sub-jobs) locally.
|
|
1187
1287
|
*
|
|
1188
|
-
*
|
|
1189
|
-
*
|
|
1190
|
-
*
|
|
1191
|
-
*
|
|
1288
|
+
* A `job` argument only applies to tasks that declare `jobs`; for any other
|
|
1289
|
+
* task it is folded back into the passthrough (`holocron run delivery.build
|
|
1290
|
+
* src/`).
|
|
1291
|
+
*
|
|
1292
|
+
* Resolution:
|
|
1293
|
+
*
|
|
1294
|
+
* J. job given → TASKS[task].jobs[job].local (unknown job → exit 1)
|
|
1295
|
+
* 1. turbo.json defines the task → `turbo run <task>`
|
|
1296
|
+
* 2. package.json has a `<task>` script → `<pm> run <task>`
|
|
1297
|
+
* (unless it's the `holocron run …` thin caller — that recurses)
|
|
1298
|
+
* 3. TASKS[task].local resolves → `<tool> <args> <org-flags> <passthrough>`
|
|
1299
|
+
* 3b. TASKS[task].linterGroup → run each resolved linter natively (see below)
|
|
1300
|
+
* 4. TASKS[task].jobs has entries → run each job in declared order
|
|
1301
|
+
* 5. TASKS[task].local === null → "enforced in CI" (skip, even with --required)
|
|
1302
|
+
* 5b. known task, nothing resolved → "no <task> task" (exit 0, or 1 with --required)
|
|
1303
|
+
* 6. unknown task → "unknown task" (exit 1)
|
|
1304
|
+
*
|
|
1305
|
+
* A `linterGroup` task (`sourceQuality.staticAnalysis`, etc.) runs its fixed
|
|
1306
|
+
* set of `linters.ts` entries, each still gated by that linter's own
|
|
1307
|
+
* `detect`/`always` rule — every entry runs its `localBin` natively when
|
|
1308
|
+
* found on PATH. Missing tools are flagged; the exit code is the worst of
|
|
1309
|
+
* the lot. Unlike the old single `lint` task, this check runs *after*
|
|
1310
|
+
* turbo/`package.json` resolution (step 3b, not step 0) — a `linterGroup`
|
|
1311
|
+
* task is a normal turbo-delegatable task like any other.
|
|
1192
1312
|
*/
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
const
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
break;
|
|
1211
|
-
}
|
|
1212
|
-
if (sibIndent === indent.length && /^\s*(- |\w)/.test(sib) && !/^\s*(threshold|flags|paths|branches)\s*:/.test(sib)) break;
|
|
1313
|
+
/** The per-command runner — `runTask` and `runLintAggregate` share it. */
|
|
1314
|
+
function makeRunOne(input) {
|
|
1315
|
+
const { print, logger, exec, task, cwd } = input;
|
|
1316
|
+
const dryRun = input.dryRun ?? false;
|
|
1317
|
+
return (cmd, args) => {
|
|
1318
|
+
const command = [cmd, ...args].join(" ");
|
|
1319
|
+
if (dryRun) {
|
|
1320
|
+
print(`would run: ${command}`);
|
|
1321
|
+
logger.debug({
|
|
1322
|
+
task,
|
|
1323
|
+
command,
|
|
1324
|
+
status: "dry-run"
|
|
1325
|
+
}, `run: ${task}`);
|
|
1326
|
+
return {
|
|
1327
|
+
status: "dry-run",
|
|
1328
|
+
command
|
|
1329
|
+
};
|
|
1213
1330
|
}
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1331
|
+
print(`→ ${command}`);
|
|
1332
|
+
const { exitCode } = exec(cmd, args, { cwd });
|
|
1333
|
+
const status = exitCode === 0 ? "ok" : "fail";
|
|
1334
|
+
logger[status === "fail" ? "warn" : "debug"]({
|
|
1335
|
+
task,
|
|
1336
|
+
command,
|
|
1337
|
+
exitCode,
|
|
1338
|
+
status
|
|
1339
|
+
}, `run: ${task}`);
|
|
1340
|
+
return {
|
|
1341
|
+
status,
|
|
1342
|
+
command,
|
|
1343
|
+
...status === "fail" ? { message: `\`${command}\` exited ${exitCode}` } : {}
|
|
1344
|
+
};
|
|
1345
|
+
};
|
|
1222
1346
|
}
|
|
1223
|
-
function
|
|
1224
|
-
const
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1347
|
+
function runTask(input) {
|
|
1348
|
+
const { print, logger, readFile, fileExists, listDir, task, cwd } = input;
|
|
1349
|
+
const def = TASKS[task];
|
|
1350
|
+
let job = input.job;
|
|
1351
|
+
let passthrough = input.passthrough ?? [];
|
|
1352
|
+
if (job !== void 0 && !def?.jobs) {
|
|
1353
|
+
passthrough = [job, ...passthrough];
|
|
1354
|
+
job = void 0;
|
|
1355
|
+
}
|
|
1356
|
+
if (job !== void 0) {
|
|
1357
|
+
const jobDef = def.jobs[job];
|
|
1358
|
+
if (!jobDef) {
|
|
1359
|
+
const known = Object.keys(def.jobs).join(", ");
|
|
1360
|
+
print(`✗ unknown job "${job}" for "${task}" (known: ${known})`);
|
|
1361
|
+
logger.warn({
|
|
1362
|
+
task,
|
|
1363
|
+
job,
|
|
1364
|
+
status: "unknown"
|
|
1365
|
+
}, `run: ${task} ${job}`);
|
|
1366
|
+
return {
|
|
1367
|
+
status: "unknown",
|
|
1368
|
+
message: `unknown job "${task} ${job}"`
|
|
1369
|
+
};
|
|
1370
|
+
}
|
|
1371
|
+
const label = `${task} ${job}`;
|
|
1372
|
+
return runUnit(input, label, jobDef.local, makeRunOne({
|
|
1373
|
+
...input,
|
|
1374
|
+
task: label
|
|
1375
|
+
}), passthrough);
|
|
1376
|
+
}
|
|
1377
|
+
const run = makeRunOne(input);
|
|
1378
|
+
if (turboDefinesTask(cwd, task, readFile, fileExists)) {
|
|
1379
|
+
const filterArg = input.filter ? [`--filter=${input.filter}`] : [];
|
|
1380
|
+
const forwarded = [...def?.local && !def.local.command && def.local.tool ? def.flags?.[def.local.tool] ?? [] : [], ...passthrough];
|
|
1381
|
+
const args = [
|
|
1382
|
+
"run",
|
|
1383
|
+
task,
|
|
1384
|
+
...filterArg,
|
|
1385
|
+
...forwarded.length ? ["--", ...forwarded] : []
|
|
1386
|
+
];
|
|
1387
|
+
return run(resolveBin(cwd, "turbo", fileExists), args);
|
|
1388
|
+
}
|
|
1389
|
+
const script = packageJsonScript(cwd, task, readFile, fileExists);
|
|
1390
|
+
if (script && !/^holocron run\b/.test(script.trim())) return run(packageManager(cwd, readFile, fileExists), [
|
|
1391
|
+
"run",
|
|
1392
|
+
task,
|
|
1393
|
+
...passthrough.length ? ["--", ...passthrough] : []
|
|
1394
|
+
]);
|
|
1395
|
+
if (def?.local) {
|
|
1396
|
+
if (def.local.command) return run(process.execPath, [
|
|
1397
|
+
process.argv[1],
|
|
1398
|
+
def.local.command,
|
|
1399
|
+
...passthrough
|
|
1400
|
+
]);
|
|
1401
|
+
const runner = resolveRunner(def.local, cwd, listDir, readFile);
|
|
1402
|
+
if (runner) {
|
|
1403
|
+
const flags = (def.flags?.[runner.tool] ?? []).filter((f) => !passthrough.includes(f));
|
|
1404
|
+
const configFlags = resolveToolConfig(runner.tool, cwd, {
|
|
1405
|
+
readFile,
|
|
1406
|
+
fileExists
|
|
1236
1407
|
});
|
|
1237
|
-
|
|
1408
|
+
return run(resolveBin(cwd, runner.tool, fileExists), [
|
|
1409
|
+
...runner.args,
|
|
1410
|
+
...configFlags,
|
|
1411
|
+
...flags,
|
|
1412
|
+
...passthrough
|
|
1413
|
+
]);
|
|
1414
|
+
}
|
|
1238
1415
|
}
|
|
1239
|
-
return
|
|
1416
|
+
if (def?.linterGroup) return runLinterGroup(input, def.linterGroup, passthrough);
|
|
1417
|
+
if (def?.jobs && Object.keys(def.jobs).length > 0) return runAllJobs(input, def.jobs, passthrough);
|
|
1418
|
+
if (def?.local === null) {
|
|
1419
|
+
const msg = `no local equivalent for ${task} — enforced in CI`;
|
|
1420
|
+
print(`· ${msg}`);
|
|
1421
|
+
logger.debug({
|
|
1422
|
+
task,
|
|
1423
|
+
status: "skip"
|
|
1424
|
+
}, `run: ${task}`);
|
|
1425
|
+
return {
|
|
1426
|
+
status: "skip",
|
|
1427
|
+
message: msg
|
|
1428
|
+
};
|
|
1429
|
+
}
|
|
1430
|
+
if (KNOWN_TASKS.has(task)) {
|
|
1431
|
+
const msg = `no ${task} task for this repo`;
|
|
1432
|
+
print(input.required ? `✗ ${msg} (required)` : `· ${msg}`);
|
|
1433
|
+
logger[input.required ? "warn" : "debug"]({
|
|
1434
|
+
task,
|
|
1435
|
+
status: input.required ? "fail" : "skip"
|
|
1436
|
+
}, `run: ${task}`);
|
|
1437
|
+
return {
|
|
1438
|
+
status: input.required ? "fail" : "skip",
|
|
1439
|
+
message: msg
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
1442
|
+
print(`✗ unknown task "${task}"`);
|
|
1443
|
+
logger.warn({
|
|
1444
|
+
task,
|
|
1445
|
+
status: "unknown"
|
|
1446
|
+
}, `run: ${task}`);
|
|
1447
|
+
return {
|
|
1448
|
+
status: "unknown",
|
|
1449
|
+
message: `unknown task "${task}"`
|
|
1450
|
+
};
|
|
1240
1451
|
}
|
|
1241
1452
|
/**
|
|
1242
|
-
*
|
|
1243
|
-
*
|
|
1244
|
-
*
|
|
1245
|
-
*
|
|
1246
|
-
*
|
|
1453
|
+
* Run one registry unit — a task's own runner or a single sub-job. Resolution
|
|
1454
|
+
* is registry-only (`turbo` / `package.json` scripts are keyed by task, not
|
|
1455
|
+
* `task/job`). `label` names the unit in messages (`audit performance`).
|
|
1456
|
+
*
|
|
1457
|
+
* `local: null` → CI-only by design, always a skip (never a failure, even with
|
|
1458
|
+
* `--required`). A declared runner with no matching repo file, or whose tool
|
|
1459
|
+
* isn't installed locally → a skip (`--required` turns it into a failure — the
|
|
1460
|
+
* repo claims a check it can't back). Mirrors the lint aggregate: run what's
|
|
1461
|
+
* here, flag the rest, let CI enforce.
|
|
1247
1462
|
*/
|
|
1248
|
-
function
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1463
|
+
function runUnit(input, label, local, run, passthrough) {
|
|
1464
|
+
const { print, logger, listDir, lookPath, cwd, readFile } = input;
|
|
1465
|
+
const skip = (msg) => {
|
|
1466
|
+
print(input.required ? `✗ ${msg} (required)` : `· ${msg}`);
|
|
1467
|
+
logger[input.required ? "warn" : "debug"]({
|
|
1468
|
+
task: label,
|
|
1469
|
+
status: input.required ? "fail" : "skip"
|
|
1470
|
+
}, `run: ${label}`);
|
|
1471
|
+
return {
|
|
1472
|
+
status: input.required ? "fail" : "skip",
|
|
1473
|
+
message: msg
|
|
1474
|
+
};
|
|
1475
|
+
};
|
|
1476
|
+
if (local) {
|
|
1477
|
+
const runner = resolveRunner(local, cwd, listDir, readFile);
|
|
1478
|
+
if (!runner) return skip(`no ${label} runner for this repo`);
|
|
1479
|
+
const found = lookPath(cwd, runner.tool);
|
|
1480
|
+
if (!found) return skip(`${runner.tool} not installed locally for ${label} — enforced in CI`);
|
|
1481
|
+
return run(found, [...runner.args, ...passthrough]);
|
|
1482
|
+
}
|
|
1483
|
+
const msg = `no local equivalent for ${label} — enforced in CI`;
|
|
1484
|
+
print(`· ${msg}`);
|
|
1485
|
+
logger.debug({
|
|
1486
|
+
task: label,
|
|
1487
|
+
status: "skip"
|
|
1488
|
+
}, `run: ${label}`);
|
|
1489
|
+
return {
|
|
1490
|
+
status: "skip",
|
|
1491
|
+
message: msg
|
|
1492
|
+
};
|
|
1254
1493
|
}
|
|
1255
1494
|
/**
|
|
1256
|
-
*
|
|
1257
|
-
*
|
|
1258
|
-
*
|
|
1495
|
+
* `holocron run <task>` for a task that is a container of sub-jobs (`audit`) —
|
|
1496
|
+
* run each job in declared order. Status precedence: any `fail` → `fail`; else
|
|
1497
|
+
* any `ok` → `ok`; else any `dry-run` → `dry-run`; else `skip` (nothing ran).
|
|
1259
1498
|
*/
|
|
1260
|
-
function
|
|
1261
|
-
const
|
|
1262
|
-
|
|
1499
|
+
function runAllJobs(input, jobs, passthrough) {
|
|
1500
|
+
const { print, task } = input;
|
|
1501
|
+
const perJob = {
|
|
1502
|
+
...input,
|
|
1503
|
+
required: false
|
|
1504
|
+
};
|
|
1505
|
+
const reports = [];
|
|
1506
|
+
for (const [name, jobDef] of Object.entries(jobs)) {
|
|
1507
|
+
const label = `${task} ${name}`;
|
|
1508
|
+
print(`▶ ${jobDef.checkContext}`);
|
|
1509
|
+
reports.push(runUnit(perJob, label, jobDef.local, makeRunOne({
|
|
1510
|
+
...perJob,
|
|
1511
|
+
task: label
|
|
1512
|
+
}), passthrough));
|
|
1513
|
+
}
|
|
1514
|
+
const command = reports.map((r) => r.command).filter((c) => Boolean(c)).join(" && ");
|
|
1515
|
+
const statuses = new Set(reports.map((r) => r.status));
|
|
1516
|
+
if (statuses.has("fail")) return {
|
|
1517
|
+
status: "fail",
|
|
1518
|
+
command,
|
|
1519
|
+
message: `one or more ${task} jobs failed`
|
|
1520
|
+
};
|
|
1521
|
+
if (statuses.has("ok")) return {
|
|
1522
|
+
status: "ok",
|
|
1523
|
+
command
|
|
1524
|
+
};
|
|
1525
|
+
if (statuses.has("dry-run")) return {
|
|
1526
|
+
status: "dry-run",
|
|
1527
|
+
command
|
|
1528
|
+
};
|
|
1529
|
+
return {
|
|
1530
|
+
status: "skip",
|
|
1531
|
+
message: `no ${task} job has a local equivalent — enforced in CI`
|
|
1532
|
+
};
|
|
1263
1533
|
}
|
|
1264
|
-
//#endregion
|
|
1265
|
-
//#region src/required-checks.ts
|
|
1266
1534
|
/**
|
|
1267
|
-
* `
|
|
1268
|
-
*
|
|
1269
|
-
*
|
|
1270
|
-
*
|
|
1271
|
-
*
|
|
1272
|
-
*
|
|
1273
|
-
* hand-maintained `repo.requiredChecks` array. Policy-free — it only knows the
|
|
1274
|
-
* manifest.
|
|
1535
|
+
* `holocron run <task>` for a `linterGroup` task — the task's fixed linter
|
|
1536
|
+
* set, each still gated by its own `detect`/`always` rule, run natively.
|
|
1537
|
+
* Worst exit code wins. Runs *after* the caller's own turbo/`package.json`
|
|
1538
|
+
* resolution (step 3b) — no per-linter turbo passthrough here, unlike the
|
|
1539
|
+
* old single `lint` task's eslint-only special case; the whole task is what
|
|
1540
|
+
* turbo/a package script would have already caught in steps 1–2.
|
|
1275
1541
|
*/
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
const
|
|
1279
|
-
const
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1542
|
+
function runLinterGroup(input, names, passthrough) {
|
|
1543
|
+
const { print, logger, listDir, lookPath, cwd, task, readFile, fileExists } = input;
|
|
1544
|
+
const dryRun = input.dryRun ?? false;
|
|
1545
|
+
const runOne = makeRunOne(input);
|
|
1546
|
+
let rootFiles;
|
|
1547
|
+
try {
|
|
1548
|
+
rootFiles = listDir(cwd);
|
|
1549
|
+
} catch {
|
|
1550
|
+
rootFiles = [];
|
|
1551
|
+
}
|
|
1552
|
+
const sharedConfigAvailable = new Set(names.filter((name) => resolveToolConfig(name, cwd, {
|
|
1553
|
+
readFile,
|
|
1554
|
+
fileExists
|
|
1555
|
+
}).length > 0));
|
|
1556
|
+
const resolved = resolveLinters({
|
|
1557
|
+
explicit: names,
|
|
1558
|
+
rootFiles,
|
|
1559
|
+
sharedConfigAvailable
|
|
1560
|
+
});
|
|
1561
|
+
const reports = [];
|
|
1562
|
+
let anyHasLocalBin = false;
|
|
1563
|
+
for (const { name, def } of resolved) {
|
|
1564
|
+
const bin = def.localBin;
|
|
1565
|
+
if (!bin) {
|
|
1566
|
+
print(`· ${name} (CI only)`);
|
|
1567
|
+
continue;
|
|
1568
|
+
}
|
|
1569
|
+
anyHasLocalBin = true;
|
|
1570
|
+
const found = lookPath(cwd, bin);
|
|
1571
|
+
if (!found) {
|
|
1572
|
+
print(`! ${name} — ${bin} not on PATH${def.installHint ? `. ${def.installHint}` : ""} (enforced in CI)`);
|
|
1573
|
+
continue;
|
|
1286
1574
|
}
|
|
1575
|
+
const configFlags = resolveToolConfig(name, cwd, {
|
|
1576
|
+
readFile,
|
|
1577
|
+
fileExists
|
|
1578
|
+
});
|
|
1579
|
+
reports.push(runOne(found, [
|
|
1580
|
+
...configFlags,
|
|
1581
|
+
...def.localArgs ?? [],
|
|
1582
|
+
...passthrough
|
|
1583
|
+
]));
|
|
1287
1584
|
}
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1585
|
+
if (reports.length === 0) {
|
|
1586
|
+
const required = input.required && anyHasLocalBin;
|
|
1587
|
+
const msg = `no tooling available locally for ${task} — every linter in this group is CI-only here`;
|
|
1588
|
+
print(required ? `✗ ${msg} (required)` : `· ${msg}`);
|
|
1589
|
+
logger[required ? "warn" : "debug"]({
|
|
1590
|
+
task,
|
|
1591
|
+
status: required ? "fail" : "skip"
|
|
1592
|
+
}, `run: ${task}`);
|
|
1593
|
+
return {
|
|
1594
|
+
status: required ? "fail" : "skip",
|
|
1595
|
+
message: msg
|
|
1596
|
+
};
|
|
1291
1597
|
}
|
|
1292
|
-
|
|
1598
|
+
const command = reports.map((r) => r.command).filter((c) => Boolean(c)).join(" && ");
|
|
1599
|
+
if (dryRun) return {
|
|
1600
|
+
status: "dry-run",
|
|
1601
|
+
command
|
|
1602
|
+
};
|
|
1603
|
+
return reports.some((r) => r.status === "fail") ? {
|
|
1604
|
+
status: "fail",
|
|
1605
|
+
command,
|
|
1606
|
+
message: "one or more linters failed"
|
|
1607
|
+
} : {
|
|
1608
|
+
status: "ok",
|
|
1609
|
+
command
|
|
1610
|
+
};
|
|
1293
1611
|
}
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
//#region src/templates/reusable/actions/holocron.yml
|
|
1299
|
-
var holocron_default = "name: Holocron\ndescription: >\n Run the Holocron CLI — `holocron run <task> [job]` by default, or any other\n subcommand via `command:` (`sync`, `sync-github`, …). Resolves\n `@theholocron/cli` from `node_modules` (published tarball) or builds it from\n source when it resolves to an unbuilt workspace checkout (the holocron repo\n itself). Requires dependencies to be installed already (run the setup action\n first).\n\ninputs:\n command:\n description: >\n Holocron subcommand. Default `run` — then `task` (and optional `job`)\n name the manifest task. Set to `sync` / `sync-github` / … to run that\n subcommand directly; `args` becomes everything after it.\n required: false\n default: run\n task:\n description: >\n Manifest task to run when command is run — typecheck, test, build, audit.\n required: false\n default: \"\"\n job:\n description: Optional sub-job within the task (e.g. knip, performance for audit).\n required: false\n default: \"\"\n args:\n description: >\n For `command: run`, extra arguments forwarded to the tool after `--`.\n For any other command, the arguments passed straight to it.\n required: false\n default: \"\"\n\nruns:\n using: composite\n\n steps:\n - name: Build the workspace when the CLI resolves to an unbuilt checkout\n shell: bash\n # The holocron repo carries @theholocron/cli as `workspace:*`; its bin\n # (dist/cli.mjs) does not exist on a fresh checkout. `pnpm build` is a\n # turbo run — cached and cheap once warm. A no-op everywhere else, where\n # the published tarball already ships dist/.\n #\n # pnpm creates every workspace package's node_modules/.bin/holocron shim\n # once, during the setup action's earlier `pnpm install` — before this\n # build has run, so every one of them is silently skipped (\"Failed to\n # create bin ... ENOENT\"), not just @theholocron/cli's own. That's a\n # one-time decision: a later build doesn't retroactively fix a shim that\n # was never created. Re-running `pnpm install` here (fast — the lockfile\n # hasn't changed, so it only re-links `.bin`) is what actually fixes it,\n # confirmed against a real cold install: `holocron` stays unresolvable\n # from inside any package until this second install runs.\n run: |\n if [ ! -f node_modules/@theholocron/cli/dist/cli.mjs ]; then\n echo \"→ @theholocron/cli dist/ missing — building the workspace from source\" >&2\n pnpm build\n echo \"→ re-linking node_modules/.bin — pnpm skipped it pre-build\" >&2\n pnpm install --frozen-lockfile\n fi\n\n - name: holocron ${{ inputs.command }} ${{ inputs.task }}\n shell: bash\n env:\n HOLOCRON_COMMAND: ${{ inputs.command }}\n HOLOCRON_TASK: ${{ inputs.task }}\n HOLOCRON_JOB: ${{ inputs.job }}\n HOLOCRON_ARGS: ${{ inputs.args }}\n run: |\n if [ \"$HOLOCRON_COMMAND\" = \"run\" ]; then\n set -- run \"$HOLOCRON_TASK\"\n [ -n \"$HOLOCRON_JOB\" ] && set -- \"$@\" \"$HOLOCRON_JOB\"\n # shellcheck disable=SC2086\n [ -n \"$HOLOCRON_ARGS\" ] && set -- \"$@\" -- $HOLOCRON_ARGS\n else\n # shellcheck disable=SC2086\n set -- \"$HOLOCRON_COMMAND\" $HOLOCRON_ARGS\n fi\n # Invoke the built entry directly. `pnpm exec holocron` relies on a\n # node_modules/.bin/holocron shim that pnpm does not create for a\n # workspace package whose dist/ was absent at install time (the holocron\n # repo itself). Resolving the package's main export gives dist/index.mjs;\n # the bin sits next to it as dist/cli.mjs. This is `require.resolve` from\n # node_modules — it works in the holocron monorepo AND in every consumer\n # repo (theholocron/holocron#655), unlike a hard-coded `packages/cli/…`.\n cli=\"$(dirname \"$(node -e 'process.stdout.write(require.resolve(\"@theholocron/cli\"))')\")/cli.mjs\"\n if [ ! -f \"$cli\" ]; then\n echo \"::error::@theholocron/cli entry not found at $cli — is the workspace built?\" >&2\n exit 1\n fi\n echo \"→ $cli $*\" >&2\n node \"$cli\" \"$@\"\n";
|
|
1300
|
-
//#endregion
|
|
1301
|
-
//#region src/templates/reusable/actions/install.yml
|
|
1302
|
-
var install_default = "name: Install dependencies\ndescription: Install project dependencies with pnpm frozen lockfile.\n\nruns:\n using: composite\n\n steps:\n - name: Install dependencies\n if: ${{ hashFiles('pnpm-lock.yaml') != '' }}\n shell: bash\n run: pnpm install --frozen-lockfile\n";
|
|
1303
|
-
//#endregion
|
|
1304
|
-
//#region src/templates/reusable/actions/setup.yml
|
|
1305
|
-
var setup_default = "name: Setup\ndescription: Prepare the environment and install project dependencies.\n\ninputs:\n node-version:\n description: Node.js version\n required: false\n default: \"22.x\"\n\nruns:\n using: composite\n\n steps:\n - uses: theholocron/.github/.github/actions/setup-node@main\n with:\n node-version: ${{ inputs.node-version }}\n\n - uses: theholocron/.github/.github/actions/install@main\n\n # `@theholocron/cli`'s `dist/cli.mjs` doesn't exist yet at this point, so\n # pnpm's workspace bin-linking can't create `node_modules/.bin/holocron`\n # (silent `WARN ... ENOENT`, not a hard failure). Every package whose\n # scripts delegate via `holocron run <task> --` then fails at run time\n # with `holocron: not found` (#698). Same fix the `holocron` composite\n # action already applies for its own direct invocation (see its \"Build\n # the workspace when the CLI resolves to an unbuilt checkout\" step) —\n # mirrored here so it also covers *nested* `holocron run <task> --`\n # calls from inside package.json scripts, which that action's own guard\n # doesn't reach. `pnpm build` (not a scoped `--filter`) so the CLI's own\n # workspace dependencies (e.g. rollup-plugin-transform-template) build\n # in topological order too — a scoped build skips them and fails.\n # No-op in every consumer repo, where the published tarball already\n # ships `dist/`.\n - name: Build the Holocron CLI\n shell: bash\n run: |\n if [ ! -f node_modules/@theholocron/cli/dist/cli.mjs ]; then\n echo \"→ @theholocron/cli dist/ missing — building the workspace from source\" >&2\n pnpm build\n echo \"→ re-linking node_modules/.bin — pnpm skipped it pre-build\" >&2\n pnpm install --frozen-lockfile\n fi\n\n - name: Cache Turbo artifacts\n if: ${{ hashFiles('turbo.json') != '' }}\n uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0\n with:\n path: .turbo\n key: turbo-${{ runner.os }}-${{ github.sha }}\n restore-keys: |\n turbo-${{ runner.os }}-\n";
|
|
1306
|
-
//#endregion
|
|
1307
|
-
//#region src/templates/reusable/actions/setup-node.yml
|
|
1308
|
-
var setup_node_default = "name: Setup Node\ndescription: Install pnpm and Node.js with pnpm dependency caching.\n\ninputs:\n node-version:\n description: Node.js version\n required: false\n default: \"22.x\"\nruns:\n using: composite\n\n steps:\n - name: Setup pnpm\n if: ${{ hashFiles('pnpm-lock.yaml') != '' }}\n uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4\n\n - name: Setup Node.js\n uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0\n with:\n node-version: ${{ hashFiles('.node-version') != '' && '' || inputs.node-version }}\n node-version-file: ${{ hashFiles('.node-version') != '' && '.node-version' || '' }}\n cache: ${{ hashFiles('pnpm-lock.yaml') != '' && 'pnpm' || '' }}\n # Do NOT add registry-url here. setup-node writes .npmrc with\n # _authToken=${NODE_AUTH_TOKEN} and sets NPM_CONFIG_USERCONFIG to it.\n # pnpm reads that file, fails env-var substitution when the token is\n # empty, and loses auth entirely. Without registry-url, pnpm 10.15+\n # handles Trusted Publishing via its own native OIDC exchange.\n\n - name: Add node_modules/.bin to PATH\n shell: bash\n run: echo \"$GITHUB_WORKSPACE/node_modules/.bin\" >> $GITHUB_PATH\n";
|
|
1309
|
-
//#endregion
|
|
1310
|
-
//#region src/reusable.ts
|
|
1612
|
+
const mkRunner = (tool, args = []) => ({
|
|
1613
|
+
tool,
|
|
1614
|
+
args
|
|
1615
|
+
});
|
|
1311
1616
|
/**
|
|
1312
|
-
*
|
|
1313
|
-
*
|
|
1314
|
-
*
|
|
1617
|
+
* Resolve `{ tool, args }` for a runner, applying `detect[]` against repo
|
|
1618
|
+
* files, falling back (#750) to each candidate's `tool` name against
|
|
1619
|
+
* `package.json`'s own declared dependencies when neither matches by
|
|
1620
|
+
* filename. Only called for `tool` / `detect` runners — the caller handles
|
|
1621
|
+
* `command` runners itself, so `local.detect` is present whenever
|
|
1622
|
+
* `local.tool` is not.
|
|
1315
1623
|
*
|
|
1316
|
-
*
|
|
1317
|
-
*
|
|
1318
|
-
*
|
|
1319
|
-
*
|
|
1624
|
+
* The dependency fallback matters once a Bucket A migration (#680) deletes
|
|
1625
|
+
* a build tool's own config file (`tsdown.config.ts`, `vite.config.ts`, …):
|
|
1626
|
+
* that file was the *only* signal `detect[]` had to offer, so removing it
|
|
1627
|
+
* used to make `delivery.build` report "no task for this repo" and silently
|
|
1628
|
+
* skip the build, even though the tool is still very much in use — a
|
|
1629
|
+
* package can't actually build with `tsdown` without it in
|
|
1630
|
+
* `dependencies`/`devDependencies`, so that's a signal at least as reliable
|
|
1631
|
+
* as the config file, and one deleting the file doesn't remove.
|
|
1320
1632
|
*
|
|
1321
|
-
*
|
|
1322
|
-
*
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
"security.secretDetection": "name: Secret Detection\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n secrets:\n TURBO_TOKEN:\n required: false\n\njobs:\n secret-detection:\n name: Run gitleaks\n permissions:\n contents: read\n runs-on: ubuntu-latest\n timeout-minutes: 15\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n with:\n fetch-depth: 0\n\n # gitleaks has no npm distribution — install the prebuilt binary via\n # the maintainer's own documented install script.\n # NOTE: unverified against a live CI run at authoring time (D12) — flag\n # here if this needs adjustment.\n - name: Install gitleaks\n run: |\n curl -sSfL https://raw.githubusercontent.com/gitleaks/gitleaks/master/install.sh | sh -s -- -b /usr/local/bin\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - uses: theholocron/.github/.github/actions/holocron@main\n name: Run gitleaks\n with:\n task: security.secretDetection\n",
|
|
1334
|
-
"security.codeScanning": "name: Security\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n language:\n description: CodeQL language to analyze\n type: string\n required: false\n default: javascript-typescript\n\njobs:\n analyze:\n name: Analyze (${{ inputs.language }})\n permissions:\n actions: read\n contents: read\n security-events: write\n runs-on: ubuntu-latest\n timeout-minutes: 45\n # Do not cancel in-progress security scans.\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0\n name: Initialize CodeQL\n with:\n languages: ${{ inputs.language }}\n\n - uses: github/codeql-action/autobuild@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0\n name: Autobuild\n\n - uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0\n name: Analyze\n with:\n category: /language:${{ inputs.language }}\n",
|
|
1335
|
-
"delivery.build": "name: Build\n\n# Sanity check that every workspace actually compiles to dist/ — doesn't\n# upload or publish anything. delivery.publish builds again before\n# publishing; this just answers \"does it build?\" on every push/PR.\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n secrets:\n TURBO_TOKEN:\n required: false\n\njobs:\n build:\n name: Build every workspace\n permissions:\n contents: read\n runs-on: ubuntu-latest\n timeout-minutes: 10\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - uses: theholocron/.github/.github/actions/holocron@main\n name: Build\n with:\n task: delivery.build\n",
|
|
1336
|
-
"delivery.publish": "name: Release\n\n# Semantic-release with OIDC Trusted Publishing.\n# actions/setup-node writes a default NODE_AUTH_TOKEN=${{ github.token }}\n# which shadows OIDC auth. We explicitly clear it so npm falls through to\n# the Trusted Publisher OIDC exchange.\n# The calling repo must have a .releaserc.json that configures branches,\n# plugins, and any publish options. npm@11+ is installed to support OIDC.\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n dry-run:\n description: >\n When true, runs semantic-release --dry-run: analyzes commits and\n previews the next release without git writes or publish. Only\n meaningful for workflow_dispatch triggers; push-triggered runs\n always run fully.\n type: boolean\n required: false\n default: false\n post-release:\n description: >\n When true, dispatches a readme sync broadcast after a successful\n release. Enable for packages consumed by other repos via the\n registry-doc system so downstream README installation blocks\n stay current.\n type: boolean\n required: false\n default: false\n run-build:\n description: Run `pnpm build` before releasing\n type: boolean\n required: false\n default: true\n sentry-project:\n description: >\n Sentry project slug for sourcemap upload and release creation after\n publishing. Omit to skip the Sentry release step entirely.\n type: string\n required: false\n default: \"\"\n secrets:\n HOLOCRON_RELEASE_TOKEN:\n description: >\n Fine-grained PAT (Contents + Issues + Pull requests: write) owned by\n an admin. Required when the default branch is protected by a ruleset —\n github.token cannot push through rulesets, but an admin PAT can.\n Takes priority over HOLOCRON_SYNC_TOKEN. Falls back to github.token.\n required: false\n HOLOCRON_SYNC_TOKEN:\n description: >\n Legacy alias for HOLOCRON_RELEASE_TOKEN — kept for backward compatibility.\n Prefer HOLOCRON_RELEASE_TOKEN for new repos.\n required: false\n HOLOCRON_READ_TOKEN:\n description: >\n Fine-grained PAT for read-only GitHub API calls (e.g. resolving git\n committer identity via `gh api user`). Falls back to github.token.\n required: false\n GH_TOKEN:\n description: >\n Generic GitHub token fallback for `gh` CLI calls. Used when\n HOLOCRON_READ_TOKEN is not set.\n required: false\n SENTRY_AUTH_TOKEN:\n description: >\n Sentry auth token for sourcemap upload and release creation.\n Required when sentry-project is set. Use the org-level secret.\n required: false\n TURBO_TOKEN:\n required: false\n\njobs:\n release:\n name: Semantic release\n permissions:\n contents: write\n id-token: write\n issues: write\n pull-requests: write\n runs-on: ubuntu-latest\n timeout-minutes: 30\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n # Do not cancel in-progress releases — a partial release is worse than a slow one.\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n with:\n fetch-depth: 0\n persist-credentials: false\n # Use HOLOCRON_RELEASE_TOKEN when available — git push (tags, release commits)\n # uses the checkout credential, not GITHUB_TOKEN env var. The\n # built-in github.token cannot push through branch protection rulesets.\n token: ${{ secrets.HOLOCRON_RELEASE_TOKEN || secrets.HOLOCRON_SYNC_TOKEN || github.token }}\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - name: Configure git identity\n run: |\n GIT_NAME=$(gh api user --jq .name 2>/dev/null || echo \"github-actions[bot]\")\n GIT_EMAIL=$(gh api user --jq '\"\\(.id)+\\(.login)@users.noreply.github.com\"' 2>/dev/null || echo \"41898282+github-actions[bot]@users.noreply.github.com\")\n git config --global user.name \"$GIT_NAME\"\n git config --global user.email \"$GIT_EMAIL\"\n git config --global format.signoff true\n env:\n GH_TOKEN: ${{ secrets.HOLOCRON_READ_TOKEN || secrets.GH_TOKEN || github.token }}\n\n - name: Upgrade npm for OIDC support\n run: npm install -g npm@11 sigstore\n # sigstore is required by libnpmpublish/provenance.js at module parse\n # time — before any config takes effect. Some npm 11.x builds stopped\n # bundling it; installing it globally into the same prefix ensures it\n # resolves regardless of npm version. (Discovered 2026-07-09.)\n\n - run: pnpm build\n name: Build\n if: ${{ inputs.run-build == true }}\n\n - name: Release\n id: release\n run: |\n BEFORE=$(git rev-parse HEAD)\n if [ \"$DRY_RUN\" = \"true\" ]; then\n npx semantic-release --dry-run\n else\n npx semantic-release\n fi\n AFTER=$(git rev-parse HEAD)\n # semantic-release only commits (`chore(release): … [skip ci]`) when it\n # actually publishes. That commit gets no CI (and so no coverage\n # upload), which makes the next PR's `codecov/project` compare a full\n # report against a stale ancestor — a phantom drop (holocron#644).\n # Emit the new SHA so the steps below upload coverage for it.\n if [ \"$BEFORE\" != \"$AFTER\" ]; then echo \"commit=$AFTER\" >> \"$GITHUB_OUTPUT\"; fi\n env:\n DRY_RUN: ${{ inputs.dry-run }}\n # Prefer HOLOCRON_RELEASE_TOKEN (fine-grained PAT, Contents+Issues+PRs write,\n # owned by an admin with ruleset bypass) so @semantic-release/git can\n # push the version-bump commit through branch protection. Falls back to\n # HOLOCRON_SYNC_TOKEN (legacy) then github.token for unprotected repos.\n GITHUB_TOKEN: ${{ secrets.HOLOCRON_RELEASE_TOKEN || secrets.HOLOCRON_SYNC_TOKEN || github.token }}\n HUSKY: \"0\"\n NPM_CONFIG_PROVENANCE: true\n\n - uses: theholocron/.github/.github/actions/holocron@main\n name: Coverage for the release commit\n if: ${{ steps.release.outputs.commit != '' }}\n with:\n task: verification.unitTests\n\n - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0\n name: Upload release-commit coverage to Codecov\n if: ${{ steps.release.outputs.commit != '' }}\n with:\n use_oidc: true\n override_commit: ${{ steps.release.outputs.commit }}\n override_branch: ${{ github.ref_name }}\n\n - name: Get release version\n id: release_version\n if: ${{ inputs.sentry-project != '' && inputs.dry-run != true }}\n env:\n SENTRY_PROJECT: ${{ inputs.sentry-project }}\n run: |\n TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo \"\")\n if [ -n \"$TAG\" ]; then\n echo \"release=${SENTRY_PROJECT}@${TAG#v}\" >> \"$GITHUB_OUTPUT\"\n fi\n\n - name: Create Sentry release\n if: ${{ inputs.sentry-project != '' && steps.release_version.outputs.release != '' && inputs.dry-run != true }}\n uses: getsentry/action-release@ff07929a6537bac57790c3451cf4d364aca38528 # v3\n env:\n SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}\n SENTRY_ORG: theholocron\n SENTRY_PROJECT: ${{ inputs.sentry-project }}\n with:\n environment: production\n version: ${{ steps.release_version.outputs.release }}\n sourcemaps: \"**/dist\"\n\n sync-readme:\n name: Post-release readme sync\n needs: release\n if: ${{ inputs.post-release == true && inputs.dry-run != true }}\n runs-on: ubuntu-latest\n timeout-minutes: 5\n steps:\n - name: Trigger broadcast readme sync\n run: |\n gh workflow run sync-dispatch.yml \\\n --repo theholocron/.github \\\n --field \"steps=readme\"\n env:\n GH_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN }}\n",
|
|
1337
|
-
"delivery.deploy": "name: Deploy\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n type:\n description: \"Type of deployment: docs or storybook\"\n required: true\n type: string\n storybook-projects:\n description: >\n JSON array of { \"name\"?, \"workingDir\", \"outputDir\"? } objects for storybook deploys.\n Each is built via `pnpm -C <workingDir> build:storybook`. If \"name\" is provided the\n output is placed under `sandbox/<name>/`; omit \"name\" for single-repo deploys and the\n output lands directly in `sandbox/`.\n type: string\n required: false\n default: \"[]\"\n build-script:\n description: pnpm script that builds the Storybook static output (single storybook, type:storybook only)\n type: string\n required: false\n default: build:storybook\n output-dir:\n description: Directory where Storybook writes its static output (single storybook, type:storybook only)\n type: string\n required: false\n default: storybook-static\n\njobs:\n deploy:\n name: Deploy\n runs-on: ubuntu-latest\n permissions:\n contents: read\n pages: write\n id-token: write\n environment:\n name: github-pages\n url: ${{ steps.deployment.outputs.page_url }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - name: Build docs site\n if: ${{ inputs.type == 'docs' }}\n run: pnpm -C docs build\n\n - name: Build Storybook projects\n if: ${{ inputs.storybook-projects != '[]' }}\n env:\n PROJECTS: ${{ inputs.storybook-projects }}\n run: |\n echo \"$PROJECTS\" | jq -c '.[]' | while IFS= read -r project; do\n workingDir=$(echo \"$project\" | jq -r '.workingDir')\n pnpm -C \"$workingDir\" build:storybook\n done\n\n - name: Build Storybook\n if: ${{ inputs.type == 'storybook' && inputs.storybook-projects == '[]' }}\n env:\n BUILD_SCRIPT: ${{ inputs.build-script }}\n run: pnpm run \"$BUILD_SCRIPT\"\n\n - name: Assemble site\n env:\n DEPLOY_TYPE: ${{ inputs.type }}\n PROJECTS: ${{ inputs.storybook-projects }}\n STORYBOOK_OUTPUT_DIR: ${{ inputs.output-dir }}\n run: |\n mkdir -p _site\n if [ \"$DEPLOY_TYPE\" = \"docs\" ]; then\n cp -r docs/dist/. _site/\n fi\n if [ \"$PROJECTS\" != \"[]\" ]; then\n echo \"$PROJECTS\" | jq -c '.[]' | while IFS= read -r project; do\n name=$(echo \"$project\" | jq -r '.name // \"\"')\n workingDir=$(echo \"$project\" | jq -r '.workingDir')\n outputDir=$(echo \"$project\" | jq -r '.outputDir // \"storybook-static\"')\n if [ -n \"$name\" ]; then\n target=\"_site/sandbox/${name}\"\n else\n target=\"_site/sandbox\"\n fi\n mkdir -p \"$target\"\n cp -r \"${workingDir}/${outputDir}/.\" \"$target/\"\n done\n elif [ \"$DEPLOY_TYPE\" = \"storybook\" ]; then\n mkdir -p _site/sandbox\n cp -r \"${STORYBOOK_OUTPUT_DIR}/.\" _site/sandbox/\n fi\n\n - uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3.0.1\n name: Upload pages artifact\n with:\n path: _site\n\n - uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5\n id: deployment\n name: Deploy to GitHub Pages\n",
|
|
1338
|
-
"delivery.bundleSize": "name: Audit the Bundle Size\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n secrets:\n CODECOV_TOKEN:\n required: false\n TURBO_TOKEN:\n required: false\n\njobs:\n bundle-size:\n name: Upload bundle stats to Codecov\n permissions:\n contents: read\n runs-on: ubuntu-latest\n timeout-minutes: 15\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n # Read by the bundle-stats uploader inside `holocron run delivery.build`.\n CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - uses: theholocron/.github/.github/actions/holocron@main\n name: Build and upload bundle stats\n with:\n task: delivery.build\n",
|
|
1339
|
-
"platform.repoSync": "name: Sync\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n steps:\n description: >\n Sync steps to run (default: all). Valid values:\n labels, properties, teams, topics, keywords, description, homepage, readme, workflows, wiki.\n Pass a space-separated list to run a subset, e.g. \"readme\" or \"readme wiki\".\n type: string\n required: false\n secrets:\n HOLOCRON_ADMIN_TOKEN:\n description: Fine-grained PAT with admin scopes (labels, properties, teams).\n required: false\n HOLOCRON_AXIOM_TOKEN:\n description: >\n Axiom API token. When set alongside the HOLOCRON_AXIOM_DATASET\n repo/org variable, the CLI ships this run's structured logs to Axiom.\n Falls back to the vendor-native AXIOM_TOKEN secret.\n required: false\n AXIOM_TOKEN:\n description: Vendor-native fallback for HOLOCRON_AXIOM_TOKEN.\n required: false\n HOLOCRON_DEPLOY_TOKEN:\n description: Fine-grained PAT for GitHub Pages configuration.\n required: false\n HOLOCRON_ISSUES_TOKEN:\n description: Fine-grained PAT for issue management.\n required: false\n HOLOCRON_ORG_TOKEN:\n description: Org-scoped fine-grained PAT for team sync and org properties.\n required: false\n HOLOCRON_READ_TOKEN:\n description: Fine-grained PAT for read-only GitHub API calls.\n required: false\n HOLOCRON_SYNC_TOKEN:\n required: false\n GH_TOKEN:\n description: >\n Generic GitHub token fallback for gh CLI calls. Used when\n HOLOCRON_SYNC_TOKEN is not set.\n required: false\n\njobs:\n sync:\n name: Run holocron sync\n runs-on: ubuntu-latest\n timeout-minutes: 10\n permissions:\n contents: write\n pull-requests: write\n # Job-level so the `holocron` composite action's steps inherit them.\n env:\n HOLOCRON_ADMIN_TOKEN: ${{ secrets.HOLOCRON_ADMIN_TOKEN }}\n HOLOCRON_AXIOM_TOKEN: ${{ secrets.HOLOCRON_AXIOM_TOKEN || secrets.AXIOM_TOKEN }}\n HOLOCRON_AXIOM_DATASET: ${{ vars.HOLOCRON_AXIOM_DATASET || vars.AXIOM_DATASET }}\n POSTHOG_PROJECT_TOKEN: ${{ vars.POSTHOG_PROJECT_TOKEN }}\n HOLOCRON_DEPLOY_TOKEN: ${{ secrets.HOLOCRON_DEPLOY_TOKEN }}\n HOLOCRON_ISSUES_TOKEN: ${{ secrets.HOLOCRON_ISSUES_TOKEN }}\n HOLOCRON_ORG_TOKEN: ${{ secrets.HOLOCRON_ORG_TOKEN }}\n HOLOCRON_READ_TOKEN: ${{ secrets.HOLOCRON_READ_TOKEN }}\n HOLOCRON_SYNC_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n with:\n token: ${{ secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN || github.token }}\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - name: Run holocron sync\n # The `holocron` action resolves @theholocron/cli from node_modules (or\n # builds the workspace when it's an unbuilt checkout — the holocron repo\n # itself). `inputs.steps` is a space-separated list or empty; the\n # `--steps <list>` arg is omitted when empty.\n uses: theholocron/.github/.github/actions/holocron@main\n with:\n command: sync\n args: ${{ inputs.steps != '' && format('--steps {0}', inputs.steps) || '' }}\n\n - name: Format generated files\n run: |\n pnpm exec prettier --write README.md docs/src/content/docs/index.mdx 2>/dev/null || true\n # `holocron sync` writes package.json fields (keywords, description,\n # homepage) with a plain assignment, which appends new keys at the\n # end — re-apply the canonical order the lint config enforces.\n pnpm exec eslint --fix --no-warn-ignored package.json 2>/dev/null || true\n\n - uses: theholocron/.github/.github/actions/auto-commit@main\n id: auto-commit\n name: Commit sync changes\n with:\n token: ${{ secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN || github.token }}\n branch: chore/auto-sync\n commit-message: \"chore: sync from holocron.config\"\n commit-options: \"--no-verify\"\n\n - name: Open PR if changes were committed\n if: steps.auto-commit.outputs.changes-detected == 'true'\n run: |\n gh pr create \\\n --title \"chore: sync README and repo metadata\" \\\n --body \"Automated sync triggered by changes to config or package files. Merge to apply.\" \\\n --base main \\\n --head chore/auto-sync \\\n || echo \"PR already open — branch updated.\"\n env:\n GH_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN || github.token }}\n\n - name: Broadcast wiki sync if navbar changed\n if: >-\n steps.auto-commit.outputs.changes-detected == 'true' &&\n github.event_name == 'push' &&\n (inputs.steps == '' || contains(inputs.steps, 'wiki'))\n run: |\n if git diff HEAD~1 --name-only | grep -q 'fern/docs.yml'; then\n gh workflow run sync-dispatch.yml \\\n --repo theholocron/.github \\\n --field \"steps=wiki\" \\\n || echo \"skipping broadcast — insufficient permissions\"\n fi\n env:\n GH_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN || github.token }}\n",
|
|
1340
|
-
"platform.commitStandards": "name: Commit Standards\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n secrets:\n TURBO_TOKEN:\n required: false\n\njobs:\n commit-standards:\n name: Run commitlint\n permissions:\n contents: read\n runs-on: ubuntu-latest\n timeout-minutes: 10\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n with:\n fetch-depth: 0\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n # No local equivalent (linters.ts: commitlint has no localBin — enforced\n # only at commit time via the commit-msg hook, and here in CI over the\n # PR's commit range). Not run through the holocron composite action.\n #\n # --config <resolved shared path> (config-resolution workstream, #676)\n # — falls back to commitlint's own auto-discovery of a local\n # commitlint.config.ts when the shared package's built dist/ isn't\n # present, same guard as the commit-msg hook.\n - name: Run commitlint over the PR range\n if: github.event_name == 'pull_request'\n run: |\n COMMITLINT_CONFIG=\"node_modules/@theholocron/commitlint-config/dist/index.js\"\n if [ -f \"$COMMITLINT_CONFIG\" ]; then\n pnpm exec commitlint --config \"$COMMITLINT_CONFIG\" --from \"${{ github.event.pull_request.base.sha }}\" --to \"${{ github.sha }}\"\n else\n pnpm exec commitlint --from \"${{ github.event.pull_request.base.sha }}\" --to \"${{ github.sha }}\"\n fi\n",
|
|
1341
|
-
"platform.repoValidation": "name: Repo Validation\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n secrets:\n TURBO_TOKEN:\n required: false\n\njobs:\n adrs:\n name: Validate ADRs and specs\n permissions:\n contents: read\n runs-on: ubuntu-latest\n timeout-minutes: 10\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n with:\n fetch-depth: 0\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - uses: theholocron/.github/.github/actions/holocron@main\n name: Validate ADR and spec frontmatter\n with:\n task: platform.repoValidation\n job: adrs\n\n registry:\n name: Validate registry consistency\n permissions:\n contents: read\n runs-on: ubuntu-latest\n timeout-minutes: 10\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - uses: theholocron/.github/.github/actions/holocron@main\n name: Validate registry consistency\n with:\n task: platform.repoValidation\n job: registry\n\n docs-presence:\n name: Validate docs presence\n permissions:\n contents: read\n runs-on: ubuntu-latest\n timeout-minutes: 10\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n with:\n fetch-depth: 0\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - uses: theholocron/.github/.github/actions/holocron@main\n name: Validate docs presence for new packages\n with:\n task: platform.repoValidation\n job: docsPresence\n env:\n BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}\n\n conclusion:\n name: Conclusion\n runs-on: ubuntu-latest\n if: always()\n needs: [adrs, registry, docs-presence]\n steps:\n - name: Check job statuses\n run: |\n if [[ \"$RESULTS\" == *\"failure\"* ]] || [[ \"$RESULTS\" == *\"cancelled\"* ]]; then\n exit 1\n fi\n env:\n RESULTS: ${{ join(needs.*.result, ',') }}\n",
|
|
1342
|
-
"knowledge.wiki": "name: Wiki\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n fern-version:\n description: >\n Fern CLI version to install. Pin this to avoid breaking changes when\n Fern updates their config schema.\n type: string\n required: false\n default: \"5.114.1\"\n preview:\n description: >\n When true, publishes a preview instead of production.\n Requires preview-id to be set.\n type: boolean\n required: false\n default: false\n preview-id:\n description: >\n Stable ID for the preview build. Use the PR number (e.g. \"pr-123\")\n so the same preview URL is reused on every push to the branch.\n type: string\n required: false\n default: \"\"\n fern-org:\n description: >\n DEPRECATED — the preview URL is now read from Fern's own output.\n Kept as a fallback for building the URL when that parse fails, and so\n existing callers do not error.\n type: string\n required: false\n default: \"\"\n base-path:\n description: >\n DEPRECATED — see fern-org. Basepath appended to the fallback preview\n URL (e.g. \"holocron\" for multi-source routing).\n type: string\n required: false\n default: \"\"\n secrets:\n HOLOCRON_FERN_TOKEN:\n required: false\n FERN_TOKEN:\n required: false\n\njobs:\n publish:\n name: Publish to Fern\n runs-on: ubuntu-latest\n timeout-minutes: 10\n permissions:\n contents: read\n deployments: write\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - name: Install Fern CLI\n run: npm install -g \"fern-api@$FERN_VERSION\"\n env:\n FERN_VERSION: ${{ inputs.fern-version }}\n\n - name: Publish docs\n if: ${{ !inputs.preview }}\n run: fern generate --docs\n env:\n FERN_TOKEN: ${{ secrets.HOLOCRON_FERN_TOKEN || secrets.FERN_TOKEN }}\n\n - name: Preview docs\n id: preview\n if: ${{ inputs.preview && inputs.preview-id != '' }}\n env:\n FERN_TOKEN: ${{ secrets.HOLOCRON_FERN_TOKEN || secrets.FERN_TOKEN }}\n PREVIEW_ID: ${{ inputs.preview-id }}\n run: |\n out=$(fern generate --docs --preview --id \"$PREVIEW_ID\" 2>&1) || { printf '%s\\n' \"$out\"; exit 1; }\n printf '%s\\n' \"$out\"\n # Fern prints \"Published docs to https://<org>-preview-<id>.docs.buildwithfern.com/<path>\"\n url=$(printf '%s\\n' \"$out\" | grep -oiE 'https://[a-z0-9.-]+\\.docs\\.buildwithfern\\.com[^ )]*' | head -n1)\n echo \"url=$url\" >> \"$GITHUB_OUTPUT\"\n\n - name: Report preview URL\n if: ${{ inputs.preview && inputs.preview-id != '' }}\n env:\n GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n CAPTURED_URL: ${{ steps.preview.outputs.url }}\n FERN_ORG: ${{ inputs.fern-org }}\n PREVIEW_ID: ${{ inputs.preview-id }}\n BASE_PATH: ${{ inputs.base-path }}\n run: |\n PREVIEW_URL=\"$CAPTURED_URL\"\n if [ -z \"$PREVIEW_URL\" ] && [ -n \"$FERN_ORG\" ]; then\n PREVIEW_URL=\"https://${FERN_ORG}-preview-${PREVIEW_ID}.docs.buildwithfern.com${BASE_PATH:+/${BASE_PATH}}\"\n fi\n if [ -z \"$PREVIEW_URL\" ]; then\n echo \"::warning::no preview URL parsed from Fern output and no fern-org fallback — skipping the deployment widget\"\n exit 0\n fi\n PAYLOAD=$(printf '{\"ref\":\"%s\",\"environment\":\"wiki (Preview)\",\"description\":\"Wiki\",\"production_environment\":false,\"auto_merge\":false,\"required_contexts\":[]}' \\\n \"$GITHUB_HEAD_REF\")\n DEPLOY_ID=$(printf '%s\\n' \"$PAYLOAD\" | gh api \"repos/${GITHUB_REPOSITORY}/deployments\" \\\n --method POST --input - | jq -r '.id')\n gh api \"repos/${GITHUB_REPOSITORY}/deployments/${DEPLOY_ID}/statuses\" \\\n --method POST --field state=success --field environment_url=\"$PREVIEW_URL\"\n",
|
|
1343
|
-
bookkeeping: "name: Bookkeeping\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n configuration-path:\n description: Path to the labeler configuration file in the calling repo\n type: string\n required: false\n default: .github/labeler.yml\n\njobs:\n label:\n name: Apply Labels\n permissions:\n contents: read\n issues: write\n pull-requests: write\n runs-on: ubuntu-latest\n timeout-minutes: 5\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n with:\n sparse-checkout: ${{ inputs.configuration-path || '.github/labeler.yml' }}\n sparse-checkout-cone-mode: false\n\n - uses: github/issue-labeler@c1b0f9f52a63158c4adc09425e858e87b32e9685 # v3.4\n if: ${{ github.event_name == 'pull_request' && hashFiles(inputs.configuration-path || '.github/labeler.yml') != '' }}\n # v3.4 bundles Node 20; allow it to run under Actions' current default.\n env:\n ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true\n with:\n # Fall back to default path when triggered directly (not via workflow_call)\n # because inputs.* defaults only apply on workflow_call events.\n configuration-path: ${{ inputs.configuration-path || '.github/labeler.yml' }}\n include-title: 1\n include-body: 0\n sync-labels: 1\n enable-versioned-regex: 0\n repo-token: ${{ github.token }}\n",
|
|
1344
|
-
dependencies: "name: Dependencies\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n secrets:\n merge-token:\n description: >\n Optional privileged token for auto-merge. Falls back to GITHUB_TOKEN.\n Required when branch protection enforces required reviews — GITHUB_TOKEN\n cannot approve its own PRs.\n required: false\n\njobs:\n dependabot:\n name: Update the dependencies\n permissions:\n contents: write\n pull-requests: write\n runs-on: ubuntu-latest\n timeout-minutes: 5\n if: github.event.pull_request.user.login == 'dependabot[bot]'\n steps:\n - uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3.1.0\n name: Fetch Dependabot metadata\n id: metadata\n\n - run: gh pr merge --auto --squash \"$PR_URL\"\n # --squash is intentional: repo protection sets allow_merge_commit: false,\n # so --merge would fail on any repo using the standard preset.\n name: Enable auto-merge for Dependabot PRs\n if: steps.metadata.outputs.update-type == 'version-update:semver-patch'\n env:\n PR_URL: ${{ github.event.pull_request.html_url }}\n GH_TOKEN: ${{ secrets.merge-token || github.token }}\n",
|
|
1345
|
-
preview: "name: Preview\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n type:\n description: \"Type of deployment: docs or storybook\"\n required: true\n type: string\n name:\n description: Repo name prefix used to filter the docs site package (<name>-site); omit to run pnpm -C docs build\n required: false\n type: string\n default: \"\"\n storybook-projects:\n description: >\n JSON array of { \"name\"?, \"workingDir\", \"outputDir\"? } objects for storybook deploys.\n Each is built via `pnpm -C <workingDir> build:storybook`. If \"name\" is provided the\n output is placed under `sandbox/<name>/`; omit \"name\" for single-repo deploys and the\n output lands directly in `sandbox/`.\n type: string\n required: false\n default: \"[]\"\n build-script:\n description: pnpm script that builds the Storybook static output (single storybook, type:storybook only)\n type: string\n required: false\n default: build:storybook\n output-dir:\n description: Directory where Storybook writes its static output (single storybook, type:storybook only)\n type: string\n required: false\n default: storybook-static\n cloudflare-project:\n description: >\n Cloudflare Pages project name. Falls back to the CLOUDFLARE_PAGES_PROJECT\n org variable when omitted — set that variable once and all repos get previews\n without per-repo config.\n required: false\n type: string\n default: \"\"\n\njobs:\n deploy-preview:\n name: Preview\n if: ${{ github.event.action != 'closed' }}\n runs-on: ubuntu-latest\n permissions:\n contents: read\n deployments: write\n pull-requests: write\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - name: Build workspace dependencies\n # The docs site imports workspace packages (e.g. a shared theme) via\n # `workspace:*` — its exports map points at dist/, which doesn't\n # exist until something builds it. Nothing else in this job does,\n # so without this step Vite fails to resolve the import entirely.\n if: ${{ inputs.type == 'docs' }}\n run: pnpm run build\n\n - name: Build docs site\n if: ${{ inputs.type == 'docs' && inputs.name != '' }}\n env:\n SITE_NAME: ${{ inputs.name }}\n run: pnpm --filter @theholocron/\"$SITE_NAME\"-site build\n\n - name: Build docs site\n if: ${{ inputs.type == 'docs' && inputs.name == '' }}\n run: pnpm -C docs build\n\n - name: Build Storybook projects\n if: ${{ inputs.storybook-projects != '[]' }}\n env:\n PROJECTS: ${{ inputs.storybook-projects }}\n run: |\n echo \"$PROJECTS\" | jq -c '.[]' | while IFS= read -r project; do\n workingDir=$(echo \"$project\" | jq -r '.workingDir')\n pnpm -C \"$workingDir\" build:storybook\n done\n\n - name: Build Storybook\n if: ${{ inputs.type == 'storybook' && inputs.storybook-projects == '[]' }}\n env:\n BUILD_SCRIPT: ${{ inputs.build-script }}\n run: pnpm run \"$BUILD_SCRIPT\"\n\n - name: Assemble site\n env:\n DEPLOY_TYPE: ${{ inputs.type }}\n SITE_NAME: ${{ inputs.name }}\n PROJECTS: ${{ inputs.storybook-projects }}\n STORYBOOK_OUTPUT_DIR: ${{ inputs.output-dir }}\n run: |\n mkdir -p _site\n if [ \"$DEPLOY_TYPE\" = \"docs\" ]; then\n if [ -n \"$SITE_NAME\" ]; then\n # Site built with base: /<name>/ — nest under _site/<name>/ so asset\n # paths match (/name/_astro/... → served at /name/_astro/...).\n mkdir -p \"_site/${SITE_NAME}\"\n cp -r docs/dist/. \"_site/${SITE_NAME}/\"\n # Redirect root to the base path so the preview URL lands correctly.\n printf \"/ /%s/ 301\\n\" \"$SITE_NAME\" > _site/_redirects\n else\n cp -r docs/dist/. _site/\n fi\n fi\n if [ \"$PROJECTS\" != \"[]\" ]; then\n echo \"$PROJECTS\" | jq -c '.[]' | while IFS= read -r project; do\n name=$(echo \"$project\" | jq -r '.name // \"\"')\n workingDir=$(echo \"$project\" | jq -r '.workingDir')\n outputDir=$(echo \"$project\" | jq -r '.outputDir // \"storybook-static\"')\n if [ -n \"$name\" ]; then\n target=\"_site/sandbox/${name}\"\n else\n target=\"_site/sandbox\"\n fi\n mkdir -p \"$target\"\n cp -r \"${workingDir}/${outputDir}/.\" \"$target/\"\n done\n elif [ \"$DEPLOY_TYPE\" = \"storybook\" ]; then\n mkdir -p _site/sandbox\n cp -r \"${STORYBOOK_OUTPUT_DIR}/.\" _site/sandbox/\n fi\n\n - name: Install Wrangler\n # The action's install step runs npm/pnpm in the repo root; pnpm fails\n # because it rejects adding to a workspace root without -w, and npm fails\n # because package.json contains pnpm catalog: references it cannot parse.\n # Pre-installing from /tmp (no package.json) lets the action find wrangler\n # already on PATH and skip its own install entirely.\n working-directory: /tmp\n run: npm install -g wrangler@4\n\n - uses: cloudflare/wrangler-action@ebbaa1584979971c8614a24965b4405ff95890e0 # v4.0.0\n id: deploy\n name: Deploy to Cloudflare Pages\n # Skip when neither the per-repo input nor the org variable is set.\n if: ${{ inputs.cloudflare-project != '' || vars.CLOUDFLARE_PAGES_PROJECT != '' }}\n with:\n apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}\n accountId: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}\n packageManager: npm\n command: >-\n pages deploy _site\n --project-name ${{ inputs.cloudflare-project || vars.CLOUDFLARE_PAGES_PROJECT }}\n --branch ${{ github.event.repository.name }}-pr-${{ github.event.pull_request.number }}\n --commit-dirty=true\n\n - name: Report preview URL\n if: ${{ steps.deploy.outputs.pages-deployment-alias-url != '' }}\n env:\n GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n ALIAS: ${{ steps.deploy.outputs.pages-deployment-alias-url }}\n SITE_NAME: ${{ inputs.name }}\n PROJECT: ${{ inputs.cloudflare-project || vars.CLOUDFLARE_PAGES_PROJECT }}\n run: |\n PREVIEW_URL=\"${ALIAS%/}${SITE_NAME:+/${SITE_NAME}}/\"\n # GITHUB_HEAD_REF is the PR branch name — links the deployment to this PR's sidebar widget.\n PAYLOAD=$(printf '{\"ref\":\"%s\",\"environment\":\"%s\",\"description\":\"Cloudflare Pages\",\"production_environment\":false,\"auto_merge\":false,\"required_contexts\":[]}' \\\n \"$GITHUB_HEAD_REF\" \"${PROJECT} (Preview)\")\n DEPLOY_ID=$(echo \"$PAYLOAD\" | gh api \"repos/${GITHUB_REPOSITORY}/deployments\" \\\n --method POST --input - | jq -r '.id')\n gh api \"repos/${GITHUB_REPOSITORY}/deployments/${DEPLOY_ID}/statuses\" \\\n --method POST --field state=success --field environment_url=\"${PREVIEW_URL}\"\n\n cleanup:\n name: Clean up Preview\n if: ${{ github.event.action == 'closed' }}\n runs-on: ubuntu-latest\n permissions:\n contents: read\n deployments: write\n pull-requests: write\n steps:\n - name: Delete Cloudflare Pages deployments for branch\n if: ${{ inputs.cloudflare-project != '' || vars.CLOUDFLARE_PAGES_PROJECT != '' }}\n env:\n CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}\n CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}\n PROJECT: ${{ inputs.cloudflare-project || vars.CLOUDFLARE_PAGES_PROJECT }}\n BRANCH: ${{ github.event.repository.name }}-pr-${{ github.event.pull_request.number }}\n GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n run: |\n DEPLOYMENTS=$(curl -s \\\n \"https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/pages/projects/${PROJECT}/deployments\" \\\n -H \"Authorization: Bearer ${CLOUDFLARE_API_TOKEN}\" \\\n | jq -r --arg b \"$BRANCH\" \\\n '.result[] | select(.deployment_trigger.metadata.branch == $b) | .id')\n\n if [ -z \"$DEPLOYMENTS\" ]; then\n echo \"No deployments found for branch ${BRANCH} — nothing to clean up.\"\n exit 0\n fi\n\n for id in $DEPLOYMENTS; do\n echo \"Deleting CF Pages deployment: $id\"\n curl -s -X DELETE \\\n \"https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/pages/projects/${PROJECT}/deployments/${id}?force=true\" \\\n -H \"Authorization: Bearer ${CLOUDFLARE_API_TOKEN}\" | jq -r 'if .success then \" ✓ deleted\" else \" ✗ \\(.errors[0].message)\" end'\n done\n\n # Mark the GitHub Deployment environment as inactive.\n ENV_NAME=\"${PROJECT} (Preview)\"\n gh api \"repos/${GITHUB_REPOSITORY}/deployments\" \\\n | jq -r \".[] | select(.environment == \\\"${ENV_NAME}\\\") | .id\" \\\n | while read -r deploy_id; do\n gh api \"repos/${GITHUB_REPOSITORY}/deployments/${deploy_id}/statuses\" \\\n --method POST --field state=inactive 2>/dev/null || true\n done\n",
|
|
1346
|
-
greetings: "name: Greetings\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n\njobs:\n greeting:\n name: Greet first-time contributors\n permissions:\n issues: write\n pull-requests: write\n runs-on: ubuntu-latest\n timeout-minutes: 5\n # Group by the issue/PR number so duplicate events don't race each other.\n steps:\n - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0\n name: Greet on first contribution\n with:\n script: |\n // Only greet on the initial open — ignore synchronize, reopened, etc.\n if (context.payload.action !== 'opened') return;\n\n const actor = context.actor;\n const { owner, repo } = context.repo;\n\n // Payload inspection is more reliable than context.eventName for detecting\n // whether this is an issue vs. PR event — works regardless of how GitHub\n // propagates event names through workflow_call chains.\n const isIssue = !!context.payload.issue && !context.payload.pull_request;\n // listForRepo returns both issues and PRs (GitHub treats PRs as issues),\n // sorted newest-first. Filter by type to track first-issue and first-PR\n // independently, and avoid search-index eventual-consistency lag.\n const { data: recent } = await github.rest.issues.listForRepo({\n owner, repo,\n creator: actor,\n state: 'all',\n per_page: 100\n });\n\n const sameType = recent.filter(item =>\n isIssue ? !item.pull_request : !!item.pull_request\n );\n\n if (sameType.length !== 1) return;\n const body = isIssue\n ? `Hey @${actor}!\\n\\nWe really appreciate you taking the time to report an issue. The collaborators on this project attempt to help as many people as possible, but we are a limited number of volunteers, so it is possible that this will not be addressed as swiftly.\\n\\nYour patience is much appreciated and we will get back to you as quickly as possible.`\n : `Hey @${actor}!\\n\\nWe really appreciate you taking the time to help out with this PR. The collaborators on this project attempt to help as many people as possible, but we are a limited number of volunteers, so it is possible that this will not be addressed as swiftly.\\n\\nYour patience is much appreciated and we will get back to you as quickly as possible.`;\n\n await github.rest.issues.createComment({\n owner,\n repo,\n issue_number: context.issue.number,\n body\n });\n",
|
|
1347
|
-
review: "name: Review\n\n# ReviewDog is the annotation layer — posts inline PR diff annotations.\n# Runs on pull_request only: inline annotations require PR context,\n# and branch protection ensures all changes go through PRs anyway.\n# security.secretDetection.yml / sourceQuality.structuredDataValidation.yml\n# are the CI gates covering push + PR events. Gitleaks and YAML are\n# intentionally duplicated: those gate merges; ReviewDog surfaces exact\n# line annotations in the PR diff.\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n\njobs:\n reviewdog:\n name: Post inline lint annotations\n runs-on: ubuntu-latest\n timeout-minutes: 20\n permissions:\n contents: read\n pull-requests: write\n\n steps:\n - name: Checkout repository\n uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n with:\n fetch-depth: 0\n\n - name: Setup\n if: ${{ hashFiles('pnpm-lock.yaml') != '' }}\n uses: theholocron/.github/.github/actions/setup@main\n\n - name: Install ReviewDog\n uses: reviewdog/action-setup@d8a7baabd7f3e8544ee4dbde3ee41d0011c3a93f # v1\n with:\n reviewdog_version: latest\n\n # Detect which tools are relevant for this repo, excluding node_modules.\n # hashFiles('**/*') recurses into node_modules/.pnpm and produces false\n # positives for repos that don't own those file types.\n # -print -quit stops find after the first match without a pipe, avoiding\n # the SIGPIPE/pipefail exit-141 that find|head-1 triggers under\n # GitHub Actions' default bash --noprofile --norc -e -o pipefail mode.\n - name: Detect project features\n id: detect\n shell: bash\n run: |\n has() { find . -not -path '*/node_modules/*' -name \"$1\" -print -quit 2>/dev/null | grep -q .; }\n has_ext() { find . -not -path '*/node_modules/*' -name \"$1\" -print -quit 2>/dev/null | grep -q .; }\n { { has 'eslint.config.js' || has 'eslint.config.mjs' || has 'eslint.config.cjs' || \\\n has 'eslint.config.ts' || has '.eslintrc' || has '.eslintrc.js' || \\\n has '.eslintrc.cjs' || has '.eslintrc.json' || has '.eslintrc.yaml' || \\\n has '.eslintrc.yml'; } && grep -qF '\"eslint\":' package.json 2>/dev/null; } && echo \"eslint=true\" >> \"$GITHUB_OUTPUT\" || echo \"eslint=false\" >> \"$GITHUB_OUTPUT\"\n { has 'tsconfig.json' && grep -qF '\"typescript\":' package.json 2>/dev/null; } && echo \"tsconfig=true\" >> \"$GITHUB_OUTPUT\" || echo \"tsconfig=false\" >> \"$GITHUB_OUTPUT\"\n has_ext '*.sh' && echo \"shell=true\" >> \"$GITHUB_OUTPUT\" || echo \"shell=false\" >> \"$GITHUB_OUTPUT\"\n has 'Dockerfile' || has_ext '*.Dockerfile' || has 'Containerfile' && \\\n echo \"docker=true\" >> \"$GITHUB_OUTPUT\" || echo \"docker=false\" >> \"$GITHUB_OUTPUT\"\n has_ext '.env*' && echo \"dotenv=true\" >> \"$GITHUB_OUTPUT\" || echo \"dotenv=false\" >> \"$GITHUB_OUTPUT\"\n has_ext '*.md' && echo \"markdown=true\" >> \"$GITHUB_OUTPUT\" || echo \"markdown=false\" >> \"$GITHUB_OUTPUT\"\n\n #\n # Always applicable\n #\n\n - name: Gitleaks (secrets)\n uses: reviewdog/action-gitleaks@2b7b5685e3e3eecddab5d30cfa04f18123031421 # v1\n with:\n reporter: github-pr-annotations\n tool_name: \"Review / gitleaks\"\n fail_level: error\n gitleaks_flags: --log-opts=${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}\n\n - name: YamlLint\n if: ${{ hashFiles('.yamllint.yml') != '' }}\n uses: reviewdog/action-yamllint@b5f7217d8c815ae374d1d55840d5e569d82f01f0 # v1\n with:\n reporter: github-pr-annotations\n tool_name: \"Review / yamllint\"\n fail_level: error\n yamllint_flags: -c ${{ github.workspace }}/.yamllint.yml ${{ github.workspace }}\n\n - name: ActionLint (GitHub Actions)\n if: ${{ hashFiles('.github/workflows/*.yml', '.github/workflows/*.yaml') != '' }}\n uses: reviewdog/action-actionlint@6fb7acc99f4a1008869fa8a0f09cfca740837d9d # v1\n with:\n reporter: github-pr-annotations\n tool_name: \"Review / actionlint\"\n fail_level: error\n\n #\n # TypeScript / JavaScript\n #\n\n - name: ESLint\n if: steps.detect.outputs.eslint == 'true'\n uses: reviewdog/action-eslint@556a3fdaf8b4201d4d74d406013386aa4f7dab96 # v1.34.0\n with:\n reporter: github-pr-annotations\n tool_name: \"Review / eslint\"\n fail_level: error\n eslint_flags: .\n\n - name: TypeScript\n if: steps.detect.outputs.tsconfig == 'true'\n uses: EPMatt/reviewdog-action-tsc@63d923a3c5b4497671940b8874f58a404e2351b5 # v1\n with:\n reporter: github-pr-annotations\n tool_name: \"Review / tsc\"\n fail_level: error\n\n #\n # Shell\n #\n\n - name: ShellCheck\n if: steps.detect.outputs.shell == 'true'\n uses: reviewdog/action-shellcheck@4c07458293ac342d477251099501a718ae5ef86e # v1\n with:\n reporter: github-pr-annotations\n tool_name: \"Review / shellcheck\"\n fail_level: none\n\n #\n # Docker\n #\n\n - name: Hadolint\n if: steps.detect.outputs.docker == 'true'\n uses: reviewdog/action-hadolint@1b2cfa6ba72072ad35158d7ff3aa49bbdc03506d # v1\n with:\n reporter: github-pr-annotations\n tool_name: \"Review / hadolint\"\n fail_level: none\n\n #\n # Environment files\n #\n\n - name: dotenv-linter\n if: steps.detect.outputs.dotenv == 'true'\n uses: dotenv-linter/action-dotenv-linter@afde61cfda2ecffe7bea35837b6f20b956c88689 # v3.0.0\n with:\n reporter: github-code-suggestions\n\n #\n # Documentation\n #\n\n - name: Alex (inclusive language)\n if: steps.detect.outputs.markdown == 'true'\n uses: reviewdog/action-alex@347481655add010a2ae302df34b57c9bcfa0d6e4 # v1\n with:\n reporter: github-pr-annotations\n tool_name: \"Review / alex\"\n",
|
|
1348
|
-
stale: "name: Stale\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n days-before-stale:\n description: Days of inactivity before an issue or PR is marked stale (applies to both unless type-specific value is set)\n type: number\n required: false\n default: 30\n days-before-close:\n description: Days after stale label before closing (applies to both unless type-specific value is set)\n type: number\n required: false\n default: 5\n days-before-issue-stale:\n description: Days of inactivity before an issue is marked stale; overrides days-before-stale when set to a non-negative value\n type: number\n required: false\n default: -1\n days-before-issue-close:\n description: Days after stale label before closing an issue; overrides days-before-close when set to a non-negative value\n type: number\n required: false\n default: -1\n days-before-pr-stale:\n description: Days of inactivity before a PR is marked stale; overrides days-before-stale when set to a non-negative value\n type: number\n required: false\n default: -1\n days-before-pr-close:\n description: Days after stale label before closing a PR; overrides days-before-close when set to a non-negative value\n type: number\n required: false\n default: -1\n exempt-issue-labels:\n description: Comma-separated labels that exempt an issue from being marked stale\n type: string\n required: false\n default: \"in-progress,wip\"\n exempt-pr-labels:\n description: Comma-separated labels that exempt a PR from being marked stale\n type: string\n required: false\n default: \"\"\n exempt-all-issue-milestones:\n description: Issues assigned to any milestone are never marked stale\n type: boolean\n required: false\n default: true\n exempt-all-pr-milestones:\n description: PRs assigned to any milestone are never marked stale\n type: boolean\n required: false\n default: true\n exempt-all-issue-projects:\n description: Issues assigned to any project are never marked stale\n type: boolean\n required: false\n default: true\n exempt-all-pr-projects:\n description: PRs assigned to any project are never marked stale\n type: boolean\n required: false\n default: true\n\njobs:\n stale:\n name: Mark stale issues and pull requests\n permissions:\n contents: write\n issues: write\n pull-requests: write\n runs-on: ubuntu-latest\n timeout-minutes: 10\n steps:\n - uses: actions/stale@1e223db275d687790206a7acac4d1a11bd6fe629 # v10.4.0\n name: Run Stale\n with:\n close-issue-message: >\n This issue was closed because it has been stalled for\n ${{ inputs.days-before-close }} days with no activity.\n days-before-close: ${{ inputs.days-before-close }}\n days-before-issue-close: ${{ inputs.days-before-issue-close }}\n days-before-issue-stale: ${{ inputs.days-before-issue-stale }}\n days-before-pr-close: ${{ inputs.days-before-pr-close }}\n days-before-pr-stale: ${{ inputs.days-before-pr-stale }}\n days-before-stale: ${{ inputs.days-before-stale }}\n exempt-all-issue-milestones: ${{ inputs.exempt-all-issue-milestones }}\n exempt-all-issue-projects: ${{ inputs.exempt-all-issue-projects }}\n exempt-all-pr-milestones: ${{ inputs.exempt-all-pr-milestones }}\n exempt-all-pr-projects: ${{ inputs.exempt-all-pr-projects }}\n exempt-issue-labels: ${{ inputs.exempt-issue-labels }}\n exempt-pr-labels: ${{ inputs.exempt-pr-labels }}\n stale-issue-label: wontfix\n stale-issue-message: >\n This issue is stale because it has been open ${{ inputs.days-before-stale }}\n days with no activity. Remove the stale label or comment, or this will be\n closed in ${{ inputs.days-before-close }} days.\n stale-pr-label: wontfix\n stale-pr-message: >\n This PR is stale because it has been open ${{ inputs.days-before-stale }}\n days with no activity. Remove the stale label or comment, or this will be\n closed in ${{ inputs.days-before-close }} days.\n",
|
|
1349
|
-
"sync-dispatch": "name: Sync Dispatch\n\non: # yamllint disable-line rule:truthy\n workflow_dispatch:\n inputs:\n steps:\n description: >\n Sync steps to pass to each repo's sync.yml. Default is \"readme\"\n (only README marker blocks are updated).\n type: string\n required: false\n default: readme\n\npermissions:\n contents: read\n\njobs:\n broadcast:\n name: Broadcast sync to all repos\n runs-on: ubuntu-latest\n timeout-minutes: 15\n steps:\n - name: Dispatch sync to all repos with sync.yml\n run: |\n gh api /orgs/theholocron/repos --paginate --jq '.[].name' \\\n | while IFS= read -r repo; do\n gh api \"/repos/theholocron/$repo/contents/.github/workflows/sync.yml\" --silent 2>/dev/null || continue\n echo \"Dispatching sync to theholocron/$repo\"\n gh workflow run sync.yml \\\n --repo \"theholocron/$repo\" \\\n --field \"steps=$STEPS\" \\\n || echo \"Warning: could not dispatch to theholocron/$repo — skipping\"\n done\n env:\n GH_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN }}\n STEPS: ${{ inputs.steps }}\n",
|
|
1350
|
-
tag: "name: Tag\n\n# Release Please — fully automated tag and GitHub Release from Conventional Commits.\n# No package.json or npm publishing required. Operates in \"simple\" mode by default:\n# analyzes commits since the last tag, maintains a rolling Release PR, and creates\n# a tag + GitHub Release when that PR is merged.\n#\n# The calling repo must have two files at the root:\n# release-please-config.json — declares packages and release-type\n# .release-please-manifest.json — tracks the current version\n\non: # yamllint disable-line rule:truthy\n # Self-trigger: when this workflow lives in theholocron/.github itself,\n # push to main runs Release Please for that repo's own releases.\n push:\n branches:\n - main\n workflow_call:\n inputs:\n release-type:\n description: Release Please release type (simple, node, python, etc.)\n type: string\n required: false\n default: simple\n config-file:\n description: Path to release-please-config.json\n type: string\n required: false\n default: release-please-config.json\n manifest-file:\n description: Path to .release-please-manifest.json\n type: string\n required: false\n default: .release-please-manifest.json\n\njobs:\n tag:\n name: Tag release\n permissions:\n contents: write\n pull-requests: write\n runs-on: ubuntu-latest\n timeout-minutes: 10\n steps:\n - uses: google-github-actions/release-please-action@e4dc86ba9405554aeba3c6bb2d169500e7d3b4ee # v4.1.1\n name: Run Release Please\n with:\n release-type: ${{ inputs.release-type }}\n config-file: ${{ inputs.config-file }}\n manifest-file: ${{ inputs.manifest-file }}\n",
|
|
1351
|
-
"sync-github": "name: Sync workflow templates\n\n# Builds the holocron CLI from source and pushes updated workflow templates\n# and composite actions to downstream .github repos. Runs whenever the\n# template source files change on main or alpha.\n#\n# Secrets required:\n# HOLOCRON_SYNC_TOKEN — fine-grained PAT (resource owner: org) with:\n# Actions: Read and write (dispatch workflow runs via gh workflow run)\n# Contents: Read and write (git trees, blobs, refs)\n# Pull requests: Read and write (open sync PR)\n# Workflows: Read and write (write .github/workflows/*.yml)\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n primary-repo:\n description: >\n Primary .github repo — receives composite actions, reusable workflows,\n and thin-caller templates. Requires a PR (branch protection assumed).\n type: string\n required: false\n default: theholocron/.github\n secondary-repos:\n description: >\n Space-separated list of secondary repos (reusable workflows + thin\n callers only, no composite actions). Changes are delivered via pull\n request, same as the primary repo.\n type: string\n required: false\n default: \"\"\n sync-branch:\n description: Branch name used for the primary and secondary repo PRs\n type: string\n required: false\n default: chore/sync-templates\n secrets:\n HOLOCRON_SYNC_TOKEN:\n required: true\n HOLOCRON_READ_TOKEN:\n description: >\n Fine-grained PAT for read-only GitHub API calls (e.g. resolving git\n committer identity via `gh api user`). Falls back to HOLOCRON_SYNC_TOKEN.\n required: false\n GH_TOKEN:\n description: >\n Generic GitHub token fallback for `gh` CLI calls. Used when neither\n HOLOCRON_READ_TOKEN nor HOLOCRON_SYNC_TOKEN is set.\n required: false\n HOLOCRON_AXIOM_TOKEN:\n description: >\n Axiom API token. When set alongside the HOLOCRON_AXIOM_DATASET\n repo/org variable, the CLI ships this run's structured logs to Axiom.\n Falls back to the vendor-native AXIOM_TOKEN secret.\n required: false\n AXIOM_TOKEN:\n description: Vendor-native fallback for HOLOCRON_AXIOM_TOKEN.\n required: false\n TURBO_TOKEN:\n required: false\n\njobs:\n sync:\n name: Sync templates\n runs-on: ubuntu-latest\n timeout-minutes: 15\n permissions:\n contents: read\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n HOLOCRON_AXIOM_TOKEN: ${{ secrets.HOLOCRON_AXIOM_TOKEN || secrets.AXIOM_TOKEN }}\n HOLOCRON_AXIOM_DATASET: ${{ vars.HOLOCRON_AXIOM_DATASET || vars.AXIOM_DATASET }}\n POSTHOG_PROJECT_TOKEN: ${{ vars.POSTHOG_PROJECT_TOKEN }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - run: pnpm build\n name: Build CLI\n\n - name: Cache actionlint\n id: cache-actionlint\n uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0\n with:\n path: /tmp/actionlint\n key: actionlint-v1.7.7-linux-amd64\n\n - name: Download actionlint\n if: steps.cache-actionlint.outputs.cache-hit != 'true'\n run: |\n curl -fsSL https://github.com/rhysd/actionlint/releases/download/v1.7.7/actionlint_1.7.7_linux_amd64.tar.gz \\\n | tar -xz -C /tmp actionlint\n\n - name: Validate generated workflows\n run: |\n node packages/cli/dist/cli.mjs sync-github \\\n --repo \"$PRIMARY_REPO\" \\\n --output-dir /tmp/sync-validate\n /tmp/actionlint /tmp/sync-validate/.github/workflows/*.yml\n env:\n PRIMARY_REPO: ${{ inputs.primary-repo }}\n\n - name: Sync primary repo (PR)\n run: |\n GIT_NAME=$(gh api user --jq .name 2>/dev/null || echo \"github-actions[bot]\")\n GIT_EMAIL=$(gh api user --jq '\"\\(.id)+\\(.login)@users.noreply.github.com\"' 2>/dev/null || echo \"41898282+github-actions[bot]@users.noreply.github.com\")\n COMMIT_MSG=\"chore: sync from theholocron/holocron\"$'\\n\\n'\"Signed-off-by: $GIT_NAME <$GIT_EMAIL>\"\n node packages/cli/dist/cli.mjs sync-github \\\n --repo \"$PRIMARY_REPO\" \\\n --branch \"$SYNC_BRANCH\" \\\n --pr \\\n --message \"$COMMIT_MSG\"\n GH_TOKEN=\"$HOLOCRON_SYNC_TOKEN\" gh pr merge --auto --squash \\\n --repo \"$PRIMARY_REPO\" \"$SYNC_BRANCH\" 2>/dev/null || true\n env:\n GH_TOKEN: ${{ secrets.HOLOCRON_READ_TOKEN || secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN }}\n HOLOCRON_SYNC_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN }}\n PRIMARY_REPO: ${{ inputs.primary-repo }}\n SYNC_BRANCH: ${{ inputs.sync-branch }}\n\n - name: Sync secondary repos (PR)\n if: ${{ inputs.secondary-repos != '' }}\n run: |\n GIT_NAME=$(gh api user --jq .name 2>/dev/null || echo \"github-actions[bot]\")\n GIT_EMAIL=$(gh api user --jq '\"\\(.id)+\\(.login)@users.noreply.github.com\"' 2>/dev/null || echo \"41898282+github-actions[bot]@users.noreply.github.com\")\n COMMIT_MSG=\"chore: sync from theholocron/holocron\"$'\\n\\n'\"Signed-off-by: $GIT_NAME <$GIT_EMAIL>\"\n for repo in $SECONDARY_REPOS; do\n node packages/cli/dist/cli.mjs sync-github \\\n --repo \"$repo\" \\\n --branch \"$SYNC_BRANCH\" \\\n --pr \\\n --message \"$COMMIT_MSG\"\n GH_TOKEN=\"$HOLOCRON_SYNC_TOKEN\" gh pr merge --auto --squash \\\n --repo \"$repo\" \"$SYNC_BRANCH\" 2>/dev/null || true\n done\n env:\n GH_TOKEN: ${{ secrets.HOLOCRON_READ_TOKEN || secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN }}\n HOLOCRON_SYNC_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN }}\n SECONDARY_REPOS: ${{ inputs.secondary-repos }}\n SYNC_BRANCH: ${{ inputs.sync-branch }}\n"
|
|
1352
|
-
};
|
|
1353
|
-
/** Composite actions → `.github/actions/<name>/action.yml` (key includes `/action`). */
|
|
1354
|
-
const REUSABLE_ACTIONS = {
|
|
1355
|
-
"auto-commit/action": auto_commit_default,
|
|
1356
|
-
"holocron/action": holocron_default,
|
|
1357
|
-
"install/action": install_default,
|
|
1358
|
-
"setup/action": setup_default,
|
|
1359
|
-
"setup-node/action": setup_node_default
|
|
1360
|
-
};
|
|
1361
|
-
/** GitHub starter-workflow metadata → `workflow-templates/<name>.properties.json`. */
|
|
1362
|
-
const WORKFLOW_TEMPLATE_PROPERTIES = { bookkeeping: JSON.stringify({
|
|
1363
|
-
name: "Bookkeeping",
|
|
1364
|
-
description: "Label and track issues and pull requests.",
|
|
1365
|
-
iconName: "octicon tag"
|
|
1366
|
-
}, null, 2) };
|
|
1367
|
-
const SOURCE = "packages/astromech/src/reusable.ts";
|
|
1368
|
-
/**
|
|
1369
|
-
* "AUTO-GENERATED — do not edit" header for the YAML pushed to
|
|
1370
|
-
* `theholocron/.github`. Matches `createHeader().workflowHeader({ forPrimary: true })`
|
|
1371
|
-
* field-for-field, minus any timestamp — a live `Synced:` line makes the content
|
|
1372
|
-
* hash change on every run and defeats `sync-github`'s unchanged-file skip.
|
|
1633
|
+
* **Per-candidate, not two full passes** (#750 follow-up — the first
|
|
1634
|
+
* shipped version got this wrong): `delivery.build`'s own `detect[]` ends
|
|
1635
|
+
* with `{ when: /^tsconfig\.json$/, tool: "tsc" }` as its last-resort
|
|
1636
|
+
* fallback, and `tsconfig.json` exists in essentially every package
|
|
1637
|
+
* regardless of build tool. A "check every candidate's filename, *then*
|
|
1638
|
+
* check every candidate's dependency" two-pass design would let that
|
|
1639
|
+
* always-present fallback file win over a real `tsdown`/`vite`/`rollup`
|
|
1640
|
+
* dependency match, the moment the earlier candidates' own config files are
|
|
1641
|
+
* gone — exactly backwards. Checking `(file match) || (dependency match)`
|
|
1642
|
+
* together for each candidate before moving to the next preserves the
|
|
1643
|
+
* intended precedence: a real signal for an *earlier* candidate, by either
|
|
1644
|
+
* means, still wins over a later candidate's mere filename presence.
|
|
1373
1645
|
*/
|
|
1374
|
-
function
|
|
1375
|
-
return
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1646
|
+
function resolveRunner(local, cwd, listDir, readFile) {
|
|
1647
|
+
if (local.tool) return mkRunner(local.tool, local.args);
|
|
1648
|
+
let files;
|
|
1649
|
+
try {
|
|
1650
|
+
files = listDir(cwd);
|
|
1651
|
+
} catch {
|
|
1652
|
+
files = [];
|
|
1653
|
+
}
|
|
1654
|
+
const deps = packageJsonDependencies(cwd, readFile);
|
|
1655
|
+
for (const candidate of local.detect) if (files.some((f) => candidate.when.test(f)) || deps.has(candidate.tool)) return mkRunner(candidate.tool, candidate.args);
|
|
1382
1656
|
}
|
|
1383
|
-
/**
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
*/
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
const header = reusableHeader();
|
|
1391
|
-
for (const [name, body] of Object.entries(REUSABLE_ACTIONS)) out.set(`.github/actions/${name}.yml`, `${header}${body}`);
|
|
1392
|
-
for (const [name, body] of Object.entries(REUSABLE_WORKFLOWS)) out.set(`.github/workflows/${name}.yml`, `${header}${body}`);
|
|
1393
|
-
for (const [name, body] of Object.entries(WORKFLOW_TEMPLATES)) {
|
|
1394
|
-
out.set(`workflow-templates/${name}.yml`, `${header}${body}`);
|
|
1395
|
-
const props = WORKFLOW_TEMPLATE_PROPERTIES[name];
|
|
1396
|
-
if (props) out.set(`workflow-templates/${name}.properties.json`, props);
|
|
1657
|
+
/** Every key from `package.json`'s `dependencies` + `devDependencies`, or an empty set on any read/parse failure. */
|
|
1658
|
+
function packageJsonDependencies(cwd, readFile) {
|
|
1659
|
+
try {
|
|
1660
|
+
const pkg = JSON.parse(readFile(join(cwd, "package.json")));
|
|
1661
|
+
return /* @__PURE__ */ new Set([...Object.keys(pkg.dependencies ?? {}), ...Object.keys(pkg.devDependencies ?? {})]);
|
|
1662
|
+
} catch {
|
|
1663
|
+
return /* @__PURE__ */ new Set();
|
|
1397
1664
|
}
|
|
1398
|
-
return out;
|
|
1399
1665
|
}
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1666
|
+
function resolveBin(cwd, tool, fileExists) {
|
|
1667
|
+
const local = join(cwd, "node_modules", ".bin", tool);
|
|
1668
|
+
return fileExists(local) ? local : tool;
|
|
1669
|
+
}
|
|
1670
|
+
function packageManager(cwd, readFile, fileExists) {
|
|
1671
|
+
try {
|
|
1672
|
+
const pkg = JSON.parse(readFile(join(cwd, "package.json")));
|
|
1673
|
+
if (typeof pkg.packageManager === "string") return pkg.packageManager.split("@")[0];
|
|
1674
|
+
} catch {}
|
|
1675
|
+
if (fileExists(join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
1676
|
+
if (fileExists(join(cwd, "bun.lockb"))) return "bun";
|
|
1677
|
+
if (fileExists(join(cwd, "yarn.lock"))) return "yarn";
|
|
1678
|
+
if (fileExists(join(cwd, "package-lock.json"))) return "npm";
|
|
1679
|
+
return "pnpm";
|
|
1680
|
+
}
|
|
1681
|
+
function turboDefinesTask(cwd, task, readFile, fileExists) {
|
|
1682
|
+
if (!fileExists(join(cwd, "turbo.json"))) return false;
|
|
1683
|
+
try {
|
|
1684
|
+
const turbo = JSON.parse(readFile(join(cwd, "turbo.json")));
|
|
1685
|
+
return Boolean(turbo.tasks?.[task] ?? turbo.pipeline?.[task]);
|
|
1686
|
+
} catch {
|
|
1687
|
+
return false;
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
function packageJsonScript(cwd, task, readFile, fileExists) {
|
|
1691
|
+
if (!fileExists(join(cwd, "package.json"))) return void 0;
|
|
1692
|
+
try {
|
|
1693
|
+
return JSON.parse(readFile(join(cwd, "package.json"))).scripts?.[task];
|
|
1694
|
+
} catch {
|
|
1695
|
+
return;
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
//#endregion
|
|
1699
|
+
//#region src/tasks/ci.ts
|
|
1421
1700
|
/**
|
|
1422
|
-
* `
|
|
1423
|
-
*
|
|
1424
|
-
*
|
|
1425
|
-
*
|
|
1701
|
+
* `holocron run ci` / `astromech.ci()` — run the merge-gating checks locally, in CI
|
|
1702
|
+
* order, and exit non-zero on the first failure. The "will CI pass?" command a
|
|
1703
|
+
* `pre-push` hook and the agent skills point at.
|
|
1704
|
+
*
|
|
1705
|
+
* Default scope: every `required: true` task. If the manifest marks nothing
|
|
1706
|
+
* required (an un-migrated repo), fall back to every `ci: true` task. `--all`
|
|
1707
|
+
* forces the full set. Each task runs through {@link runTask}: a `required`
|
|
1708
|
+
* task whose local runner can't run is a failure; a `local: null` task with
|
|
1709
|
+
* no turbo task / `package.json` script is skipped, never failed — even when
|
|
1710
|
+
* `required` (it's a CI-only check, not a local one).
|
|
1426
1711
|
*/
|
|
1427
|
-
function
|
|
1428
|
-
const
|
|
1429
|
-
const
|
|
1430
|
-
for (const
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1712
|
+
function runCi(input) {
|
|
1713
|
+
const { print } = input;
|
|
1714
|
+
const byName = /* @__PURE__ */ new Map();
|
|
1715
|
+
for (const item of input.config?.tasks ?? []) byName.set(taskName(item), normalizeTaskEntry(item));
|
|
1716
|
+
const gating = [...byName.values()].filter((e) => CI_ORDER.includes(e.name));
|
|
1717
|
+
const all = gating.filter((e) => e.ci !== false);
|
|
1718
|
+
const required = gating.filter((e) => e.required === true);
|
|
1719
|
+
let selected = input.scope === "all" ? all : required;
|
|
1720
|
+
const fellBack = input.scope !== "all" && selected.length === 0 && all.length > 0;
|
|
1721
|
+
if (fellBack) selected = all;
|
|
1722
|
+
const ordered = [...selected].sort((a, b) => CI_ORDER.indexOf(a.name) - CI_ORDER.indexOf(b.name));
|
|
1723
|
+
print(`holocron run ci — ${input.scope === "all" ? "all CI checks" : "required checks"} (${ordered.length})`);
|
|
1724
|
+
if (fellBack) print(" (no required tasks in the manifest — running every CI task)");
|
|
1725
|
+
print("");
|
|
1726
|
+
const jobs = [];
|
|
1727
|
+
for (const entry of ordered) {
|
|
1728
|
+
const context = WORKFLOW_CHECK_CONTEXTS[entry.name] ?? null;
|
|
1729
|
+
print(`▶ ${context ?? entry.name}`);
|
|
1730
|
+
const r = runTask({
|
|
1731
|
+
print: (line) => print(` ${line}`),
|
|
1732
|
+
logger: input.logger,
|
|
1733
|
+
exec: input.exec,
|
|
1734
|
+
readFile: input.readFile,
|
|
1735
|
+
fileExists: input.fileExists,
|
|
1736
|
+
listDir: input.listDir,
|
|
1737
|
+
lookPath: input.lookPath,
|
|
1738
|
+
task: entry.name,
|
|
1739
|
+
cwd: input.cwd,
|
|
1740
|
+
dryRun: input.dryRun ?? false,
|
|
1741
|
+
required: entry.required === true,
|
|
1742
|
+
...input.filter ? { filter: input.filter } : {}
|
|
1743
|
+
});
|
|
1744
|
+
const status = r.status;
|
|
1745
|
+
jobs.push({
|
|
1746
|
+
task: entry.name,
|
|
1747
|
+
checkContext: context,
|
|
1748
|
+
status,
|
|
1749
|
+
command: r.command,
|
|
1750
|
+
message: r.message
|
|
1751
|
+
});
|
|
1439
1752
|
}
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
}
|
|
1753
|
+
const failed = jobs.filter((j) => j.status === "fail").length;
|
|
1754
|
+
const passed = jobs.filter((j) => j.status === "ok" || j.status === "dry-run").length;
|
|
1755
|
+
const skipped = jobs.filter((j) => j.status === "skip").length;
|
|
1756
|
+
print("");
|
|
1757
|
+
if (ordered.length === 0) print("holocron run ci — nothing to run");
|
|
1758
|
+
else if (failed > 0) print(`✗ ${passed} passed, ${failed} failed${skipped ? `, ${skipped} skipped` : ""}`);
|
|
1759
|
+
else print(`✓ ${passed} passed${skipped ? `, ${skipped} skipped` : ""}${input.dryRun ? " (plan only)" : ""}`);
|
|
1760
|
+
return {
|
|
1761
|
+
status: failed > 0 ? "fail" : "ok",
|
|
1762
|
+
jobs
|
|
1763
|
+
};
|
|
1764
|
+
}
|
|
1765
|
+
function taskName(item) {
|
|
1766
|
+
return typeof item === "string" ? item : item.name;
|
|
1446
1767
|
}
|
|
1768
|
+
//#endregion
|
|
1769
|
+
//#region src/tasks/group.ts
|
|
1447
1770
|
/**
|
|
1448
|
-
*
|
|
1449
|
-
*
|
|
1450
|
-
*
|
|
1451
|
-
*
|
|
1452
|
-
*
|
|
1453
|
-
*
|
|
1454
|
-
* root's own script directly to `turbo run <task>`, which only sees declared
|
|
1455
|
-
* workspace members — root vanishes, the task "succeeds" with zero real work
|
|
1456
|
-
* done (confirmed empirically: `Packages in scope: docs`, root never
|
|
1457
|
-
* mentioned, exit 0).
|
|
1458
|
-
*
|
|
1459
|
-
* Detection is narrow and specific: root only needs to be an explicit
|
|
1460
|
-
* workspace member if its *own* `package.json` has a script literally named
|
|
1461
|
-
* after one of the tasks turbo.json is about to fan out — a plain
|
|
1462
|
-
* orchestrator root (`holocron`'s own `"build": "turbo run delivery.build"`,
|
|
1463
|
-
* which has no `delivery.build` script itself) never trips this; only a root
|
|
1464
|
-
* that's genuinely a directly-buildable package does. Confirmed the fix
|
|
1465
|
-
* empirically too — adding `.` to `packages:` makes turbo pick root back up
|
|
1466
|
-
* (`Packages in scope: docs, root-lib`) without disturbing the sibling
|
|
1467
|
-
* package's own caching.
|
|
1771
|
+
* `holocron run <namespace>` — run every configured task under one
|
|
1772
|
+
* namespace prefix (`sourceQuality.*`, `verification.*`, …) in sequence.
|
|
1773
|
+
* `Astromech.run()` falls back here whenever the given name isn't an exact
|
|
1774
|
+
* `TASKS` entry but matches the prefix of one or more configured tasks —
|
|
1775
|
+
* same "aggregate several real tasks into one report" shape `runCi` already
|
|
1776
|
+
* established, scoped to one namespace instead of the merge-gating set.
|
|
1468
1777
|
*
|
|
1469
|
-
*
|
|
1470
|
-
*
|
|
1471
|
-
*
|
|
1472
|
-
* `catalog:`/`catalogs:`/`overrides:` block after `packages:`; a real parser
|
|
1473
|
-
* round-trip risks reformatting or reordering content nobody asked to touch.
|
|
1474
|
-
* Only ever *adds* a line — never rewrites or reorders anything already
|
|
1475
|
-
* there — and is a no-op (`changed: false`) whenever root doesn't need it,
|
|
1476
|
-
* root is already listed, or `packages:` isn't in the plain block-list form
|
|
1477
|
-
* every repo checked actually uses.
|
|
1778
|
+
* Every task in the namespace always runs — one task's failure never skips
|
|
1779
|
+
* the rest, same as `runCi`'s own loop — so a single invocation surfaces
|
|
1780
|
+
* every problem in the namespace at once instead of stopping at the first.
|
|
1478
1781
|
*/
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1782
|
+
/** Every configured task whose name starts with `"<namespace>."`, in manifest declaration order. */
|
|
1783
|
+
function matchNamespace(config, namespace) {
|
|
1784
|
+
const prefix = `${namespace}.`;
|
|
1785
|
+
return (config.tasks ?? []).map(normalizeTaskEntry).reduce((names, entry) => {
|
|
1786
|
+
if (entry.name.startsWith(prefix) && !names.includes(entry.name)) names.push(entry.name);
|
|
1787
|
+
return names;
|
|
1788
|
+
}, []);
|
|
1789
|
+
}
|
|
1790
|
+
function runGroup(input) {
|
|
1791
|
+
const { print, namespace } = input;
|
|
1792
|
+
const names = matchNamespace(input.config, namespace);
|
|
1793
|
+
if (names.length === 0) return {
|
|
1794
|
+
status: "unknown",
|
|
1795
|
+
jobs: []
|
|
1489
1796
|
};
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1797
|
+
print(`holocron run ${namespace} — ${names.length} task(s)`);
|
|
1798
|
+
print("");
|
|
1799
|
+
const jobs = [];
|
|
1800
|
+
for (const name of names) {
|
|
1801
|
+
print(`▶ ${name}`);
|
|
1802
|
+
const r = runTask({
|
|
1803
|
+
print: (line) => print(` ${line}`),
|
|
1804
|
+
logger: input.logger,
|
|
1805
|
+
exec: input.exec,
|
|
1806
|
+
readFile: input.readFile,
|
|
1807
|
+
fileExists: input.fileExists,
|
|
1808
|
+
listDir: input.listDir,
|
|
1809
|
+
lookPath: input.lookPath,
|
|
1810
|
+
task: name,
|
|
1811
|
+
cwd: input.cwd,
|
|
1812
|
+
dryRun: input.dryRun ?? false,
|
|
1813
|
+
...input.filter ? { filter: input.filter } : {}
|
|
1814
|
+
});
|
|
1815
|
+
jobs.push({
|
|
1816
|
+
task: name,
|
|
1817
|
+
status: r.status,
|
|
1818
|
+
command: r.command,
|
|
1819
|
+
message: r.message
|
|
1820
|
+
});
|
|
1495
1821
|
}
|
|
1496
|
-
if (items.some((item) => {
|
|
1497
|
-
const value = item.replace(/^\s*-\s*/, "").replace(/^["']|["']$/g, "").trim();
|
|
1498
|
-
return value === "." || value === "";
|
|
1499
|
-
})) return {
|
|
1500
|
-
content: workspaceYaml,
|
|
1501
|
-
changed: false
|
|
1502
|
-
};
|
|
1503
|
-
const newLine = `${items[0]?.match(/^(\s*-\s*)/)?.[1] ?? " - "}"."`;
|
|
1504
1822
|
return {
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
newLine,
|
|
1508
|
-
...lines.slice(packagesLineIdx + 1)
|
|
1509
|
-
].join("\n"),
|
|
1510
|
-
changed: true
|
|
1823
|
+
status: jobs.some((j) => j.status === "fail") ? "fail" : jobs.every((j) => j.status === "dry-run") ? "dry-run" : "ok",
|
|
1824
|
+
jobs
|
|
1511
1825
|
};
|
|
1512
1826
|
}
|
|
1513
1827
|
//#endregion
|
|
@@ -1545,17 +1859,46 @@ function createAstromech(options) {
|
|
|
1545
1859
|
lookPath: options.lookPath ?? realLookPath
|
|
1546
1860
|
};
|
|
1547
1861
|
const items = () => (options.config?.tasks ?? []).map((i) => normalizeTaskEntry(i));
|
|
1862
|
+
/**
|
|
1863
|
+
* Task names this repo's manifest can actually run locally — shared by
|
|
1864
|
+
* {@link packageScripts} and by `holocron`'s own interactive task picker.
|
|
1865
|
+
* Deliberately independent of `syncScripts`/`holocronScript`/`hooks`:
|
|
1866
|
+
* those govern whether/how `package.json` scripts get *written*, not
|
|
1867
|
+
* which tasks exist to run. A repo with `syncScripts: false` (this repo
|
|
1868
|
+
* itself, among others) still has real runnable tasks — found live when
|
|
1869
|
+
* `packageScripts()`'s `{}` short-circuit silently emptied the picker too.
|
|
1870
|
+
*/
|
|
1871
|
+
const runnableTaskNames = () => {
|
|
1872
|
+
const out = [];
|
|
1873
|
+
for (const entry of items()) {
|
|
1874
|
+
const def = TASKS[entry.name];
|
|
1875
|
+
const hasNoLocalRunner = def?.local === null && !def.linterGroup && !def.jobs;
|
|
1876
|
+
if (entry.local === false || !KNOWN_TASKS.has(entry.name) || hasNoLocalRunner) continue;
|
|
1877
|
+
out.push(entry.name);
|
|
1878
|
+
}
|
|
1879
|
+
return out;
|
|
1880
|
+
};
|
|
1548
1881
|
return {
|
|
1549
|
-
run: (task, opts = {}) =>
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1882
|
+
run: (task, opts = {}) => {
|
|
1883
|
+
if (!TASKS[task] && options.config && matchNamespace(options.config, task).length > 0) return runGroup({
|
|
1884
|
+
...deps,
|
|
1885
|
+
cwd: options.cwd,
|
|
1886
|
+
config: options.config,
|
|
1887
|
+
namespace: task,
|
|
1888
|
+
dryRun: opts.dryRun ?? false,
|
|
1889
|
+
...opts.filter ? { filter: opts.filter } : {}
|
|
1890
|
+
});
|
|
1891
|
+
return runTask({
|
|
1892
|
+
...deps,
|
|
1893
|
+
task,
|
|
1894
|
+
cwd: options.cwd,
|
|
1895
|
+
...opts.job !== void 0 ? { job: opts.job } : {},
|
|
1896
|
+
passthrough: opts.passthrough ?? [],
|
|
1897
|
+
dryRun: opts.dryRun ?? false,
|
|
1898
|
+
required: opts.required ?? false,
|
|
1899
|
+
...opts.filter ? { filter: opts.filter } : {}
|
|
1900
|
+
});
|
|
1901
|
+
},
|
|
1559
1902
|
ci: (opts = {}) => runCi({
|
|
1560
1903
|
...deps,
|
|
1561
1904
|
cwd: options.cwd,
|
|
@@ -1569,6 +1912,7 @@ function createAstromech(options) {
|
|
|
1569
1912
|
const isDocsSite = entry.name === "knowledge.docs";
|
|
1570
1913
|
const isComponents = entry.name === "knowledge.components";
|
|
1571
1914
|
if (entry.ci === false || !KNOWN_WORKFLOWS.has(entry.name) && !isDocsSite && !isComponents) continue;
|
|
1915
|
+
if (entry.name === "delivery.deploy" && entry.with?.["on"] === "release") continue;
|
|
1572
1916
|
const rawWith = isDocsSite ? {
|
|
1573
1917
|
preview: true,
|
|
1574
1918
|
...entry.with,
|
|
@@ -1599,27 +1943,24 @@ function createAstromech(options) {
|
|
|
1599
1943
|
const out = {};
|
|
1600
1944
|
if (!options.config || options.config.syncScripts === false) return out;
|
|
1601
1945
|
out.holocron = options.config.holocronScript ?? "holocron";
|
|
1602
|
-
for (const
|
|
1603
|
-
const def = TASKS[entry.name];
|
|
1604
|
-
const hasNoLocalRunner = def?.local === null && !def.linterGroup && !def.jobs;
|
|
1605
|
-
if (entry.local === false || !KNOWN_TASKS.has(entry.name) || hasNoLocalRunner) continue;
|
|
1606
|
-
out[entry.name] = `holocron run ${entry.name} --`;
|
|
1607
|
-
}
|
|
1946
|
+
for (const name of runnableTaskNames()) out[name] = `holocron run ${name} --`;
|
|
1608
1947
|
const hooks = options.config.hooks;
|
|
1609
1948
|
if (hooks === true || typeof hooks === "object" && hooks.prePush !== false) out.prepare = "husky";
|
|
1610
1949
|
return out;
|
|
1611
1950
|
},
|
|
1951
|
+
runnableTasks: () => runnableTaskNames(),
|
|
1612
1952
|
reusableTemplates: () => reusableTemplates(),
|
|
1613
1953
|
requiredChecks: () => requiredChecks(options.config ?? {}),
|
|
1614
1954
|
codecovConfig: (existing) => codecovConfig(options.cwd, existing),
|
|
1615
1955
|
turboConfig: () => turboConfig(options.config ?? {}),
|
|
1616
1956
|
ensureRootWorkspaceMember: (workspaceYaml, rootScripts) => {
|
|
1617
1957
|
return ensureRootWorkspaceMember(workspaceYaml, rootScripts, (options.config?.tasks ?? []).map(normalizeTaskEntry).filter((e) => e.local !== false).map((e) => e.name));
|
|
1618
|
-
}
|
|
1958
|
+
},
|
|
1959
|
+
ensureTurboDependency: (packageJson, workspaceYaml) => ensureTurboDependency(packageJson, workspaceYaml)
|
|
1619
1960
|
};
|
|
1620
1961
|
}
|
|
1621
1962
|
//#endregion
|
|
1622
|
-
//#region src/tsconfig.ts
|
|
1963
|
+
//#region src/generators/tsconfig.ts
|
|
1623
1964
|
/**
|
|
1624
1965
|
* A package-level `tsconfig.json` — the uniform shape confirmed across
|
|
1625
1966
|
* every package checked, parameterized only by the two fields that
|
|
@@ -1646,4 +1987,267 @@ function createTsconfig(options) {
|
|
|
1646
1987
|
return JSON.stringify(config, null, 2) + "\n";
|
|
1647
1988
|
}
|
|
1648
1989
|
//#endregion
|
|
1649
|
-
|
|
1990
|
+
//#region src/tasks/deploy-on-release.ts
|
|
1991
|
+
/**
|
|
1992
|
+
* Deploy workspace packages once a release has published (holocron#930).
|
|
1993
|
+
*
|
|
1994
|
+
* A package opts in with a `delivery.deploy` task in its own `holocron.config`
|
|
1995
|
+
* whose `with.on` is `"release"`:
|
|
1996
|
+
*
|
|
1997
|
+
* ```ts
|
|
1998
|
+
* // packages/<app>/holocron.config.ts
|
|
1999
|
+
* tasks: [{ name: "delivery.deploy", with: { on: "release", channel: "alpha" } }];
|
|
2000
|
+
* ```
|
|
2001
|
+
*
|
|
2002
|
+
* `channel` defaults to `main` (a stable release); set a prerelease identifier
|
|
2003
|
+
* such as `alpha` to deploy those releases instead.
|
|
2004
|
+
*
|
|
2005
|
+
* The shared `delivery.publish` workflow runs this (`holocron deploy on-release`)
|
|
2006
|
+
* as its own `deploy` job after the release job, so a deploy is a separate,
|
|
2007
|
+
* re-runnable check rather than a step buried in the release log. Each package
|
|
2008
|
+
* with such a task deploys when the release is on its `channel` and changes a file under its paths: the package itself plus its
|
|
2009
|
+
* `workspace:*` dependencies ({@link defaultPaths}), or an explicit
|
|
2010
|
+
* `with.paths`. It first waits for npm to serve the release's new versions of
|
|
2011
|
+
* those dependencies ({@link waitForPublished}) — the registry can lag a version
|
|
2012
|
+
* it has just accepted, and the deploy installs them — then runs
|
|
2013
|
+
* `pnpm --filter <package> delivery.deploy`.
|
|
2014
|
+
*
|
|
2015
|
+
* The release is already published by then, so a failed deploy can't undo it;
|
|
2016
|
+
* it fails this job instead, loudly and re-runnable on its own. A task is
|
|
2017
|
+
* `skip`ped (not failed) only when it doesn't apply to this release.
|
|
2018
|
+
*
|
|
2019
|
+
* A `delivery.deploy` task with `on: "release"` generates no CI workflow.
|
|
2020
|
+
*/
|
|
2021
|
+
/** A stable release (no prerelease identifier) is on `main`, the default channel for a task. */
|
|
2022
|
+
const MAIN_CHANNEL = "main";
|
|
2023
|
+
const DEPLOY_TASK = "delivery.deploy";
|
|
2024
|
+
/** How long to wait for npm to serve the release's versions, and how often to ask. */
|
|
2025
|
+
const WAIT_TIMEOUT_MS = 3e5;
|
|
2026
|
+
const WAIT_INTERVAL_MS = 1e4;
|
|
2027
|
+
function scanWorkspaceDir(dirPath, { readJson, listDirs }) {
|
|
2028
|
+
let folders;
|
|
2029
|
+
try {
|
|
2030
|
+
folders = listDirs(dirPath);
|
|
2031
|
+
} catch {
|
|
2032
|
+
return [];
|
|
2033
|
+
}
|
|
2034
|
+
return folders.flatMap((folder) => {
|
|
2035
|
+
try {
|
|
2036
|
+
const manifest = readJson(join(dirPath, folder, "package.json"));
|
|
2037
|
+
if (!manifest.name) return [];
|
|
2038
|
+
return [{
|
|
2039
|
+
name: manifest.name,
|
|
2040
|
+
dir: join(dirPath, folder),
|
|
2041
|
+
version: manifest.version,
|
|
2042
|
+
dependencies: manifest.dependencies ?? {}
|
|
2043
|
+
}];
|
|
2044
|
+
} catch {
|
|
2045
|
+
return [];
|
|
2046
|
+
}
|
|
2047
|
+
});
|
|
2048
|
+
}
|
|
2049
|
+
/**
|
|
2050
|
+
* Every package under `<repoRoot>/packages/*` and `<repoRoot>/plugins/*`
|
|
2051
|
+
* with a readable, named `package.json` — `holocron`'s own layout splits
|
|
2052
|
+
* vendor plugin packages into `plugins/*`, sibling to `packages/*`.
|
|
2053
|
+
*/
|
|
2054
|
+
function workspacePackages(repoRoot, tree) {
|
|
2055
|
+
return [...scanWorkspaceDir(join(repoRoot, "packages"), tree), ...scanWorkspaceDir(join(repoRoot, "plugins"), tree)];
|
|
2056
|
+
}
|
|
2057
|
+
/** The `workspace:*` dependencies of `pkg`, as workspace packages (with their current version). */
|
|
2058
|
+
function workspaceDependencies(pkg, all) {
|
|
2059
|
+
const names = new Set(Object.entries(pkg.dependencies).filter(([, spec]) => String(spec).startsWith("workspace:")).map(([name]) => name));
|
|
2060
|
+
return all.filter((candidate) => names.has(candidate.name));
|
|
2061
|
+
}
|
|
2062
|
+
/**
|
|
2063
|
+
* The paths a release must touch to redeploy `pkg` when its manifest doesn't
|
|
2064
|
+
* list them: its own folder plus the folder of every
|
|
2065
|
+
* {@link workspaceDependencies} entry. Derived rather than listed so a new or
|
|
2066
|
+
* dropped workspace dependency needs no second edit.
|
|
2067
|
+
*
|
|
2068
|
+
* @returns Repo-relative prefixes, each ending in `/`.
|
|
2069
|
+
*/
|
|
2070
|
+
function defaultPaths(repoRoot, pkg, all) {
|
|
2071
|
+
return [pkg, ...workspaceDependencies(pkg, all)].map(({ dir }) => `${relative(repoRoot, dir)}/`);
|
|
2072
|
+
}
|
|
2073
|
+
/**
|
|
2074
|
+
* The packages whose manifest declares a release-time `delivery.deploy`
|
|
2075
|
+
* (`with.on === "release"`), with the task's options resolved.
|
|
2076
|
+
*/
|
|
2077
|
+
function deployTargets(packages, manifests) {
|
|
2078
|
+
return packages.flatMap((pkg) => (manifests.get(pkg.dir)?.tasks ?? []).flatMap((item) => {
|
|
2079
|
+
const entry = normalizeTaskEntry(item);
|
|
2080
|
+
if (entry.name !== DEPLOY_TASK || entry.with?.["on"] !== "release") return [];
|
|
2081
|
+
const { channel, paths } = entry.with;
|
|
2082
|
+
return [{
|
|
2083
|
+
pkg,
|
|
2084
|
+
channel: typeof channel === "string" ? channel : MAIN_CHANNEL,
|
|
2085
|
+
paths: Array.isArray(paths) ? paths.map(String) : void 0
|
|
2086
|
+
}];
|
|
2087
|
+
}));
|
|
2088
|
+
}
|
|
2089
|
+
/**
|
|
2090
|
+
* Waits until npm serves every `name@version` in `packages`, asking with
|
|
2091
|
+
* `npm view --prefer-online` (so npm's local cache can't answer for the
|
|
2092
|
+
* registry) every `intervalMs`, for up to `timeoutMs`.
|
|
2093
|
+
*
|
|
2094
|
+
* @returns The specs npm still doesn't serve; empty once all are available.
|
|
2095
|
+
*/
|
|
2096
|
+
async function waitForPublished({ packages, run, sleep, now, cwd, timeoutMs = WAIT_TIMEOUT_MS, intervalMs = WAIT_INTERVAL_MS }) {
|
|
2097
|
+
const deadline = now() + timeoutMs;
|
|
2098
|
+
let pending = packages.map(({ name, version }) => `${name}@${version}`);
|
|
2099
|
+
for (;;) {
|
|
2100
|
+
pending = pending.filter((spec) => {
|
|
2101
|
+
const version = spec.slice(spec.lastIndexOf("@") + 1);
|
|
2102
|
+
const result = run("npm", [
|
|
2103
|
+
"view",
|
|
2104
|
+
spec,
|
|
2105
|
+
"version",
|
|
2106
|
+
"--prefer-online"
|
|
2107
|
+
], { cwd });
|
|
2108
|
+
return !(result.status === 0 && result.stdout.trim() === version);
|
|
2109
|
+
});
|
|
2110
|
+
if (pending.length === 0 || now() + intervalMs > deadline) return pending;
|
|
2111
|
+
await sleep(intervalMs);
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
/**
|
|
2115
|
+
* @param channel The release's channel: its prerelease identifier (`alpha`), or `main` for a stable release.
|
|
2116
|
+
* @param changedFiles `undefined` when there's no previous release to diff against.
|
|
2117
|
+
*/
|
|
2118
|
+
function shouldDeploy({ channel, configuredChannel, changedFiles, paths }) {
|
|
2119
|
+
if (channel !== configuredChannel) return {
|
|
2120
|
+
deploy: false,
|
|
2121
|
+
reason: `channel "${channel}" isn't "${configuredChannel}"`
|
|
2122
|
+
};
|
|
2123
|
+
if (changedFiles === void 0) return {
|
|
2124
|
+
deploy: true,
|
|
2125
|
+
reason: "no previous release to diff against"
|
|
2126
|
+
};
|
|
2127
|
+
const touched = changedFiles.find((file) => paths.some((path) => file.startsWith(path)));
|
|
2128
|
+
return touched ? {
|
|
2129
|
+
deploy: true,
|
|
2130
|
+
reason: `the release changes ${touched}`
|
|
2131
|
+
} : {
|
|
2132
|
+
deploy: false,
|
|
2133
|
+
reason: `the release changes nothing under ${paths.join(", ")}`
|
|
2134
|
+
};
|
|
2135
|
+
}
|
|
2136
|
+
const defaultRun = (command, args, opts) => spawnSync(command, args, {
|
|
2137
|
+
encoding: "utf8",
|
|
2138
|
+
...opts
|
|
2139
|
+
});
|
|
2140
|
+
const defaultReadJson = (path) => JSON.parse(readFileSync(path, "utf8"));
|
|
2141
|
+
const defaultListDirs = (path) => readdirSync(path, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
2142
|
+
const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
2143
|
+
/**
|
|
2144
|
+
* Deploys every package whose manifest declares a release-time deploy that
|
|
2145
|
+
* applies to this release. Never throws for a deploy problem: each package's
|
|
2146
|
+
* outcome is in the report, and `status: "fail"` is the caller's exit code.
|
|
2147
|
+
*/
|
|
2148
|
+
async function deployOnRelease(options, { run = defaultRun, readJson = defaultReadJson, listDirs = defaultListDirs, sleep = defaultSleep, now = Date.now, loadTasks = loadTasksConfig } = {}) {
|
|
2149
|
+
const { cwd, from, print } = options;
|
|
2150
|
+
const channel = options.channel || MAIN_CHANNEL;
|
|
2151
|
+
const to = options.to ?? "HEAD";
|
|
2152
|
+
const packages = workspacePackages(cwd, {
|
|
2153
|
+
readJson,
|
|
2154
|
+
listDirs
|
|
2155
|
+
});
|
|
2156
|
+
const manifests = /* @__PURE__ */ new Map();
|
|
2157
|
+
for (const pkg of packages) try {
|
|
2158
|
+
manifests.set(pkg.dir, await loadTasks(pkg.dir));
|
|
2159
|
+
} catch (err) {
|
|
2160
|
+
print(`${pkg.name}: couldn't read its task manifest (${err.message}); not deploying it.`);
|
|
2161
|
+
}
|
|
2162
|
+
const targets = deployTargets(packages, manifests);
|
|
2163
|
+
if (targets.length === 0) {
|
|
2164
|
+
print("No package declares a release-time delivery.deploy (with.on: release).");
|
|
2165
|
+
return {
|
|
2166
|
+
status: "skip",
|
|
2167
|
+
results: []
|
|
2168
|
+
};
|
|
2169
|
+
}
|
|
2170
|
+
let diff;
|
|
2171
|
+
const changedFiles = () => {
|
|
2172
|
+
if (diff) return diff;
|
|
2173
|
+
if (!from) return diff = { files: void 0 };
|
|
2174
|
+
const result = run("git", [
|
|
2175
|
+
"diff",
|
|
2176
|
+
"--name-only",
|
|
2177
|
+
from,
|
|
2178
|
+
to
|
|
2179
|
+
], { cwd });
|
|
2180
|
+
return diff = result.status === 0 ? { files: result.stdout.split("\n").filter(Boolean) } : {
|
|
2181
|
+
files: [],
|
|
2182
|
+
error: `git diff ${from} ${to} failed: ${result.stderr || result.status}`
|
|
2183
|
+
};
|
|
2184
|
+
};
|
|
2185
|
+
const results = [];
|
|
2186
|
+
const record = (pkg, status, message) => {
|
|
2187
|
+
results.push({
|
|
2188
|
+
pkg,
|
|
2189
|
+
status,
|
|
2190
|
+
message
|
|
2191
|
+
});
|
|
2192
|
+
print(`${status === "fail" ? "✗ " : ""}${pkg}: ${message}`);
|
|
2193
|
+
};
|
|
2194
|
+
for (const target of targets) {
|
|
2195
|
+
const name = target.pkg.name;
|
|
2196
|
+
const { files, error } = target.channel === channel ? changedFiles() : {
|
|
2197
|
+
files: [],
|
|
2198
|
+
error: void 0
|
|
2199
|
+
};
|
|
2200
|
+
if (error) {
|
|
2201
|
+
record(name, "fail", error);
|
|
2202
|
+
continue;
|
|
2203
|
+
}
|
|
2204
|
+
const decision = shouldDeploy({
|
|
2205
|
+
channel,
|
|
2206
|
+
configuredChannel: target.channel,
|
|
2207
|
+
changedFiles: files,
|
|
2208
|
+
paths: target.paths ?? defaultPaths(cwd, target.pkg, packages)
|
|
2209
|
+
});
|
|
2210
|
+
if (!decision.deploy) {
|
|
2211
|
+
record(name, "skip", `not deploying: ${decision.reason}.`);
|
|
2212
|
+
continue;
|
|
2213
|
+
}
|
|
2214
|
+
if (options.dryRun) {
|
|
2215
|
+
record(name, "dry-run", `would deploy because ${decision.reason}.`);
|
|
2216
|
+
continue;
|
|
2217
|
+
}
|
|
2218
|
+
const dependencies = workspaceDependencies(target.pkg, packages).flatMap(({ name: dep, version }) => version ? [{
|
|
2219
|
+
name: dep,
|
|
2220
|
+
version
|
|
2221
|
+
}] : []);
|
|
2222
|
+
print(`${name}: waiting for npm to serve ${dependencies.map((d) => `${d.name}@${d.version}`).join(", ") || "nothing"}.`);
|
|
2223
|
+
const unpublished = await waitForPublished({
|
|
2224
|
+
packages: dependencies,
|
|
2225
|
+
run,
|
|
2226
|
+
sleep,
|
|
2227
|
+
now,
|
|
2228
|
+
cwd
|
|
2229
|
+
});
|
|
2230
|
+
if (unpublished.length > 0) {
|
|
2231
|
+
record(name, "fail", `npm still doesn't serve ${unpublished.join(", ")} after ${WAIT_TIMEOUT_MS / 6e4} minutes. Re-run this job once it does.`);
|
|
2232
|
+
continue;
|
|
2233
|
+
}
|
|
2234
|
+
print(`${name}: deploying because ${decision.reason}.`);
|
|
2235
|
+
const result = run("pnpm", [
|
|
2236
|
+
"--filter",
|
|
2237
|
+
name,
|
|
2238
|
+
DEPLOY_TASK
|
|
2239
|
+
], {
|
|
2240
|
+
cwd,
|
|
2241
|
+
env: process.env,
|
|
2242
|
+
stdio: "inherit"
|
|
2243
|
+
});
|
|
2244
|
+
if (result.status === 0) record(name, "ok", "deployed.");
|
|
2245
|
+
else record(name, "fail", `the deploy failed (exit ${result.status}).`);
|
|
2246
|
+
}
|
|
2247
|
+
return {
|
|
2248
|
+
status: results.some((r) => r.status === "fail") ? "fail" : results.every((r) => r.status === "skip") ? "skip" : "ok",
|
|
2249
|
+
results
|
|
2250
|
+
};
|
|
2251
|
+
}
|
|
2252
|
+
//#endregion
|
|
2253
|
+
export { CI_ORDER, KNOWN_TASKS, KNOWN_WORKFLOWS, REUSABLE_ACTIONS, REUSABLE_WORKFLOWS, TASKS, WORKFLOW_CHECK_CONTEXTS, WORKFLOW_TEMPLATES, WORKFLOW_TEMPLATE_PROPERTIES, codecovComponentBlock, codecovConfig, createAstromech, createCodecovConfig, createTsconfig, deployOnRelease, deriveDeployPaths, ensureIfNotFound, extractPreviewConfig, generateCombinedDeployContent, generateThinCallerContent, matchNamespace, mergeCodecovComponents, normalizeWorkflowWith, readWorkspacePackages, requiredChecks, reusableTemplates, runCi, runGroup, runTask };
|