@theholocron/astromech 5.0.0-alpha.11 → 5.0.0-alpha.110
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 +52 -26
- package/dist/config/index.d.mts +16 -1
- package/dist/config/index.mjs +3 -25
- package/dist/index.d.mts +102 -15
- package/dist/index.mjs +451 -28
- package/dist/load-XHHfD9TX.mjs +44 -0
- package/package.json +7 -7
- package/dist/schema-Cf5dfaDO.mjs +0 -11
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { r as normalizeTaskEntry, t as loadTasksConfig } from "./load-XHHfD9TX.mjs";
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
3
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
4
|
-
import { join } from "node:path";
|
|
4
|
+
import { join, relative } from "node:path";
|
|
5
5
|
//#region src/registry.ts
|
|
6
6
|
const TASKS = {
|
|
7
7
|
"verification.unitTests": {
|
|
@@ -299,11 +299,20 @@ const LINTER_NAMES = new Set(Object.keys(LINTERS));
|
|
|
299
299
|
* Resolve the linter set for a repo. An `explicit` list (from `config.tasks`)
|
|
300
300
|
* picks the candidate set; otherwise the candidates are every `always` linter
|
|
301
301
|
* plus every `detect`-gated linter whose filenames are present at the repo
|
|
302
|
-
* root
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
*
|
|
302
|
+
* root, OR whose mapped `@theholocron/*-config` package resolves per
|
|
303
|
+
* {@link sharedConfigAvailable} — a repo can't run eslint with genuinely no
|
|
304
|
+
* config anywhere (super-linter FATALs if you ask it to,
|
|
305
|
+
* theholocron/holocron#654), but a *shared* one it has installed counts
|
|
306
|
+
* (holocron#795: proving `clients`' eslint.config.ts files reduce to zero
|
|
307
|
+
* remaining content is only half the story — the file itself was still load-
|
|
308
|
+
* bearing here as the sole "eslint is even configured" signal until this
|
|
309
|
+
* fallback existed). `always` linters are unconditional. Result is ordered
|
|
310
|
+
* by {@link LINTERS} declaration order.
|
|
311
|
+
*
|
|
312
|
+
* `sharedConfigAvailable` is caller-computed (`run.ts`'s `runLinterGroup`
|
|
313
|
+
* already resolves each linter's `resolveToolConfig()` per member to build
|
|
314
|
+
* its actual command — reusing that instead of duplicating the package-
|
|
315
|
+
* resolution logic here keeps this function itself pure and file-I/O-free).
|
|
307
316
|
*
|
|
308
317
|
* @throws when an `explicit` name is not in the registry — a typo is a
|
|
309
318
|
* config bug, not a linter to silently skip.
|
|
@@ -311,17 +320,18 @@ const LINTER_NAMES = new Set(Object.keys(LINTERS));
|
|
|
311
320
|
function resolveLinters(opts) {
|
|
312
321
|
const order = Object.keys(LINTERS);
|
|
313
322
|
const present = new Set(opts.rootFiles);
|
|
314
|
-
const
|
|
323
|
+
const shared = opts.sharedConfigAvailable ?? /* @__PURE__ */ new Set();
|
|
324
|
+
const configPresent = (name, def) => def.always === true || (def.detect ?? []).some((f) => present.has(f)) || shared.has(name);
|
|
315
325
|
if (opts.explicit && opts.explicit.length > 0) {
|
|
316
326
|
const unknown = opts.explicit.filter((n) => !LINTER_NAMES.has(n));
|
|
317
327
|
if (unknown.length > 0) throw new Error(`unknown linter${unknown.length > 1 ? "s" : ""} ${unknown.map((n) => `"${n}"`).join(", ")} — known: ${order.join(", ")}`);
|
|
318
328
|
const wanted = new Set(opts.explicit);
|
|
319
|
-
return order.filter((n) => wanted.has(n) && configPresent(LINTERS[n])).map((name) => ({
|
|
329
|
+
return order.filter((n) => wanted.has(n) && configPresent(n, LINTERS[n])).map((name) => ({
|
|
320
330
|
name,
|
|
321
331
|
def: LINTERS[name]
|
|
322
332
|
}));
|
|
323
333
|
}
|
|
324
|
-
return order.filter((name) => configPresent(LINTERS[name])).map((name) => ({
|
|
334
|
+
return order.filter((name) => configPresent(name, LINTERS[name])).map((name) => ({
|
|
325
335
|
name,
|
|
326
336
|
def: LINTERS[name]
|
|
327
337
|
}));
|
|
@@ -364,18 +374,61 @@ const TOOL_CONFIGS = {
|
|
|
364
374
|
flag: ["--config"]
|
|
365
375
|
}
|
|
366
376
|
};
|
|
377
|
+
/**
|
|
378
|
+
* Every filename a tool's own auto-discovery would pick up from `cwd` —
|
|
379
|
+
* same extension set `registry.ts`'s `turbo.inputs`/`local.detect` entries
|
|
380
|
+
* already use for these tools, kept in sync by hand (no shared source of
|
|
381
|
+
* truth between the two today). Presence of any one of these means the
|
|
382
|
+
* package has its own local config — customized or not, `resolveToolConfig`
|
|
383
|
+
* can't tell the difference from the filename alone, so it defers to it
|
|
384
|
+
* either way (#749).
|
|
385
|
+
*/
|
|
386
|
+
const LOCAL_CONFIG_FILES = {
|
|
387
|
+
eslint: [
|
|
388
|
+
"eslint.config.ts",
|
|
389
|
+
"eslint.config.js",
|
|
390
|
+
"eslint.config.mjs",
|
|
391
|
+
"eslint.config.cjs"
|
|
392
|
+
],
|
|
393
|
+
prettier: [
|
|
394
|
+
"prettier.config.ts",
|
|
395
|
+
"prettier.config.js",
|
|
396
|
+
"prettier.config.mjs",
|
|
397
|
+
"prettier.config.cjs"
|
|
398
|
+
],
|
|
399
|
+
vitest: [
|
|
400
|
+
"vitest.config.ts",
|
|
401
|
+
"vitest.config.js",
|
|
402
|
+
"vitest.config.mjs"
|
|
403
|
+
],
|
|
404
|
+
tsdown: [
|
|
405
|
+
"tsdown.config.ts",
|
|
406
|
+
"tsdown.config.js",
|
|
407
|
+
"tsdown.config.mjs",
|
|
408
|
+
"tsdown.config.cjs"
|
|
409
|
+
],
|
|
410
|
+
commitlint: [
|
|
411
|
+
"commitlint.config.ts",
|
|
412
|
+
"commitlint.config.js",
|
|
413
|
+
"commitlint.config.mjs",
|
|
414
|
+
"commitlint.config.cjs"
|
|
415
|
+
]
|
|
416
|
+
};
|
|
367
417
|
/** Every tool name the resolver knows how to point at a shared config. */
|
|
368
418
|
const RESOLVABLE_TOOLS = new Set(Object.keys(TOOL_CONFIGS));
|
|
369
419
|
/**
|
|
370
420
|
* `<flag> <absolute path>` for `tool`, resolved against `cwd`'s
|
|
371
|
-
* `node_modules` — or `[]` when the tool isn't mapped,
|
|
372
|
-
*
|
|
373
|
-
*
|
|
374
|
-
*
|
|
421
|
+
* `node_modules` — or `[]` when the tool isn't mapped, `cwd` already has its
|
|
422
|
+
* own local config file for it (#749 — local always wins, customized or
|
|
423
|
+
* not), the shared package isn't installed, its `package.json` doesn't
|
|
424
|
+
* declare the export subpath this needs, or the resolved file doesn't
|
|
425
|
+
* actually exist on disk (a stale install, or a package version that
|
|
426
|
+
* predates the export existing).
|
|
375
427
|
*/
|
|
376
428
|
function resolveToolConfig(tool, cwd, deps) {
|
|
377
429
|
const spec = TOOL_CONFIGS[tool];
|
|
378
430
|
if (!spec) return [];
|
|
431
|
+
if ((LOCAL_CONFIG_FILES[tool] ?? []).some((name) => deps.fileExists(joinPath(cwd, name)))) return [];
|
|
379
432
|
const pkgDir = joinPath(cwd, "node_modules", ...spec.package.split("/"));
|
|
380
433
|
const pkgJsonPath = joinPath(pkgDir, "package.json");
|
|
381
434
|
if (!deps.fileExists(pkgJsonPath)) return [];
|
|
@@ -537,7 +590,7 @@ function runTask(input) {
|
|
|
537
590
|
def.local.command,
|
|
538
591
|
...passthrough
|
|
539
592
|
]);
|
|
540
|
-
const runner = resolveRunner(def.local, cwd, listDir);
|
|
593
|
+
const runner = resolveRunner(def.local, cwd, listDir, readFile);
|
|
541
594
|
if (runner) {
|
|
542
595
|
const flags = (def.flags?.[runner.tool] ?? []).filter((f) => !passthrough.includes(f));
|
|
543
596
|
const configFlags = resolveToolConfig(runner.tool, cwd, {
|
|
@@ -600,7 +653,7 @@ function runTask(input) {
|
|
|
600
653
|
* here, flag the rest, let CI enforce.
|
|
601
654
|
*/
|
|
602
655
|
function runUnit(input, label, local, run, passthrough) {
|
|
603
|
-
const { print, logger, listDir, lookPath, cwd } = input;
|
|
656
|
+
const { print, logger, listDir, lookPath, cwd, readFile } = input;
|
|
604
657
|
const skip = (msg) => {
|
|
605
658
|
print(input.required ? `✗ ${msg} (required)` : `· ${msg}`);
|
|
606
659
|
logger[input.required ? "warn" : "debug"]({
|
|
@@ -613,7 +666,7 @@ function runUnit(input, label, local, run, passthrough) {
|
|
|
613
666
|
};
|
|
614
667
|
};
|
|
615
668
|
if (local) {
|
|
616
|
-
const runner = resolveRunner(local, cwd, listDir);
|
|
669
|
+
const runner = resolveRunner(local, cwd, listDir, readFile);
|
|
617
670
|
if (!runner) return skip(`no ${label} runner for this repo`);
|
|
618
671
|
const found = lookPath(cwd, runner.tool);
|
|
619
672
|
if (!found) return skip(`${runner.tool} not installed locally for ${label} — enforced in CI`);
|
|
@@ -688,9 +741,14 @@ function runLinterGroup(input, names, passthrough) {
|
|
|
688
741
|
} catch {
|
|
689
742
|
rootFiles = [];
|
|
690
743
|
}
|
|
744
|
+
const sharedConfigAvailable = new Set(names.filter((name) => resolveToolConfig(name, cwd, {
|
|
745
|
+
readFile,
|
|
746
|
+
fileExists
|
|
747
|
+
}).length > 0));
|
|
691
748
|
const resolved = resolveLinters({
|
|
692
749
|
explicit: names,
|
|
693
|
-
rootFiles
|
|
750
|
+
rootFiles,
|
|
751
|
+
sharedConfigAvailable
|
|
694
752
|
});
|
|
695
753
|
const reports = [];
|
|
696
754
|
let anyHasLocalBin = false;
|
|
@@ -749,19 +807,53 @@ const mkRunner = (tool, args = []) => ({
|
|
|
749
807
|
});
|
|
750
808
|
/**
|
|
751
809
|
* Resolve `{ tool, args }` for a runner, applying `detect[]` against repo
|
|
752
|
-
* files
|
|
810
|
+
* files, falling back (#750) to each candidate's `tool` name against
|
|
811
|
+
* `package.json`'s own declared dependencies when neither matches by
|
|
812
|
+
* filename. Only called for `tool` / `detect` runners — the caller handles
|
|
753
813
|
* `command` runners itself, so `local.detect` is present whenever
|
|
754
814
|
* `local.tool` is not.
|
|
815
|
+
*
|
|
816
|
+
* The dependency fallback matters once a Bucket A migration (#680) deletes
|
|
817
|
+
* a build tool's own config file (`tsdown.config.ts`, `vite.config.ts`, …):
|
|
818
|
+
* that file was the *only* signal `detect[]` had to offer, so removing it
|
|
819
|
+
* used to make `delivery.build` report "no task for this repo" and silently
|
|
820
|
+
* skip the build, even though the tool is still very much in use — a
|
|
821
|
+
* package can't actually build with `tsdown` without it in
|
|
822
|
+
* `dependencies`/`devDependencies`, so that's a signal at least as reliable
|
|
823
|
+
* as the config file, and one deleting the file doesn't remove.
|
|
824
|
+
*
|
|
825
|
+
* **Per-candidate, not two full passes** (#750 follow-up — the first
|
|
826
|
+
* shipped version got this wrong): `delivery.build`'s own `detect[]` ends
|
|
827
|
+
* with `{ when: /^tsconfig\.json$/, tool: "tsc" }` as its last-resort
|
|
828
|
+
* fallback, and `tsconfig.json` exists in essentially every package
|
|
829
|
+
* regardless of build tool. A "check every candidate's filename, *then*
|
|
830
|
+
* check every candidate's dependency" two-pass design would let that
|
|
831
|
+
* always-present fallback file win over a real `tsdown`/`vite`/`rollup`
|
|
832
|
+
* dependency match, the moment the earlier candidates' own config files are
|
|
833
|
+
* gone — exactly backwards. Checking `(file match) || (dependency match)`
|
|
834
|
+
* together for each candidate before moving to the next preserves the
|
|
835
|
+
* intended precedence: a real signal for an *earlier* candidate, by either
|
|
836
|
+
* means, still wins over a later candidate's mere filename presence.
|
|
755
837
|
*/
|
|
756
|
-
function resolveRunner(local, cwd, listDir) {
|
|
838
|
+
function resolveRunner(local, cwd, listDir, readFile) {
|
|
757
839
|
if (local.tool) return mkRunner(local.tool, local.args);
|
|
758
840
|
let files;
|
|
759
841
|
try {
|
|
760
842
|
files = listDir(cwd);
|
|
761
843
|
} catch {
|
|
762
|
-
|
|
844
|
+
files = [];
|
|
845
|
+
}
|
|
846
|
+
const deps = packageJsonDependencies(cwd, readFile);
|
|
847
|
+
for (const candidate of local.detect) if (files.some((f) => candidate.when.test(f)) || deps.has(candidate.tool)) return mkRunner(candidate.tool, candidate.args);
|
|
848
|
+
}
|
|
849
|
+
/** Every key from `package.json`'s `dependencies` + `devDependencies`, or an empty set on any read/parse failure. */
|
|
850
|
+
function packageJsonDependencies(cwd, readFile) {
|
|
851
|
+
try {
|
|
852
|
+
const pkg = JSON.parse(readFile(join(cwd, "package.json")));
|
|
853
|
+
return /* @__PURE__ */ new Set([...Object.keys(pkg.dependencies ?? {}), ...Object.keys(pkg.devDependencies ?? {})]);
|
|
854
|
+
} catch {
|
|
855
|
+
return /* @__PURE__ */ new Set();
|
|
763
856
|
}
|
|
764
|
-
for (const candidate of local.detect) if (files.some((f) => candidate.when.test(f))) return mkRunner(candidate.tool, candidate.args);
|
|
765
857
|
}
|
|
766
858
|
function resolveBin(cwd, tool, fileExists) {
|
|
767
859
|
const local = join(cwd, "node_modules", ".bin", tool);
|
|
@@ -823,7 +915,7 @@ const WORKFLOW_TEMPLATES = {
|
|
|
823
915
|
"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",
|
|
824
916
|
"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",
|
|
825
917
|
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",
|
|
826
|
-
review: "name:
|
|
918
|
+
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",
|
|
827
919
|
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
920
|
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
921
|
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",
|
|
@@ -1296,7 +1388,7 @@ function requiredChecks(config) {
|
|
|
1296
1388
|
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";
|
|
1297
1389
|
//#endregion
|
|
1298
1390
|
//#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";
|
|
1391
|
+
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\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 # 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";
|
|
1300
1392
|
//#endregion
|
|
1301
1393
|
//#region src/templates/reusable/actions/install.yml
|
|
1302
1394
|
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";
|
|
@@ -1323,7 +1415,7 @@ var setup_node_default = "name: Setup Node\ndescription: Install pnpm and Node.j
|
|
|
1323
1415
|
*/
|
|
1324
1416
|
/** `workflow_call` implementations → `.github/workflows/<name>.yml`. */
|
|
1325
1417
|
const REUSABLE_WORKFLOWS = {
|
|
1326
|
-
"verification.unitTests": "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 - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0\n name: Upload coverage to Codecov\n with:\n use_oidc: true\n\n - uses: codecov/
|
|
1418
|
+
"verification.unitTests": "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",
|
|
1327
1419
|
"verification.typeSafety": "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",
|
|
1328
1420
|
"verification.performance": "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",
|
|
1329
1421
|
"sourceQuality.staticAnalysis": "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",
|
|
@@ -1333,12 +1425,13 @@ const REUSABLE_WORKFLOWS = {
|
|
|
1333
1425
|
"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
1426
|
"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
1427
|
"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",
|
|
1428
|
+
"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 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",
|
|
1337
1429
|
"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
1430
|
"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
1431
|
"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
1432
|
"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
1433
|
"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",
|
|
1434
|
+
"platform.dispatchedCheck": "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",
|
|
1342
1435
|
"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
1436
|
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
1437
|
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",
|
|
@@ -1466,6 +1559,17 @@ function turboConfig(config) {
|
|
|
1466
1559
|
* (`Packages in scope: docs, root-lib`) without disturbing the sibling
|
|
1467
1560
|
* package's own caching.
|
|
1468
1561
|
*
|
|
1562
|
+
* A matching script key alone isn't enough — its *value* has to be a real
|
|
1563
|
+
* command, not the generated `holocron run <task> --` wrapper (#846, the
|
|
1564
|
+
* counterpart `mergePackageJsonScripts()` already had to learn for #747/
|
|
1565
|
+
* #748). A repo whose root predates this tooling can already have that exact
|
|
1566
|
+
* wrapper sitting in a task-name-matching script with nothing ever having
|
|
1567
|
+
* generated it this session; adding `.` there fires #747's recursion
|
|
1568
|
+
* immediately (`turbo run <task>` → root's own `<task>` script → the wrapper
|
|
1569
|
+
* → `holocron run <task>` sees `turbo.json` still defines it → `turbo run
|
|
1570
|
+
* <task>` again, forever). Only a matching script whose value is *not* that
|
|
1571
|
+
* wrapper counts as "root is genuinely a directly-buildable package".
|
|
1572
|
+
*
|
|
1469
1573
|
* A targeted line-based edit, not a full YAML parse/reserialize — same
|
|
1470
1574
|
* pattern `codecov.ts`'s `mergeCodecovComponents()` already uses for editing
|
|
1471
1575
|
* an existing generated file. `pnpm-workspace.yaml` routinely carries a
|
|
@@ -1477,7 +1581,10 @@ function turboConfig(config) {
|
|
|
1477
1581
|
* every repo checked actually uses.
|
|
1478
1582
|
*/
|
|
1479
1583
|
function ensureRootWorkspaceMember(workspaceYaml, rootScripts, taskNames) {
|
|
1480
|
-
if (!taskNames.some((t) =>
|
|
1584
|
+
if (!taskNames.some((t) => {
|
|
1585
|
+
const script = rootScripts[t];
|
|
1586
|
+
return script !== void 0 && !isGeneratedRunWrapper(script, t);
|
|
1587
|
+
})) return {
|
|
1481
1588
|
content: workspaceYaml,
|
|
1482
1589
|
changed: false
|
|
1483
1590
|
};
|
|
@@ -1510,6 +1617,69 @@ function ensureRootWorkspaceMember(workspaceYaml, rootScripts, taskNames) {
|
|
|
1510
1617
|
changed: true
|
|
1511
1618
|
};
|
|
1512
1619
|
}
|
|
1620
|
+
/**
|
|
1621
|
+
* Matches the exact shape `packageScripts()` generates for `taskName` —
|
|
1622
|
+
* `<bin> run <taskName> --`, `<bin>` normally `holocron` but substitutable
|
|
1623
|
+
* (`holocronScript`, itself allowed to be multi-word, e.g. `"node
|
|
1624
|
+
* packages/cli/dist/cli.mjs"` — see its own test). Checking the *suffix*
|
|
1625
|
+
* rather than the whole string means an old wrapper survives a
|
|
1626
|
+
* `holocronScript` rename intact — it's still recognized as a wrapper (safe
|
|
1627
|
+
* to update to the new one), not mistaken for a real command. Mirrors
|
|
1628
|
+
* `sync.ts`'s private `isGeneratedRunWrapper()` — same predicate, needed on
|
|
1629
|
+
* both sides of #692/#747: whether to *write* the wrapper, and here, whether
|
|
1630
|
+
* an *existing* one should count as "root is directly buildable".
|
|
1631
|
+
*/
|
|
1632
|
+
function isGeneratedRunWrapper(command, taskName) {
|
|
1633
|
+
return command.endsWith(` run ${taskName} --`);
|
|
1634
|
+
}
|
|
1635
|
+
/**
|
|
1636
|
+
* Fallback pin when a repo's own `pnpm-workspace.yaml` has no `turbo`
|
|
1637
|
+
* catalog entry yet — matches this repo's own catalog version (the
|
|
1638
|
+
* canonical source-of-truth repo for the whole org's tooling).
|
|
1639
|
+
*/
|
|
1640
|
+
const DEFAULT_TURBO_VERSION = "^2.10.12";
|
|
1641
|
+
/**
|
|
1642
|
+
* Ensures `turbo` is declared in `package.json#devDependencies` — found
|
|
1643
|
+
* the hard way rolling this out to `observability` (predates #691):
|
|
1644
|
+
* `turboConfig()` writes a `turbo.json` the moment any task has fan-out
|
|
1645
|
+
* config, but never touches `package.json`, so a repo that predates this
|
|
1646
|
+
* feature (or never had `turbo` installed for any other reason) ends up
|
|
1647
|
+
* with a `turbo.json` and nothing local to run it. `resolveBin()`
|
|
1648
|
+
* (`run.ts`) falls back to a bare `turbo` on `PATH` when
|
|
1649
|
+
* `node_modules/.bin/turbo` doesn't exist — on a machine with no global
|
|
1650
|
+
* `turbo` at all that's a hard failure for every task; on one that
|
|
1651
|
+
* happens to have an unrelated global install, silent version skew
|
|
1652
|
+
* (confirmed empirically: a global 2.6.0 rejected this repo's generated
|
|
1653
|
+
* `turbo.json` outright — `Found an unknown key "globalDependencies"`,
|
|
1654
|
+
* a schema key from a newer version than the one actually resolved).
|
|
1655
|
+
*
|
|
1656
|
+
* Prefers `"catalog:"` when the repo's own `pnpm-workspace.yaml` already
|
|
1657
|
+
* has a `turbo` catalog entry — matches every already-migrated repo
|
|
1658
|
+
* (`holocron`, `clients`, …) — falling back to a real pinned version
|
|
1659
|
+
* otherwise. Never touches an existing `devDependencies.turbo` entry, so
|
|
1660
|
+
* a repo pinning its own version on purpose is left alone — idempotent,
|
|
1661
|
+
* a no-op once set either way.
|
|
1662
|
+
*/
|
|
1663
|
+
function ensureTurboDependency(packageJson, workspaceYaml) {
|
|
1664
|
+
const pkg = JSON.parse(packageJson);
|
|
1665
|
+
if (typeof pkg.devDependencies?.turbo === "string") return {
|
|
1666
|
+
content: packageJson,
|
|
1667
|
+
changed: false
|
|
1668
|
+
};
|
|
1669
|
+
const hasTurboCatalogEntry = /^\s*turbo:\s*\S/m.test(workspaceYaml);
|
|
1670
|
+
const devDependencies = {
|
|
1671
|
+
...pkg.devDependencies,
|
|
1672
|
+
turbo: hasTurboCatalogEntry ? "catalog:" : DEFAULT_TURBO_VERSION
|
|
1673
|
+
};
|
|
1674
|
+
const updated = {
|
|
1675
|
+
...pkg,
|
|
1676
|
+
devDependencies
|
|
1677
|
+
};
|
|
1678
|
+
return {
|
|
1679
|
+
content: `${JSON.stringify(updated, null, 2)}\n`,
|
|
1680
|
+
changed: true
|
|
1681
|
+
};
|
|
1682
|
+
}
|
|
1513
1683
|
//#endregion
|
|
1514
1684
|
//#region src/astromech.ts
|
|
1515
1685
|
/**
|
|
@@ -1569,6 +1739,7 @@ function createAstromech(options) {
|
|
|
1569
1739
|
const isDocsSite = entry.name === "knowledge.docs";
|
|
1570
1740
|
const isComponents = entry.name === "knowledge.components";
|
|
1571
1741
|
if (entry.ci === false || !KNOWN_WORKFLOWS.has(entry.name) && !isDocsSite && !isComponents) continue;
|
|
1742
|
+
if (entry.name === "delivery.deploy" && entry.with?.["on"] === "release") continue;
|
|
1572
1743
|
const rawWith = isDocsSite ? {
|
|
1573
1744
|
preview: true,
|
|
1574
1745
|
...entry.with,
|
|
@@ -1615,7 +1786,259 @@ function createAstromech(options) {
|
|
|
1615
1786
|
turboConfig: () => turboConfig(options.config ?? {}),
|
|
1616
1787
|
ensureRootWorkspaceMember: (workspaceYaml, rootScripts) => {
|
|
1617
1788
|
return ensureRootWorkspaceMember(workspaceYaml, rootScripts, (options.config?.tasks ?? []).map(normalizeTaskEntry).filter((e) => e.local !== false).map((e) => e.name));
|
|
1789
|
+
},
|
|
1790
|
+
ensureTurboDependency: (packageJson, workspaceYaml) => ensureTurboDependency(packageJson, workspaceYaml)
|
|
1791
|
+
};
|
|
1792
|
+
}
|
|
1793
|
+
//#endregion
|
|
1794
|
+
//#region src/deploy-on-release.ts
|
|
1795
|
+
/**
|
|
1796
|
+
* Deploy workspace packages once a release has published (holocron#930).
|
|
1797
|
+
*
|
|
1798
|
+
* A package opts in with a `delivery.deploy` task in its own `holocron.config`
|
|
1799
|
+
* whose `with.on` is `"release"`:
|
|
1800
|
+
*
|
|
1801
|
+
* ```ts
|
|
1802
|
+
* // packages/<app>/holocron.config.ts
|
|
1803
|
+
* tasks: [{ name: "delivery.deploy", with: { on: "release", channel: "alpha" } }];
|
|
1804
|
+
* ```
|
|
1805
|
+
*
|
|
1806
|
+
* `channel` defaults to `main` (a stable release); set a prerelease identifier
|
|
1807
|
+
* such as `alpha` to deploy those releases instead.
|
|
1808
|
+
*
|
|
1809
|
+
* The shared `delivery.publish` workflow runs this (`holocron deploy-on-release`)
|
|
1810
|
+
* as its own `deploy` job after the release job, so a deploy is a separate,
|
|
1811
|
+
* re-runnable check rather than a step buried in the release log. Each package
|
|
1812
|
+
* with such a task deploys when the release is on its `channel` and changes a file under its paths: the package itself plus its
|
|
1813
|
+
* `workspace:*` dependencies ({@link defaultPaths}), or an explicit
|
|
1814
|
+
* `with.paths`. It first waits for npm to serve the release's new versions of
|
|
1815
|
+
* those dependencies ({@link waitForPublished}) — the registry can lag a version
|
|
1816
|
+
* it has just accepted, and the deploy installs them — then runs
|
|
1817
|
+
* `pnpm --filter <package> delivery.deploy`.
|
|
1818
|
+
*
|
|
1819
|
+
* The release is already published by then, so a failed deploy can't undo it;
|
|
1820
|
+
* it fails this job instead, loudly and re-runnable on its own. A task is
|
|
1821
|
+
* `skip`ped (not failed) only when it doesn't apply to this release.
|
|
1822
|
+
*
|
|
1823
|
+
* A `delivery.deploy` task with `on: "release"` generates no CI workflow.
|
|
1824
|
+
*/
|
|
1825
|
+
/** A stable release (no prerelease identifier) is on `main`, the default channel for a task. */
|
|
1826
|
+
const MAIN_CHANNEL = "main";
|
|
1827
|
+
const DEPLOY_TASK = "delivery.deploy";
|
|
1828
|
+
/** How long to wait for npm to serve the release's versions, and how often to ask. */
|
|
1829
|
+
const WAIT_TIMEOUT_MS = 3e5;
|
|
1830
|
+
const WAIT_INTERVAL_MS = 1e4;
|
|
1831
|
+
/** Every package under `<repoRoot>/packages/*` with a readable, named `package.json`. */
|
|
1832
|
+
function workspacePackages(repoRoot, { readJson, listDirs }) {
|
|
1833
|
+
const packagesDir = join(repoRoot, "packages");
|
|
1834
|
+
return listDirs(packagesDir).flatMap((folder) => {
|
|
1835
|
+
try {
|
|
1836
|
+
const manifest = readJson(join(packagesDir, folder, "package.json"));
|
|
1837
|
+
if (!manifest.name) return [];
|
|
1838
|
+
return [{
|
|
1839
|
+
name: manifest.name,
|
|
1840
|
+
dir: join(packagesDir, folder),
|
|
1841
|
+
version: manifest.version,
|
|
1842
|
+
dependencies: manifest.dependencies ?? {}
|
|
1843
|
+
}];
|
|
1844
|
+
} catch {
|
|
1845
|
+
return [];
|
|
1846
|
+
}
|
|
1847
|
+
});
|
|
1848
|
+
}
|
|
1849
|
+
/** The `workspace:*` dependencies of `pkg`, as workspace packages (with their current version). */
|
|
1850
|
+
function workspaceDependencies(pkg, all) {
|
|
1851
|
+
const names = new Set(Object.entries(pkg.dependencies).filter(([, spec]) => String(spec).startsWith("workspace:")).map(([name]) => name));
|
|
1852
|
+
return all.filter((candidate) => names.has(candidate.name));
|
|
1853
|
+
}
|
|
1854
|
+
/**
|
|
1855
|
+
* The paths a release must touch to redeploy `pkg` when its manifest doesn't
|
|
1856
|
+
* list them: its own folder plus the folder of every
|
|
1857
|
+
* {@link workspaceDependencies} entry. Derived rather than listed so a new or
|
|
1858
|
+
* dropped workspace dependency needs no second edit.
|
|
1859
|
+
*
|
|
1860
|
+
* @returns Repo-relative prefixes, each ending in `/`.
|
|
1861
|
+
*/
|
|
1862
|
+
function defaultPaths(repoRoot, pkg, all) {
|
|
1863
|
+
return [pkg, ...workspaceDependencies(pkg, all)].map(({ dir }) => `${relative(repoRoot, dir)}/`);
|
|
1864
|
+
}
|
|
1865
|
+
/**
|
|
1866
|
+
* The packages whose manifest declares a release-time `delivery.deploy`
|
|
1867
|
+
* (`with.on === "release"`), with the task's options resolved.
|
|
1868
|
+
*/
|
|
1869
|
+
function deployTargets(packages, manifests) {
|
|
1870
|
+
return packages.flatMap((pkg) => (manifests.get(pkg.dir)?.tasks ?? []).flatMap((item) => {
|
|
1871
|
+
const entry = normalizeTaskEntry(item);
|
|
1872
|
+
if (entry.name !== DEPLOY_TASK || entry.with?.["on"] !== "release") return [];
|
|
1873
|
+
const { channel, paths } = entry.with;
|
|
1874
|
+
return [{
|
|
1875
|
+
pkg,
|
|
1876
|
+
channel: typeof channel === "string" ? channel : MAIN_CHANNEL,
|
|
1877
|
+
paths: Array.isArray(paths) ? paths.map(String) : void 0
|
|
1878
|
+
}];
|
|
1879
|
+
}));
|
|
1880
|
+
}
|
|
1881
|
+
/**
|
|
1882
|
+
* Waits until npm serves every `name@version` in `packages`, asking with
|
|
1883
|
+
* `npm view --prefer-online` (so npm's local cache can't answer for the
|
|
1884
|
+
* registry) every `intervalMs`, for up to `timeoutMs`.
|
|
1885
|
+
*
|
|
1886
|
+
* @returns The specs npm still doesn't serve; empty once all are available.
|
|
1887
|
+
*/
|
|
1888
|
+
async function waitForPublished({ packages, run, sleep, now, cwd, timeoutMs = WAIT_TIMEOUT_MS, intervalMs = WAIT_INTERVAL_MS }) {
|
|
1889
|
+
const deadline = now() + timeoutMs;
|
|
1890
|
+
let pending = packages.map(({ name, version }) => `${name}@${version}`);
|
|
1891
|
+
for (;;) {
|
|
1892
|
+
pending = pending.filter((spec) => {
|
|
1893
|
+
const version = spec.slice(spec.lastIndexOf("@") + 1);
|
|
1894
|
+
const result = run("npm", [
|
|
1895
|
+
"view",
|
|
1896
|
+
spec,
|
|
1897
|
+
"version",
|
|
1898
|
+
"--prefer-online"
|
|
1899
|
+
], { cwd });
|
|
1900
|
+
return !(result.status === 0 && result.stdout.trim() === version);
|
|
1901
|
+
});
|
|
1902
|
+
if (pending.length === 0 || now() + intervalMs > deadline) return pending;
|
|
1903
|
+
await sleep(intervalMs);
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
/**
|
|
1907
|
+
* @param channel The release's channel: its prerelease identifier (`alpha`), or `main` for a stable release.
|
|
1908
|
+
* @param changedFiles `undefined` when there's no previous release to diff against.
|
|
1909
|
+
*/
|
|
1910
|
+
function shouldDeploy({ channel, configuredChannel, changedFiles, paths }) {
|
|
1911
|
+
if (channel !== configuredChannel) return {
|
|
1912
|
+
deploy: false,
|
|
1913
|
+
reason: `channel "${channel}" isn't "${configuredChannel}"`
|
|
1914
|
+
};
|
|
1915
|
+
if (changedFiles === void 0) return {
|
|
1916
|
+
deploy: true,
|
|
1917
|
+
reason: "no previous release to diff against"
|
|
1918
|
+
};
|
|
1919
|
+
const touched = changedFiles.find((file) => paths.some((path) => file.startsWith(path)));
|
|
1920
|
+
return touched ? {
|
|
1921
|
+
deploy: true,
|
|
1922
|
+
reason: `the release changes ${touched}`
|
|
1923
|
+
} : {
|
|
1924
|
+
deploy: false,
|
|
1925
|
+
reason: `the release changes nothing under ${paths.join(", ")}`
|
|
1926
|
+
};
|
|
1927
|
+
}
|
|
1928
|
+
const defaultRun = (command, args, opts) => spawnSync(command, args, {
|
|
1929
|
+
encoding: "utf8",
|
|
1930
|
+
...opts
|
|
1931
|
+
});
|
|
1932
|
+
const defaultReadJson = (path) => JSON.parse(readFileSync(path, "utf8"));
|
|
1933
|
+
const defaultListDirs = (path) => readdirSync(path, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
1934
|
+
const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
1935
|
+
/**
|
|
1936
|
+
* Deploys every package whose manifest declares a release-time deploy that
|
|
1937
|
+
* applies to this release. Never throws for a deploy problem: each package's
|
|
1938
|
+
* outcome is in the report, and `status: "fail"` is the caller's exit code.
|
|
1939
|
+
*/
|
|
1940
|
+
async function deployOnRelease(options, { run = defaultRun, readJson = defaultReadJson, listDirs = defaultListDirs, sleep = defaultSleep, now = Date.now, loadTasks = loadTasksConfig } = {}) {
|
|
1941
|
+
const { cwd, from, print } = options;
|
|
1942
|
+
const channel = options.channel || MAIN_CHANNEL;
|
|
1943
|
+
const to = options.to ?? "HEAD";
|
|
1944
|
+
const packages = workspacePackages(cwd, {
|
|
1945
|
+
readJson,
|
|
1946
|
+
listDirs
|
|
1947
|
+
});
|
|
1948
|
+
const manifests = /* @__PURE__ */ new Map();
|
|
1949
|
+
for (const pkg of packages) try {
|
|
1950
|
+
manifests.set(pkg.dir, await loadTasks(pkg.dir));
|
|
1951
|
+
} catch (err) {
|
|
1952
|
+
print(`${pkg.name}: couldn't read its task manifest (${err.message}); not deploying it.`);
|
|
1953
|
+
}
|
|
1954
|
+
const targets = deployTargets(packages, manifests);
|
|
1955
|
+
if (targets.length === 0) {
|
|
1956
|
+
print("No package declares a release-time delivery.deploy (with.on: release).");
|
|
1957
|
+
return {
|
|
1958
|
+
status: "skip",
|
|
1959
|
+
results: []
|
|
1960
|
+
};
|
|
1961
|
+
}
|
|
1962
|
+
let diff;
|
|
1963
|
+
const changedFiles = () => {
|
|
1964
|
+
if (diff) return diff;
|
|
1965
|
+
if (!from) return diff = { files: void 0 };
|
|
1966
|
+
const result = run("git", [
|
|
1967
|
+
"diff",
|
|
1968
|
+
"--name-only",
|
|
1969
|
+
from,
|
|
1970
|
+
to
|
|
1971
|
+
], { cwd });
|
|
1972
|
+
return diff = result.status === 0 ? { files: result.stdout.split("\n").filter(Boolean) } : {
|
|
1973
|
+
files: [],
|
|
1974
|
+
error: `git diff ${from} ${to} failed: ${result.stderr || result.status}`
|
|
1975
|
+
};
|
|
1976
|
+
};
|
|
1977
|
+
const results = [];
|
|
1978
|
+
const record = (pkg, status, message) => {
|
|
1979
|
+
results.push({
|
|
1980
|
+
pkg,
|
|
1981
|
+
status,
|
|
1982
|
+
message
|
|
1983
|
+
});
|
|
1984
|
+
print(`${status === "fail" ? "✗ " : ""}${pkg}: ${message}`);
|
|
1985
|
+
};
|
|
1986
|
+
for (const target of targets) {
|
|
1987
|
+
const name = target.pkg.name;
|
|
1988
|
+
const { files, error } = target.channel === channel ? changedFiles() : {
|
|
1989
|
+
files: [],
|
|
1990
|
+
error: void 0
|
|
1991
|
+
};
|
|
1992
|
+
if (error) {
|
|
1993
|
+
record(name, "fail", error);
|
|
1994
|
+
continue;
|
|
1618
1995
|
}
|
|
1996
|
+
const decision = shouldDeploy({
|
|
1997
|
+
channel,
|
|
1998
|
+
configuredChannel: target.channel,
|
|
1999
|
+
changedFiles: files,
|
|
2000
|
+
paths: target.paths ?? defaultPaths(cwd, target.pkg, packages)
|
|
2001
|
+
});
|
|
2002
|
+
if (!decision.deploy) {
|
|
2003
|
+
record(name, "skip", `not deploying: ${decision.reason}.`);
|
|
2004
|
+
continue;
|
|
2005
|
+
}
|
|
2006
|
+
if (options.dryRun) {
|
|
2007
|
+
record(name, "dry-run", `would deploy because ${decision.reason}.`);
|
|
2008
|
+
continue;
|
|
2009
|
+
}
|
|
2010
|
+
const dependencies = workspaceDependencies(target.pkg, packages).flatMap(({ name: dep, version }) => version ? [{
|
|
2011
|
+
name: dep,
|
|
2012
|
+
version
|
|
2013
|
+
}] : []);
|
|
2014
|
+
print(`${name}: waiting for npm to serve ${dependencies.map((d) => `${d.name}@${d.version}`).join(", ") || "nothing"}.`);
|
|
2015
|
+
const unpublished = await waitForPublished({
|
|
2016
|
+
packages: dependencies,
|
|
2017
|
+
run,
|
|
2018
|
+
sleep,
|
|
2019
|
+
now,
|
|
2020
|
+
cwd
|
|
2021
|
+
});
|
|
2022
|
+
if (unpublished.length > 0) {
|
|
2023
|
+
record(name, "fail", `npm still doesn't serve ${unpublished.join(", ")} after ${WAIT_TIMEOUT_MS / 6e4} minutes. Re-run this job once it does.`);
|
|
2024
|
+
continue;
|
|
2025
|
+
}
|
|
2026
|
+
print(`${name}: deploying because ${decision.reason}.`);
|
|
2027
|
+
const result = run("pnpm", [
|
|
2028
|
+
"--filter",
|
|
2029
|
+
name,
|
|
2030
|
+
DEPLOY_TASK
|
|
2031
|
+
], {
|
|
2032
|
+
cwd,
|
|
2033
|
+
env: process.env,
|
|
2034
|
+
stdio: "inherit"
|
|
2035
|
+
});
|
|
2036
|
+
if (result.status === 0) record(name, "ok", "deployed.");
|
|
2037
|
+
else record(name, "fail", `the deploy failed (exit ${result.status}).`);
|
|
2038
|
+
}
|
|
2039
|
+
return {
|
|
2040
|
+
status: results.some((r) => r.status === "fail") ? "fail" : results.every((r) => r.status === "skip") ? "skip" : "ok",
|
|
2041
|
+
results
|
|
1619
2042
|
};
|
|
1620
2043
|
}
|
|
1621
2044
|
//#endregion
|
|
@@ -1646,4 +2069,4 @@ function createTsconfig(options) {
|
|
|
1646
2069
|
return JSON.stringify(config, null, 2) + "\n";
|
|
1647
2070
|
}
|
|
1648
2071
|
//#endregion
|
|
1649
|
-
export { CI_ORDER, KNOWN_TASKS, KNOWN_WORKFLOWS, LINTERS, LINTER_NAMES, RESOLVABLE_TOOLS, REUSABLE_ACTIONS, REUSABLE_WORKFLOWS, TASKS, WORKFLOW_CHECK_CONTEXTS, WORKFLOW_TEMPLATES, WORKFLOW_TEMPLATE_PROPERTIES, codecovComponentBlock, codecovConfig, createAstromech, createCodecovConfig, createTsconfig, deriveDeployPaths, ensureIfNotFound, extractPreviewConfig, generateCombinedDeployContent, generateThinCallerContent, mergeCodecovComponents, normalizeWorkflowWith, readWorkspacePackages, requiredChecks, resolveLinters, resolveToolConfig, reusableTemplates, runCi, runTask };
|
|
2072
|
+
export { CI_ORDER, KNOWN_TASKS, KNOWN_WORKFLOWS, LINTERS, LINTER_NAMES, RESOLVABLE_TOOLS, REUSABLE_ACTIONS, REUSABLE_WORKFLOWS, TASKS, WORKFLOW_CHECK_CONTEXTS, WORKFLOW_TEMPLATES, WORKFLOW_TEMPLATE_PROPERTIES, codecovComponentBlock, codecovConfig, createAstromech, createCodecovConfig, createTsconfig, deployOnRelease, deriveDeployPaths, ensureIfNotFound, extractPreviewConfig, generateCombinedDeployContent, generateThinCallerContent, mergeCodecovComponents, normalizeWorkflowWith, readWorkspacePackages, requiredChecks, resolveLinters, resolveToolConfig, reusableTemplates, runCi, runTask };
|