@theholocron/astromech 5.0.0-alpha.20 → 5.0.0-alpha.201

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,8 +1,636 @@
1
- import { t as normalizeTaskEntry } from "./schema-Cf5dfaDO.mjs";
2
- import { spawnSync } from "node:child_process";
1
+ import { r as normalizeTaskEntry, t as loadTasksConfig } from "./load-DdHEVFlG.mjs";
2
+ import { n as alexrc_default, t as editorconfig_checker_default } from "./editorconfig-checker-DPejtR5z.mjs";
3
3
  import { existsSync, readFileSync, readdirSync } from "node:fs";
4
- import { join } from "node:path";
5
- //#region src/registry.ts
4
+ import { dependencyNames, detectPackageManager, listWorkspacePackagesSync, readPackageJsonSync } from "@theholocron/pkg-utils";
5
+ import { stringifyJson } from "@theholocron/json-utils";
6
+ import { join, relative } from "node:path";
7
+ import { spawnSync } from "node:child_process";
8
+ import { findExecutableSync } from "@theholocron/fs-utils";
9
+ //#region src/templates/codecov/codecov.yml
10
+ var codecov_default = "codecov:\n require_ci_to_pass: true\n\ncoverage:\n precision: 2\n round: down\n status:\n project:\n default:\n target: auto\n threshold: 2%\n # Infra-only PRs (dep bumps, config, lockfile) touch no coverable code,\n # so codecov produces no report to compare. Pass instead of blocking\n # forever on a required check that will never post.\n if_not_found: success\n patch:\n default:\n target: 80%\n if_not_found: success\n\ncomment:\n layout: \"reach,diff,flags,components\"\n behavior: default\n require_changes: true\n\ncomponent_management:\n default_rules:\n statuses:\n - type: patch\n target: 80%\n if_not_found: success\n individual_components:\n";
11
+ //#endregion
12
+ //#region src/generators/codecov/codecov.ts
13
+ /**
14
+ * `codecov.yml` generation — a manifest-derived artifact (component `paths`
15
+ * come from workspace packages; the status targets track the `test` task's
16
+ * coverage setup), the same category as the reusable `test.yml` this package
17
+ * also generates. Moved out of `@theholocron/cli` (theholocron/holocron#650)
18
+ * — `holocron repo setup`'s codecov step now calls {@link codecovConfig} instead
19
+ * of importing this logic directly.
20
+ */
21
+ const INDIVIDUAL_COMPONENTS_MARKER = " individual_components:";
22
+ function scaffoldHeader() {
23
+ return [
24
+ `# Scaffolded by holocron repo setup — edit this file freely.`,
25
+ `# Source: theholocron/holocron · packages/astromech/src/generators/codecov/codecov.ts`,
26
+ ``
27
+ ].join("\n");
28
+ }
29
+ function codecovComponentBlock(packages) {
30
+ if (packages.length === 0) return "\n []\n";
31
+ return "\n" + packages.flatMap(({ slug, dir }) => [
32
+ ` - component_id: ${slug}`,
33
+ ` name: "${slug}"`,
34
+ ` paths:`,
35
+ ` - ${dir ?? "packages"}/${slug}/**`,
36
+ ``
37
+ ]).join("\n");
38
+ }
39
+ /**
40
+ * Ensure every codecov status default carries `if_not_found: success`, so an
41
+ * infra-only PR (dep bump, config, lockfile — no coverable change) passes
42
+ * instead of hanging forever on a required check codecov never posts.
43
+ *
44
+ * Injects the key on the line after each `target:` that sits inside a status
45
+ * block (`coverage.status.*.default` or a `statuses:` list item — indent ≥ 8,
46
+ * or ≥ 8 for a `- type:` list entry), when the immediate sibling lines do not
47
+ * already declare it.
48
+ */
49
+ function ensureIfNotFound(yaml) {
50
+ const lines = yaml.split("\n");
51
+ const out = [];
52
+ for (let i = 0; i < lines.length; i++) {
53
+ const line = lines[i];
54
+ out.push(line);
55
+ const m = /^(\s{8,})target:\s+\S+\s*$/.exec(line);
56
+ if (!m) continue;
57
+ const indent = m[1];
58
+ let alreadySet = false;
59
+ for (let j = i + 1; j < lines.length; j++) {
60
+ const sib = lines[j];
61
+ if (sib.trim() === "") continue;
62
+ const sibIndent = sib.match(/^\s*/)[0].length;
63
+ if (sibIndent < indent.length) break;
64
+ if (sibIndent === indent.length && /^\s*if_not_found\s*:/.test(sib)) {
65
+ alreadySet = true;
66
+ break;
67
+ }
68
+ if (sibIndent === indent.length && /^\s*(- |\w)/.test(sib) && !/^\s*(threshold|flags|paths|branches)\s*:/.test(sib)) break;
69
+ }
70
+ if (!alreadySet) out.push(`${indent}if_not_found: success`);
71
+ }
72
+ return out.join("\n");
73
+ }
74
+ function mergeCodecovComponents(existing, packages) {
75
+ const idx = existing.indexOf(INDIVIDUAL_COMPONENTS_MARKER);
76
+ if (idx === -1) return ensureIfNotFound(existing);
77
+ return ensureIfNotFound(existing.slice(0, idx + 24)) + codecovComponentBlock(packages);
78
+ }
79
+ /**
80
+ * Every public `packages/*`, `apps/*`, and `plugins/*` workspace, sorted by
81
+ * slug. `[]` when none of the three directories exist. Monorepo templates
82
+ * (Next.js, React, …) ship user-facing code under `apps/` alongside library
83
+ * code under `packages/`; `holocron` itself splits vendor plugin packages
84
+ * into their own `plugins/*` root — every recognized root needs a codecov
85
+ * component so `codecov/patch/<slug>` actually posts for each.
86
+ */
87
+ function readWorkspacePackages(repoRoot) {
88
+ return listWorkspacePackagesSync(repoRoot, { roots: [
89
+ "packages",
90
+ "apps",
91
+ "plugins"
92
+ ] }).flatMap(({ name, relativeDir }) => {
93
+ const [dir, slug] = relativeDir.split("/");
94
+ return name ? [{
95
+ slug,
96
+ name,
97
+ dir
98
+ }] : [];
99
+ }).sort((a, b) => a.slug.localeCompare(b.slug));
100
+ }
101
+ /** A brand-new `codecov.yml` — the scaffold header + the base template + this repo's component list. */
102
+ function createCodecovConfig(packages) {
103
+ return `${scaffoldHeader()}${codecov_default.trimEnd()}${codecovComponentBlock(packages)}`;
104
+ }
105
+ /**
106
+ * The full pipeline `holocron repo setup` needs: read this repo's public
107
+ * `packages/*`, then either merge them into an `existing` `codecov.yml` or
108
+ * scaffold a new one.
109
+ */
110
+ function codecovConfig(repoRoot, existing) {
111
+ const packages = readWorkspacePackages(repoRoot);
112
+ return existing != null ? mergeCodecovComponents(existing, packages) : createCodecovConfig(packages);
113
+ }
114
+ //#endregion
115
+ //#region src/templates/reusable/actions/auto-commit.yml
116
+ 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-<sha>) 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";
117
+ //#endregion
118
+ //#region src/templates/reusable/actions/holocron.yml
119
+ var holocron_default = "name: Holocron\ndescription: >\n Run the Holocron CLI — `holocron run <task> [job]` by default, or any other\n subcommand via `command:` (`repo sync`, `sync github`, …). Resolves\n `@theholocron/cli` from `node_modules` (published tarball) or builds it from\n source when it resolves to an unbuilt workspace checkout (the holocron repo\n itself). Requires dependencies to be installed already (run the setup action\n first).\n\ninputs:\n command:\n description: >\n Holocron subcommand. Default `run` — then `task` (and optional `job`)\n name the manifest task. Set to `repo sync` / `sync github` / … to run that\n subcommand directly; `args` becomes everything after it.\n required: false\n default: run\n task:\n description: >\n Manifest task to run when command is run — typecheck, test, build, audit.\n required: false\n default: \"\"\n job:\n description: Optional sub-job within the task (e.g. knip, performance for audit).\n required: false\n default: \"\"\n args:\n description: >\n For `command: run`, extra arguments forwarded to the tool after `--`.\n For any other command, the arguments passed straight to it.\n required: false\n default: \"\"\n\noutputs:\n log-file:\n description: >\n Path to a file holding the full captured stdout+stderr of this\n invocation — the same text the live Actions log shows, saved for a\n caller that wants to ship it elsewhere (e.g. Sentinel's dispatched\n checks shipping it to Axiom, holocron#769/#794).\n value: ${{ steps.run.outputs.log-file }}\n\nruns:\n using: composite\n\n steps:\n - name: Build the workspace when the CLI resolves to an unbuilt checkout\n shell: bash\n # The holocron repo carries @theholocron/cli as `workspace:*`; its bin\n # (dist/cli.mjs) does not exist on a fresh checkout. `pnpm build` is a\n # turbo run — cached and cheap once warm. A no-op everywhere else, where\n # the published tarball already ships dist/.\n #\n # pnpm creates every workspace package's node_modules/.bin/holocron shim\n # once, during the setup action's earlier `pnpm install` — before this\n # build has run, so every one of them is silently skipped (\"Failed to\n # create bin ... ENOENT\"), not just @theholocron/cli's own. That's a\n # one-time decision: a later build doesn't retroactively fix a shim that\n # was never created. Re-running `pnpm install` here (fast — the lockfile\n # hasn't changed, so it only re-links `.bin`) is what actually fixes it,\n # confirmed against a real cold install: `holocron` stays unresolvable\n # from inside any package until this second install runs.\n run: |\n if [ ! -f node_modules/@theholocron/cli/dist/cli.mjs ]; then\n echo \"→ @theholocron/cli dist/ missing — building the workspace from source\" >&2\n pnpm build\n echo \"→ re-linking node_modules/.bin — pnpm skipped it pre-build\" >&2\n pnpm install --frozen-lockfile\n fi\n\n - name: holocron ${{ inputs.command }} ${{ inputs.task }}\n id: run\n shell: bash\n env:\n HOLOCRON_COMMAND: ${{ inputs.command }}\n HOLOCRON_TASK: ${{ inputs.task }}\n HOLOCRON_JOB: ${{ inputs.job }}\n HOLOCRON_ARGS: ${{ inputs.args }}\n run: |\n if [ \"$HOLOCRON_COMMAND\" = \"run\" ]; then\n set -- run \"$HOLOCRON_TASK\"\n [ -n \"$HOLOCRON_JOB\" ] && set -- \"$@\" \"$HOLOCRON_JOB\"\n # shellcheck disable=SC2086\n [ -n \"$HOLOCRON_ARGS\" ] && set -- \"$@\" -- $HOLOCRON_ARGS\n else\n # `command` may be a multi-word path (`repo sync`, `deploy on-release`):\n # word-split it so each token is its own argument.\n # shellcheck disable=SC2086\n set -- $HOLOCRON_COMMAND $HOLOCRON_ARGS\n fi\n # Invoke the built entry directly. `pnpm exec holocron` relies on a\n # node_modules/.bin/holocron shim that pnpm does not create for a\n # workspace package whose dist/ was absent at install time (the holocron\n # repo itself). Resolving the package's main export gives dist/index.mjs;\n # the bin sits next to it as dist/cli.mjs. This is `require.resolve` from\n # node_modules — it works in the holocron monorepo AND in every consumer\n # repo (theholocron/holocron#655), unlike a hard-coded `packages/cli/…`.\n cli=\"$(dirname \"$(node -e 'process.stdout.write(require.resolve(\"@theholocron/cli\"))')\")/cli.mjs\"\n if [ ! -f \"$cli\" ]; then\n echo \"::error::@theholocron/cli entry not found at $cli — is the workspace built?\" >&2\n exit 1\n fi\n echo \"→ $cli $*\" >&2\n # `tee` keeps the live Actions log streaming exactly as before while\n # also capturing it to a file callers can ship elsewhere (log-file\n # output, above). PIPESTATUS[0] -- not $? -- is `node`'s own exit\n # code; $? after a pipe is tee's, which succeeds even when node fails.\n log_file=\"${RUNNER_TEMP}/holocron-output.log\"\n node \"$cli\" \"$@\" 2>&1 | tee \"$log_file\"\n exit_code=${PIPESTATUS[0]}\n echo \"log-file=$log_file\" >> \"$GITHUB_OUTPUT\"\n exit \"$exit_code\"\n";
120
+ //#endregion
121
+ //#region src/templates/reusable/actions/install.yml
122
+ 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";
123
+ //#endregion
124
+ //#region src/templates/reusable/actions/notify.yml
125
+ var notify_default = "name: Notify\ndescription: >\n Post a message to a chat channel through an incoming webhook. The request\n body is Discord-compatible (`content`, `username`, `allowed_mentions`), so\n the webhook must accept that shape. Does nothing (and never fails the\n calling job) when no webhook is provided, so repos that haven't set one up\n are unaffected. Set `strict` for a check whose only purpose is to prove the\n notification works: it then fails when the webhook is missing or rejects the post.\n\ninputs:\n webhook:\n description: >\n Incoming webhook URL for the target channel.\n A webhook posts to exactly one channel, so pass the secret for the\n channel this message belongs in (for example\n `secrets.HOLOCRON_NOTIFY_WEBHOOK_ALERTS`). Empty = skip.\n required: false\n default: \"\"\n message:\n description: Message text. Truncated to 2000 characters.\n required: true\n mention:\n description: >\n Optional role id (digits only) to ping. Only that one role can\n be mentioned; anything else in the message never pings.\n required: false\n default: \"\"\n username:\n description: Display name for the post.\n required: false\n default: \"Holocron\"\n strict:\n description: >\n \"true\" fails the step when no webhook is set or the post is rejected.\n Off by default: a notification problem must never fail the job it reports on.\n required: false\n default: \"false\"\n\nruns:\n using: composite\n steps:\n - name: Post the notification\n shell: bash\n run: |\n if [ -z \"$WEBHOOK\" ]; then\n if [ \"$STRICT\" = \"true\" ]; then\n echo \"::error::No notification webhook set.\"\n exit 1\n fi\n echo \"::notice::No notification webhook set — skipping notification.\"\n exit 0\n fi\n echo \"::add-mask::$WEBHOOK\"\n # allowed_mentions.parse = [] so message text can never ping @everyone\n # or a role; only the explicit `mention` role id is allowed through. The\n # body is built with jq so quotes/newlines in the message can't break the JSON.\n content=\"$MESSAGE\"\n roles='[]'\n # A mention that is not a role id is dropped, which is right for a real\n # notification but would let a check pass without ever pinging anyone.\n if [ \"$STRICT\" = \"true\" ] && [ -n \"$MENTION\" ] && ! [[ \"$MENTION\" =~ ^[0-9]+$ ]]; then\n echo \"::error::The mention is not a role id (digits only).\"\n exit 1\n fi\n if [[ \"$MENTION\" =~ ^[0-9]+$ ]]; then\n content=\"<@&$MENTION> $MESSAGE\"\n roles=$(jq -cn --arg r \"$MENTION\" '[$r]')\n fi\n body=$(jq -n --arg content \"${content:0:2000}\" --arg username \"$USERNAME\" --argjson roles \"$roles\" \\\n '{content: $content, username: $username, allowed_mentions: {parse: [], roles: $roles}}')\n status=$(curl -sS -o /dev/null -w '%{http_code}' --max-time 15 \\\n -H 'Content-Type: application/json' -d \"$body\" \"$WEBHOOK\" || true)\n case \"${status:-000}\" in\n 2??) echo \"Notification posted.\" ;;\n *)\n if [ \"$STRICT\" = \"true\" ]; then\n echo \"::error::Notification failed (HTTP ${status:-000}).\"\n exit 1\n fi\n echo \"::warning::Notification failed (HTTP ${status:-000}).\"\n ;;\n esac\n env:\n WEBHOOK: ${{ inputs.webhook }}\n MESSAGE: ${{ inputs.message }}\n MENTION: ${{ inputs.mention }}\n USERNAME: ${{ inputs.username }}\n STRICT: ${{ inputs.strict }}\n";
126
+ //#endregion
127
+ //#region src/templates/reusable/actions/setup.yml
128
+ var setup_default = "name: Setup\ndescription: Prepare the environment and install project dependencies.\n\ninputs:\n node-version:\n description: Node.js version\n required: false\n default: \"22.x\"\n\nruns:\n using: composite\n\n steps:\n - uses: theholocron/.github/.github/actions/setup-node@main\n with:\n node-version: ${{ inputs.node-version }}\n\n - uses: theholocron/.github/.github/actions/install@main\n\n # `@theholocron/cli`'s `dist/cli.mjs` doesn't exist yet at this point, so\n # pnpm's workspace bin-linking can't create `node_modules/.bin/holocron`\n # (silent `WARN ... ENOENT`, not a hard failure). Every package whose\n # scripts delegate via `holocron run <task> --` then fails at run time\n # with `holocron: not found` (#698). Same fix the `holocron` composite\n # action already applies for its own direct invocation (see its \"Build\n # the workspace when the CLI resolves to an unbuilt checkout\" step) —\n # mirrored here so it also covers *nested* `holocron run <task> --`\n # calls from inside package.json scripts, which that action's own guard\n # doesn't reach. `pnpm build` (not a scoped `--filter`) so the CLI's own\n # workspace dependencies (e.g. astromech) build\n # in topological order too — a scoped build skips them and fails.\n # No-op in every consumer repo, where the published tarball already\n # ships `dist/`.\n - name: Build the Holocron CLI\n shell: bash\n run: |\n if [ ! -f node_modules/@theholocron/cli/dist/cli.mjs ]; then\n echo \"→ @theholocron/cli dist/ missing — building the workspace from source\" >&2\n pnpm build\n echo \"→ re-linking node_modules/.bin — pnpm skipped it pre-build\" >&2\n pnpm install --frozen-lockfile\n fi\n\n - name: Cache Turbo artifacts\n if: ${{ hashFiles('turbo.json') != '' }}\n uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0\n with:\n path: .turbo\n key: turbo-${{ runner.os }}-${{ github.sha }}\n restore-keys: |\n turbo-${{ runner.os }}-\n";
129
+ //#endregion
130
+ //#region src/templates/reusable/actions/setup-node.yml
131
+ var setup_node_default = "name: Setup Node\ndescription: Install pnpm and Node.js with pnpm dependency caching.\n\ninputs:\n node-version:\n description: Node.js version\n required: false\n default: \"22.x\"\nruns:\n using: composite\n\n steps:\n - name: Setup pnpm\n if: ${{ hashFiles('pnpm-lock.yaml') != '' }}\n uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4\n\n - name: Setup Node.js\n uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0\n with:\n node-version: ${{ hashFiles('.node-version') != '' && '' || inputs.node-version }}\n node-version-file: ${{ hashFiles('.node-version') != '' && '.node-version' || '' }}\n cache: ${{ hashFiles('pnpm-lock.yaml') != '' && 'pnpm' || '' }}\n # Do NOT add registry-url here. setup-node writes .npmrc with\n # _authToken=${NODE_AUTH_TOKEN} and sets NPM_CONFIG_USERCONFIG to it.\n # pnpm reads that file, fails env-var substitution when the token is\n # empty, and loses auth entirely. Without registry-url, pnpm 10.15+\n # handles Trusted Publishing via its own native OIDC exchange.\n\n - name: Add node_modules/.bin to PATH\n shell: bash\n run: echo \"$GITHUB_WORKSPACE/node_modules/.bin\" >> $GITHUB_PATH\n";
132
+ //#endregion
133
+ //#region src/templates/reusable/bookkeeping.yml
134
+ var bookkeeping_default$1 = "name: Bookkeeping\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n configuration-path:\n description: Path to the labeler configuration file in the calling repo\n type: string\n required: false\n default: .github/labeler.yml\n\njobs:\n label:\n name: Apply Labels\n permissions:\n contents: read\n issues: write\n pull-requests: write\n runs-on: ubuntu-latest\n timeout-minutes: 5\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n with:\n sparse-checkout: ${{ inputs.configuration-path || '.github/labeler.yml' }}\n sparse-checkout-cone-mode: false\n\n - uses: github/issue-labeler@c1b0f9f52a63158c4adc09425e858e87b32e9685 # v3.4\n if: ${{ github.event_name == 'pull_request' && hashFiles(inputs.configuration-path || '.github/labeler.yml') != '' }}\n # v3.4 bundles Node 20; allow it to run under Actions' current default.\n env:\n ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true\n with:\n # Fall back to default path when triggered directly (not via workflow_call)\n # because inputs.* defaults only apply on workflow_call events.\n configuration-path: ${{ inputs.configuration-path || '.github/labeler.yml' }}\n include-title: 1\n include-body: 0\n sync-labels: 1\n enable-versioned-regex: 0\n repo-token: ${{ github.token }}\n";
135
+ //#endregion
136
+ //#region src/templates/reusable/delivery.build.yml
137
+ var delivery_build_default$1 = "name: Build\n\n# Sanity check that every workspace actually compiles to dist/ — doesn't\n# upload or publish anything. delivery.publish builds again before\n# publishing; this just answers \"does it build?\" on every push/PR.\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n secrets:\n TURBO_TOKEN:\n required: false\n\njobs:\n build:\n name: Build every workspace\n permissions:\n contents: read\n runs-on: ubuntu-latest\n timeout-minutes: 10\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - uses: theholocron/.github/.github/actions/holocron@main\n name: Build\n with:\n task: delivery.build\n";
138
+ //#endregion
139
+ //#region src/templates/reusable/delivery.bundleSize.yml
140
+ var delivery_bundleSize_default$1 = "name: Audit the Bundle Size\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n secrets:\n CODECOV_TOKEN:\n required: false\n TURBO_TOKEN:\n required: false\n\njobs:\n bundle-size:\n name: Upload bundle stats to Codecov\n permissions:\n contents: read\n runs-on: ubuntu-latest\n timeout-minutes: 15\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n # Read by the bundle-stats uploader inside `holocron run delivery.build`.\n CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - uses: theholocron/.github/.github/actions/holocron@main\n name: Build and upload bundle stats\n with:\n task: delivery.build\n";
141
+ //#endregion
142
+ //#region src/templates/reusable/delivery.deploy.yml
143
+ var delivery_deploy_default$1 = "name: Deploy\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n type:\n description: \"Type of deployment: docs or storybook\"\n required: true\n type: string\n storybook-projects:\n description: >\n JSON array of { \"name\"?, \"workingDir\", \"outputDir\"? } objects for storybook deploys.\n Each is built via `pnpm -C <workingDir> build:storybook`. If \"name\" is provided the\n output is placed under `sandbox/<name>/`; omit \"name\" for single-repo deploys and the\n output lands directly in `sandbox/`.\n type: string\n required: false\n default: \"[]\"\n build-script:\n description: pnpm script that builds the Storybook static output (single storybook, type:storybook only)\n type: string\n required: false\n default: build:storybook\n output-dir:\n description: Directory where Storybook writes its static output (single storybook, type:storybook only)\n type: string\n required: false\n default: storybook-static\n\njobs:\n deploy:\n name: Deploy\n runs-on: ubuntu-latest\n permissions:\n contents: read\n pages: write\n id-token: write\n environment:\n name: github-pages\n url: ${{ steps.deployment.outputs.page_url }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - name: Build docs site\n if: ${{ inputs.type == 'docs' }}\n run: pnpm -C docs build\n\n - name: Build Storybook projects\n if: ${{ inputs.storybook-projects != '[]' }}\n env:\n PROJECTS: ${{ inputs.storybook-projects }}\n run: |\n echo \"$PROJECTS\" | jq -c '.[]' | while IFS= read -r project; do\n workingDir=$(echo \"$project\" | jq -r '.workingDir')\n pnpm -C \"$workingDir\" build:storybook\n done\n\n - name: Build Storybook\n if: ${{ inputs.type == 'storybook' && inputs.storybook-projects == '[]' }}\n env:\n BUILD_SCRIPT: ${{ inputs.build-script }}\n run: pnpm run \"$BUILD_SCRIPT\"\n\n - name: Assemble site\n env:\n DEPLOY_TYPE: ${{ inputs.type }}\n PROJECTS: ${{ inputs.storybook-projects }}\n STORYBOOK_OUTPUT_DIR: ${{ inputs.output-dir }}\n run: |\n mkdir -p _site\n if [ \"$DEPLOY_TYPE\" = \"docs\" ]; then\n cp -r docs/dist/. _site/\n fi\n if [ \"$PROJECTS\" != \"[]\" ]; then\n echo \"$PROJECTS\" | jq -c '.[]' | while IFS= read -r project; do\n name=$(echo \"$project\" | jq -r '.name // \"\"')\n workingDir=$(echo \"$project\" | jq -r '.workingDir')\n outputDir=$(echo \"$project\" | jq -r '.outputDir // \"storybook-static\"')\n if [ -n \"$name\" ]; then\n target=\"_site/sandbox/${name}\"\n else\n target=\"_site/sandbox\"\n fi\n mkdir -p \"$target\"\n cp -r \"${workingDir}/${outputDir}/.\" \"$target/\"\n done\n elif [ \"$DEPLOY_TYPE\" = \"storybook\" ]; then\n mkdir -p _site/sandbox\n cp -r \"${STORYBOOK_OUTPUT_DIR}/.\" _site/sandbox/\n fi\n\n - uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3.0.1\n name: Upload pages artifact\n with:\n path: _site\n\n - uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5\n id: deployment\n name: Deploy to GitHub Pages\n";
144
+ //#endregion
145
+ //#region src/templates/reusable/delivery.previewCheck.yml
146
+ var delivery_previewCheck_default$1 = "name: Preview Check\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n preview-url:\n description: The PR's Vercel preview deployment URL, extracted from the triggering `deployment_status` event.\n required: true\n type: string\n sha:\n description: The deployed commit SHA — checked out explicitly, since GitHub evaluates which version of this workflow file to run from the default branch for a deployment_status event (unlike push/pull_request), and actions/checkout defaults to that same ref absent an explicit override.\n required: true\n type: string\n cwd:\n description: Directory to run the check from — the opted-in package's own directory in a monorepo. Defaults to the repo root.\n required: false\n type: string\n default: \".\"\n vercel-project:\n description: >\n Expected Vercel project id/name the preview URL must resolve\n to (verified live via Vercel's API) before\n VERCEL_PROTECTION_BYPASS_SECRET is ever attached to a request\n (holocron#1032 review) — a bare *.vercel.app hostname proves\n nothing on its own, since anyone can register their own\n Vercel project. Omitted → the bypass secret is never\n attached, regardless of whether it's configured.\n required: false\n type: string\n default: \"\"\n secrets:\n VERCEL_TOKEN:\n description: >\n Required alongside vercel-project to verify deployment\n ownership via Vercel's API (read-only use here). Not required\n when vercel-project is omitted.\n required: false\n VERCEL_PROTECTION_BYPASS_SECRET:\n description: >\n Vercel's \"Protection Bypass for Automation\" secret for the\n target project (holocron#1030) — sent as the\n x-vercel-protection-bypass header so this check's request\n isn't redirected to Vercel's own SSO login by Deployment\n Protection, which is on by default for Preview deployments.\n Optional: a check against a URL with protection disabled (or\n a provider other than Vercel) needs nothing here.\n required: false\n\njobs:\n preview-check:\n name: Run monitoring checks against the preview deployment\n permissions:\n contents: read\n # Both read-only, needed by the \"Resolve author trust\" step\n # below: pull-requests to find which PR `inputs.sha` belongs to,\n # and the collaborators/permission lookup (Metadata: read on a\n # PAT, already covered by contents) that step makes against the\n # real PR author.\n pull-requests: read\n runs-on: ubuntu-latest\n timeout-minutes: 15\n steps:\n - name: Validate the preview URL\n id: validate\n env:\n CANDIDATE: ${{ inputs.preview-url }}\n run: |\n # `preview-url` comes from the triggering deployment_status\n # event's environment_url -- set by whoever created that\n # deployment status, not necessarily this repo's own\n # delivery.previewDeploy job (anyone with `deployments: write`\n # could create one directly via the API). Validated here,\n # independently of the event's origin, before this runner\n # ever issues an HTTP request to it (the monitoring check\n # below): without this, a crafted environment_url could point\n # the runner at an internal or attacker-controlled host\n # (SSRF) instead of a real Vercel preview. Forwarding the\n # re-serialized `url.href` rather than the raw input also\n # closes off argument injection through embedded whitespace\n # or shell metacharacters -- a string that parses can't carry\n # either.\n host=$(node -e 'try { process.stdout.write(new URL(process.argv[1]).hostname) } catch { process.exit(1) }' \"$CANDIDATE\") || host=\"\"\n if [[ \"$host\" != *.vercel.app ]]; then\n echo \"::error::preview-url '$CANDIDATE' does not parse to a *.vercel.app host (parsed host: '${host:-<unparseable>}')\"\n exit 1\n fi\n url=$(node -e 'const u = new URL(process.argv[1]); if (u.protocol !== \"https:\") process.exit(1); process.stdout.write(u.href)' \"$CANDIDATE\") || {\n echo \"::error::preview-url '$CANDIDATE' did not parse to an https:// URL\"\n exit 1\n }\n echo \"url=$url\" >> \"$GITHUB_OUTPUT\"\n\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n with:\n ref: ${{ inputs.sha }}\n # This checks out and runs a PR's own holocron.config.ts —\n # untrusted until reviewed. Never leave a credential sitting\n # in .git/config for that code to read and exfiltrate; no\n # later step in this job needs to push or pull authenticated.\n persist-credentials: false\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - name: Resolve author trust\n id: trust\n env:\n # Ambient, always-available -- no new secret. Only used for\n # two read-only lookups, both within what this token already\n # has by default.\n GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n SHA: ${{ inputs.sha }}\n run: |\n # Deliberately NOT a value carried in through the\n # deployment's own `payload` field (sourcery review on\n # #1058's first attempt): that field is attacker-controlled\n # by anyone with `deployments: write` -- same precondition\n # already needed to create a deployment_status event at all\n # -- so trusting whatever it claims would let that same\n # attacker forge payload.collaborator_permission = \"admin\"\n # on their own crafted deployment and unlock the bypass\n # secret regardless of who actually authored the code. This\n # step derives trust itself, from data it verifies\n # independently: which real PR `inputs.sha` belongs to, and\n # that PR's real author's current push-access level -- an\n # attacker can't forge either without already having\n # genuine write access to this repo.\n #\n # First attempt used\n # github.event.pull_request.author_association instead, and\n # separately turned out to be wrong for a different reason:\n # that field only reports MEMBER for a *publicly visible* org\n # membership, so a maintainer with private org membership\n # reads as CONTRIBUTOR and would wrongly fail-closed on their\n # own PRs (confirmed live on this exact repo, holocron#1057\n # follow-up). This lookup has neither problem: it reflects\n # real access -- direct collaborator, team membership, or\n # org default -- independent of any profile visibility\n # setting, and needs only \"Pull requests: read\" plus the\n # baseline \"Metadata: read\" every token already has.\n #\n # No PR found for this SHA, or no login on it -> \"none\" ->\n # untrusted. admin/maintain/write are the three roles GitHub\n # itself treats as push-capable. Missing or unrecognized ->\n # untrusted (fail-closed): an absent credential is safer\n # than a wrongly granted one.\n author=$(gh api \"repos/${GITHUB_REPOSITORY}/commits/${SHA}/pulls\" --jq '(.[0] // {}) | .user.login // empty' 2>/dev/null) || author=\"\"\n permission=\"none\"\n if [ -n \"$author\" ]; then\n permission=$(gh api \"repos/${GITHUB_REPOSITORY}/collaborators/${author}/permission\" --jq '.permission // \"none\"' 2>/dev/null) || permission=\"none\"\n fi\n case \"$permission\" in\n admin | maintain | write) trusted=true ;;\n *) trusted=false ;;\n esac\n echo \"trusted=$trusted\" >> \"$GITHUB_OUTPUT\"\n\n - name: Resolve monitoring checks\n id: resolve\n # Untrusted: loads and runs a PR's own holocron.config.ts, same\n # as the checkout above. No secrets in this step's env at all\n # (holocron#1032 review) -- VERCEL_TOKEN and\n # VERCEL_PROTECTION_BYPASS_SECRET live only on the step below,\n # which never loads this repo's config and so never runs a\n # single line of PR-authored code.\n uses: theholocron/.github/.github/actions/holocron@main\n with:\n command: monitoring resolve-checks\n args: --cwd=${{ inputs.cwd }} --out=${{ runner.temp }}/monitoring-checks.json\n\n - name: Verify project and run monitoring checks\n env:\n # Read directly by `monitoring run-resolved`\n # (packages/cli/src/cli.ts) -- not part of the CLI's own\n # `--token`-resolution chain, since neither is a vendor API\n # credential this command calls anything with: VERCEL_TOKEN\n # only verifies, read-only, that the preview URL resolves to\n # vercel-project before VERCEL_PROTECTION_BYPASS_SECRET (a\n # plain header forwarded as-is) is ever attached. The command\n # itself never attaches the bypass header when vercel-project\n # is empty, regardless of whether these secrets are\n # configured -- never attached to an unverified URL.\n #\n # Both gated on `steps.trust.outputs.trusted` (holocron#1057):\n # confirmed live that Vercel forwards this header straight\n # through to the deployed function, so an untrusted PR's own\n # code could read and exfiltrate it off its own incoming\n # request. An untrusted deployment gets neither secret.\n VERCEL_TOKEN: ${{ steps.trust.outputs.trusted == 'true' && secrets.VERCEL_TOKEN || '' }}\n VERCEL_PROTECTION_BYPASS_SECRET: ${{ steps.trust.outputs.trusted == 'true' && secrets.VERCEL_PROTECTION_BYPASS_SECRET || '' }}\n uses: theholocron/.github/.github/actions/holocron@main\n with:\n command: monitoring run-resolved\n # vercel-project is NOT gated on trust (sourcery review on\n # #1058's first attempt) -- it isn't a secret, and clearing\n # it for an untrusted deployment would make\n # resolveVercelBypassHeaders treat \"no project given\" as\n # trivially verified (its early-return for an omitted\n # project), skipping ownership verification entirely rather\n # than failing. Always passing the real project here means\n # an untrusted deployment instead hits the \"project given\n # but no token\" branch -- VERCEL_TOKEN is empty above --\n # which fails the check outright, before ever issuing the\n # request, rather than letting an unverified host's response\n # (e.g. a 3xx redirect a status-less assertion wouldn't\n # catch) report green.\n args: --checks-file=${{ runner.temp }}/monitoring-checks.json --vercel-project=${{ inputs.vercel-project }} ${{ steps.validate.outputs.url }}\n";
147
+ //#endregion
148
+ //#region src/templates/reusable/delivery.previewDeploy.yml
149
+ var delivery_previewDeploy_default$1 = "name: Preview Deploy\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n cwd:\n description: Directory to run the delivery.previewDeploy task from — the opted-in package's own directory in a monorepo. Defaults to the repo root.\n required: false\n type: string\n default: \".\"\n paths:\n description: >\n JSON array of repo-relative path prefixes (each ending in \"/\") --\n a PR touching none of them skips the actual deploy work instead\n of spinning up a Vercel preview for every PR in the monorepo\n regardless of relevance (holocron#1023). Defaults to an empty\n array: no filter, every PR is relevant -- correct for a\n single-repo consumer, where every PR IS a PR to that package.\n required: false\n type: string\n default: \"[]\"\n secrets:\n VERCEL_TOKEN:\n required: false\n HOLOCRON_DEPLOYMENTS_TOKEN:\n description: >\n A dedicated fine-grained PAT (Deployments: read/write only — see\n docs/tokens.md), not the default GITHUB_TOKEN — GitHub does not\n start a new workflow run for a deployment_status event created\n using a run's own GITHUB_TOKEN, which would make a\n deployment_status-triggered check (e.g. delivery.previewCheck)\n silently never fire. No existing HOLOCRON_<FEATURE>_TOKEN carries\n the Deployments permission, so `secrets: inherit` on the thin\n caller forwards nothing for this unless a repo/org secret\n literally named HOLOCRON_DEPLOYMENTS_TOKEN has been provisioned.\n required: false\n\njobs:\n # Two jobs, deliberately not one: \"report\" never checks out the repo at\n # all, so it never runs a single line of this PR's own code. \"deploy\"\n # does — it has to, to build and stage the preview — and runs with\n # VERCEL_TOKEN present for that (tracked as a residual risk, #1020: the\n # build step still runs this monorepo's own PR-affectable CLI build,\n # pre-review). Without the split, a malicious build/stage script could\n # persist a `gh` shim onto $GITHUB_PATH (or similar runner-state\n # tampering) that a LATER step in the SAME job — the one that finally\n # gets GH_TOKEN — would then execute instead of the real `gh` CLI,\n # stealing that token too. Separate jobs run on separate, fresh\n # runners; \"report\" has nothing of the first job's runner state to\n # inherit.\n deploy:\n name: Deploy preview\n runs-on: ubuntu-latest\n # Setup/build/stage, plus the deploy capability's own\n # wait-for-ready polling (up to 10 minutes by itself) — 10 would kill\n # this before that polling could even finish.\n timeout-minutes: 20\n permissions:\n contents: read\n # Listing the PR's changed files (below, via `gh api`) needs this —\n # must be granted here too, not only on the thin caller: GitHub\n # hard-fails the whole run at startup, zero jobs dispatched, if a\n # called reusable workflow's job asks for more than the caller\n # grants (holocron#1058).\n pull-requests: read\n outputs:\n url: ${{ steps.extract.outputs.url }}\n steps:\n - name: Check whether the PR touches any watched path\n id: filter\n env:\n GH_TOKEN: ${{ github.token }}\n PATHS: ${{ inputs.paths }}\n run: |\n # No checkout needed — the PR's changed-file list comes from the\n # API, not local git history (which this job's later shallow\n # checkout, below, doesn't have enough of to diff against the\n # base branch anyway).\n #\n # `jq -e` both parses PATHS and rejects anything that isn't a\n # JSON array (holocron#1069 review) -- a malformed input (bad\n # JSON, or valid JSON that isn't an array) fails the job loudly\n # instead of the process-substitution loop below silently\n # reading zero prefixes and skipping the deploy with a green\n # check and no explanation.\n paths=$(echo \"$PATHS\" | jq -e 'if type != \"array\" then error(\"paths input is not a JSON array\") else . end') || {\n echo \"::error::delivery.previewDeploy's paths input is not a valid JSON array: $PATHS\"\n exit 1\n }\n # `jq 'length'` on the PARSED value, not a comparison against the\n # literal string \"[]\" -- any valid JSON encoding of an empty\n # array (e.g. \" [ ] \") means the same \"no filter\" thing.\n if [ \"$(echo \"$paths\" | jq 'length')\" = \"0\" ]; then\n echo \"relevant=true\" >> \"$GITHUB_OUTPUT\"\n exit 0\n fi\n # Both the new name AND the old one for a rename -- a file\n # renamed OUT of a watched package (old path matches, new\n # doesn't) is still a real change to that package's tree.\n files=$(gh api \"repos/${GITHUB_REPOSITORY}/pulls/${{ github.event.pull_request.number }}/files\" --paginate --jq '.[] | .filename, (.previous_filename // empty)')\n file_count=$(echo \"$files\" | grep -c . || true)\n if [ \"$file_count\" -ge 3000 ]; then\n # The pulls-files endpoint caps out at 3,000 entries even with\n # --paginate -- past that, a watched path could be among the\n # files we never saw. Can't prove the PR irrelevant, so fail\n # safe to relevant rather than risk silently skipping a real\n # deploy.\n echo \"::warning::PR has >= 3000 changed files -- can't reliably determine path relevance, treating as relevant\"\n echo \"relevant=true\" >> \"$GITHUB_OUTPUT\"\n exit 0\n fi\n relevant=false\n while IFS= read -r prefix; do\n [ -z \"$prefix\" ] && continue\n while IFS= read -r file; do\n [ -z \"$file\" ] && continue\n # Unquoted $prefix (deliberately) -- case pattern-matches it\n # as a glob, anchored at the START of the repo-relative path\n # (holocron#1069 review: grep -F matched the prefix as a\n # literal substring anywhere in the filename, and couldn't\n # match a configured glob like \"custom/**\" at all). The\n # trailing `*` covers a plain directory prefix like\n # \"apps/sentinel/\"; a caller-supplied prefix that already\n # has its own wildcards (e.g. \"custom/**\") just gets one\n # more, harmlessly.\n case \"$file\" in\n $prefix*) relevant=true; break 2 ;;\n esac\n done <<< \"$files\"\n done < <(echo \"$paths\" | jq -r '.[]')\n echo \"relevant=$relevant\" >> \"$GITHUB_OUTPUT\"\n\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n if: ${{ steps.filter.outputs.relevant == 'true' }}\n with:\n # The PR's exact head SHA, not the default pull_request\n # merge-commit ref — what gets built and deployed must be the\n # exact commit the PR actually points at.\n ref: ${{ github.event.pull_request.head.sha }}\n persist-credentials: false\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n if: ${{ steps.filter.outputs.relevant == 'true' }}\n\n - name: Check deploy credentials are available\n if: ${{ steps.filter.outputs.relevant == 'true' }}\n id: creds\n env:\n VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}\n run: |\n # `secrets` isn't valid in a step `if:` (actionlint-enforced) —\n # a fork PR never receives this secret, so the deploy step below\n # skips cleanly on an empty token instead of failing on a\n # missing credential -- correct, and must stay that way: a\n # fork-triggered pull_request run never gets a write-scoped\n # token either, so there's no way to post a PR comment or check\n # run from here regardless (that needs a separate,\n # pull_request_target-triggered job in the base repo's\n # context -- a real feature with its own review surface, not\n # this step; holocron#1027 explicitly scoped this fix to\n # observability only). The ::notice:: below needs no extra\n # permission -- it's a log annotation the runner itself renders,\n # not an API write -- so it's the cheap half of #1027 that IS\n # worth doing here: a visible, on-PR explanation instead of\n # silence.\n if [ -n \"$VERCEL_TOKEN\" ]; then\n echo \"available=true\" >> \"$GITHUB_OUTPUT\"\n else\n echo \"available=false\" >> \"$GITHUB_OUTPUT\"\n echo \"::notice title=No preview deployed::This run has no VERCEL_TOKEN available, so no preview was deployed -- most commonly because GitHub withholds repository secrets from a pull_request run triggered by a fork, though any run missing this secret ends up here the same way. Since this run creates no deployment, it won't trigger delivery.previewCheck for this PR. A maintainer can deploy one manually if needed.\"\n fi\n\n - name: Deploy preview\n id: deploy\n if: ${{ steps.creds.outputs.available == 'true' }}\n env:\n VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}\n uses: theholocron/.github/.github/actions/holocron@main\n with:\n command: run delivery.previewDeploy\n args: --cwd=${{ inputs.cwd }}\n\n - name: Extract and validate the preview URL\n id: extract\n # Only after a real deploy — false both when the step above\n # failed and when it skipped cleanly (fork PR, no credential).\n if: ${{ steps.deploy.outcome == 'success' }}\n env:\n LOG_FILE: ${{ steps.deploy.outputs.log-file }}\n run: |\n # A loose https://\\S+ grab would match whatever text follows an\n # attacker-chosen URL this log happens to contain; grabbing\n # candidate text here and separately requiring the PARSED\n # hostname end in .vercel.app (below) closes that regardless of\n # how the candidate text is shaped — e.g. a userinfo trick like\n # \"https://x.vercel.app@127.0.0.1/\" would satisfy almost any\n # hand-written regex character class while actually parsing to\n # host 127.0.0.1, which a naive regex-only check already missed\n # once.\n candidate=$(grep -oE 'https://[^[:space:]\"]+' \"$LOG_FILE\" | tail -1)\n if [ -z \"$candidate\" ]; then\n echo \"::error::deploy succeeded but no https:// URL was found in its output\"\n exit 1\n fi\n host=$(node -e 'try { process.stdout.write(new URL(process.argv[1]).hostname) } catch { process.exit(1) }' \"$candidate\") || host=\"\"\n if [[ \"$host\" != *.vercel.app ]]; then\n echo \"::error::extracted URL '$candidate' does not parse to a *.vercel.app host (parsed host: '${host:-<unparseable>}')\"\n exit 1\n fi\n echo \"url=$candidate\" >> \"$GITHUB_OUTPUT\"\n\n report:\n name: Report deployment status\n needs: deploy\n if: ${{ needs.deploy.outputs.url != '' }}\n runs-on: ubuntu-latest\n timeout-minutes: 5\n permissions:\n deployments: write\n steps:\n - name: Create GitHub deployment and report status\n env:\n # gh reads GH_TOKEN as its auto-auth env var by its own\n # convention -- the secret feeding it is named\n # HOLOCRON_DEPLOYMENTS_TOKEN, matching this org's\n # HOLOCRON_<FEATURE>_TOKEN naming (docs/tokens.md).\n GH_TOKEN: ${{ secrets.HOLOCRON_DEPLOYMENTS_TOKEN }}\n # The PR's head BRANCH, not its SHA — GitHub's PR \"Environments\"\n # widget associates a Deployment with a PR by matching `ref` to\n # the PR's own branch name, not by SHA. GitHub resolves the\n # branch to whatever commit it points to AT CREATION TIME,\n # though, which is a separate moment from the deploy job's own\n # checkout (below, SHA) -- cross-checked after creation rather\n # than assumed.\n REF: ${{ github.event.pull_request.head.ref }}\n # The exact commit the deploy job checked out and built,\n # captured from the same frozen event payload this whole run\n # was triggered by -- stays constant for the run's lifetime\n # even if the PR branch itself moves while this job is running.\n SHA: ${{ github.event.pull_request.head.sha }}\n URL: ${{ needs.deploy.outputs.url }}\n run: |\n if [ -z \"$GH_TOKEN\" ]; then\n echo \"::error::HOLOCRON_DEPLOYMENTS_TOKEN secret is not set -- posting this status with the default GITHUB_TOKEN would not trigger a deployment_status-triggered check\"\n exit 1\n fi\n\n post_status() {\n gh api \"repos/${GITHUB_REPOSITORY}/deployments/${deploy_id}/statuses\" \\\n --method POST --field state=\"$1\" --field environment_url=\"$URL\" >/dev/null\n }\n latest_status() {\n gh api \"repos/${GITHUB_REPOSITORY}/deployments/${deploy_id}/statuses\" --jq '.[0].state // empty' 2>/dev/null\n }\n # A deployment this very run may have already created\n # server-side on an earlier attempt whose client-side response\n # got lost (e.g. a dropped connection) -- reused instead of\n # creating a second one, which would leave the first\n # permanently pending (nothing ever posts its status). Scoped\n # to the last 2 minutes so an OLDER deployment for the same\n # ref/environment (a previous, unrelated run) is never\n # mistaken for this one.\n find_recent_deployment() {\n gh api \"repos/${GITHUB_REPOSITORY}/deployments\" --method GET -f ref=\"$REF\" -f environment=Preview --jq '\n [.[] | select((now - (.created_at | fromdateiso8601)) < 120)] | sort_by(.created_at) | last | .id // empty' 2>/dev/null\n }\n\n # jq, not a hand-built string -- a branch name containing a\n # literal double quote (git allows it; GitHub's branch-naming\n # rules don't forbid it either) would otherwise break out of a\n # printf-constructed JSON string and produce an invalid payload\n # gh api can't send.\n payload=$(jq -n --arg ref \"$REF\" '{\n ref: $ref,\n environment: \"Preview\",\n description: \"Vercel Preview (file deploy)\",\n production_environment: false,\n auto_merge: false,\n required_contexts: []\n }')\n deploy_id=\"\"\n for attempt in 1 2 3; do\n deploy_id=$(find_recent_deployment)\n if [ -n \"$deploy_id\" ]; then\n echo \"reusing deployment ${deploy_id} created by an earlier attempt of this run\" >&2\n break\n fi\n deploy_id=$(echo \"$payload\" | gh api \"repos/${GITHUB_REPOSITORY}/deployments\" --method POST --input - | jq -r '.id // empty')\n if [ -n \"$deploy_id\" ]; then break; fi\n echo \"deployment creation failed (attempt ${attempt}/3) -- retrying...\" >&2\n sleep 5\n done\n if [ -z \"$deploy_id\" ]; then\n echo \"::error::failed to create a GitHub deployment after 3 attempts -- Vercel already has the preview, but nothing will report or check it\"\n exit 1\n fi\n\n # `ref` resolves to the branch's CURRENT tip at creation time --\n # only guaranteed to be the commit this job actually built if\n # the branch hasn't moved since the triggering push. A push to\n # the same PR while this job was still running would otherwise\n # silently associate the preview URL (built from the OLD\n # commit) with the NEW commit's deployment. A newer workflow\n # run already covers that new commit (cancel-in-progress:\n # false queues rather than cancels), so failing this stale one\n # loses nothing.\n deployed_sha=$(gh api \"repos/${GITHUB_REPOSITORY}/deployments/${deploy_id}\" --jq '.sha // empty')\n if [ \"$deployed_sha\" != \"$SHA\" ]; then\n echo \"::error::deployment ${deploy_id} resolved to ${deployed_sha}, not the deployed commit ${SHA} -- the PR branch moved while this job was running; a newer run already covers the new commit\"\n exit 1\n fi\n\n for attempt in 1 2 3; do\n if post_status success; then\n exit 0\n fi\n # The POST may have actually succeeded server-side even though\n # this client call reported failure (e.g. a dropped connection\n # after GitHub processed it) — checked before retrying, so a\n # transient client-side error never posts a second success and\n # fires delivery.previewCheck twice for the same preview.\n if [ \"$(latest_status)\" = \"success\" ]; then\n exit 0\n fi\n echo \"status POST failed (attempt ${attempt}/3) for deployment ${deploy_id} -- retrying...\" >&2\n sleep 5\n done\n\n # Exhausted retries — one last attempt to mark the deployment\n # failed rather than leaving it permanently \"pending\" in GitHub's\n # UI; best-effort, its own failure doesn't change this step's\n # already-failed outcome.\n post_status error || true\n echo \"::error::failed to post a success status for deployment ${deploy_id} after 3 attempts -- marked it failed instead of leaving it pending\"\n exit 1\n";
150
+ //#endregion
151
+ //#region src/templates/reusable/delivery.publish.yml
152
+ var delivery_publish_default$1 = "name: Release\n\n# Semantic-release with OIDC Trusted Publishing.\n# actions/setup-node writes a default NODE_AUTH_TOKEN=${{ github.token }}\n# which shadows OIDC auth. We explicitly clear it so npm falls through to\n# the Trusted Publisher OIDC exchange.\n# The calling repo must have a .releaserc.json that configures branches,\n# plugins, and any publish options. npm@11+ is installed to support OIDC.\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n dry-run:\n description: >\n When true, runs semantic-release --dry-run: analyzes commits and\n previews the next release without git writes or publish. Only\n meaningful for workflow_dispatch triggers; push-triggered runs\n always run fully.\n type: boolean\n required: false\n default: false\n post-release:\n description: >\n When true, dispatches a readme sync broadcast after a successful\n release. Enable for packages consumed by other repos via the\n registry-doc system so downstream README installation blocks\n stay current.\n type: boolean\n required: false\n default: false\n run-build:\n description: Run `pnpm build` before releasing\n type: boolean\n required: false\n default: true\n deploy-on-release:\n description: >\n When true, runs a separate `deploy` job after a successful release\n (holocron#930): `holocron deploy on-release` deploys every workspace\n package whose `delivery.deploy` task is `with: { on: release }`, when\n the release is on its channel and changes its paths. The job is its\n own check, re-runnable on its own, and gets VERCEL_TOKEN; the release\n job never sees it. Off by default, so a repo that doesn't deploy from\n its release never exposes the token.\n type: boolean\n required: false\n default: false\n sentry-project:\n description: >\n Sentry project slug for sourcemap upload and release creation after\n publishing. Omit to skip the Sentry release step entirely.\n type: string\n required: false\n default: \"\"\n secrets:\n HOLOCRON_RELEASE_TOKEN:\n description: >\n Fine-grained PAT (Contents + Issues + Pull requests: write) owned by\n an admin. Required when the default branch is protected by a ruleset —\n github.token cannot push through rulesets, but an admin PAT can.\n Takes priority over HOLOCRON_SYNC_TOKEN. Falls back to github.token.\n required: false\n HOLOCRON_SYNC_TOKEN:\n description: >\n Legacy alias for HOLOCRON_RELEASE_TOKEN — kept for backward compatibility.\n Prefer HOLOCRON_RELEASE_TOKEN for new repos.\n required: false\n HOLOCRON_READ_TOKEN:\n description: >\n Fine-grained PAT for read-only GitHub API calls (e.g. resolving git\n committer identity via `gh api user`). Falls back to github.token.\n required: false\n GH_TOKEN:\n description: >\n Generic GitHub token fallback for `gh` CLI calls. Used when\n HOLOCRON_READ_TOKEN is not set.\n required: false\n SENTRY_AUTH_TOKEN:\n description: >\n Sentry auth token for sourcemap upload and release creation.\n Required when sentry-project is set. Use the org-level secret.\n required: false\n TURBO_TOKEN:\n required: false\n VERCEL_TOKEN:\n description: >\n Vercel token for the deploy job. Only used when deploy-on-release is\n true, and only by that job.\n required: false\n\njobs:\n release:\n name: Semantic release\n permissions:\n contents: write\n id-token: write\n issues: write\n pull-requests: write\n runs-on: ubuntu-latest\n timeout-minutes: 30\n outputs:\n # Read by the deploy job (holocron#930). Empty unless semantic-release\n # published: `commit` is the release commit, `previous` the commit of\n # the release before it (to diff against), `channel` the new version's\n # prerelease identifier (`alpha`), empty for a stable release (`main`).\n commit: ${{ steps.release.outputs.commit }}\n previous: ${{ steps.release.outputs.previous }}\n channel: ${{ steps.release.outputs.channel }}\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n # Do not cancel in-progress releases — a partial release is worse than a slow one.\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n with:\n fetch-depth: 0\n persist-credentials: false\n # Use HOLOCRON_RELEASE_TOKEN when available — git push (tags, release commits)\n # uses the checkout credential, not GITHUB_TOKEN env var. The\n # built-in github.token cannot push through branch protection rulesets.\n token: ${{ secrets.HOLOCRON_RELEASE_TOKEN || secrets.HOLOCRON_SYNC_TOKEN || github.token }}\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - name: Configure git identity\n run: |\n GIT_NAME=$(gh api user --jq .name 2>/dev/null || echo \"github-actions[bot]\")\n GIT_EMAIL=$(gh api user --jq '\"\\(.id)+\\(.login)@users.noreply.github.com\"' 2>/dev/null || echo \"41898282+github-actions[bot]@users.noreply.github.com\")\n git config --global user.name \"$GIT_NAME\"\n git config --global user.email \"$GIT_EMAIL\"\n git config --global format.signoff true\n env:\n GH_TOKEN: ${{ secrets.HOLOCRON_READ_TOKEN || secrets.GH_TOKEN || github.token }}\n\n - name: Upgrade npm for OIDC support\n run: npm install -g npm@11 sigstore\n # sigstore is required by libnpmpublish/provenance.js at module parse\n # time — before any config takes effect. Some npm 11.x builds stopped\n # bundling it; installing it globally into the same prefix ensures it\n # resolves regardless of npm version. (Discovered 2026-07-09.)\n\n - run: pnpm build\n name: Build\n if: ${{ inputs.run-build == true }}\n\n - name: Release\n id: release\n run: |\n BEFORE=$(git rev-parse HEAD)\n PREVIOUS_TAG=$(git describe --tags --abbrev=0 2>/dev/null || true)\n PREVIOUS=\"\"\n if [ -n \"$PREVIOUS_TAG\" ]; then PREVIOUS=$(git rev-list -n 1 \"$PREVIOUS_TAG\"); fi\n if [ \"$DRY_RUN\" = \"true\" ]; then\n npx semantic-release --dry-run\n else\n npx semantic-release\n fi\n AFTER=$(git rev-parse HEAD)\n # semantic-release only commits (`chore(release): … [skip ci]`) when it\n # actually publishes. That commit gets no CI (and so no coverage\n # upload), which makes the next PR's `codecov/project` compare a full\n # report against a stale ancestor — a phantom drop (holocron#644).\n # Emit the new SHA so the steps below upload coverage for it.\n if [ \"$BEFORE\" != \"$AFTER\" ]; then\n echo \"commit=$AFTER\" >> \"$GITHUB_OUTPUT\"\n echo \"previous=$PREVIOUS\" >> \"$GITHUB_OUTPUT\"\n # The new version's channel: the prerelease identifier of its tag\n # (v5.0.0-alpha.108 -> alpha), empty for a stable release.\n VERSION=$(git describe --tags --abbrev=0)\n VERSION=${VERSION#v}\n CHANNEL=\"\"\n case \"$VERSION\" in *-*) PRE=${VERSION#*-}; CHANNEL=${PRE%%.*} ;; esac\n echo \"channel=$CHANNEL\" >> \"$GITHUB_OUTPUT\"\n fi\n env:\n DRY_RUN: ${{ inputs.dry-run }}\n # Prefer HOLOCRON_RELEASE_TOKEN (fine-grained PAT, Contents+Issues+PRs write,\n # owned by an admin with ruleset bypass) so @semantic-release/git can\n # push the version-bump commit through branch protection. Falls back to\n # HOLOCRON_SYNC_TOKEN (legacy) then github.token for unprotected repos.\n GITHUB_TOKEN: ${{ secrets.HOLOCRON_RELEASE_TOKEN || secrets.HOLOCRON_SYNC_TOKEN || github.token }}\n HUSKY: \"0\"\n NPM_CONFIG_PROVENANCE: true\n\n - uses: theholocron/.github/.github/actions/holocron@main\n name: Coverage for the release commit\n if: ${{ steps.release.outputs.commit != '' }}\n with:\n task: verification.unitTests\n\n - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0\n name: Upload release-commit coverage to Codecov\n if: ${{ steps.release.outputs.commit != '' }}\n # fail_ci_if_error makes a failed upload fail this step, so it is visible, but\n # continue-on-error keeps it from failing the job: the release has already\n # published, and the Sentry release steps below must still run (holocron#934).\n continue-on-error: true\n with:\n use_oidc: true\n fail_ci_if_error: true\n override_commit: ${{ steps.release.outputs.commit }}\n override_branch: ${{ github.ref_name }}\n\n - name: Get release version\n id: release_version\n if: ${{ inputs.sentry-project != '' && inputs.dry-run != true }}\n env:\n SENTRY_PROJECT: ${{ inputs.sentry-project }}\n run: |\n TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo \"\")\n if [ -n \"$TAG\" ]; then\n echo \"release=${SENTRY_PROJECT}@${TAG#v}\" >> \"$GITHUB_OUTPUT\"\n fi\n\n - name: Create Sentry release\n if: ${{ inputs.sentry-project != '' && steps.release_version.outputs.release != '' && inputs.dry-run != true }}\n uses: getsentry/action-release@ff07929a6537bac57790c3451cf4d364aca38528 # v3\n env:\n SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}\n SENTRY_ORG: theholocron\n SENTRY_PROJECT: ${{ inputs.sentry-project }}\n with:\n environment: production\n version: ${{ steps.release_version.outputs.release }}\n sourcemaps: \"**/dist\"\n\n deploy:\n name: Deploy\n needs: release\n # Only after a release that actually published (`commit` is empty on a dry\n # run or when nothing was released). A failed deploy fails only this job:\n # the release is already on npm, and this job re-runs on its own.\n if: ${{ inputs.deploy-on-release == true && inputs.dry-run != true && needs.release.outputs.commit != '' }}\n permissions:\n contents: read\n runs-on: ubuntu-latest\n timeout-minutes: 30\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout the release commit\n with:\n ref: ${{ needs.release.outputs.commit }}\n fetch-depth: 0\n persist-credentials: false\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - uses: theholocron/.github/.github/actions/holocron@main\n name: Deploy packages that deploy on release\n env:\n VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}\n with:\n command: deploy on-release\n args: >-\n --channel=${{ needs.release.outputs.channel }}\n --from=${{ needs.release.outputs.previous }}\n --to=${{ needs.release.outputs.commit }}\n\n sync-readme:\n name: Post-release readme sync\n needs: release\n if: ${{ inputs.post-release == true && inputs.dry-run != true }}\n runs-on: ubuntu-latest\n timeout-minutes: 5\n steps:\n - name: Trigger broadcast readme sync\n run: |\n gh workflow run sync-dispatch.yml \\\n --repo theholocron/.github \\\n --field \"steps=readme\"\n env:\n GH_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN }}\n";
153
+ //#endregion
154
+ //#region src/templates/reusable/dependencies.yml
155
+ var dependencies_default$1 = "name: Dependencies\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n secrets:\n merge-token:\n description: >\n Optional privileged token for auto-merge. Falls back to GITHUB_TOKEN.\n Required when branch protection enforces required reviews — GITHUB_TOKEN\n cannot approve its own PRs.\n required: false\n\njobs:\n dependabot:\n name: Update the dependencies\n permissions:\n contents: write\n pull-requests: write\n runs-on: ubuntu-latest\n timeout-minutes: 5\n if: github.event.pull_request.user.login == 'dependabot[bot]'\n steps:\n - uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3.1.0\n name: Fetch Dependabot metadata\n id: metadata\n\n - run: gh pr merge --auto --squash \"$PR_URL\"\n # --squash is intentional: repo protection sets allow_merge_commit: false,\n # so --merge would fail on any repo using the standard preset.\n name: Enable auto-merge for Dependabot PRs\n if: steps.metadata.outputs.update-type == 'version-update:semver-patch'\n env:\n PR_URL: ${{ github.event.pull_request.html_url }}\n GH_TOKEN: ${{ secrets.merge-token || github.token }}\n";
156
+ //#endregion
157
+ //#region src/templates/reusable/greetings.yml
158
+ var greetings_default$1 = "name: Greetings\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n\njobs:\n greeting:\n name: Greet first-time contributors\n permissions:\n issues: write\n pull-requests: write\n runs-on: ubuntu-latest\n timeout-minutes: 5\n # Group by the issue/PR number so duplicate events don't race each other.\n steps:\n - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0\n name: Greet on first contribution\n with:\n script: |\n // Only greet on the initial open — ignore synchronize, reopened, etc.\n if (context.payload.action !== 'opened') return;\n\n const actor = context.actor;\n const { owner, repo } = context.repo;\n\n // Payload inspection is more reliable than context.eventName for detecting\n // whether this is an issue vs. PR event — works regardless of how GitHub\n // propagates event names through workflow_call chains.\n const isIssue = !!context.payload.issue && !context.payload.pull_request;\n // listForRepo returns both issues and PRs (GitHub treats PRs as issues),\n // sorted newest-first. Filter by type to track first-issue and first-PR\n // independently, and avoid search-index eventual-consistency lag.\n const { data: recent } = await github.rest.issues.listForRepo({\n owner, repo,\n creator: actor,\n state: 'all',\n per_page: 100\n });\n\n const sameType = recent.filter(item =>\n isIssue ? !item.pull_request : !!item.pull_request\n );\n\n if (sameType.length !== 1) return;\n const body = isIssue\n ? `Hey @${actor}!\\n\\nWe really appreciate you taking the time to report an issue. The collaborators on this project attempt to help as many people as possible, but we are a limited number of volunteers, so it is possible that this will not be addressed as swiftly.\\n\\nYour patience is much appreciated and we will get back to you as quickly as possible.`\n : `Hey @${actor}!\\n\\nWe really appreciate you taking the time to help out with this PR. The collaborators on this project attempt to help as many people as possible, but we are a limited number of volunteers, so it is possible that this will not be addressed as swiftly.\\n\\nYour patience is much appreciated and we will get back to you as quickly as possible.`;\n\n await github.rest.issues.createComment({\n owner,\n repo,\n issue_number: context.issue.number,\n body\n });\n";
159
+ //#endregion
160
+ //#region src/templates/reusable/knowledge.wiki.yml
161
+ var knowledge_wiki_default$1 = "name: Wiki\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n fern-version:\n description: >\n Fern CLI version to install. Pin this to avoid breaking changes when\n Fern updates their config schema.\n type: string\n required: false\n default: \"5.114.1\"\n preview:\n description: >\n When true, publishes a preview instead of production.\n Requires preview-id to be set.\n type: boolean\n required: false\n default: false\n preview-id:\n description: >\n Stable ID for the preview build. Use the PR number (e.g. \"pr-123\")\n so the same preview URL is reused on every push to the branch.\n type: string\n required: false\n default: \"\"\n fern-org:\n description: >\n DEPRECATED — the preview URL is now read from Fern's own output.\n Kept as a fallback for building the URL when that parse fails, and so\n existing callers do not error.\n type: string\n required: false\n default: \"\"\n base-path:\n description: >\n DEPRECATED — see fern-org. Basepath appended to the fallback preview\n URL (e.g. \"holocron\" for multi-source routing).\n type: string\n required: false\n default: \"\"\n secrets:\n HOLOCRON_FERN_TOKEN:\n required: false\n FERN_TOKEN:\n required: false\n\njobs:\n publish:\n name: Publish to Fern\n runs-on: ubuntu-latest\n timeout-minutes: 10\n permissions:\n contents: read\n deployments: write\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - name: Install Fern CLI\n run: npm install -g \"fern-api@$FERN_VERSION\"\n env:\n FERN_VERSION: ${{ inputs.fern-version }}\n\n - name: Publish docs\n if: ${{ !inputs.preview }}\n run: fern generate --docs\n env:\n FERN_TOKEN: ${{ secrets.HOLOCRON_FERN_TOKEN || secrets.FERN_TOKEN }}\n\n - name: Preview docs\n id: preview\n if: ${{ inputs.preview && inputs.preview-id != '' }}\n env:\n FERN_TOKEN: ${{ secrets.HOLOCRON_FERN_TOKEN || secrets.FERN_TOKEN }}\n PREVIEW_ID: ${{ inputs.preview-id }}\n run: |\n out=$(fern generate --docs --preview --id \"$PREVIEW_ID\" 2>&1) || { printf '%s\\n' \"$out\"; exit 1; }\n printf '%s\\n' \"$out\"\n # Fern prints \"Published docs to https://<org>-preview-<id>.docs.buildwithfern.com/<path>\"\n url=$(printf '%s\\n' \"$out\" | grep -oiE 'https://[a-z0-9.-]+\\.docs\\.buildwithfern\\.com[^ )]*' | head -n1)\n echo \"url=$url\" >> \"$GITHUB_OUTPUT\"\n\n - name: Report preview URL\n if: ${{ inputs.preview && inputs.preview-id != '' }}\n env:\n GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n CAPTURED_URL: ${{ steps.preview.outputs.url }}\n FERN_ORG: ${{ inputs.fern-org }}\n PREVIEW_ID: ${{ inputs.preview-id }}\n BASE_PATH: ${{ inputs.base-path }}\n run: |\n PREVIEW_URL=\"$CAPTURED_URL\"\n if [ -z \"$PREVIEW_URL\" ] && [ -n \"$FERN_ORG\" ]; then\n PREVIEW_URL=\"https://${FERN_ORG}-preview-${PREVIEW_ID}.docs.buildwithfern.com${BASE_PATH:+/${BASE_PATH}}\"\n fi\n if [ -z \"$PREVIEW_URL\" ]; then\n echo \"::warning::no preview URL parsed from Fern output and no fern-org fallback — skipping the deployment widget\"\n exit 0\n fi\n PAYLOAD=$(printf '{\"ref\":\"%s\",\"environment\":\"wiki (Preview)\",\"description\":\"Wiki\",\"production_environment\":false,\"auto_merge\":false,\"required_contexts\":[]}' \\\n \"$GITHUB_HEAD_REF\")\n DEPLOY_ID=$(printf '%s\\n' \"$PAYLOAD\" | gh api \"repos/${GITHUB_REPOSITORY}/deployments\" \\\n --method POST --input - | jq -r '.id')\n gh api \"repos/${GITHUB_REPOSITORY}/deployments/${DEPLOY_ID}/statuses\" \\\n --method POST --field state=success --field environment_url=\"$PREVIEW_URL\"\n";
162
+ //#endregion
163
+ //#region src/templates/reusable/platform.commitStandards.yml
164
+ var platform_commitStandards_default$1 = "name: Commit Standards\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n secrets:\n TURBO_TOKEN:\n required: false\n\njobs:\n commit-standards:\n name: Run commitlint\n permissions:\n contents: read\n runs-on: ubuntu-latest\n timeout-minutes: 10\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n with:\n fetch-depth: 0\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n # No local equivalent (linters.ts: commitlint has no localBin — enforced\n # only at commit time via the commit-msg hook, and here in CI over the\n # PR's commit range). Not run through the holocron composite action.\n #\n # --config <resolved shared path> (config-resolution workstream, #676)\n # — falls back to commitlint's own auto-discovery of a local\n # commitlint.config.ts when the shared package's built dist/ isn't\n # present, same guard as the commit-msg hook.\n - name: Run commitlint over the PR range\n if: github.event_name == 'pull_request'\n run: |\n COMMITLINT_CONFIG=\"node_modules/@theholocron/commitlint-config/dist/index.js\"\n if [ -f \"$COMMITLINT_CONFIG\" ]; then\n pnpm exec commitlint --config \"$COMMITLINT_CONFIG\" --from \"${{ github.event.pull_request.base.sha }}\" --to \"${{ github.sha }}\"\n else\n pnpm exec commitlint --from \"${{ github.event.pull_request.base.sha }}\" --to \"${{ github.sha }}\"\n fi\n";
165
+ //#endregion
166
+ //#region src/templates/reusable/platform.dispatchedCheck.yml
167
+ var platform_dispatchedCheck_default = "name: Dispatched Check\n\non: # yamllint disable-line rule:truthy\n workflow_dispatch:\n inputs:\n repo:\n description: Target repo, \"owner/name\", to check out and run the task against.\n type: string\n required: true\n ref:\n description: Git ref (branch or commit SHA) to check out.\n type: string\n required: true\n task:\n description: The holocron task name to run (e.g. \"verification.typeSafety\").\n type: string\n required: true\n check-run-id:\n description: >\n ID of the check run already posted as \"queued\" on the target repo —\n patched to \"completed\" once the task finishes.\n type: string\n required: true\n\npermissions:\n contents: read\n\njobs:\n run-task:\n name: Run a Sentinel-dispatched task against another repo\n runs-on: ubuntu-latest\n timeout-minutes: 20\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n steps:\n # actions/create-github-app-token defaults to scoping its token to the\n # *current* repo (.github, where this workflow runs) when owner/\n # repositories are omitted — useless here, since every real use of this\n # token targets inputs.repo instead. Split \"owner/name\" up front so the\n # token step below can scope explicitly to the actual target.\n - name: Parse target repo\n id: target\n run: |\n echo \"owner=${INPUT_REPO%%/*}\" >> \"$GITHUB_OUTPUT\"\n echo \"name=${INPUT_REPO##*/}\" >> \"$GITHUB_OUTPUT\"\n env:\n INPUT_REPO: ${{ inputs.repo }}\n\n # Sentinel's own dispatch call only carries plain, non-secret inputs\n # (repo/ref/task/check-run-id — all visible in the Actions UI/logs).\n # This step mints a fresh, short-lived installation token here instead,\n # scoped by the same App's stored credentials — never a raw token\n # passed through a dispatch input.\n - name: Generate installation token\n id: app-token\n uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0\n with:\n app-id: ${{ secrets.SENTINEL_APP_ID }}\n private-key: ${{ secrets.SENTINEL_APP_PRIVATE_KEY }}\n owner: ${{ steps.target.outputs.owner }}\n repositories: ${{ steps.target.outputs.name }}\n\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout target repository\n with:\n repository: ${{ inputs.repo }}\n ref: ${{ inputs.ref }}\n token: ${{ steps.app-token.outputs.token }}\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - name: Run the task\n id: run-task\n continue-on-error: true\n uses: theholocron/.github/.github/actions/holocron@main\n with:\n task: ${{ inputs.task }}\n\n # A bare .../actions/runs/<id> link lands on the run summary, one click\n # short of the actual step-by-step log a normal check's details_url\n # points at. This job is always alone in its run, so the first (only)\n # job in the run's own job list is unambiguously this one.\n - name: Resolve this job's numeric ID\n id: job-info\n if: always()\n run: |\n job_id=$(gh api \"repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/jobs\" --jq '.jobs[0].id')\n echo \"job_id=$job_id\" >> \"$GITHUB_OUTPUT\"\n env:\n GH_TOKEN: ${{ steps.app-token.outputs.token }}\n\n # Ships the task's real captured output (tsc/vitest/etc — whatever\n # `holocron run` actually printed) to the same Axiom dataset Sentinel's\n # own structured logs already flow into, tagged so it's queryable by\n # this exact run. Reuses the existing dataset rather than provisioning\n # a new one — a prototype-stage call, revisit if CI-log volume ever\n # warrants a dedicated dataset. Builds a permalink filtered to this\n # run's own logs (Axiom's own \"copy query link\" URL shape:\n # /<org>/query?initForm={\"apl\":\"...\"} ) so a viewer lands directly on\n # the relevant lines, not a bare dataset view.\n - name: Ship task output to Axiom\n id: axiom\n if: always()\n run: |\n message=$(tail -c 40000 \"$LOG_FILE\" 2>/dev/null || echo \"(no output captured)\")\n # \"level\" is one of the field names Axiom's Stream/Query tabs\n # recognize for severity-color highlighting (docs: level, @level,\n # severity, @severity, status.code) -- conclusion alone isn't.\n level=$([ \"$CONCLUSION\" = \"success\" ] && echo \"info\" || echo \"error\")\n event=$(jq -cn \\\n --arg time \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\" \\\n --arg repo \"$REPO\" --arg task \"$TASK\" --arg run_id \"$GITHUB_RUN_ID\" \\\n --arg job_id \"$JOB_ID\" --arg check_run_id \"$CHECK_RUN_ID\" \\\n --arg conclusion \"$CONCLUSION\" --arg level \"$level\" --arg message \"$message\" \\\n '{_time: $time, source: \"dispatched-check\", repo: $repo, task: $task,\n run_id: $run_id, job_id: $job_id, check_run_id: $check_run_id,\n conclusion: $conclusion, level: $level, message: $message}')\n curl -sf -X POST \"https://api.axiom.co/v1/datasets/${AXIOM_DATASET}/ingest\" \\\n -H \"Authorization: Bearer ${AXIOM_TOKEN}\" \\\n -H \"Content-Type: application/json\" \\\n -d \"[${event}]\" || echo \"::warning::Axiom ingest failed — continuing, the check itself still reports the real result\"\n\n # The ['...'] is Axiom's own APL array-dataset-reference syntax, not\n # a shell quote boundary — $AXIOM_DATASET/$CHECK_RUN_ID both expand\n # fine here; shellcheck's single-quote heuristic just can't tell.\n # shellcheck disable=SC2016\n apl=\"['${AXIOM_DATASET}'] | where check_run_id == \\\"${CHECK_RUN_ID}\\\"\"\n init_form=$(jq -cn --arg apl \"$apl\" '{apl: $apl}')\n encoded=$(jq -rn --arg s \"$init_form\" '$s | @uri')\n echo \"query-url=https://app.axiom.co/${AXIOM_ORG}/query?initForm=${encoded}\" >> \"$GITHUB_OUTPUT\"\n env:\n LOG_FILE: ${{ steps.run-task.outputs.log-file }}\n REPO: ${{ inputs.repo }}\n TASK: ${{ inputs.task }}\n JOB_ID: ${{ steps.job-info.outputs.job_id }}\n CHECK_RUN_ID: ${{ inputs.check-run-id }}\n CONCLUSION: ${{ steps.run-task.outcome == 'success' && 'success' || 'failure' }}\n AXIOM_ORG: the-holocron-7bbe\n AXIOM_DATASET: holocron-sentinel\n AXIOM_TOKEN: ${{ secrets.SENTINEL_AXIOM_INGEST_TOKEN }}\n\n # Always runs, success or failure — the queued check run on the target\n # repo must be resolved either way, not left hanging. Full JSON body via\n # jq + --input, not -f flags: output.text needs real nesting (gh api -f\n # has no dot-notation for that), and the captured task output can\n # contain characters (quotes, $, backticks) that would break a\n # hand-built -f string.\n - name: Report result to the target repo's check run\n if: always()\n run: |\n excerpt=$(tail -c 3000 \"$LOG_FILE\" 2>/dev/null || echo \"(no output captured)\")\n # The single-quoted format string is intentional — every %s fills\n # via printf's own args below, not shell expansion; nothing in it\n # needs to expand.\n # shellcheck disable=SC2016\n text=$(printf '**Task:** `%s`\\n\\n**Result:** %s\\n\\n[Full logs on Axiom](%s)\\n\\n### Last output\\n\\n```\\n%s\\n```\\n' \\\n \"$TASK\" \"$CONCLUSION\" \"$QUERY_URL\" \"$excerpt\")\n body=$(jq -n \\\n --arg conclusion \"$CONCLUSION\" \\\n --arg details_url \"$RUN_URL\" \\\n --arg title \"$TASK ($CONCLUSION)\" \\\n --arg summary \"Dispatched via theholocron/.github — see the log excerpt below, or the full run/Axiom links.\" \\\n --arg text \"$text\" \\\n '{status: \"completed\", conclusion: $conclusion, details_url: $details_url,\n output: {title: $title, summary: $summary, text: $text}}')\n echo \"$body\" | gh api --method PATCH \"/repos/${REPO}/check-runs/${CHECK_RUN_ID}\" --input -\n env:\n # `GH_TOKEN` is the literal name `gh api` requires to auto-authenticate\n # -- not a stand-in for a shared/blanket credential. Its value is the\n # short-lived, Sentinel-App-scoped token minted above, same as the\n # checkout step uses; never secrets.HOLOCRON_SYNC_TOKEN (the one\n # genuinely shared PAT in this org, used by sync-dispatch.yml).\n GH_TOKEN: ${{ steps.app-token.outputs.token }}\n REPO: ${{ inputs.repo }}\n TASK: ${{ inputs.task }}\n CHECK_RUN_ID: ${{ inputs.check-run-id }}\n CONCLUSION: ${{ steps.run-task.outcome == 'success' && 'success' || 'failure' }}\n LOG_FILE: ${{ steps.run-task.outputs.log-file }}\n QUERY_URL: ${{ steps.axiom.outputs.query-url }}\n RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}/job/${{ steps.job-info.outputs.job_id }}\n";
168
+ //#endregion
169
+ //#region src/templates/reusable/platform.notifyCheck.yml
170
+ var platform_notifyCheck_default$1 = "name: Notify Check\n\n# Proves the notification path end to end, from a real run: that each channel's\n# webhook secret exists, that its webhook accepts a post, and that the alert\n# role ping resolves. It posts one clearly labelled test message per channel and\n# fails the matching job when any step does not work, so a red job names the\n# channel to fix. Manual (workflow_dispatch) only: it posts real messages.\n#\n# Each channel names its own webhook secret, because `notifications setup` lets a\n# layout override the default name (and normalises punctuation), so deriving the\n# name from the channel would miss those. A channel with `\"mention\": true` also\n# requires the role-id secret and pings it: a webhook accepting the post proves\n# the channel works, not that the role ping does, so a missing or malformed role\n# id fails that job instead of passing quietly. Whether Discord lets that role be\n# mentioned at all is checked by `holocron doctor notifications`, not here.\n#\n# Secrets reach this workflow through `secrets: inherit`, so any secret named in\n# `channels` is readable; the declared list below is for callers that pass secrets\n# explicitly.\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n channels:\n description: >\n JSON array of channels to check, each `{ \"name\", \"secret\", \"mention\" }`:\n `secret` is the org secret holding that channel's webhook URL and\n `mention: true` also requires and pings the role in\n HOLOCRON_NOTIFY_ALERT_ROLE.\n type: string\n required: false\n default: >-\n [{\"name\":\"sync\",\"secret\":\"HOLOCRON_NOTIFY_WEBHOOK_SYNC\"},\n {\"name\":\"alerts\",\"secret\":\"HOLOCRON_NOTIFY_WEBHOOK_ALERTS\",\"mention\":true},\n {\"name\":\"errors\",\"secret\":\"HOLOCRON_NOTIFY_WEBHOOK_ERRORS\"},\n {\"name\":\"uptime\",\"secret\":\"HOLOCRON_NOTIFY_WEBHOOK_UPTIME\"},\n {\"name\":\"releases\",\"secret\":\"HOLOCRON_NOTIFY_WEBHOOK_RELEASES\"},\n {\"name\":\"deploys\",\"secret\":\"HOLOCRON_NOTIFY_WEBHOOK_DEPLOYS\"}]\n secrets:\n HOLOCRON_NOTIFY_WEBHOOK_SYNC:\n description: Webhook for the sync channel.\n required: false\n HOLOCRON_NOTIFY_WEBHOOK_ALERTS:\n description: Webhook for the alerts channel.\n required: false\n HOLOCRON_NOTIFY_WEBHOOK_ERRORS:\n description: Webhook for the errors channel.\n required: false\n HOLOCRON_NOTIFY_WEBHOOK_UPTIME:\n description: Webhook for the uptime channel.\n required: false\n HOLOCRON_NOTIFY_WEBHOOK_RELEASES:\n description: Webhook for the releases channel.\n required: false\n HOLOCRON_NOTIFY_WEBHOOK_DEPLOYS:\n description: Webhook for the deploys channel.\n required: false\n HOLOCRON_NOTIFY_ALERT_ROLE:\n description: Id of the role to mention on the alerts channel's test message. Optional.\n required: false\n\njobs:\n check:\n name: Check ${{ matrix.channel.name }}\n runs-on: ubuntu-latest\n timeout-minutes: 2\n permissions: {}\n strategy:\n # One channel failing must not hide the others: the point is the full picture.\n fail-fast: false\n matrix:\n channel: ${{ fromJSON(inputs.channels) }}\n steps:\n - name: Require the alert role id\n if: matrix.channel.mention\n run: |\n if [ -z \"$ROLE\" ]; then\n echo \"::error::HOLOCRON_NOTIFY_ALERT_ROLE is not set, so the ping on #$CHANNEL cannot be checked.\"\n exit 1\n fi\n env:\n ROLE: ${{ secrets.HOLOCRON_NOTIFY_ALERT_ROLE }}\n CHANNEL: ${{ matrix.channel.name }}\n\n - uses: theholocron/.github/.github/actions/notify@main\n name: Post a test message to ${{ matrix.channel.name }}\n with:\n strict: \"true\"\n webhook: ${{ secrets[matrix.channel.secret] }}\n # Only a channel marked `mention` pings, and strict mode rejects a malformed id.\n mention: ${{ matrix.channel.mention && secrets.HOLOCRON_NOTIFY_ALERT_ROLE || '' }}\n message: \"🧪 notify check — #${{ matrix.channel.name }} is wired up (${{ github.repository }}, ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})\"\n";
171
+ //#endregion
172
+ //#region src/templates/reusable/platform.repoSync.yml
173
+ var platform_repoSync_default$1 = "name: Sync\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n steps:\n description: >\n Sync steps to run (default: all). Valid values:\n labels, properties, teams, topics, keywords, description, homepage, readme, workflows, wiki.\n Pass a space-separated list to run a subset, e.g. \"readme\" or \"readme wiki\".\n type: string\n required: false\n secrets:\n HOLOCRON_ADMIN_TOKEN:\n description: Fine-grained PAT with admin scopes (labels, properties, teams).\n required: false\n HOLOCRON_AXIOM_TOKEN:\n description: >\n Axiom API token. When set alongside the HOLOCRON_AXIOM_DATASET\n repo/org variable, the CLI ships this run's structured logs to Axiom.\n Falls back to the vendor-native AXIOM_TOKEN secret.\n required: false\n AXIOM_TOKEN:\n description: Vendor-native fallback for HOLOCRON_AXIOM_TOKEN.\n required: false\n HOLOCRON_DEPLOY_TOKEN:\n description: Fine-grained PAT for GitHub Pages configuration.\n required: false\n HOLOCRON_ISSUES_TOKEN:\n description: Fine-grained PAT for issue management.\n required: false\n HOLOCRON_ORG_TOKEN:\n description: Org-scoped fine-grained PAT for team sync and org properties.\n required: false\n HOLOCRON_READ_TOKEN:\n description: Fine-grained PAT for read-only GitHub API calls.\n required: false\n HOLOCRON_SYNC_TOKEN:\n required: false\n HOLOCRON_NOTIFY_WEBHOOK_SYNC:\n description: >\n Webhook for the sync channel: a sync PR opened and its\n auto-merge result. Unset = no post.\n required: false\n HOLOCRON_NOTIFY_WEBHOOK_ALERTS:\n description: >\n Webhook for the alerts channel: a failed sync run, or a sync\n PR that needs a manual merge. Unset = no post. The optional\n HOLOCRON_NOTIFY_ALERT_ROLE secret (a role id) makes these posts\n mention that role.\n required: false\n HOLOCRON_NOTIFY_ALERT_ROLE:\n description: >\n Id of the role to mention on alerts. Written by `holocron\n notifications setup`; not sensitive, kept as a secret so the same\n token that writes the webhook secrets can write it. Optional.\n required: false\n GH_TOKEN:\n description: >\n Generic GitHub token fallback for gh CLI calls. Used when\n HOLOCRON_SYNC_TOKEN is not set.\n required: false\n\njobs:\n sync:\n name: Run holocron repo sync\n runs-on: ubuntu-latest\n timeout-minutes: 10\n # Serialise the find-or-create-PR and supersede steps repo-wide: the caller's\n # concurrency key is per ref, so runs from different refs could otherwise\n # interleave and leave several sync PRs open.\n concurrency:\n group: repo-sync-pr-${{ github.repository }}\n cancel-in-progress: false\n permissions:\n contents: write\n pull-requests: write\n # Job-level so the `holocron` composite action's steps inherit them.\n env:\n HOLOCRON_ADMIN_TOKEN: ${{ secrets.HOLOCRON_ADMIN_TOKEN }}\n HOLOCRON_AXIOM_TOKEN: ${{ secrets.HOLOCRON_AXIOM_TOKEN || secrets.AXIOM_TOKEN }}\n HOLOCRON_AXIOM_DATASET: ${{ vars.HOLOCRON_AXIOM_DATASET || vars.AXIOM_DATASET }}\n POSTHOG_PROJECT_TOKEN: ${{ vars.POSTHOG_PROJECT_TOKEN }}\n HOLOCRON_DEPLOY_TOKEN: ${{ secrets.HOLOCRON_DEPLOY_TOKEN }}\n HOLOCRON_ISSUES_TOKEN: ${{ secrets.HOLOCRON_ISSUES_TOKEN }}\n HOLOCRON_ORG_TOKEN: ${{ secrets.HOLOCRON_ORG_TOKEN }}\n HOLOCRON_READ_TOKEN: ${{ secrets.HOLOCRON_READ_TOKEN }}\n HOLOCRON_SYNC_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n with:\n token: ${{ secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN || github.token }}\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - name: Run holocron repo sync\n # The `holocron` action resolves @theholocron/cli from node_modules (or\n # builds the workspace when it's an unbuilt checkout — the holocron repo\n # itself). `inputs.steps` is a space-separated list or empty; the\n # `--steps <list>` arg is omitted when empty.\n uses: theholocron/.github/.github/actions/holocron@main\n with:\n command: repo sync\n args: ${{ inputs.steps != '' && format('--steps {0}', inputs.steps) || '' }}\n\n - name: Format generated files\n run: |\n pnpm exec prettier --write README.md docs/src/content/docs/index.mdx 2>/dev/null || true\n # `holocron repo sync` writes package.json fields (keywords, description,\n # homepage) with a plain assignment, which appends new keys at the\n # end — re-apply the canonical order the lint config enforces.\n pnpm exec eslint --fix --no-warn-ignored package.json 2>/dev/null || true\n\n - uses: theholocron/.github/.github/actions/auto-commit@main\n id: auto-commit\n name: Commit sync changes\n with:\n token: ${{ secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN || github.token }}\n # One branch per source commit (full SHA, so distinct commits never collide): a PR left unreviewed never blocks the\n # next sync, and a re-run for the same commit updates its own branch.\n branch: ${{ format('chore/auto-sync-{0}', github.sha) }}\n commit-message: \"chore: sync from holocron.config\"\n commit-options: \"--no-verify\"\n\n - name: Open PR and enable auto-merge\n id: pr\n if: steps.auto-commit.outputs.changes-detected == 'true'\n run: |\n # Find or create this run's PR first: older PRs are only closed once\n # a replacement exists, so a failure here never leaves the changes\n # without an open PR.\n url=$(gh pr view \"$BRANCH\" --json url --jq .url 2>/dev/null || gh pr create \\\n --title \"chore: sync README and repo metadata\" \\\n --body \"Automated sync from holocron.config. Auto-merges once required checks pass.\" \\\n --base main \\\n --head \"$BRANCH\")\n created=$(gh pr view \"$url\" --json createdAt --jq .createdAt)\n\n # Supersede older sync PRs: same-repo only, exact generated or legacy\n # branch name, and created before this one (a slow older run can\n # never close a newer PR). --limit covers more than the default page.\n gh pr list --state open --limit 200 --json number,headRefName,isCrossRepository,createdAt \\\n --jq \".[] | select(.isCrossRepository | not) | select(.headRefName == \\\"chore/auto-sync\\\" or (.headRefName | test(\\\"^chore/auto-sync-[0-9a-f]{40}$\\\"))) | select(.headRefName != \\\"$BRANCH\\\") | select(.createdAt < \\\"$created\\\") | .number\" |\n while read -r n; do\n gh pr close \"$n\" --delete-branch --comment \"Superseded by $url.\" || true\n done\n\n # Delete pushed sync branches that never got a PR (e.g. a run\n # cancelled between push and PR creation). The repo-wide concurrency\n # group means no other run is mid-flight here.\n open=$(gh pr list --state open --limit 200 --json headRefName --jq '.[].headRefName')\n git ls-remote --heads origin 'refs/heads/chore/auto-sync-*' | sed 's|.*refs/heads/||' |\n while read -r b; do\n [ \"$b\" = \"$BRANCH\" ] && continue\n echo \"$b\" | grep -Eq '^chore/auto-sync-[0-9a-f]{40}$' || continue\n echo \"$open\" | grep -qxF \"$b\" || git push origin --delete \"$b\" || true\n done\n\n # A PR opened with the default GITHUB_TOKEN does not trigger\n # pull_request workflows, so required checks would never start and\n # auto-merge would wait forever.\n if [ \"$HAS_PAT\" != \"true\" ]; then\n echo \"::warning::Sync PR $url opened with GITHUB_TOKEN: checks will not run and auto-merge is skipped. Set HOLOCRON_SYNC_TOKEN.\"\n echo \"Sync PR $url — needs manual merge (no PAT).\" >> \"$GITHUB_STEP_SUMMARY\"\n result=\"needs a manual merge (no PAT, so checks will not run)\"\n attention=true\n elif gh pr merge \"$url\" --auto --squash; then\n echo \"Sync PR $url — auto-merge enabled.\" | tee -a \"$GITHUB_STEP_SUMMARY\"\n result=\"auto-merge enabled\"\n else\n echo \"::warning::Sync PR $url opened but auto-merge could not be enabled; merge it manually.\"\n echo \"Sync PR $url — needs manual merge.\" >> \"$GITHUB_STEP_SUMMARY\"\n result=\"needs a manual merge (auto-merge could not be enabled)\"\n attention=true\n fi\n {\n echo \"url=$url\"\n echo \"result=$result\"\n echo \"attention=${attention:-false}\"\n } >> \"$GITHUB_OUTPUT\"\n env:\n BRANCH: ${{ format('chore/auto-sync-{0}', github.sha) }}\n HAS_PAT: ${{ secrets.HOLOCRON_SYNC_TOKEN != '' || secrets.GH_TOKEN != '' }}\n GH_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN || github.token }}\n\n - uses: theholocron/.github/.github/actions/notify@main\n name: Notify of the sync PR\n if: steps.pr.outputs.url != '' && steps.pr.outputs.attention != 'true'\n with:\n webhook: ${{ secrets.HOLOCRON_NOTIFY_WEBHOOK_SYNC }}\n message: \"🔄 ${{ github.repository }}: sync PR ${{ steps.pr.outputs.url }} — ${{ steps.pr.outputs.result }}${{ github.run_attempt > 1 && format(' (attempt {0})', github.run_attempt) || '' }}\"\n\n - uses: theholocron/.github/.github/actions/notify@main\n name: Notify that a sync PR needs attention\n if: steps.pr.outputs.attention == 'true'\n with:\n webhook: ${{ secrets.HOLOCRON_NOTIFY_WEBHOOK_ALERTS }}\n mention: ${{ secrets.HOLOCRON_NOTIFY_ALERT_ROLE }}\n message: \"⚠️ ${{ github.repository }}: sync PR ${{ steps.pr.outputs.url }} — ${{ steps.pr.outputs.result }}${{ github.run_attempt > 1 && format(' (attempt {0})', github.run_attempt) || '' }}\"\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\n # A separate job, not a final step: a job-level timeout cancels the sync job\n # before any later step in it can run, so an in-job `if: failure()` step would\n # stay silent on exactly the hang we most want to hear about. `!cancelled()` is\n # false when the whole run was cancelled (a newer run superseded this one), so\n # that expected case stays quiet, while a failed or timed-out sync job alerts.\n notify-failure:\n name: Notify of a failed sync\n needs: sync\n if: ${{ !cancelled() && (needs.sync.result == 'failure' || needs.sync.result == 'cancelled') }}\n runs-on: ubuntu-latest\n timeout-minutes: 2\n permissions: {}\n steps:\n - uses: theholocron/.github/.github/actions/notify@main\n name: Notify of a failed sync\n with:\n webhook: ${{ secrets.HOLOCRON_NOTIFY_WEBHOOK_ALERTS }}\n mention: ${{ secrets.HOLOCRON_NOTIFY_ALERT_ROLE }}\n message: \"❌ ${{ github.repository }}: repo sync ${{ needs.sync.result == 'cancelled' && 'timed out or was cancelled' || 'failed' }} — ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}${{ github.run_attempt > 1 && format(' (attempt {0})', github.run_attempt) || '' }}\"\n";
174
+ //#endregion
175
+ //#region src/templates/reusable/platform.repoValidation.yml
176
+ var platform_repoValidation_default$1 = "name: Repo Validation\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n secrets:\n TURBO_TOKEN:\n required: false\n\njobs:\n adrs:\n name: Validate ADRs and specs\n permissions:\n contents: read\n runs-on: ubuntu-latest\n timeout-minutes: 10\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n with:\n fetch-depth: 0\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - uses: theholocron/.github/.github/actions/holocron@main\n name: Validate ADR and spec frontmatter\n with:\n task: platform.repoValidation\n job: adrs\n\n registry:\n name: Validate registry consistency\n permissions:\n contents: read\n runs-on: ubuntu-latest\n timeout-minutes: 10\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - uses: theholocron/.github/.github/actions/holocron@main\n name: Validate registry consistency\n with:\n task: platform.repoValidation\n job: registry\n\n docs-presence:\n name: Validate docs presence\n permissions:\n contents: read\n runs-on: ubuntu-latest\n timeout-minutes: 10\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n with:\n fetch-depth: 0\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - uses: theholocron/.github/.github/actions/holocron@main\n name: Validate docs presence for new packages\n with:\n task: platform.repoValidation\n job: docsPresence\n env:\n BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}\n\n conclusion:\n name: Conclusion\n runs-on: ubuntu-latest\n if: always()\n needs: [adrs, registry, docs-presence]\n steps:\n - name: Check job statuses\n run: |\n if [[ \"$RESULTS\" == *\"failure\"* ]] || [[ \"$RESULTS\" == *\"cancelled\"* ]]; then\n exit 1\n fi\n env:\n RESULTS: ${{ join(needs.*.result, ',') }}\n";
177
+ //#endregion
178
+ //#region src/templates/reusable/platform.syncTemplates.yml
179
+ var platform_syncTemplates_default$1 = "name: Sync Templates\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n secrets:\n HOLOCRON_SYNC_TOKEN:\n description: >\n Fine-grained PAT (resource owner org) with BOTH contents:write and\n workflows:write on theholocron/.github — contents:write alone is\n rejected for anything under .github/workflows/, a GitHub\n fine-grained-PAT restriction independent of repo permissions.\n required: false\n GH_TOKEN:\n description: >\n Fallback PAT with the same two permissions, used only when\n HOLOCRON_SYNC_TOKEN is not set. The default GITHUB_TOKEN is never\n an acceptable fallback here — it cannot write to a different repo\n no matter what this job's own `permissions:` block grants, so it\n is deliberately not offered as a third fallback; the CLI's own\n token resolver already fails with a clear, actionable message\n when both secrets above are unset.\n required: false\n\njobs:\n sync-templates:\n name: Run holocron sync github\n runs-on: ubuntu-latest\n timeout-minutes: 10\n permissions:\n contents: read\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: Run holocron sync github\n # No local commit, no branch, no PR — pushes directly to\n # theholocron/.github's own default branch.\n #\n # HOLOCRON_SYNC_TOKEN is scoped to ONLY this step, not the job —\n # \"Setup\" (above) runs this ref's own pnpm install, and the\n # \"Build the workspace\" step this composite action may run (an\n # unbuilt workspace checkout — the holocron repo itself) runs this\n # ref's own build, including any postinstall/build-time script a\n # malicious push could carry. Neither sees a write credential.\n uses: theholocron/.github/.github/actions/holocron@main\n env:\n HOLOCRON_SYNC_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN }}\n with:\n command: sync github\n";
180
+ //#endregion
181
+ //#region src/templates/reusable/preview.yml
182
+ var preview_default$1 = "name: Preview\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n type:\n description: \"Type of deployment: docs or storybook\"\n required: true\n type: string\n name:\n description: Repo name prefix used to filter the docs site package (<name>-site); omit to run pnpm -C docs build\n required: false\n type: string\n default: \"\"\n storybook-projects:\n description: >\n JSON array of { \"name\"?, \"workingDir\", \"outputDir\"? } objects for storybook deploys.\n Each is built via `pnpm -C <workingDir> build:storybook`. If \"name\" is provided the\n output is placed under `sandbox/<name>/`; omit \"name\" for single-repo deploys and the\n output lands directly in `sandbox/`.\n type: string\n required: false\n default: \"[]\"\n build-script:\n description: pnpm script that builds the Storybook static output (single storybook, type:storybook only)\n type: string\n required: false\n default: build:storybook\n output-dir:\n description: Directory where Storybook writes its static output (single storybook, type:storybook only)\n type: string\n required: false\n default: storybook-static\n cloudflare-project:\n description: >\n Cloudflare Pages project name. Falls back to the CLOUDFLARE_PAGES_PROJECT\n org variable when omitted — set that variable once and all repos get previews\n without per-repo config.\n required: false\n type: string\n default: \"\"\n\njobs:\n deploy-preview:\n name: Preview\n if: ${{ github.event.action != 'closed' }}\n runs-on: ubuntu-latest\n permissions:\n contents: read\n deployments: write\n pull-requests: write\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - name: Build workspace dependencies\n # The docs site imports workspace packages (e.g. a shared theme) via\n # `workspace:*` — its exports map points at dist/, which doesn't\n # exist until something builds it. Nothing else in this job does,\n # so without this step Vite fails to resolve the import entirely.\n if: ${{ inputs.type == 'docs' }}\n run: pnpm run build\n\n - name: Build docs site\n if: ${{ inputs.type == 'docs' && inputs.name != '' }}\n env:\n SITE_NAME: ${{ inputs.name }}\n run: pnpm --filter @theholocron/\"$SITE_NAME\"-site build\n\n - name: Build docs site\n if: ${{ inputs.type == 'docs' && inputs.name == '' }}\n run: pnpm -C docs build\n\n - name: Build Storybook projects\n if: ${{ inputs.storybook-projects != '[]' }}\n env:\n PROJECTS: ${{ inputs.storybook-projects }}\n run: |\n echo \"$PROJECTS\" | jq -c '.[]' | while IFS= read -r project; do\n workingDir=$(echo \"$project\" | jq -r '.workingDir')\n pnpm -C \"$workingDir\" build:storybook\n done\n\n - name: Build Storybook\n if: ${{ inputs.type == 'storybook' && inputs.storybook-projects == '[]' }}\n env:\n BUILD_SCRIPT: ${{ inputs.build-script }}\n run: pnpm run \"$BUILD_SCRIPT\"\n\n - name: Assemble site\n env:\n DEPLOY_TYPE: ${{ inputs.type }}\n SITE_NAME: ${{ inputs.name }}\n PROJECTS: ${{ inputs.storybook-projects }}\n STORYBOOK_OUTPUT_DIR: ${{ inputs.output-dir }}\n run: |\n mkdir -p _site\n if [ \"$DEPLOY_TYPE\" = \"docs\" ]; then\n if [ -n \"$SITE_NAME\" ]; then\n # Site built with base: /<name>/ — nest under _site/<name>/ so asset\n # paths match (/name/_astro/... → served at /name/_astro/...).\n mkdir -p \"_site/${SITE_NAME}\"\n cp -r docs/dist/. \"_site/${SITE_NAME}/\"\n # Redirect root to the base path so the preview URL lands correctly.\n printf \"/ /%s/ 301\\n\" \"$SITE_NAME\" > _site/_redirects\n else\n cp -r docs/dist/. _site/\n fi\n fi\n if [ \"$PROJECTS\" != \"[]\" ]; then\n echo \"$PROJECTS\" | jq -c '.[]' | while IFS= read -r project; do\n name=$(echo \"$project\" | jq -r '.name // \"\"')\n workingDir=$(echo \"$project\" | jq -r '.workingDir')\n outputDir=$(echo \"$project\" | jq -r '.outputDir // \"storybook-static\"')\n if [ -n \"$name\" ]; then\n target=\"_site/sandbox/${name}\"\n else\n target=\"_site/sandbox\"\n fi\n mkdir -p \"$target\"\n cp -r \"${workingDir}/${outputDir}/.\" \"$target/\"\n done\n elif [ \"$DEPLOY_TYPE\" = \"storybook\" ]; then\n mkdir -p _site/sandbox\n cp -r \"${STORYBOOK_OUTPUT_DIR}/.\" _site/sandbox/\n fi\n\n - name: Install Wrangler\n # The action's install step runs npm/pnpm in the repo root; pnpm fails\n # because it rejects adding to a workspace root without -w, and npm fails\n # because package.json contains pnpm catalog: references it cannot parse.\n # Pre-installing from /tmp (no package.json) lets the action find wrangler\n # already on PATH and skip its own install entirely.\n working-directory: /tmp\n run: npm install -g wrangler@4\n\n - uses: cloudflare/wrangler-action@ebbaa1584979971c8614a24965b4405ff95890e0 # v4.0.0\n id: deploy\n name: Deploy to Cloudflare Pages\n # Skip when neither the per-repo input nor the org variable is set.\n if: ${{ inputs.cloudflare-project != '' || vars.CLOUDFLARE_PAGES_PROJECT != '' }}\n with:\n apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}\n accountId: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}\n packageManager: npm\n command: >-\n pages deploy _site\n --project-name ${{ inputs.cloudflare-project || vars.CLOUDFLARE_PAGES_PROJECT }}\n --branch ${{ github.event.repository.name }}-pr-${{ github.event.pull_request.number }}\n --commit-dirty=true\n\n - name: Report preview URL\n if: ${{ steps.deploy.outputs.pages-deployment-alias-url != '' }}\n env:\n GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n ALIAS: ${{ steps.deploy.outputs.pages-deployment-alias-url }}\n SITE_NAME: ${{ inputs.name }}\n PROJECT: ${{ inputs.cloudflare-project || vars.CLOUDFLARE_PAGES_PROJECT }}\n run: |\n PREVIEW_URL=\"${ALIAS%/}${SITE_NAME:+/${SITE_NAME}}/\"\n # GITHUB_HEAD_REF is the PR branch name — links the deployment to this PR's sidebar widget.\n PAYLOAD=$(printf '{\"ref\":\"%s\",\"environment\":\"%s\",\"description\":\"Cloudflare Pages\",\"production_environment\":false,\"auto_merge\":false,\"required_contexts\":[]}' \\\n \"$GITHUB_HEAD_REF\" \"${PROJECT} (Preview)\")\n DEPLOY_ID=$(echo \"$PAYLOAD\" | gh api \"repos/${GITHUB_REPOSITORY}/deployments\" \\\n --method POST --input - | jq -r '.id')\n gh api \"repos/${GITHUB_REPOSITORY}/deployments/${DEPLOY_ID}/statuses\" \\\n --method POST --field state=success --field environment_url=\"${PREVIEW_URL}\"\n\n cleanup:\n name: Clean up Preview\n if: ${{ github.event.action == 'closed' }}\n runs-on: ubuntu-latest\n permissions:\n contents: read\n deployments: write\n pull-requests: write\n steps:\n - name: Delete Cloudflare Pages deployments for branch\n if: ${{ inputs.cloudflare-project != '' || vars.CLOUDFLARE_PAGES_PROJECT != '' }}\n env:\n CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}\n CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}\n PROJECT: ${{ inputs.cloudflare-project || vars.CLOUDFLARE_PAGES_PROJECT }}\n BRANCH: ${{ github.event.repository.name }}-pr-${{ github.event.pull_request.number }}\n GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n run: |\n DEPLOYMENTS=$(curl -s \\\n \"https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/pages/projects/${PROJECT}/deployments\" \\\n -H \"Authorization: Bearer ${CLOUDFLARE_API_TOKEN}\" \\\n | jq -r --arg b \"$BRANCH\" \\\n '.result[] | select(.deployment_trigger.metadata.branch == $b) | .id')\n\n if [ -z \"$DEPLOYMENTS\" ]; then\n echo \"No deployments found for branch ${BRANCH} — nothing to clean up.\"\n exit 0\n fi\n\n for id in $DEPLOYMENTS; do\n echo \"Deleting CF Pages deployment: $id\"\n curl -s -X DELETE \\\n \"https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/pages/projects/${PROJECT}/deployments/${id}?force=true\" \\\n -H \"Authorization: Bearer ${CLOUDFLARE_API_TOKEN}\" | jq -r 'if .success then \" ✓ deleted\" else \" ✗ \\(.errors[0].message)\" end'\n done\n\n # Mark the GitHub Deployment environment as inactive.\n ENV_NAME=\"${PROJECT} (Preview)\"\n gh api \"repos/${GITHUB_REPOSITORY}/deployments\" \\\n | jq -r \".[] | select(.environment == \\\"${ENV_NAME}\\\") | .id\" \\\n | while read -r deploy_id; do\n gh api \"repos/${GITHUB_REPOSITORY}/deployments/${deploy_id}/statuses\" \\\n --method POST --field state=inactive 2>/dev/null || true\n done\n";
183
+ //#endregion
184
+ //#region src/templates/reusable/review.yml
185
+ var review_default$1 = "name: Review\n\n# ReviewDog is the annotation layer — posts inline PR diff annotations.\n# Runs on pull_request only: inline annotations require PR context,\n# and branch protection ensures all changes go through PRs anyway.\n# security.secretDetection.yml / sourceQuality.structuredDataValidation.yml\n# are the CI gates covering push + PR events. Gitleaks and YAML are\n# intentionally duplicated: those gate merges; ReviewDog surfaces exact\n# line annotations in the PR diff.\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n\njobs:\n reviewdog:\n name: Post inline lint annotations\n runs-on: ubuntu-latest\n timeout-minutes: 20\n permissions:\n contents: read\n pull-requests: write\n\n steps:\n - name: Checkout repository\n uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n with:\n fetch-depth: 0\n\n - name: Setup\n if: ${{ hashFiles('pnpm-lock.yaml') != '' }}\n uses: theholocron/.github/.github/actions/setup@main\n\n - name: Install ReviewDog\n uses: reviewdog/action-setup@d8a7baabd7f3e8544ee4dbde3ee41d0011c3a93f # v1\n with:\n reviewdog_version: latest\n\n # Detect which tools are relevant for this repo, excluding node_modules.\n # hashFiles('**/*') recurses into node_modules/.pnpm and produces false\n # positives for repos that don't own those file types.\n # -print -quit stops find after the first match without a pipe, avoiding\n # the SIGPIPE/pipefail exit-141 that find|head-1 triggers under\n # GitHub Actions' default bash --noprofile --norc -e -o pipefail mode.\n - name: Detect project features\n id: detect\n shell: bash\n run: |\n has() { find . -not -path '*/node_modules/*' -name \"$1\" -print -quit 2>/dev/null | grep -q .; }\n has_ext() { find . -not -path '*/node_modules/*' -name \"$1\" -print -quit 2>/dev/null | grep -q .; }\n { { has 'eslint.config.js' || has 'eslint.config.mjs' || has 'eslint.config.cjs' || \\\n has 'eslint.config.ts' || has '.eslintrc' || has '.eslintrc.js' || \\\n has '.eslintrc.cjs' || has '.eslintrc.json' || has '.eslintrc.yaml' || \\\n has '.eslintrc.yml'; } && grep -qF '\"eslint\":' package.json 2>/dev/null; } && echo \"eslint=true\" >> \"$GITHUB_OUTPUT\" || echo \"eslint=false\" >> \"$GITHUB_OUTPUT\"\n { has 'tsconfig.json' && grep -qF '\"typescript\":' package.json 2>/dev/null; } && echo \"tsconfig=true\" >> \"$GITHUB_OUTPUT\" || echo \"tsconfig=false\" >> \"$GITHUB_OUTPUT\"\n has_ext '*.sh' && echo \"shell=true\" >> \"$GITHUB_OUTPUT\" || echo \"shell=false\" >> \"$GITHUB_OUTPUT\"\n has 'Dockerfile' || has_ext '*.Dockerfile' || has 'Containerfile' && \\\n echo \"docker=true\" >> \"$GITHUB_OUTPUT\" || echo \"docker=false\" >> \"$GITHUB_OUTPUT\"\n has_ext '.env*' && echo \"dotenv=true\" >> \"$GITHUB_OUTPUT\" || echo \"dotenv=false\" >> \"$GITHUB_OUTPUT\"\n has_ext '*.md' && echo \"markdown=true\" >> \"$GITHUB_OUTPUT\" || echo \"markdown=false\" >> \"$GITHUB_OUTPUT\"\n\n #\n # Always applicable\n #\n\n - name: Gitleaks (secrets)\n uses: reviewdog/action-gitleaks@2b7b5685e3e3eecddab5d30cfa04f18123031421 # v1\n with:\n reporter: github-pr-annotations\n tool_name: \"Review / gitleaks\"\n fail_level: error\n gitleaks_flags: --log-opts=${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}\n\n - name: YamlLint\n if: ${{ hashFiles('.yamllint.yml') != '' }}\n uses: reviewdog/action-yamllint@b5f7217d8c815ae374d1d55840d5e569d82f01f0 # v1\n with:\n reporter: github-pr-annotations\n tool_name: \"Review / yamllint\"\n fail_level: error\n yamllint_flags: -c ${{ github.workspace }}/.yamllint.yml ${{ github.workspace }}\n\n - name: ActionLint (GitHub Actions)\n if: ${{ hashFiles('.github/workflows/*.yml', '.github/workflows/*.yaml') != '' }}\n uses: reviewdog/action-actionlint@6fb7acc99f4a1008869fa8a0f09cfca740837d9d # v1\n with:\n reporter: github-pr-annotations\n tool_name: \"Review / actionlint\"\n fail_level: error\n\n #\n # TypeScript / JavaScript\n #\n\n - name: ESLint\n if: steps.detect.outputs.eslint == 'true'\n uses: reviewdog/action-eslint@556a3fdaf8b4201d4d74d406013386aa4f7dab96 # v1.34.0\n with:\n reporter: github-pr-annotations\n tool_name: \"Review / eslint\"\n fail_level: error\n eslint_flags: .\n\n - name: TypeScript\n if: steps.detect.outputs.tsconfig == 'true'\n uses: EPMatt/reviewdog-action-tsc@63d923a3c5b4497671940b8874f58a404e2351b5 # v1\n with:\n reporter: github-pr-annotations\n tool_name: \"Review / tsc\"\n fail_level: error\n\n #\n # Shell\n #\n\n - name: ShellCheck\n if: steps.detect.outputs.shell == 'true'\n uses: reviewdog/action-shellcheck@4c07458293ac342d477251099501a718ae5ef86e # v1\n with:\n reporter: github-pr-annotations\n tool_name: \"Review / shellcheck\"\n fail_level: none\n\n #\n # Docker\n #\n\n - name: Hadolint\n if: steps.detect.outputs.docker == 'true'\n uses: reviewdog/action-hadolint@1b2cfa6ba72072ad35158d7ff3aa49bbdc03506d # v1\n with:\n reporter: github-pr-annotations\n tool_name: \"Review / hadolint\"\n fail_level: none\n\n #\n # Environment files\n #\n\n - name: dotenv-linter\n if: steps.detect.outputs.dotenv == 'true'\n uses: dotenv-linter/action-dotenv-linter@afde61cfda2ecffe7bea35837b6f20b956c88689 # v3.0.0\n with:\n reporter: github-code-suggestions\n\n #\n # Documentation\n #\n\n - name: Alex (inclusive language)\n if: steps.detect.outputs.markdown == 'true'\n uses: reviewdog/action-alex@347481655add010a2ae302df34b57c9bcfa0d6e4 # v1\n with:\n reporter: github-pr-annotations\n tool_name: \"Review / alex\"\n";
186
+ //#endregion
187
+ //#region src/templates/reusable/security.codeScanning.yml
188
+ var security_codeScanning_default$1 = "name: Security\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n language:\n description: CodeQL language to analyze\n type: string\n required: false\n default: javascript-typescript\n\njobs:\n analyze:\n name: Analyze (${{ inputs.language }})\n permissions:\n actions: read\n contents: read\n security-events: write\n runs-on: ubuntu-latest\n timeout-minutes: 45\n # Do not cancel in-progress security scans.\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0\n name: Initialize CodeQL\n with:\n languages: ${{ inputs.language }}\n\n - uses: github/codeql-action/autobuild@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0\n name: Autobuild\n\n - uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0\n name: Analyze\n with:\n category: /language:${{ inputs.language }}\n";
189
+ //#endregion
190
+ //#region src/templates/reusable/security.secretDetection.yml
191
+ var security_secretDetection_default$1 = "name: Secret Detection\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n secrets:\n TURBO_TOKEN:\n required: false\n\njobs:\n secret-detection:\n name: Run gitleaks\n permissions:\n contents: read\n runs-on: ubuntu-latest\n timeout-minutes: 15\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n with:\n fetch-depth: 0\n\n # gitleaks has no npm distribution — install the prebuilt binary via\n # the maintainer's own documented install script.\n # NOTE: unverified against a live CI run at authoring time (D12) — flag\n # here if this needs adjustment.\n - name: Install gitleaks\n run: |\n curl -sSfL https://raw.githubusercontent.com/gitleaks/gitleaks/master/install.sh | sh -s -- -b /usr/local/bin\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - uses: theholocron/.github/.github/actions/holocron@main\n name: Run gitleaks\n with:\n task: security.secretDetection\n";
192
+ //#endregion
193
+ //#region src/templates/reusable/sourceQuality.deadCodeAnalysis.yml
194
+ var sourceQuality_deadCodeAnalysis_default$1 = "name: Dead Code Analysis\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n secrets:\n TURBO_TOKEN:\n required: false\n\njobs:\n dead-code-analysis:\n name: Run knip\n permissions:\n contents: read\n runs-on: ubuntu-latest\n timeout-minutes: 10\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n with:\n persist-credentials: false\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - uses: theholocron/.github/.github/actions/holocron@main\n name: Run knip\n with:\n task: sourceQuality.deadCodeAnalysis\n";
195
+ //#endregion
196
+ //#region src/templates/reusable/sourceQuality.formatting.yml
197
+ var sourceQuality_formatting_default$1 = "name: Formatting\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n enable-auto-commit:\n description: >\n Auto-commit prettier/editorconfig/markdownlint --fix results as a\n verified commit via a GitHub App (same App used across every\n formatting-capable task — D12).\n type: boolean\n required: false\n default: false\n secrets:\n SUPER_LINTER_APP_ID:\n required: false\n SUPER_LINTER_PRIVATE_KEY:\n required: false\n TURBO_TOKEN:\n required: false\n\njobs:\n formatting:\n name: Run prettier, editorconfig-checker, markdownlint\n permissions:\n contents: write\n statuses: write\n runs-on: ubuntu-latest\n timeout-minutes: 15\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n APP_ID_SET: ${{ secrets.SUPER_LINTER_APP_ID != '' }}\n steps:\n - name: Generate GitHub App token\n id: app-token\n if: >\n inputs.enable-auto-commit == true &&\n github.event.pull_request != null &&\n github.event.pull_request.head.repo.full_name == github.repository &&\n github.ref_name != github.event.repository.default_branch &&\n env.APP_ID_SET == 'true'\n uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0\n with:\n app-id: ${{ secrets.SUPER_LINTER_APP_ID }}\n private-key: ${{ secrets.SUPER_LINTER_PRIVATE_KEY }}\n\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n with:\n fetch-depth: 0\n token: ${{ steps.app-token.outputs.token || github.token }}\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n # editorconfig-checker has no npm distribution — install the prebuilt\n # binary from its release tarball (v4.0.1+ ships the binary at the\n # tarball root as `editorconfig-checker`, not `bin/ec-<os>-<arch>` —\n # confirmed against a live CI run; the old asset name/path 404s now).\n - name: Install editorconfig-checker\n run: |\n curl -sSL https://github.com/editorconfig-checker/editorconfig-checker/releases/latest/download/editorconfig-checker-linux-amd64.tar.gz \\\n | tar xz -C /usr/local/bin editorconfig-checker\n\n - uses: theholocron/.github/.github/actions/holocron@main\n name: Run prettier, editorconfig-checker, markdownlint\n with:\n task: sourceQuality.formatting\n\n - uses: theholocron/.github/.github/actions/auto-commit@main\n name: Commit and push formatting fixes\n if: >\n inputs.enable-auto-commit == true &&\n github.event.pull_request != null &&\n github.event.pull_request.head.repo.full_name == github.repository &&\n github.ref_name != github.event.repository.default_branch &&\n env.APP_ID_SET == 'true' &&\n steps.app-token.conclusion == 'success'\n with:\n token: ${{ steps.app-token.outputs.token }}\n branch: ${{ github.event.pull_request.head.ref || github.head_ref || github.ref }}\n commit-message: \"chore: fix formatting issues\"\n commit-options: \"--no-verify\"\n";
198
+ //#endregion
199
+ //#region src/templates/reusable/sourceQuality.staticAnalysis.yml
200
+ var sourceQuality_staticAnalysis_default$1 = "name: Static Analysis\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n enable-auto-commit:\n description: >\n Auto-commit eslint --fix results as a verified commit via a GitHub App.\n Requires SUPER_LINTER_APP_ID and SUPER_LINTER_PRIVATE_KEY secrets\n (same App used across every formatting-capable task — D12).\n type: boolean\n required: false\n default: false\n secrets:\n SUPER_LINTER_APP_ID:\n required: false\n SUPER_LINTER_PRIVATE_KEY:\n required: false\n TURBO_TOKEN:\n required: false\n\njobs:\n static-analysis:\n name: Run eslint and actionlint\n permissions:\n contents: write\n statuses: write\n runs-on: ubuntu-latest\n timeout-minutes: 15\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n APP_ID_SET: ${{ secrets.SUPER_LINTER_APP_ID != '' }}\n steps:\n - name: Generate GitHub App token\n id: app-token\n if: >\n inputs.enable-auto-commit == true &&\n github.event.pull_request != null &&\n github.event.pull_request.head.repo.full_name == github.repository &&\n github.ref_name != github.event.repository.default_branch &&\n env.APP_ID_SET == 'true'\n uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0\n with:\n app-id: ${{ secrets.SUPER_LINTER_APP_ID }}\n private-key: ${{ secrets.SUPER_LINTER_PRIVATE_KEY }}\n\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n with:\n fetch-depth: 0\n token: ${{ steps.app-token.outputs.token || github.token }}\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n # actionlint has no npm distribution — install the prebuilt binary via\n # the maintainer's own documented download script.\n # NOTE: unverified against a live CI run at authoring time (D12) — flag\n # here if this needs adjustment.\n - name: Install actionlint\n run: |\n bash <(curl -sSf https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash)\n sudo mv ./actionlint /usr/local/bin/actionlint\n\n - name: Check for merge-conflict markers\n # No package distributes this as a standalone tool — a plain grep for\n # the marker lines (the same check the old bundled linter ran, D12).\n run: |\n if grep -rlnE '^(<{7}|={7}|>{7})( |$)' \\\n --exclude-dir=.git --exclude-dir=node_modules .; then\n echo \"::error::merge-conflict markers found\"\n exit 1\n fi\n\n - uses: theholocron/.github/.github/actions/holocron@main\n name: Run eslint and actionlint\n with:\n task: sourceQuality.staticAnalysis\n\n - uses: theholocron/.github/.github/actions/auto-commit@main\n name: Commit and push eslint --fix\n if: >\n inputs.enable-auto-commit == true &&\n github.event.pull_request != null &&\n github.event.pull_request.head.repo.full_name == github.repository &&\n github.ref_name != github.event.repository.default_branch &&\n env.APP_ID_SET == 'true' &&\n steps.app-token.conclusion == 'success'\n with:\n token: ${{ steps.app-token.outputs.token }}\n branch: ${{ github.event.pull_request.head.ref || github.head_ref || github.ref }}\n commit-message: \"chore: fix static-analysis issues\"\n commit-options: \"--no-verify\"\n";
201
+ //#endregion
202
+ //#region src/templates/reusable/sourceQuality.structuredDataValidation.yml
203
+ var sourceQuality_structuredDataValidation_default$1 = "name: Structured Data Validation\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n secrets:\n TURBO_TOKEN:\n required: false\n\njobs:\n structured-data-validation:\n name: Run yamllint\n permissions:\n contents: read\n runs-on: ubuntu-latest\n timeout-minutes: 10\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n # yamllint has no npm distribution — Python ships preinstalled on\n # ubuntu-latest runners.\n - name: Install yamllint\n run: pip install --user yamllint\n\n - uses: theholocron/.github/.github/actions/holocron@main\n name: Run yamllint\n with:\n task: sourceQuality.structuredDataValidation\n";
204
+ //#endregion
205
+ //#region src/templates/reusable/stale.yml
206
+ var stale_default$1 = "name: Stale\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n days-before-stale:\n description: Days of inactivity before an issue or PR is marked stale (applies to both unless type-specific value is set)\n type: number\n required: false\n default: 30\n days-before-close:\n description: Days after stale label before closing (applies to both unless type-specific value is set)\n type: number\n required: false\n default: 5\n days-before-issue-stale:\n description: Days of inactivity before an issue is marked stale; overrides days-before-stale when set to a non-negative value\n type: number\n required: false\n default: -1\n days-before-issue-close:\n description: Days after stale label before closing an issue; overrides days-before-close when set to a non-negative value\n type: number\n required: false\n default: -1\n days-before-pr-stale:\n description: Days of inactivity before a PR is marked stale; overrides days-before-stale when set to a non-negative value\n type: number\n required: false\n default: -1\n days-before-pr-close:\n description: Days after stale label before closing a PR; overrides days-before-close when set to a non-negative value\n type: number\n required: false\n default: -1\n exempt-issue-labels:\n description: Comma-separated labels that exempt an issue from being marked stale\n type: string\n required: false\n default: \"in-progress,wip\"\n exempt-pr-labels:\n description: Comma-separated labels that exempt a PR from being marked stale\n type: string\n required: false\n default: \"\"\n exempt-all-issue-milestones:\n description: Issues assigned to any milestone are never marked stale\n type: boolean\n required: false\n default: true\n exempt-all-pr-milestones:\n description: PRs assigned to any milestone are never marked stale\n type: boolean\n required: false\n default: true\n exempt-all-issue-projects:\n description: Issues assigned to any project are never marked stale\n type: boolean\n required: false\n default: true\n exempt-all-pr-projects:\n description: PRs assigned to any project are never marked stale\n type: boolean\n required: false\n default: true\n\njobs:\n stale:\n name: Mark stale issues and pull requests\n permissions:\n contents: write\n issues: write\n pull-requests: write\n runs-on: ubuntu-latest\n timeout-minutes: 10\n steps:\n - uses: actions/stale@1e223db275d687790206a7acac4d1a11bd6fe629 # v10.4.0\n name: Run Stale\n with:\n close-issue-message: >\n This issue was closed because it has been stalled for\n ${{ inputs.days-before-close }} days with no activity.\n days-before-close: ${{ inputs.days-before-close }}\n days-before-issue-close: ${{ inputs.days-before-issue-close }}\n days-before-issue-stale: ${{ inputs.days-before-issue-stale }}\n days-before-pr-close: ${{ inputs.days-before-pr-close }}\n days-before-pr-stale: ${{ inputs.days-before-pr-stale }}\n days-before-stale: ${{ inputs.days-before-stale }}\n exempt-all-issue-milestones: ${{ inputs.exempt-all-issue-milestones }}\n exempt-all-issue-projects: ${{ inputs.exempt-all-issue-projects }}\n exempt-all-pr-milestones: ${{ inputs.exempt-all-pr-milestones }}\n exempt-all-pr-projects: ${{ inputs.exempt-all-pr-projects }}\n exempt-issue-labels: ${{ inputs.exempt-issue-labels }}\n exempt-pr-labels: ${{ inputs.exempt-pr-labels }}\n stale-issue-label: wontfix\n stale-issue-message: >\n This issue is stale because it has been open ${{ inputs.days-before-stale }}\n days with no activity. Remove the stale label or comment, or this will be\n closed in ${{ inputs.days-before-close }} days.\n stale-pr-label: wontfix\n stale-pr-message: >\n This PR is stale because it has been open ${{ inputs.days-before-stale }}\n days with no activity. Remove the stale label or comment, or this will be\n closed in ${{ inputs.days-before-close }} days.\n";
207
+ //#endregion
208
+ //#region src/templates/reusable/sync-dispatch.yml
209
+ var sync_dispatch_default = "name: Sync Dispatch\n\non: # yamllint disable-line rule:truthy\n workflow_dispatch:\n inputs:\n steps:\n description: >\n Sync steps to pass to each repo's sync.yml. Default is \"readme\"\n (only README marker blocks are updated).\n type: string\n required: false\n default: readme\n\npermissions:\n contents: read\n\njobs:\n broadcast:\n name: Broadcast sync to all repos\n runs-on: ubuntu-latest\n timeout-minutes: 15\n steps:\n - name: Dispatch sync to all repos with sync.yml\n run: |\n gh api /orgs/theholocron/repos --paginate --jq '.[].name' \\\n | while IFS= read -r repo; do\n gh api \"/repos/theholocron/$repo/contents/.github/workflows/sync.yml\" --silent 2>/dev/null || continue\n echo \"Dispatching sync to theholocron/$repo\"\n gh workflow run sync.yml \\\n --repo \"theholocron/$repo\" \\\n --field \"steps=$STEPS\" \\\n || echo \"Warning: could not dispatch to theholocron/$repo — skipping\"\n done\n env:\n GH_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN }}\n STEPS: ${{ inputs.steps }}\n";
210
+ //#endregion
211
+ //#region src/templates/reusable/sync-github.yml
212
+ var sync_github_default = "name: Sync workflow templates\n\n# Builds the holocron CLI from source and pushes updated workflow templates\n# and composite actions to downstream .github repos. Runs whenever the\n# template source files change on main or alpha.\n#\n# Secrets required:\n# HOLOCRON_SYNC_TOKEN — fine-grained PAT (resource owner: org) with:\n# Actions: Read and write (dispatch workflow runs via gh workflow run)\n# Contents: Read and write (git trees, blobs, refs)\n# Pull requests: Read and write (open sync PR)\n# Workflows: Read and write (write .github/workflows/*.yml)\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n primary-repo:\n description: >\n Primary .github repo — receives composite actions, reusable workflows,\n and thin-caller templates. Requires a PR (branch protection assumed).\n type: string\n required: false\n default: theholocron/.github\n secondary-repos:\n description: >\n Space-separated list of secondary repos (reusable workflows + thin\n callers only, no composite actions). Changes are delivered via pull\n request, same as the primary repo.\n type: string\n required: false\n default: \"\"\n sync-branch:\n description: Branch name used for the primary and secondary repo PRs\n type: string\n required: false\n default: chore/sync-templates\n secrets:\n HOLOCRON_SYNC_TOKEN:\n required: true\n HOLOCRON_READ_TOKEN:\n description: >\n Fine-grained PAT for read-only GitHub API calls (e.g. resolving git\n committer identity via `gh api user`). Falls back to HOLOCRON_SYNC_TOKEN.\n required: false\n GH_TOKEN:\n description: >\n Generic GitHub token fallback for `gh` CLI calls. Used when neither\n HOLOCRON_READ_TOKEN nor HOLOCRON_SYNC_TOKEN is set.\n required: false\n HOLOCRON_AXIOM_TOKEN:\n description: >\n Axiom API token. When set alongside the HOLOCRON_AXIOM_DATASET\n repo/org variable, the CLI ships this run's structured logs to Axiom.\n Falls back to the vendor-native AXIOM_TOKEN secret.\n required: false\n AXIOM_TOKEN:\n description: Vendor-native fallback for HOLOCRON_AXIOM_TOKEN.\n required: false\n TURBO_TOKEN:\n required: false\n\njobs:\n sync:\n name: Sync templates\n runs-on: ubuntu-latest\n timeout-minutes: 15\n permissions:\n contents: read\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n HOLOCRON_AXIOM_TOKEN: ${{ secrets.HOLOCRON_AXIOM_TOKEN || secrets.AXIOM_TOKEN }}\n HOLOCRON_AXIOM_DATASET: ${{ vars.HOLOCRON_AXIOM_DATASET || vars.AXIOM_DATASET }}\n POSTHOG_PROJECT_TOKEN: ${{ vars.POSTHOG_PROJECT_TOKEN }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - run: pnpm build\n name: Build CLI\n\n - name: Cache actionlint\n id: cache-actionlint\n uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0\n with:\n path: /tmp/actionlint\n key: actionlint-v1.7.7-linux-amd64\n\n - name: Download actionlint\n if: steps.cache-actionlint.outputs.cache-hit != 'true'\n run: |\n curl -fsSL https://github.com/rhysd/actionlint/releases/download/v1.7.7/actionlint_1.7.7_linux_amd64.tar.gz \\\n | tar -xz -C /tmp actionlint\n\n - name: Validate generated workflows\n run: |\n node packages/cli/dist/cli.mjs sync github \\\n --repo \"$PRIMARY_REPO\" \\\n --output-dir /tmp/sync-validate\n /tmp/actionlint /tmp/sync-validate/.github/workflows/*.yml\n env:\n PRIMARY_REPO: ${{ inputs.primary-repo }}\n\n - name: Sync primary repo (PR)\n run: |\n GIT_NAME=$(gh api user --jq .name 2>/dev/null || echo \"github-actions[bot]\")\n GIT_EMAIL=$(gh api user --jq '\"\\(.id)+\\(.login)@users.noreply.github.com\"' 2>/dev/null || echo \"41898282+github-actions[bot]@users.noreply.github.com\")\n COMMIT_MSG=\"chore: sync from theholocron/holocron\"$'\\n\\n'\"Signed-off-by: $GIT_NAME <$GIT_EMAIL>\"\n node packages/cli/dist/cli.mjs sync github \\\n --repo \"$PRIMARY_REPO\" \\\n --branch \"$SYNC_BRANCH\" \\\n --pr \\\n --message \"$COMMIT_MSG\"\n GH_TOKEN=\"$HOLOCRON_SYNC_TOKEN\" gh pr merge --auto --squash \\\n --repo \"$PRIMARY_REPO\" \"$SYNC_BRANCH\" 2>/dev/null || true\n env:\n GH_TOKEN: ${{ secrets.HOLOCRON_READ_TOKEN || secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN }}\n HOLOCRON_SYNC_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN }}\n PRIMARY_REPO: ${{ inputs.primary-repo }}\n SYNC_BRANCH: ${{ inputs.sync-branch }}\n\n - name: Sync secondary repos (PR)\n if: ${{ inputs.secondary-repos != '' }}\n run: |\n GIT_NAME=$(gh api user --jq .name 2>/dev/null || echo \"github-actions[bot]\")\n GIT_EMAIL=$(gh api user --jq '\"\\(.id)+\\(.login)@users.noreply.github.com\"' 2>/dev/null || echo \"41898282+github-actions[bot]@users.noreply.github.com\")\n COMMIT_MSG=\"chore: sync from theholocron/holocron\"$'\\n\\n'\"Signed-off-by: $GIT_NAME <$GIT_EMAIL>\"\n for repo in $SECONDARY_REPOS; do\n node packages/cli/dist/cli.mjs sync github \\\n --repo \"$repo\" \\\n --branch \"$SYNC_BRANCH\" \\\n --pr \\\n --message \"$COMMIT_MSG\"\n GH_TOKEN=\"$HOLOCRON_SYNC_TOKEN\" gh pr merge --auto --squash \\\n --repo \"$repo\" \"$SYNC_BRANCH\" 2>/dev/null || true\n done\n env:\n GH_TOKEN: ${{ secrets.HOLOCRON_READ_TOKEN || secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN }}\n HOLOCRON_SYNC_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN }}\n SECONDARY_REPOS: ${{ inputs.secondary-repos }}\n SYNC_BRANCH: ${{ inputs.sync-branch }}\n";
213
+ //#endregion
214
+ //#region src/templates/reusable/tag.yml
215
+ var tag_default = "name: Tag\n\n# Release Please — fully automated tag and GitHub Release from Conventional Commits.\n# No package.json or npm publishing required. Operates in \"simple\" mode by default:\n# analyzes commits since the last tag, maintains a rolling Release PR, and creates\n# a tag + GitHub Release when that PR is merged.\n#\n# The calling repo must have two files at the root:\n# release-please-config.json — declares packages and release-type\n# .release-please-manifest.json — tracks the current version\n\non: # yamllint disable-line rule:truthy\n # Self-trigger: when this workflow lives in theholocron/.github itself,\n # push to main runs Release Please for that repo's own releases.\n push:\n branches:\n - main\n workflow_call:\n inputs:\n release-type:\n description: Release Please release type (simple, node, python, etc.)\n type: string\n required: false\n default: simple\n config-file:\n description: Path to release-please-config.json\n type: string\n required: false\n default: release-please-config.json\n manifest-file:\n description: Path to .release-please-manifest.json\n type: string\n required: false\n default: .release-please-manifest.json\n\njobs:\n tag:\n name: Tag release\n permissions:\n contents: write\n pull-requests: write\n runs-on: ubuntu-latest\n timeout-minutes: 10\n steps:\n - uses: google-github-actions/release-please-action@e4dc86ba9405554aeba3c6bb2d169500e7d3b4ee # v4.1.1\n name: Run Release Please\n with:\n release-type: ${{ inputs.release-type }}\n config-file: ${{ inputs.config-file }}\n manifest-file: ${{ inputs.manifest-file }}\n";
216
+ //#endregion
217
+ //#region src/templates/reusable/verification.performance.yml
218
+ var verification_performance_default$1 = "name: Audit the Performance\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n lighthouse-config:\n description: >\n Path to the Lighthouse CI config file, forwarded to `holocron run\n verification.performance -- --config`. Defaults to\n lighthouse.config.cjs (the org standard).\n type: string\n required: false\n default: lighthouse.config.cjs\n secrets:\n LHCI_GITHUB_APP_TOKEN:\n required: false\n TURBO_TOKEN:\n required: false\n\njobs:\n performance:\n name: Run Lighthouse CI\n permissions:\n contents: read\n runs-on: ubuntu-latest\n timeout-minutes: 15\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n # Read by `lhci autorun` inside `holocron run verification.performance`.\n LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - run: npm install -g @lhci/cli@0.14.x\n name: Install Lighthouse CLI\n\n - uses: theholocron/.github/.github/actions/holocron@main\n name: Run Lighthouse CI\n with:\n task: verification.performance\n args: --config=${{ inputs.lighthouse-config }}\n";
219
+ //#endregion
220
+ //#region src/templates/reusable/verification.typeSafety.yml
221
+ var verification_typeSafety_default$1 = "name: Typecheck\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n secrets:\n TURBO_TOKEN:\n required: false\n\njobs:\n typecheck:\n name: Run tsc --noEmit\n permissions:\n contents: read\n runs-on: ubuntu-latest\n timeout-minutes: 10\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - uses: theholocron/.github/.github/actions/holocron@main\n name: Type check\n with:\n task: verification.typeSafety\n";
222
+ //#endregion
223
+ //#region src/templates/reusable/verification.unitTests.yml
224
+ var verification_unitTests_default$1 = "name: Test\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n run-unit:\n description: Run unit tests with coverage (disable for UI-only repos that use Storybook testing exclusively)\n type: boolean\n required: false\n default: true\n run-storybook:\n description: Run Storybook vitest interaction tests (requires .storybook/ setup)\n type: boolean\n required: false\n default: false\n run-interaction:\n description: Run Storybook interaction and accessibility tests with Playwright\n type: boolean\n required: false\n default: false\n run-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 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 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, 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";
225
+ //#endregion
226
+ //#region src/templates/reusable/verification.visualRegression.yml
227
+ var verification_visualRegression_default$1 = "name: Visual Regression\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\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 Required -- this task only exists in a repo's workflow list when it\n opts into Chromatic at all (holocron#394/#1062): GitHub evaluates a\n job's `if:` before expanding its `strategy.matrix`, so no default\n here (empty or otherwise) can make a *disabled* Chromatic job\n disappear from PR checks -- only not generating this job's thin\n caller in the first place can. A repo that doesn't want Chromatic\n simply omits `verification.visualRegression` from its `tasks:`.\n type: string\n required: true\n secrets:\n CHROMATIC_PROJECT_TOKEN:\n required: false\n\njobs:\n visual-and-composition:\n name: Run visual and composition tests\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";
228
+ //#endregion
229
+ //#region src/generators/github-action-thin-callers/github-action-thin-callers.ts
230
+ /**
231
+ * Workflow templates + thin-caller generation.
232
+ *
233
+ * `WORKFLOW_TEMPLATES` holds each reusable `.github/workflows/<name>.yml`
234
+ * (synced to `theholocron/.github`); `generateThinCallerContent` wraps one
235
+ * into the thin caller `holocron repo setup` / `holocron repo sync` write locally.
236
+ */
237
+ const WORKFLOW_TEMPLATES = {
238
+ "verification.unitTests": "name: Verification\n\non: # yamllint disable-line rule:truthy\n push:\n branches: [main, alpha]\n pull_request:\n\nconcurrency:\n group: test-${{ github.ref }}\n cancel-in-progress: true\n\npermissions:\n contents: read\n id-token: write\n statuses: write\n\njobs:\n test:\n name: Test\n uses: theholocron/.github/.github/workflows/verification.unitTests.yml@main\n with:\n run-unit: true\n secrets: inherit\n",
239
+ "verification.visualRegression": "name: Visual Regression\n\non: # yamllint disable-line rule:truthy\n push:\n branches: [main, alpha]\n pull_request:\n\npermissions:\n contents: read\n statuses: write\n\njobs:\n visual-and-composition:\n name: Visual Regression\n uses: theholocron/.github/.github/workflows/verification.visualRegression.yml@main\n secrets: inherit\n",
240
+ "verification.typeSafety": "name: Verification\n\non: # yamllint disable-line rule:truthy\n push:\n branches: [main, alpha]\n pull_request:\n\nconcurrency:\n group: typecheck-${{ github.ref }}\n cancel-in-progress: true\n\npermissions:\n contents: read\n\njobs:\n typecheck:\n name: Typecheck\n uses: theholocron/.github/.github/workflows/verification.typeSafety.yml@main\n secrets: inherit\n",
241
+ "verification.performance": "name: Verification\n\non: # yamllint disable-line rule:truthy\n push:\n branches: [main, alpha]\n pull_request:\n\npermissions:\n contents: read\n\njobs:\n performance:\n name: Audit the Performance\n uses: theholocron/.github/.github/workflows/verification.performance.yml@main\n secrets: inherit\n",
242
+ "sourceQuality.staticAnalysis": "name: Source Quality\n\non: # yamllint disable-line rule:truthy\n push:\n branches: [main, alpha]\n pull_request:\n\nconcurrency:\n group: source-quality-static-analysis-${{ github.ref }}\n cancel-in-progress: true\n\npermissions:\n contents: write\n statuses: write\n\njobs:\n static-analysis:\n name: Static Analysis\n uses: theholocron/.github/.github/workflows/sourceQuality.staticAnalysis.yml@main\n secrets: inherit\n",
243
+ "sourceQuality.formatting": "name: Source Quality\n\non: # yamllint disable-line rule:truthy\n push:\n branches: [main, alpha]\n pull_request:\n\nconcurrency:\n group: source-quality-formatting-${{ github.ref }}\n cancel-in-progress: true\n\npermissions:\n contents: write\n statuses: write\n\njobs:\n formatting:\n name: Formatting\n uses: theholocron/.github/.github/workflows/sourceQuality.formatting.yml@main\n secrets: inherit\n",
244
+ "sourceQuality.structuredDataValidation": "name: Source Quality\n\non: # yamllint disable-line rule:truthy\n push:\n branches: [main, alpha]\n pull_request:\n\nconcurrency:\n group: source-quality-structured-data-validation-${{ github.ref }}\n cancel-in-progress: true\n\npermissions:\n contents: read\n\njobs:\n structured-data-validation:\n name: Structured Data Validation\n uses: theholocron/.github/.github/workflows/sourceQuality.structuredDataValidation.yml@main\n secrets: inherit\n",
245
+ "sourceQuality.deadCodeAnalysis": "name: Source Quality\n\non: # yamllint disable-line rule:truthy\n push:\n branches: [main, alpha]\n pull_request:\n\npermissions:\n contents: read\n\njobs:\n dead-code-analysis:\n name: Dead Code Analysis\n uses: theholocron/.github/.github/workflows/sourceQuality.deadCodeAnalysis.yml@main\n secrets: inherit\n",
246
+ "security.secretDetection": "name: Security\n\non: # yamllint disable-line rule:truthy\n push:\n branches: [main, alpha]\n pull_request:\n\nconcurrency:\n group: security-secret-detection-${{ github.ref }}\n cancel-in-progress: true\n\npermissions:\n contents: read\n\njobs:\n secret-detection:\n name: Secret Detection\n uses: theholocron/.github/.github/workflows/security.secretDetection.yml@main\n secrets: inherit\n",
247
+ "security.codeScanning": "name: Security\n\non: # yamllint disable-line rule:truthy\n push:\n branches:\n - main\n pull_request:\n branches:\n - main\n schedule:\n - cron: \"0 0 * * 1\"\n\npermissions:\n actions: read\n contents: read\n security-events: write\n\njobs:\n security:\n name: Code Scanning\n uses: theholocron/.github/.github/workflows/security.codeScanning.yml@main\n secrets: inherit\n",
248
+ "delivery.build": "name: Delivery\n\non: # yamllint disable-line rule:truthy\n push:\n branches-ignore:\n - main # delivery.publish builds before publishing\n pull_request:\n branches:\n - main\n\nconcurrency:\n group: delivery.build-${{ github.ref }}\n cancel-in-progress: true\n\npermissions:\n contents: read\n\njobs:\n build:\n name: Build\n uses: theholocron/.github/.github/workflows/delivery.build.yml@main\n secrets: inherit\n",
249
+ "delivery.publish": "name: Delivery\n\non: # yamllint disable-line rule:truthy\n push:\n branches:\n - main\n - alpha\n workflow_dispatch:\n inputs:\n dry_run:\n description: >\n Dry run — analyze commits and preview the release without git writes\n or publish. Push-triggered runs always run fully; this only applies\n to manual workflow_dispatch triggers.\n required: false\n default: true\n type: boolean\n\npermissions:\n contents: write\n id-token: write\n issues: write\n pull-requests: write\n\nconcurrency:\n group: ${{ github.workflow }}-${{ github.ref }}\n cancel-in-progress: false\n\njobs:\n release:\n name: Release\n uses: theholocron/.github/.github/workflows/delivery.publish.yml@main\n with:\n dry-run: ${{ inputs.dry_run == true }}\n secrets: inherit\n",
250
+ "delivery.deploy": "name: Delivery\n\non: # yamllint disable-line rule:truthy\n push:\n branches: [main]\n workflow_dispatch:\n\nconcurrency:\n group: pages\n cancel-in-progress: false\n\npermissions:\n contents: read\n pages: write\n id-token: write\n\njobs:\n deploy:\n name: Deploy\n uses: theholocron/.github/.github/workflows/delivery.deploy.yml@main\n secrets: inherit\n",
251
+ "delivery.bundleSize": "name: Delivery\n\non: # yamllint disable-line rule:truthy\n push:\n branches: [main, alpha]\n pull_request:\n\npermissions:\n contents: read\n\njobs:\n bundle-size:\n name: Audit the Bundle Size\n uses: theholocron/.github/.github/workflows/delivery.bundleSize.yml@main\n secrets: inherit\n",
252
+ "delivery.previewCheck": "name: Delivery\n\non: # yamllint disable-line rule:truthy\n deployment_status:\n\npermissions:\n contents: read\n # Must be a superset of what the reusable workflow's own job\n # requests (holocron#1058) -- GitHub hard-fails the whole run at\n # startup, zero jobs dispatched, if the callee asks for more than\n # the caller grants. Needed for the author-trust step's\n # commits/{sha}/pulls lookup.\n pull-requests: read\n\njobs:\n preview-check:\n name: Preview Check\n # environment_url is the only field documented to point at the live\n # app — target_url can point at a deployment-details page instead,\n # so there's no safe fallback to it. No environment_url means\n # nothing to check; skip the job entirely rather than hit the wrong\n # page.\n if: >-\n github.event.deployment_status.state == 'success' &&\n github.event.deployment.environment == 'Preview' &&\n github.event.deployment_status.environment_url != ''\n uses: theholocron/.github/.github/workflows/delivery.previewCheck.yml@main\n with:\n preview-url: ${{ github.event.deployment_status.environment_url }}\n # GitHub evaluates which version of this workflow file to run\n # from the default branch for a deployment_status event (unlike\n # push/pull_request) -- without this, the reusable workflow's\n # checkout would default to that same ref instead of the PR's\n # actual deployed commit.\n sha: ${{ github.event.deployment.sha }}\n secrets:\n # Named explicitly, not `secrets: inherit` -- this job needs only\n # these two secrets (holocron#1030/#1032), not every secret this\n # workflow's caller context has access to (e.g.\n # HOLOCRON_DEPLOYMENTS_TOKEN). VERCEL_TOKEN here is read-only use\n # (verifying deployment ownership), never a write/deploy\n # credential.\n VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}\n VERCEL_PROTECTION_BYPASS_SECRET: ${{ secrets.VERCEL_PROTECTION_BYPASS_SECRET }}\n",
253
+ "delivery.previewDeploy": "name: Delivery\n\non: # yamllint disable-line rule:truthy\n pull_request: {}\n\nconcurrency:\n group: preview-deploy-${{ github.event.pull_request.number }}\n # Not true: a cancelled run could land between Vercel accepting the\n # upload and the report job posting a status, leaving a real preview\n # deployment with no corresponding GitHub check — worse than letting an\n # overlapping run for the same PR simply queue.\n cancel-in-progress: false\n\npermissions:\n contents: read\n deployments: write\n # Must be a superset of what the reusable workflow's own job requests\n # (holocron#1058) -- its new path-relevance check lists the PR's\n # changed files via the API (holocron#1023).\n pull-requests: read\n\njobs:\n preview-deploy:\n name: Preview Deploy\n uses: theholocron/.github/.github/workflows/delivery.previewDeploy.yml@main\n with:\n cwd: \".\"\n secrets: inherit\n",
254
+ "platform.repoSync": "name: Platform\n\non: # yamllint disable-line rule:truthy\n push:\n branches: [main]\n paths:\n - holocron.config.ts\n - package.json\n - pnpm-workspace.yaml\n workflow_dispatch:\n inputs:\n steps:\n description: \"Sync steps to run (default: all)\"\n type: string\n required: false\n\nconcurrency:\n group: sync-${{ github.ref }}\n cancel-in-progress: true\n\npermissions:\n contents: write\n pull-requests: write\n\njobs:\n sync:\n name: Sync\n uses: theholocron/.github/.github/workflows/platform.repoSync.yml@main\n with:\n steps: ${{ inputs.steps }}\n secrets: inherit\n",
255
+ "platform.notifyCheck": "name: Platform\n\non: # yamllint disable-line rule:truthy\n workflow_dispatch: {}\n\npermissions: {}\n\njobs:\n notify-check:\n name: Notify Check\n uses: theholocron/.github/.github/workflows/platform.notifyCheck.yml@main\n secrets: inherit\n",
256
+ "platform.commitStandards": "name: Platform\n\non: # yamllint disable-line rule:truthy\n pull_request:\n\nconcurrency:\n group: platform-commit-standards-${{ github.ref }}\n cancel-in-progress: true\n\npermissions:\n contents: read\n\njobs:\n commit-standards:\n name: Commit Standards\n uses: theholocron/.github/.github/workflows/platform.commitStandards.yml@main\n secrets: inherit\n",
257
+ "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",
258
+ "platform.syncTemplates": "name: Platform\n\n# Bootstrap note: this `uses:` a reusable workflow that must already exist\n# on theholocron/.github's main branch to resolve at all — a brand-new\n# reusable workflow here needs ONE manual `holocron sync github` run\n# (pushing directly to theholocron/.github) before this thin caller can\n# ever successfully trigger it, since GitHub resolves `uses:` at run time,\n# not at merge time. Same deadlock every reusable workflow added here hits\n# on its first introduction.\non: # yamllint disable-line rule:truthy\n push:\n # main only — this publishes to theholocron/.github, consumed by every\n # other repo's CI. alpha is the prerelease channel; publishing from it\n # would let unreleased/unstable template changes overwrite what main\n # just published, the same stable-vs-prerelease split this org already\n # enforces for npm's dist-tags and Sentinel's own deploy trigger.\n branches: [main]\n paths:\n - packages/astromech/src/templates/**\n - packages/astromech/src/generators/**\n - packages/astromech/src/registry/**\n - packages/cli/src/templates/**\n workflow_dispatch: {}\n\nconcurrency:\n group: sync-templates\n cancel-in-progress: false\n\npermissions:\n contents: read\n\njobs:\n sync-templates:\n name: Sync Templates\n # workflow_dispatch can target ANY ref, not only main — without this,\n # anyone able to dispatch this workflow (a broader group than who can\n # push to main) could run it against an arbitrary branch, checking out\n # and building that ref's own code with the sync token present, then\n # publishing that ref's templates to theholocron/.github.\n if: github.ref == 'refs/heads/main'\n uses: theholocron/.github/.github/workflows/platform.syncTemplates.yml@main\n secrets: inherit\n",
259
+ "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",
260
+ 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",
261
+ 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",
262
+ 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",
263
+ 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",
264
+ 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",
265
+ bookkeeping: "name: Platform\n\non: # yamllint disable-line rule:truthy\n pull_request:\n types:\n - opened\n - edited\n\npermissions:\n contents: read\n issues: write\n pull-requests: write\n\njobs:\n bookkeeping:\n name: Bookkeeping\n uses: theholocron/.github/.github/workflows/bookkeeping.yml@main\n secrets: inherit\n"
266
+ };
267
+ const KNOWN_WORKFLOWS = new Set(Object.keys(WORKFLOW_TEMPLATES));
268
+ /**
269
+ * The GitHub status-check context a `required` task contributes to branch
270
+ * protection. Format: `"{workflow name} / {job name}"`.
271
+ *
272
+ * Most of these are single-job workflows now (D3's decomposition split what
273
+ * used to be multi-job `lint`/`audit` into one task per concern) — the
274
+ * context names that job directly, no `Conclusion` aggregator needed; a
275
+ * single job's own conclusion already *is* the workflow's conclusion.
276
+ * `Conclusion` fan-in jobs are kept only where a task genuinely has several
277
+ * conditionally-run jobs feeding one required check:
278
+ * `verification.unitTests` (unit / Storybook / Chromatic / interaction /
279
+ * user-flow, each gated by its own `run-*` input) and
280
+ * `platform.repoValidation` (its three script jobs all must pass). Only
281
+ * merge-gating workflows are listed. `astromech.requiredChecks()` reads this
282
+ * for every `required` task.
283
+ */
284
+ const WORKFLOW_CHECK_CONTEXTS = {
285
+ "delivery.build": "Build / Build every workspace",
286
+ "verification.unitTests": "Test / Conclusion",
287
+ "verification.typeSafety": "Typecheck / Run tsc --noEmit",
288
+ "verification.performance": "Audit the Performance / Run Lighthouse CI",
289
+ "sourceQuality.staticAnalysis": "Static Analysis / Run eslint and actionlint",
290
+ "sourceQuality.formatting": "Formatting / Run prettier, editorconfig-checker, markdownlint",
291
+ "sourceQuality.structuredDataValidation": "Structured Data Validation / Run yamllint",
292
+ "sourceQuality.deadCodeAnalysis": "Dead Code Analysis / Run knip",
293
+ "security.secretDetection": "Secret Detection / Run gitleaks",
294
+ "platform.commitStandards": "Commit Standards / Run commitlint",
295
+ "platform.repoValidation": "Repo Validation / Conclusion",
296
+ "delivery.bundleSize": "Audit the Bundle Size / Upload bundle stats to Codecov"
297
+ };
298
+ /**
299
+ * Generate the thin caller content for a workflow, optionally injecting or
300
+ * merging `with:` overrides into the jobs block.
301
+ *
302
+ * Two strategies are used depending on the template:
303
+ * - Templates that already have a `with:` block (e.g. lint):
304
+ * the override entries are merged in, replacing existing keys and appending
305
+ * new ones.
306
+ * - Templates that end with ` secrets: inherit`: a new `with:` block is
307
+ * injected immediately before `secrets: inherit`.
308
+ * If neither pattern matches the template, a warning is emitted and the
309
+ * base template is returned unchanged.
310
+ */
311
+ function generateThinCallerContent(name, withOverrides, additionalPaths, logger, comments) {
312
+ const base = WORKFLOW_TEMPLATES[name];
313
+ if (!base) return "";
314
+ const yamlScalar = (v) => {
315
+ if (v === true) return "true";
316
+ if (v === false) return "false";
317
+ const s = String(v);
318
+ return s.startsWith("[") || s.startsWith("{") ? `'${s}'` : s;
319
+ };
320
+ const fmt = (k, v) => {
321
+ const line = ` ${k}: ${yamlScalar(v)}`;
322
+ return comments?.[k] ? ` # ${comments[k]}\n${line}` : line;
323
+ };
324
+ let result = base;
325
+ if (additionalPaths && additionalPaths.length > 0) {
326
+ const pathsBlockRe = /( {4}paths:\n)((?:[ ]{6}- [^\n]+\n)+)/;
327
+ if (pathsBlockRe.test(result)) result = result.replace(pathsBlockRe, (_, header, existing) => {
328
+ const existingPaths = new Set([...existing.matchAll(/- (.+)/g)].map((m) => m[1]));
329
+ const newEntries = additionalPaths.filter((p) => !existingPaths.has(p)).map((p) => ` - ${p}\n`).join("");
330
+ return header + existing + newEntries;
331
+ });
332
+ else {
333
+ const pathsBlock = ` paths:\n${additionalPaths.map((p) => ` - ${p}\n`).join("")}`;
334
+ result = result.replace(/( {4}branches: \[main\]\n)/, `$1${pathsBlock}`);
335
+ }
336
+ }
337
+ if (!withOverrides || Object.keys(withOverrides).length === 0) return result;
338
+ const withBlockRe = /( {4}with:\n)((?:[ ]{6}[^\n]+\n)*)/;
339
+ const existingMatch = result.match(withBlockRe);
340
+ if (existingMatch) {
341
+ const existingEntries = new Map(existingMatch[2].split("\n").filter(Boolean).filter((line) => !line.trim().startsWith("#")).map((line) => {
342
+ const m = line.match(/^ {6}([^:]+):\s*(.*)/);
343
+ return m ? [m[1].trim(), m[2].trim()] : null;
344
+ }).filter((e) => e !== null));
345
+ for (const [k, v] of Object.entries(withOverrides)) existingEntries.set(k, yamlScalar(v));
346
+ const merged = [...existingEntries.entries()].map(([k, v]) => ` ${k}: ${v}`).join("\n");
347
+ return result.replace(withBlockRe, ` with:\n${merged}\n`);
348
+ }
349
+ const withBlock = Object.entries(withOverrides).map(([k, v]) => fmt(k, v)).join("\n");
350
+ const injected = result.replace(/ {4}secrets: inherit\n$/, ` with:\n${withBlock}\n secrets: inherit\n`);
351
+ if (injected === result) logger?.warn({ template: name }, "generateThinCallerContent: could not inject `with:` overrides");
352
+ return injected;
353
+ }
354
+ /**
355
+ * Extract the Cloudflare Pages preview config from a deploy workflow's `with:` object.
356
+ *
357
+ * Accepts three forms:
358
+ * - `preview: true` — derive both project and domain from org context
359
+ * - `preview: { project: "..." }` — explicit project; domain derived from context if omitted
360
+ * - `preview: { project: "...", domain: "..." }` — fully explicit
361
+ *
362
+ * Returns null when `preview:` is absent, false, or can't be resolved.
363
+ */
364
+ function extractPreviewConfig(raw, ctx = {}) {
365
+ const preview = raw["preview"];
366
+ if (!preview) return null;
367
+ if (preview === true) {
368
+ const project = ctx.org ? `${ctx.org}-preview` : null;
369
+ const domain = ctx.domain ? `preview.${ctx.domain}` : void 0;
370
+ if (!project) return null;
371
+ return {
372
+ project,
373
+ ...domain ? { domain } : {}
374
+ };
375
+ }
376
+ if (typeof preview !== "object") return null;
377
+ const p = preview;
378
+ const project = typeof p["project"] === "string" && p["project"] ? p["project"] : ctx.org ? `${ctx.org}-preview` : null;
379
+ if (!project) return null;
380
+ const domain = typeof p["domain"] === "string" && p["domain"] ? p["domain"] : ctx.domain ? `preview.${ctx.domain}` : void 0;
381
+ return {
382
+ project,
383
+ ...domain ? { domain } : {}
384
+ };
385
+ }
386
+ /**
387
+ * Generate the full thin-caller YAML for a `deploy.yml` that handles both
388
+ * production (push to main → GitHub Pages) and preview (pull_request →
389
+ * Cloudflare Pages) in a single file.
390
+ *
391
+ * Both jobs receive the same docs/storybook `with:` inputs. If the per-repo
392
+ * config supplies `cloudflare-project` it is forwarded; otherwise the reusable
393
+ * falls back to the `CLOUDFLARE_PAGES_PROJECT` org variable — set that once and
394
+ * all repos with a `deploy` workflow get previews without per-repo config.
395
+ */
396
+ function generateCombinedDeployContent(deployWith, paths, preview) {
397
+ const yamlScalar = (v) => {
398
+ if (v === true) return "true";
399
+ if (v === false) return "false";
400
+ const s = String(v);
401
+ return s.startsWith("[") || s.startsWith("{") ? `'${s}'` : s;
402
+ };
403
+ const withLines = (entries) => Object.entries(entries).map(([k, v]) => ` ${k}: ${yamlScalar(v)}`).join("\n");
404
+ const pathsBlock = paths.length > 0 ? ` paths:\n${paths.map((p) => ` - ${p}\n`).join("")}` : "";
405
+ const previewWith = {
406
+ ...deployWith,
407
+ "cloudflare-project": preview.project
408
+ };
409
+ const deployWithBlock = Object.keys(deployWith).length > 0 ? ` with:\n${withLines(deployWith)}\n` : "";
410
+ const previewWithBlock = ` with:\n${withLines(previewWith)}\n`;
411
+ return [
412
+ `name: Deploy`,
413
+ ``,
414
+ `on: # yamllint disable-line rule:truthy`,
415
+ ` push:`,
416
+ ` branches: [main]`,
417
+ ...pathsBlock ? [`${pathsBlock}`] : [],
418
+ ` pull_request:`,
419
+ ` branches: [main]`,
420
+ ` types: [opened, synchronize, reopened, closed]`,
421
+ ...pathsBlock ? [`${pathsBlock}`] : [],
422
+ ` workflow_dispatch:`,
423
+ ``,
424
+ `concurrency:`,
425
+ ` group: $\{{ github.event_name == 'pull_request' && format('preview-{0}', github.event.pull_request.number) || 'pages' }}`,
426
+ ` cancel-in-progress: $\{{ github.event_name == 'pull_request' && github.event.action != 'closed' }}`,
427
+ ``,
428
+ `permissions:`,
429
+ ` contents: read`,
430
+ ` deployments: write`,
431
+ ` pages: write`,
432
+ ` id-token: write`,
433
+ ` pull-requests: write`,
434
+ ``,
435
+ `jobs:`,
436
+ ` deploy:`,
437
+ ` name: Deploy`,
438
+ ` if: \${{ github.event_name != 'pull_request' }}`,
439
+ ` uses: theholocron/.github/.github/workflows/delivery.deploy.yml@main`,
440
+ ...deployWithBlock ? [deployWithBlock.trimEnd()] : [],
441
+ ` secrets: inherit`,
442
+ ``,
443
+ ` preview:`,
444
+ ` name: Preview`,
445
+ ` if: \${{ github.event_name == 'pull_request' }}`,
446
+ ` uses: theholocron/.github/.github/workflows/preview.yml@main`,
447
+ previewWithBlock.trimEnd(),
448
+ ` secrets: inherit`,
449
+ ``
450
+ ].join("\n");
451
+ }
452
+ /**
453
+ * Expand structured with-values to flat GitHub Actions inputs before
454
+ * generating the thin caller. Handles:
455
+ * - deploy shorthand: docs/storybook → type + storybook-projects
456
+ * - preview: stripped (handled separately via extractPreviewConfig)
457
+ * - chromatic-projects entries' untraced array → newline-joined string
458
+ * - plain arrays → JSON-stringified for YAML scalar quoting
459
+ *
460
+ * Used by both `holocron repo setup` and `sync-workflow-templates`.
461
+ */
462
+ function normalizeWorkflowWith(raw) {
463
+ const result = { ...raw };
464
+ delete result["preview"];
465
+ const hasDocs = raw["docs"] === true || raw["docs"] !== null && typeof raw["docs"] === "object";
466
+ const storybookProjects = raw["storybook"];
467
+ if (hasDocs) {
468
+ result["type"] = "docs";
469
+ delete result["docs"];
470
+ }
471
+ if (Array.isArray(storybookProjects)) {
472
+ if (!hasDocs) result["type"] = "storybook";
473
+ result["storybook-projects"] = JSON.stringify(storybookProjects.map(({ name, path = "." }) => ({
474
+ name,
475
+ workingDir: path
476
+ })));
477
+ delete result["storybook"];
478
+ }
479
+ const chromaticProjects = raw["chromatic-projects"];
480
+ if (Array.isArray(chromaticProjects)) result["chromatic-projects"] = chromaticProjects.map((p) => ({
481
+ ...p,
482
+ ...Array.isArray(p.untraced) ? { untraced: p.untraced.join("\n") } : {}
483
+ }));
484
+ for (const [k, v] of Object.entries(result)) if (Array.isArray(v)) result[k] = JSON.stringify(v);
485
+ return result;
486
+ }
487
+ /**
488
+ * Derive on.push.paths entries from the deploy with: shorthand.
489
+ * Used by both `holocron repo setup` and `sync-workflow-templates`.
490
+ */
491
+ function deriveDeployPaths(raw) {
492
+ const paths = [];
493
+ const docs = raw["docs"];
494
+ if (docs === true) {
495
+ paths.push("docs/**");
496
+ paths.push("astro.config.ts");
497
+ paths.push("pnpm-workspace.yaml");
498
+ paths.push("pnpm-lock.yaml");
499
+ } else if (docs !== null && typeof docs === "object" && "path" in docs) {
500
+ const p = docs.path;
501
+ if (p && p !== ".") paths.push(`${p}/**`);
502
+ }
503
+ const storybookProjects = raw["storybook"];
504
+ if (Array.isArray(storybookProjects)) for (const s of storybookProjects) {
505
+ const p = s.path || ".";
506
+ if (p === ".") {
507
+ paths.push("src/**");
508
+ paths.push(".storybook/**");
509
+ } else paths.push(`${p}/**`);
510
+ }
511
+ return paths;
512
+ }
513
+ //#endregion
514
+ //#region src/generators/github-action-reusable-workflows/github-action-reusable-workflows.ts
515
+ /**
516
+ * The **reusable** GitHub Actions surface pushed to `theholocron/.github` by
517
+ * `holocron sync github` — the `workflow_call` implementations and composite
518
+ * actions that every repo's thin callers delegate to.
519
+ *
520
+ * `theholocron/.github` is a **pure sync target**: `.github/workflows/*`,
521
+ * `.github/actions/*` and `workflow-templates/*` there are generated from the
522
+ * `.yml` files in `src/templates/reusable/` and pushed verbatim (with a
523
+ * "do not edit" header). Never hand-edit them in `.github`.
524
+ *
525
+ * Not to be confused with `WORKFLOW_TEMPLATES` in `github-action-thin-callers.ts` — those are
526
+ * the *thin-caller bases* written into each consumer repo's `.github/workflows/`.
527
+ */
528
+ /** `workflow_call` implementations → `.github/workflows/<name>.yml`. */
529
+ const REUSABLE_WORKFLOWS = {
530
+ "verification.unitTests": verification_unitTests_default$1,
531
+ "verification.visualRegression": verification_visualRegression_default$1,
532
+ "verification.typeSafety": verification_typeSafety_default$1,
533
+ "verification.performance": verification_performance_default$1,
534
+ "sourceQuality.staticAnalysis": sourceQuality_staticAnalysis_default$1,
535
+ "sourceQuality.formatting": sourceQuality_formatting_default$1,
536
+ "sourceQuality.structuredDataValidation": sourceQuality_structuredDataValidation_default$1,
537
+ "sourceQuality.deadCodeAnalysis": sourceQuality_deadCodeAnalysis_default$1,
538
+ "security.secretDetection": security_secretDetection_default$1,
539
+ "security.codeScanning": security_codeScanning_default$1,
540
+ "delivery.build": delivery_build_default$1,
541
+ "delivery.publish": delivery_publish_default$1,
542
+ "delivery.deploy": delivery_deploy_default$1,
543
+ "delivery.bundleSize": delivery_bundleSize_default$1,
544
+ "delivery.previewCheck": delivery_previewCheck_default$1,
545
+ "delivery.previewDeploy": delivery_previewDeploy_default$1,
546
+ "platform.repoSync": platform_repoSync_default$1,
547
+ "platform.notifyCheck": platform_notifyCheck_default$1,
548
+ "platform.commitStandards": platform_commitStandards_default$1,
549
+ "platform.repoValidation": platform_repoValidation_default$1,
550
+ "platform.syncTemplates": platform_syncTemplates_default$1,
551
+ "platform.dispatchedCheck": platform_dispatchedCheck_default,
552
+ "knowledge.wiki": knowledge_wiki_default$1,
553
+ bookkeeping: bookkeeping_default$1,
554
+ dependencies: dependencies_default$1,
555
+ preview: preview_default$1,
556
+ greetings: greetings_default$1,
557
+ review: review_default$1,
558
+ stale: stale_default$1,
559
+ "sync-dispatch": sync_dispatch_default,
560
+ tag: tag_default,
561
+ "sync-github": sync_github_default
562
+ };
563
+ /** Composite actions → `.github/actions/<name>/action.yml` (key includes `/action`). */
564
+ const REUSABLE_ACTIONS = {
565
+ "auto-commit/action": auto_commit_default,
566
+ "holocron/action": holocron_default,
567
+ "install/action": install_default,
568
+ "notify/action": notify_default,
569
+ "setup/action": setup_default,
570
+ "setup-node/action": setup_node_default
571
+ };
572
+ /** GitHub starter-workflow metadata → `workflow-templates/<name>.properties.json`. */
573
+ const WORKFLOW_TEMPLATE_PROPERTIES = { bookkeeping: JSON.stringify({
574
+ name: "Bookkeeping",
575
+ description: "Label and track issues and pull requests.",
576
+ iconName: "octicon tag"
577
+ }, null, 2) };
578
+ const SOURCE = "packages/astromech/src/generators/github-action-reusable-workflows/github-action-reusable-workflows.ts";
579
+ /**
580
+ * "AUTO-GENERATED — do not edit" header for the YAML pushed to
581
+ * `theholocron/.github`. Matches `createHeader().workflowHeader({ forPrimary: true })`
582
+ * field-for-field, minus any timestamp — a live `Synced:` line makes the content
583
+ * hash change on every run and defeats `sync github`'s unchanged-file skip.
584
+ */
585
+ function reusableHeader() {
586
+ return [
587
+ `# AUTO-GENERATED — do not edit in theholocron/.github directly.`,
588
+ `# Source: theholocron/holocron · ${SOURCE}`,
589
+ `# Tool: holocron sync github`,
590
+ `# Changes: edit source in theholocron/holocron`,
591
+ ``
592
+ ].join("\n");
593
+ }
594
+ /**
595
+ * The complete file batch `holocron sync github` pushes to `theholocron/.github`:
596
+ * path → content. YAML gets the "do not edit" header; the starter-workflow
597
+ * `.properties.json` files are left bare (native JSON, not a commented file).
598
+ */
599
+ function reusableTemplates() {
600
+ const out = /* @__PURE__ */ new Map();
601
+ const header = reusableHeader();
602
+ for (const [name, body] of Object.entries(REUSABLE_ACTIONS)) out.set(`.github/actions/${name}.yml`, `${header}${body}`);
603
+ for (const [name, body] of Object.entries(REUSABLE_WORKFLOWS)) out.set(`.github/workflows/${name}.yml`, `${header}${body}`);
604
+ for (const [name, body] of Object.entries(WORKFLOW_TEMPLATES)) {
605
+ out.set(`workflow-templates/${name}.yml`, `${header}${body}`);
606
+ const props = WORKFLOW_TEMPLATE_PROPERTIES[name];
607
+ if (props) out.set(`workflow-templates/${name}.properties.json`, props);
608
+ }
609
+ return out;
610
+ }
611
+ //#endregion
612
+ //#region src/registry/registry.ts
613
+ /**
614
+ * The task registry — how each task runs *locally*, without GitHub
615
+ * Actions. `holocron run <task>` and `holocron run ci` resolve against this;
616
+ * adding a task here gives every repo that task.
617
+ *
618
+ * Keyed identically to the workflow templates — a task IS a workflow. This
619
+ * is the canonical vocabulary table (epic #672, D11): every other artifact
620
+ * (`KNOWN_TASKS`, `thin-callers.ts`'s `KNOWN_WORKFLOWS`/`WORKFLOW_CHECK_CONTEXTS`,
621
+ * `CI_ORDER`) derives from these keys rather than hand-duplicating them —
622
+ * including the future GitHub App (#679), which imports this same table for
623
+ * config-schema validation instead of reimplementing its own copy.
624
+ *
625
+ * Task names are an intent-facing vocabulary (`verification.*`,
626
+ * `sourceQuality.*`, `security.*`, `delivery.*`, `platform.*`,
627
+ * `knowledge.*`), not tool names — `eslint`/`vitest`/`tsdown`/… stay
628
+ * internal to this file and `theholocron/configs`. See
629
+ * `docs/wiki/specifications/tech-vocabulary-rename.spec.md` (#675) for the full mapping and
630
+ * the reasoning behind each namespace and decomposition.
631
+ *
632
+ * Spec: `docs/wiki/specifications/tech-astromech-task-runner.spec.md` (epic #581).
633
+ */
6
634
  const TASKS = {
7
635
  "verification.unitTests": {
8
636
  local: {
@@ -33,6 +661,7 @@ const TASKS = {
33
661
  dependsOn: ["^delivery.build"]
34
662
  }
35
663
  },
664
+ "verification.visualRegression": { local: null },
36
665
  "verification.performance": { local: { detect: [{
37
666
  when: /^lighthouse\.config\.(cjs|js|mjs|ts)$/,
38
667
  tool: "lhci",
@@ -101,683 +730,783 @@ const TASKS = {
101
730
  args: ["-b"]
102
731
  }
103
732
  ] },
104
- turbo: {
105
- inputs: [
106
- "src/**",
107
- "package.json",
108
- "tsdown.config.ts",
109
- "vite.config.ts",
110
- "rollup.config.ts",
111
- "tsconfig.json"
112
- ],
113
- outputs: ["dist/**"],
114
- dependsOn: ["^delivery.build"]
115
- }
116
- },
117
- "delivery.publish": {
118
- local: null,
119
- preview: true
120
- },
121
- "delivery.deploy": {
122
- local: null,
123
- preview: true
124
- },
125
- "delivery.bundleSize": { local: null },
126
- "platform.repoSync": { local: { command: "sync" } },
127
- "platform.commitStandards": {
128
- local: null,
129
- linterGroup: ["commitlint"]
130
- },
131
- "platform.repoValidation": {
132
- local: null,
133
- jobs: {
134
- adrs: {
135
- local: {
136
- tool: "node",
137
- args: ["scripts/validate-adrs.mjs"]
138
- },
139
- checkContext: "platform.repoValidation / Validate ADRs and specs"
140
- },
141
- registry: {
142
- local: {
143
- tool: "node",
144
- args: ["scripts/validate-registry.mjs"]
145
- },
146
- checkContext: "platform.repoValidation / Validate registry consistency"
147
- },
148
- docsPresence: {
149
- local: {
150
- tool: "node",
151
- args: ["scripts/validate-docs-presence.mjs"]
152
- },
153
- checkContext: "platform.repoValidation / Validate docs presence"
154
- }
155
- }
156
- },
157
- "knowledge.wiki": {
158
- local: { command: "sync-wiki" },
159
- preview: true
160
- },
161
- "knowledge.docs": {
162
- local: null,
163
- preview: true
164
- },
165
- "knowledge.components": {
166
- local: null,
167
- preview: true
168
- }
169
- };
170
- /** Every task name the registry knows. */
171
- const KNOWN_TASKS = new Set(Object.keys(TASKS));
172
- /**
173
- * The order `holocron ci` runs tasks in — cheapest / fastest signal first, so
174
- * an agent or a `pre-push` hook fails early. Tasks not listed here run last, in
175
- * manifest order. (The generated thin callers carry no `needs:` — cross-workflow
176
- * ordering lives in `theholocron/.github` — so `holocron ci` declares its own.)
177
- */
178
- const CI_ORDER = [
179
- "verification.typeSafety",
180
- "sourceQuality.staticAnalysis",
181
- "sourceQuality.formatting",
182
- "sourceQuality.structuredDataValidation",
183
- "security.secretDetection",
184
- "platform.commitStandards",
185
- "platform.repoValidation",
186
- "verification.unitTests",
187
- "delivery.build",
188
- "sourceQuality.deadCodeAnalysis",
189
- "delivery.bundleSize",
190
- "verification.performance",
191
- "security.codeScanning",
192
- "delivery.deploy"
193
- ];
194
- //#endregion
195
- //#region src/linters.ts
196
- /**
197
- * Known linters, in execution order. `always` entries are the current
198
- * hard-coded super-linter baseline; `prettier` is always-on because the org
199
- * applies it universally (super-linter only lints files that exist).
200
- */
201
- const LINTERS = {
202
- eslint: {
203
- validate: ["VALIDATE_JAVASCRIPT_ES", "VALIDATE_TYPESCRIPT_ES"],
204
- localBin: "eslint",
205
- localArgs: ["."],
206
- detect: [
207
- "eslint.config.ts",
208
- "eslint.config.js",
209
- "eslint.config.mjs",
210
- "eslint.config.cjs",
211
- ".eslintrc",
212
- ".eslintrc.json",
213
- ".eslintrc.yml",
214
- ".eslintrc.yaml",
215
- ".eslintrc.cjs"
216
- ],
217
- configInput: "eslint-config"
218
- },
219
- prettier: {
220
- validate: [
221
- "VALIDATE_JAVASCRIPT_PRETTIER",
222
- "VALIDATE_JSX_PRETTIER",
223
- "VALIDATE_TYPESCRIPT_PRETTIER",
224
- "VALIDATE_TSX",
225
- "VALIDATE_MARKDOWN_PRETTIER"
226
- ],
227
- fix: [
228
- "FIX_JAVASCRIPT_PRETTIER",
229
- "FIX_JSX_PRETTIER",
230
- "FIX_TYPESCRIPT_PRETTIER",
231
- "FIX_TSX",
232
- "FIX_MARKDOWN_PRETTIER"
233
- ],
234
- always: true,
235
- localBin: "prettier",
236
- localArgs: ["--check", "."],
237
- configInput: "prettier-config"
238
- },
239
- yamllint: {
240
- validate: ["VALIDATE_YAML"],
241
- always: true,
242
- localBin: "yamllint",
243
- localArgs: ["."],
244
- installHint: "brew install yamllint",
245
- configInput: "yaml-config"
246
- },
247
- actionlint: {
248
- validate: ["VALIDATE_GITHUB_ACTIONS"],
249
- always: true,
250
- localBin: "actionlint",
251
- localArgs: [],
252
- installHint: "brew install actionlint"
253
- },
254
- gitleaks: {
255
- validate: ["VALIDATE_GITLEAKS"],
256
- always: true,
257
- localBin: "gitleaks",
258
- localArgs: [
259
- "dir",
260
- "--no-banner",
261
- "--config",
262
- ".gitleaks.toml"
263
- ],
264
- installHint: "brew install gitleaks"
265
- },
266
- editorconfig: {
267
- validate: ["VALIDATE_EDITORCONFIG"],
268
- always: true,
269
- localBin: "editorconfig-checker",
270
- localArgs: [],
271
- installHint: "brew install editorconfig-checker"
272
- },
273
- commitlint: {
274
- validate: ["VALIDATE_GIT_COMMITLINT"],
275
- always: true
276
- },
277
- "git-merge-conflict-markers": {
278
- validate: ["VALIDATE_GIT_MERGE_CONFLICT_MARKERS"],
279
- always: true
280
- },
281
- markdownlint: {
282
- validate: ["VALIDATE_MARKDOWN"],
283
- localBin: "markdownlint-cli2",
284
- localArgs: ["**/*.md"],
285
- detect: [
286
- ".markdownlint.json",
287
- ".markdownlint.jsonc",
288
- ".markdownlint.yaml",
289
- ".markdownlint.yml",
290
- ".markdownlint-cli2.jsonc",
291
- ".markdownlint-cli2.yaml",
292
- ".markdownlint-cli2.mjs"
293
- ]
294
- }
295
- };
296
- /** Every linter name the registry knows. */
297
- const LINTER_NAMES = new Set(Object.keys(LINTERS));
298
- /**
299
- * Resolve the linter set for a repo. An `explicit` list (from `config.tasks`)
300
- * picks the candidate set; otherwise the candidates are every `always` linter
301
- * plus every `detect`-gated linter whose filenames are present at the repo
302
- * root. Either way, a `detect`-gated linter (eslint, markdownlint) only makes
303
- * the final set when its config file is actually present — a repo can't run
304
- * eslint without an `eslint.config.*`, and super-linter FATALs if you ask it
305
- * to (theholocron/holocron#654). `always` linters are unconditional. Result
306
- * is ordered by {@link LINTERS} declaration order.
307
- *
308
- * @throws when an `explicit` name is not in the registry — a typo is a
309
- * config bug, not a linter to silently skip.
310
- */
311
- function resolveLinters(opts) {
312
- const order = Object.keys(LINTERS);
313
- const present = new Set(opts.rootFiles);
314
- const configPresent = (def) => def.always === true || (def.detect ?? []).some((f) => present.has(f));
315
- if (opts.explicit && opts.explicit.length > 0) {
316
- const unknown = opts.explicit.filter((n) => !LINTER_NAMES.has(n));
317
- if (unknown.length > 0) throw new Error(`unknown linter${unknown.length > 1 ? "s" : ""} ${unknown.map((n) => `"${n}"`).join(", ")} — known: ${order.join(", ")}`);
318
- const wanted = new Set(opts.explicit);
319
- return order.filter((n) => wanted.has(n) && configPresent(LINTERS[n])).map((name) => ({
320
- name,
321
- def: LINTERS[name]
322
- }));
323
- }
324
- return order.filter((name) => configPresent(LINTERS[name])).map((name) => ({
325
- name,
326
- def: LINTERS[name]
327
- }));
328
- }
329
- //#endregion
330
- //#region src/resolver.ts
331
- /**
332
- * `library` is every mapped tool's org-wide default variant today — every
333
- * currently-migrated repo (`holocron`, `clients`, `utils`, `configs`,
334
- * `themes`, `observability`, `skills`) is a publishable library package, and
335
- * `eslint-config`/`vitest-config`/`tsdown-config` all name their
336
- * general-purpose preset exactly that. Picking a different variant per repo
337
- * (an app-type template, say) needs new `holocron.config.ts` surface this
338
- * doesn't add yet — deliberately out of scope until a real consumer needs it.
339
- */
340
- const TOOL_CONFIGS = {
341
- eslint: {
342
- package: "@theholocron/eslint-config",
343
- exportPath: "./bundles/library",
344
- flag: ["--config"]
733
+ turbo: {
734
+ inputs: [
735
+ "src/**",
736
+ "package.json",
737
+ "tsdown.config.ts",
738
+ "vite.config.ts",
739
+ "rollup.config.ts",
740
+ "tsconfig.json"
741
+ ],
742
+ outputs: ["dist/**"],
743
+ dependsOn: ["^delivery.build"]
744
+ }
345
745
  },
346
- prettier: {
347
- package: "@theholocron/prettier-config",
348
- exportPath: ".",
349
- flag: ["--config"]
746
+ "delivery.publish": {
747
+ local: null,
748
+ preview: true
350
749
  },
351
- vitest: {
352
- package: "@theholocron/vitest-config",
353
- exportPath: "./bundles/library",
354
- flag: ["--config"]
750
+ "delivery.deploy": {
751
+ local: null,
752
+ preview: true
355
753
  },
356
- tsdown: {
357
- package: "@theholocron/tsdown-config",
358
- exportPath: "./presets/library",
359
- flag: ["--config"]
754
+ "delivery.bundleSize": { local: null },
755
+ "delivery.previewCheck": { local: null },
756
+ "delivery.previewDeploy": { local: null },
757
+ "platform.repoSync": { local: { command: "sync" } },
758
+ "platform.notifyCheck": { local: null },
759
+ "platform.syncTemplates": { local: { command: "sync github" } },
760
+ "platform.commitStandards": {
761
+ local: null,
762
+ linterGroup: ["commitlint"]
360
763
  },
361
- commitlint: {
362
- package: "@theholocron/commitlint-config",
363
- exportPath: ".",
364
- flag: ["--config"]
764
+ "platform.repoValidation": {
765
+ local: null,
766
+ jobs: {
767
+ adrs: {
768
+ local: {
769
+ tool: "node",
770
+ args: ["scripts/validate-adrs.mjs"]
771
+ },
772
+ checkContext: "platform.repoValidation / Validate ADRs and specs"
773
+ },
774
+ registry: {
775
+ local: {
776
+ tool: "node",
777
+ args: ["scripts/validate-registry.mjs"]
778
+ },
779
+ checkContext: "platform.repoValidation / Validate registry consistency"
780
+ },
781
+ docsPresence: {
782
+ local: {
783
+ tool: "node",
784
+ args: ["scripts/validate-docs-presence.mjs"]
785
+ },
786
+ checkContext: "platform.repoValidation / Validate docs presence"
787
+ }
788
+ }
789
+ },
790
+ "knowledge.wiki": {
791
+ local: { command: "sync-wiki" },
792
+ preview: true
793
+ },
794
+ "knowledge.docs": {
795
+ local: null,
796
+ preview: true
797
+ },
798
+ "knowledge.components": {
799
+ local: null,
800
+ preview: true
365
801
  }
366
802
  };
367
- /** Every tool name the resolver knows how to point at a shared config. */
368
- const RESOLVABLE_TOOLS = new Set(Object.keys(TOOL_CONFIGS));
369
- /**
370
- * `<flag> <absolute path>` for `tool`, resolved against `cwd`'s
371
- * `node_modules` — or `[]` when the tool isn't mapped, the package isn't
372
- * installed, its `package.json` doesn't declare the export subpath this
373
- * needs, or the resolved file doesn't actually exist on disk (a stale
374
- * install, or a package version that predates the export existing).
375
- */
376
- function resolveToolConfig(tool, cwd, deps) {
377
- const spec = TOOL_CONFIGS[tool];
378
- if (!spec) return [];
379
- const pkgDir = joinPath(cwd, "node_modules", ...spec.package.split("/"));
380
- const pkgJsonPath = joinPath(pkgDir, "package.json");
381
- if (!deps.fileExists(pkgJsonPath)) return [];
382
- let pkg;
383
- try {
384
- pkg = JSON.parse(deps.readFile(pkgJsonPath));
385
- } catch {
386
- return [];
387
- }
388
- const relative = resolveExportEntry(pkg.exports?.[spec.exportPath]);
389
- if (!relative) return [];
390
- const absolute = joinPath(pkgDir, relative.replace(/^\.\//, ""));
391
- if (!deps.fileExists(absolute)) return [];
392
- return [...spec.flag, absolute];
393
- }
394
- /**
395
- * Unwrap a `package.json#exports` subpath entry down to its file path —
396
- * either a bare string, or an object with condition keys (`import`,
397
- * `default`, `require`, …) that may themselves nest one level (`{ import: {
398
- * types, default } }`, the shape every `@theholocron/*-config` package
399
- * actually uses). Prefers `import` (this org ships ESM), then `default`,
400
- * then `require` as a last resort.
401
- */
402
- function resolveExportEntry(entry) {
403
- if (typeof entry === "string") return entry;
404
- if (entry === null || typeof entry !== "object") return void 0;
405
- const conditions = entry;
406
- const value = conditions.import ?? conditions.default ?? conditions.require;
407
- if (typeof value === "string") return value;
408
- if (typeof value === "object" && value !== null) return resolveExportEntry(value);
409
- }
803
+ /** Every task name the registry knows. */
804
+ const KNOWN_TASKS = new Set(Object.keys(TASKS));
410
805
  /**
411
- * A minimal, POSIX-safe `path.join` — avoids importing `node:path` just for
412
- * this. Every path in play here is either already POSIX (a `package.json`
413
- * `exports` value, always forward-slashed regardless of platform) or built
414
- * from platform-neutral segments (`cwd`, `"node_modules"`, a scoped package
415
- * name split on `/`), so a manual join is safe and keeps this module
416
- * dependency-free like the rest of `run.ts`'s helpers.
806
+ * The order `holocron run ci` runs tasks in — cheapest / fastest signal first, so
807
+ * an agent or a `pre-push` hook fails early. Tasks not listed here run last, in
808
+ * manifest order. (The generated thin callers carry no `needs:` — cross-workflow
809
+ * ordering lives in `theholocron/.github` — so `holocron run ci` declares its own.)
417
810
  */
418
- function joinPath(...segments) {
419
- return segments.map((s, i) => i === 0 ? s.replace(/\/+$/, "") : s.replace(/^\/+|\/+$/g, "")).filter(Boolean).join("/");
420
- }
811
+ const CI_ORDER = [
812
+ "verification.typeSafety",
813
+ "sourceQuality.staticAnalysis",
814
+ "sourceQuality.formatting",
815
+ "sourceQuality.structuredDataValidation",
816
+ "security.secretDetection",
817
+ "platform.commitStandards",
818
+ "platform.repoValidation",
819
+ "verification.unitTests",
820
+ "delivery.build",
821
+ "sourceQuality.deadCodeAnalysis",
822
+ "delivery.bundleSize",
823
+ "verification.performance",
824
+ "security.codeScanning",
825
+ "delivery.deploy"
826
+ ];
421
827
  //#endregion
422
- //#region src/run.ts
828
+ //#region src/generators/required-checks/required-checks.ts
423
829
  /**
424
- * `holocron run <task> [job] [-- <passthrough>]` — run a registry task (or one
425
- * of its sub-jobs) locally.
426
- *
427
- * A `job` argument only applies to tasks that declare `jobs`; for any other
428
- * task it is folded back into the passthrough (`holocron run delivery.build
429
- * src/`).
430
- *
431
- * Resolution:
432
- *
433
- * J. job given → TASKS[task].jobs[job].local (unknown job → exit 1)
434
- * 1. turbo.json defines the task → `turbo run <task>`
435
- * 2. package.json has a `<task>` script → `<pm> run <task>`
436
- * (unless it's the `holocron run …` thin caller — that recurses)
437
- * 3. TASKS[task].local resolves → `<tool> <args> <org-flags> <passthrough>`
438
- * 3b. TASKS[task].linterGroup → run each resolved linter natively (see below)
439
- * 4. TASKS[task].jobs has entries → run each job in declared order
440
- * 5. TASKS[task].local === null → "enforced in CI" (skip, even with --required)
441
- * 5b. known task, nothing resolved → "no <task> task" (exit 0, or 1 with --required)
442
- * 6. unknown task → "unknown task" (exit 1)
443
- *
444
- * A `linterGroup` task (`sourceQuality.staticAnalysis`, etc.) runs its fixed
445
- * set of `linters.ts` entries, each still gated by that linter's own
446
- * `detect`/`always` rule — every entry runs its `localBin` natively when
447
- * found on PATH. Missing tools are flagged; the exit code is the worst of
448
- * the lot. Unlike the old single `lint` task, this check runs *after*
449
- * turbo/`package.json` resolution (step 3b, not step 0) — a `linterGroup`
450
- * task is a normal turbo-delegatable task like any other.
451
- */
452
- /** The per-command runner — `runTask` and `runLintAggregate` share it. */
453
- function makeRunOne(input) {
454
- const { print, logger, exec, task, cwd } = input;
455
- const dryRun = input.dryRun ?? false;
456
- return (cmd, args) => {
457
- const command = [cmd, ...args].join(" ");
458
- if (dryRun) {
459
- print(`would run: ${command}`);
460
- logger.debug({
461
- task,
462
- command,
463
- status: "dry-run"
464
- }, `run: ${task}`);
465
- return {
466
- status: "dry-run",
467
- command
468
- };
469
- }
470
- print(`→ ${command}`);
471
- const { exitCode } = exec(cmd, args, { cwd });
472
- const status = exitCode === 0 ? "ok" : "fail";
473
- logger[status === "fail" ? "warn" : "debug"]({
474
- task,
475
- command,
476
- exitCode,
477
- status
478
- }, `run: ${task}`);
479
- return {
480
- status,
481
- command,
482
- ...status === "fail" ? { message: `\`${command}\` exited ${exitCode}` } : {}
483
- };
484
- };
485
- }
486
- function runTask(input) {
487
- const { print, logger, readFile, fileExists, listDir, task, cwd } = input;
488
- const def = TASKS[task];
489
- let job = input.job;
490
- let passthrough = input.passthrough ?? [];
491
- if (job !== void 0 && !def?.jobs) {
492
- passthrough = [job, ...passthrough];
493
- job = void 0;
494
- }
495
- if (job !== void 0) {
496
- const jobDef = def.jobs[job];
497
- if (!jobDef) {
498
- const known = Object.keys(def.jobs).join(", ");
499
- print(`✗ unknown job "${job}" for "${task}" (known: ${known})`);
500
- logger.warn({
501
- task,
502
- job,
503
- status: "unknown"
504
- }, `run: ${task} ${job}`);
505
- return {
506
- status: "unknown",
507
- message: `unknown job "${task} ${job}"`
508
- };
509
- }
510
- const label = `${task} ${job}`;
511
- return runUnit(input, label, jobDef.local, makeRunOne({
512
- ...input,
513
- task: label
514
- }), passthrough);
515
- }
516
- const run = makeRunOne(input);
517
- if (turboDefinesTask(cwd, task, readFile, fileExists)) {
518
- const filterArg = input.filter ? [`--filter=${input.filter}`] : [];
519
- const forwarded = [...def?.local && !def.local.command && def.local.tool ? def.flags?.[def.local.tool] ?? [] : [], ...passthrough];
520
- const args = [
521
- "run",
522
- task,
523
- ...filterArg,
524
- ...forwarded.length ? ["--", ...forwarded] : []
525
- ];
526
- return run(resolveBin(cwd, "turbo", fileExists), args);
527
- }
528
- const script = packageJsonScript(cwd, task, readFile, fileExists);
529
- if (script && !/^holocron run\b/.test(script.trim())) return run(packageManager(cwd, readFile, fileExists), [
530
- "run",
531
- task,
532
- ...passthrough.length ? ["--", ...passthrough] : []
533
- ]);
534
- if (def?.local) {
535
- if (def.local.command) return run(process.execPath, [
536
- process.argv[1],
537
- def.local.command,
538
- ...passthrough
539
- ]);
540
- const runner = resolveRunner(def.local, cwd, listDir);
541
- if (runner) {
542
- const flags = (def.flags?.[runner.tool] ?? []).filter((f) => !passthrough.includes(f));
543
- const configFlags = resolveToolConfig(runner.tool, cwd, {
544
- readFile,
545
- fileExists
546
- });
547
- return run(resolveBin(cwd, runner.tool, fileExists), [
548
- ...runner.args,
549
- ...configFlags,
550
- ...flags,
551
- ...passthrough
552
- ]);
830
+ * `requiredChecks(config)` — the branch-protection required-status-check list,
831
+ * derived from the task manifest. Every `required: true` task contributes its
832
+ * {@link WORKFLOW_CHECK_CONTEXTS} entry; `config.extraRequiredChecks` adds
833
+ * contexts not backed by a task (codecov, DCO is prepended by the caller).
834
+ *
835
+ * `@theholocron/cli`'s `holocron repo setup` calls this instead of the old
836
+ * hand-maintained `repo.requiredChecks` array. Policy-free — it only knows the
837
+ * manifest.
838
+ */
839
+ /** Ordered (task contexts in {@link CI_ORDER}, then extras), de-duplicated. */
840
+ function requiredChecks(config) {
841
+ const entries = (config.tasks ?? []).map(normalizeTaskEntry);
842
+ const seen = /* @__PURE__ */ new Set();
843
+ const out = [];
844
+ for (const name of CI_ORDER) {
845
+ const context = entries.some((e) => e.name === name && e.required === true) ? WORKFLOW_CHECK_CONTEXTS[name] : void 0;
846
+ if (context && !seen.has(context)) {
847
+ seen.add(context);
848
+ out.push(context);
553
849
  }
554
850
  }
555
- if (def?.linterGroup) return runLinterGroup(input, def.linterGroup, passthrough);
556
- if (def?.jobs && Object.keys(def.jobs).length > 0) return runAllJobs(input, def.jobs, passthrough);
557
- if (def?.local === null) {
558
- const msg = `no local equivalent for ${task} — enforced in CI`;
559
- print(`· ${msg}`);
560
- logger.debug({
561
- task,
562
- status: "skip"
563
- }, `run: ${task}`);
564
- return {
565
- status: "skip",
566
- message: msg
567
- };
568
- }
569
- if (KNOWN_TASKS.has(task)) {
570
- const msg = `no ${task} task for this repo`;
571
- print(input.required ? `✗ ${msg} (required)` : `· ${msg}`);
572
- logger[input.required ? "warn" : "debug"]({
573
- task,
574
- status: input.required ? "fail" : "skip"
575
- }, `run: ${task}`);
576
- return {
577
- status: input.required ? "fail" : "skip",
578
- message: msg
579
- };
851
+ for (const context of config.extraRequiredChecks ?? []) if (!seen.has(context)) {
852
+ seen.add(context);
853
+ out.push(context);
580
854
  }
581
- print(`✗ unknown task "${task}"`);
582
- logger.warn({
583
- task,
584
- status: "unknown"
585
- }, `run: ${task}`);
586
- return {
587
- status: "unknown",
588
- message: `unknown task "${task}"`
855
+ return out;
856
+ }
857
+ //#endregion
858
+ //#region src/generators/tsconfig/tsconfig.ts
859
+ /**
860
+ * `tsconfig.json` generation — Bucket B (config-resolution workstream, #676):
861
+ * the file must stay committed (the TypeScript language server discovers it
862
+ * directly from disk, no `--config` override mechanism to hand it a path
863
+ * instead), but its content is fully derivable, so it's generated rather
864
+ * than hand-authored — the same category as `.editorconfig`
865
+ * ({@link "../../templates/configs/editorconfig/create-config.js" createConfig})
866
+ * and {@link codecovConfig}.
867
+ *
868
+ * Surveyed every package-level `tsconfig.json` across the org (`holocron`,
869
+ * `clients`, `utils` — 20+ packages). `extends: "@theholocron/tsconfig/
870
+ * node-lts"`, `compilerOptions.baseUrl: "./"`, `compilerOptions.outDir:
871
+ * "./dist"`, `include: ["src/**\/*.ts"]`, and `exclude: ["node_modules",
872
+ * "dist"]` are uniform in every one — safe defaults here. `paths` (a `@/*`
873
+ * → `./src/*` import alias) is a real per-package choice, present in
874
+ * roughly 40% of packages checked — an opt-in parameter, not a default.
875
+ *
876
+ * Two fields checked and deliberately *not* absorbed as defaults, unlike
877
+ * `eslint-config`'s `tsconfigRootDir`/`settings.node` (which turned out
878
+ * redundant with the shared package's own behavior, `configs`#461):
879
+ * `compilerOptions.module`/`moduleResolution` (only `@theholocron/cli`
880
+ * overrides these, to `esnext`/`bundler` — a genuine deviation from the
881
+ * shared `node-lts` preset's `nodenext`/`nodenext`, needed for its bundler
882
+ * tooling, not something every package should inherit) and
883
+ * `compilerOptions.rootDir` (present in exactly two packages, always the
884
+ * same value `./src` that `include` already implies — an unnecessary
885
+ * override wherever it appears, not a pattern worth generalizing).
886
+ *
887
+ * Monorepo *root* `tsconfig.json` (a TS project-references "solution
888
+ * file" — `holocron`'s own root is `{ files: [], references: [...] }`,
889
+ * listing which packages to build) is a structurally different, genuinely
890
+ * per-repo document — out of scope here, same as Bucket C content.
891
+ *
892
+ * Not yet wired into `holocron repo setup`'s per-package write loop (every
893
+ * other Bucket B file there is a single repo-root file, safely
894
+ * overwritten every run — `tsconfig.json` is per-package, and most
895
+ * packages' files still carry real hand-authored content today, not yet
896
+ * migrated to a fully-generated state). That per-package iteration +
897
+ * safe-migration design is `#680`'s job, not duplicated here — this is
898
+ * the generator itself, ready for it to call.
899
+ */
900
+ /**
901
+ * A package-level `tsconfig.json` — the uniform shape confirmed across
902
+ * every package checked, parameterized only by the two fields that
903
+ * genuinely vary (`display`, `paths`). No scaffold/workflow header (matches
904
+ * `.alexrc.json`'s existing precedent for a strict-JSON Bucket B file —
905
+ * JSON has no comment syntax to carry one, and TypeScript's own tolerance
906
+ * for JSONC comments in `tsconfig.json` isn't worth relying on for a file
907
+ * this thin).
908
+ */
909
+ function createTsconfig(options) {
910
+ const variant = options.variant ?? "node-lts";
911
+ const compilerOptions = {
912
+ baseUrl: "./",
913
+ outDir: "./dist"
914
+ };
915
+ if (options.paths) compilerOptions.paths = { "@/*": ["./src/*"] };
916
+ const config = {
917
+ display: options.display,
918
+ extends: `@theholocron/tsconfig/${variant}`,
919
+ compilerOptions,
920
+ include: ["src/**/*.ts"],
921
+ exclude: ["node_modules", "dist"]
589
922
  };
923
+ return stringifyJson(config);
590
924
  }
925
+ //#endregion
926
+ //#region src/generators/turbo/turbo.ts
591
927
  /**
592
- * Run one registry unit — a task's own runner or a single sub-job. Resolution
593
- * is registry-only (`turbo` / `package.json` scripts are keyed by task, not
594
- * `task/job`). `label` names the unit in messages (`audit performance`).
928
+ * `turboConfig(config)` — the generated `turbo.json` for a repo's task
929
+ * manifest, epic #672 D9 (#681). Every workspace, monorepo or single-package
930
+ * (Turborepo's single-package mode), gets this from the same `tasks` array
931
+ * that already drives thin callers / required checks / package scripts —
932
+ * never hand-authored.
595
933
  *
596
- * `local: null` → CI-only by design, always a skip (never a failure, even with
597
- * `--required`). A declared runner with no matching repo file, or whose tool
598
- * isn't installed locally → a skip (`--required` turns it into a failure — the
599
- * repo claims a check it can't back). Mirrors the lint aggregate: run what's
600
- * here, flag the rest, let CI enforce.
934
+ * Only tasks with a {@link TurboTaskConfig} in the registry (real per-
935
+ * workspace build/compile/test steps) get an entry. Whole-repo, single-run
936
+ * tools (prettier, gitleaks, commitlint, yamllint, …) have none — they stay
937
+ * off turbo.json entirely, same as today.
938
+ *
939
+ * A task entry's own `turbo.passThroughEnv` (schema.ts) appends extra env
940
+ * vars to the registry default's cache-key hashing — the one thing that's
941
+ * genuinely per-repo (e.g. a package tagging Sentry releases during its own
942
+ * build needs `SENTRY_AUTH_TOKEN` in its cache key; most repos don't).
943
+ * `turbo.inputs` (holocron#845) appends extra input globs the same way —
944
+ * for a package whose layout the registry default doesn't cover (e.g. a
945
+ * flat-layout package with no `src/` directory at all).
601
946
  */
602
- function runUnit(input, label, local, run, passthrough) {
603
- const { print, logger, listDir, lookPath, cwd } = input;
604
- const skip = (msg) => {
605
- print(input.required ? `✗ ${msg} (required)` : `· ${msg}`);
606
- logger[input.required ? "warn" : "debug"]({
607
- task: label,
608
- status: input.required ? "fail" : "skip"
609
- }, `run: ${label}`);
610
- return {
611
- status: input.required ? "fail" : "skip",
612
- message: msg
947
+ /** Filed once per repo, unconditionally — every generated turbo.json's root. */
948
+ const GLOBAL_DEPENDENCIES = ["pnpm-workspace.yaml", "tsconfig.json"];
949
+ /**
950
+ * Guards against a malformed `turbo.inputs`/`turbo.passThroughEnv` override
951
+ * (review on holocron#845): a bare string instead of an array would
952
+ * otherwise spread into individual characters
953
+ * (`[..."**\/*.ts"]` → `["*", "*", "/", "*", ".", "t", "s"]`), silently
954
+ * emitting garbage entries instead of the intended single glob/env-var —
955
+ * the generated `turbo.json` would still be valid JSON, just wrong, with
956
+ * nothing surfacing the mistake. Only a config loaded from JSON or plain JS
957
+ * reaches this unchecked; a typed `holocron.config.ts` via `defineConfig`
958
+ * already rejects the wrong shape at compile time.
959
+ */
960
+ function validatedGlobList(value, taskName, field) {
961
+ if (value === void 0) return [];
962
+ if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) throw new Error(`task "${taskName}": turbo.${field} must be an array of strings, got ${JSON.stringify(value)}`);
963
+ return value;
964
+ }
965
+ /**
966
+ * `turbo.json` content for this manifest, pretty-printed — write it
967
+ * directly, no merge (matches `thinCallers()` / `reusableTemplates()`: this
968
+ * file is generated, not hand-edited). `null` when no task in the manifest
969
+ * has turbo fan-out config (nothing to write).
970
+ */
971
+ function turboConfig(config) {
972
+ const entries = (config.tasks ?? []).map(normalizeTaskEntry);
973
+ const tasks = {};
974
+ for (const entry of entries) {
975
+ if (entry.local === false) continue;
976
+ const turbo = TASKS[entry.name]?.turbo;
977
+ if (!turbo) continue;
978
+ const extraEnv = validatedGlobList(entry.turbo?.passThroughEnv, entry.name, "passThroughEnv");
979
+ const extraInputs = validatedGlobList(entry.turbo?.inputs, entry.name, "inputs");
980
+ const current = tasks[entry.name] ?? turbo;
981
+ const passThroughEnv = extraEnv.length > 0 ? [...current.passThroughEnv ?? [], ...extraEnv] : current.passThroughEnv;
982
+ const inputs = extraInputs.length > 0 ? [...current.inputs, ...extraInputs] : current.inputs;
983
+ tasks[entry.name] = {
984
+ ...turbo,
985
+ inputs,
986
+ ...passThroughEnv && passThroughEnv.length > 0 ? { passThroughEnv } : {}
613
987
  };
614
- };
615
- if (local) {
616
- const runner = resolveRunner(local, cwd, listDir);
617
- if (!runner) return skip(`no ${label} runner for this repo`);
618
- const found = lookPath(cwd, runner.tool);
619
- if (!found) return skip(`${runner.tool} not installed locally for ${label} — enforced in CI`);
620
- return run(found, [...runner.args, ...passthrough]);
621
988
  }
622
- const msg = `no local equivalent for ${label} — enforced in CI`;
623
- print(`· ${msg}`);
624
- logger.debug({
625
- task: label,
626
- status: "skip"
627
- }, `run: ${label}`);
628
- return {
629
- status: "skip",
630
- message: msg
631
- };
989
+ if (Object.keys(tasks).length === 0) return null;
990
+ return stringifyJson({
991
+ $schema: "https://turborepo.org/schema.json",
992
+ globalDependencies: GLOBAL_DEPENDENCIES,
993
+ tasks
994
+ });
632
995
  }
633
996
  /**
634
- * `holocron run <task>` for a task that is a container of sub-jobs (`audit`) —
635
- * run each job in declared order. Status precedence: any `fail` → `fail`; else
636
- * any `ok` → `ok`; else any `dry-run` → `dry-run`; else `skip` (nothing ran).
997
+ * Fixes #692: `turboConfig()` writes a `turbo.json` the moment *any* task in
998
+ * the manifest has fan-out config — with no check for whether the repo's own
999
+ * root package is actually covered by `pnpm-workspace.yaml`'s `packages:`
1000
+ * list. A repo shaped like `observability` (the library lives at repo root;
1001
+ * `packages:` lists only an unrelated `docs` site) silently breaks the
1002
+ * instant `turbo.json` exists: `holocron run <task>` switches from running
1003
+ * root's own script directly to `turbo run <task>`, which only sees declared
1004
+ * workspace members — root vanishes, the task "succeeds" with zero real work
1005
+ * done (confirmed empirically: `Packages in scope: docs`, root never
1006
+ * mentioned, exit 0).
1007
+ *
1008
+ * Detection is narrow and specific: root only needs to be an explicit
1009
+ * workspace member if its *own* `package.json` has a script literally named
1010
+ * after one of the tasks turbo.json is about to fan out — a plain
1011
+ * orchestrator root (`holocron`'s own `"build": "turbo run delivery.build"`,
1012
+ * which has no `delivery.build` script itself) never trips this; only a root
1013
+ * that's genuinely a directly-buildable package does. Confirmed the fix
1014
+ * empirically too — adding `.` to `packages:` makes turbo pick root back up
1015
+ * (`Packages in scope: docs, root-lib`) without disturbing the sibling
1016
+ * package's own caching.
1017
+ *
1018
+ * A matching script key alone isn't enough — its *value* has to be a real
1019
+ * command, not the generated `holocron run <task> --` wrapper (#846, the
1020
+ * counterpart `mergePackageJsonScripts()` already had to learn for #747/
1021
+ * #748). A repo whose root predates this tooling can already have that exact
1022
+ * wrapper sitting in a task-name-matching script with nothing ever having
1023
+ * generated it this session; adding `.` there fires #747's recursion
1024
+ * immediately (`turbo run <task>` → root's own `<task>` script → the wrapper
1025
+ * → `holocron run <task>` sees `turbo.json` still defines it → `turbo run
1026
+ * <task>` again, forever). Only a matching script whose value is *not* that
1027
+ * wrapper counts as "root is genuinely a directly-buildable package".
1028
+ *
1029
+ * A targeted line-based edit, not a full YAML parse/reserialize — same
1030
+ * pattern `codecov.ts`'s `mergeCodecovComponents()` already uses for editing
1031
+ * an existing generated file. `pnpm-workspace.yaml` routinely carries a
1032
+ * `catalog:`/`catalogs:`/`overrides:` block after `packages:`; a real parser
1033
+ * round-trip risks reformatting or reordering content nobody asked to touch.
1034
+ * Only ever *adds* a line — never rewrites or reorders anything already
1035
+ * there — and is a no-op (`changed: false`) whenever root doesn't need it,
1036
+ * root is already listed, or `packages:` isn't in the plain block-list form
1037
+ * every repo checked actually uses.
637
1038
  */
638
- function runAllJobs(input, jobs, passthrough) {
639
- const { print, task } = input;
640
- const perJob = {
641
- ...input,
642
- required: false
1039
+ function ensureRootWorkspaceMember(workspaceYaml, rootScripts, taskNames) {
1040
+ if (!taskNames.some((t) => {
1041
+ const script = rootScripts[t];
1042
+ return script !== void 0 && !isGeneratedRunWrapper(script, t);
1043
+ })) return {
1044
+ content: workspaceYaml,
1045
+ changed: false
643
1046
  };
644
- const reports = [];
645
- for (const [name, jobDef] of Object.entries(jobs)) {
646
- const label = `${task} ${name}`;
647
- print(`▶ ${jobDef.checkContext}`);
648
- reports.push(runUnit(perJob, label, jobDef.local, makeRunOne({
649
- ...perJob,
650
- task: label
651
- }), passthrough));
1047
+ const lines = workspaceYaml.split("\n");
1048
+ const packagesLineIdx = lines.findIndex((l) => /^packages:\s*$/.test(l));
1049
+ if (packagesLineIdx === -1) return {
1050
+ content: workspaceYaml,
1051
+ changed: false
1052
+ };
1053
+ const items = [];
1054
+ let cursor = packagesLineIdx + 1;
1055
+ while (cursor < lines.length && /^\s*-\s*/.test(lines[cursor])) {
1056
+ items.push(lines[cursor]);
1057
+ cursor++;
652
1058
  }
653
- const command = reports.map((r) => r.command).filter((c) => Boolean(c)).join(" && ");
654
- const statuses = new Set(reports.map((r) => r.status));
655
- if (statuses.has("fail")) return {
656
- status: "fail",
657
- command,
658
- message: `one or more ${task} jobs failed`
1059
+ if (items.some((item) => {
1060
+ const value = item.replace(/^\s*-\s*/, "").replace(/^["']|["']$/g, "").trim();
1061
+ return value === "." || value === "";
1062
+ })) return {
1063
+ content: workspaceYaml,
1064
+ changed: false
659
1065
  };
660
- if (statuses.has("ok")) return {
661
- status: "ok",
662
- command
1066
+ const newLine = `${items[0]?.match(/^(\s*-\s*)/)?.[1] ?? " - "}"."`;
1067
+ return {
1068
+ content: [
1069
+ ...lines.slice(0, packagesLineIdx + 1),
1070
+ newLine,
1071
+ ...lines.slice(packagesLineIdx + 1)
1072
+ ].join("\n"),
1073
+ changed: true
663
1074
  };
664
- if (statuses.has("dry-run")) return {
665
- status: "dry-run",
666
- command
1075
+ }
1076
+ /**
1077
+ * Matches the exact shape `packageScripts()` generates for `taskName` —
1078
+ * `<bin> run <taskName> --`, `<bin>` normally `holocron` but substitutable
1079
+ * (`holocronScript`, itself allowed to be multi-word, e.g. `"node
1080
+ * packages/cli/dist/cli.mjs"` — see its own test). Checking the *suffix*
1081
+ * rather than the whole string means an old wrapper survives a
1082
+ * `holocronScript` rename intact — it's still recognized as a wrapper (safe
1083
+ * to update to the new one), not mistaken for a real command. Mirrors
1084
+ * `sync.ts`'s private `isGeneratedRunWrapper()` — same predicate, needed on
1085
+ * both sides of #692/#747: whether to *write* the wrapper, and here, whether
1086
+ * an *existing* one should count as "root is directly buildable".
1087
+ */
1088
+ function isGeneratedRunWrapper(command, taskName) {
1089
+ return command.endsWith(` run ${taskName} --`);
1090
+ }
1091
+ /**
1092
+ * Fallback pin when a repo's own `pnpm-workspace.yaml` has no `turbo`
1093
+ * catalog entry yet — matches this repo's own catalog version (the
1094
+ * canonical source-of-truth repo for the whole org's tooling).
1095
+ */
1096
+ const DEFAULT_TURBO_VERSION = "^2.10.12";
1097
+ /**
1098
+ * Ensures `turbo` is declared in `package.json#devDependencies` — found
1099
+ * the hard way rolling this out to `observability` (predates #691):
1100
+ * `turboConfig()` writes a `turbo.json` the moment any task has fan-out
1101
+ * config, but never touches `package.json`, so a repo that predates this
1102
+ * feature (or never had `turbo` installed for any other reason) ends up
1103
+ * with a `turbo.json` and nothing local to run it. `resolveBin()`
1104
+ * (`run.ts`) falls back to a bare `turbo` on `PATH` when
1105
+ * `node_modules/.bin/turbo` doesn't exist — on a machine with no global
1106
+ * `turbo` at all that's a hard failure for every task; on one that
1107
+ * happens to have an unrelated global install, silent version skew
1108
+ * (confirmed empirically: a global 2.6.0 rejected this repo's generated
1109
+ * `turbo.json` outright — `Found an unknown key "globalDependencies"`,
1110
+ * a schema key from a newer version than the one actually resolved).
1111
+ *
1112
+ * Prefers `"catalog:"` when the repo's own `pnpm-workspace.yaml` already
1113
+ * has a `turbo` catalog entry — matches every already-migrated repo
1114
+ * (`holocron`, `clients`, …) — falling back to a real pinned version
1115
+ * otherwise. Never touches an existing `devDependencies.turbo` entry, so
1116
+ * a repo pinning its own version on purpose is left alone — idempotent,
1117
+ * a no-op once set either way.
1118
+ */
1119
+ function ensureTurboDependency(packageJson, workspaceYaml) {
1120
+ const pkg = JSON.parse(packageJson);
1121
+ if (typeof pkg.devDependencies?.turbo === "string") return {
1122
+ content: packageJson,
1123
+ changed: false
1124
+ };
1125
+ const hasTurboCatalogEntry = /^\s*turbo:\s*\S/m.test(workspaceYaml);
1126
+ const devDependencies = {
1127
+ ...pkg.devDependencies,
1128
+ turbo: hasTurboCatalogEntry ? "catalog:" : DEFAULT_TURBO_VERSION
1129
+ };
1130
+ const updated = {
1131
+ ...pkg,
1132
+ devDependencies
667
1133
  };
668
1134
  return {
669
- status: "skip",
670
- message: `no ${task} job has a local equivalent — enforced in CI`
1135
+ content: stringifyJson(updated, { like: packageJson }),
1136
+ changed: true
671
1137
  };
672
1138
  }
1139
+ //#endregion
1140
+ //#region src/tasks/run/linters/linters.ts
673
1141
  /**
674
- * `holocron run <task>` for a `linterGroup` task — the task's fixed linter
675
- * set, each still gated by its own `detect`/`always` rule, run natively.
676
- * Worst exit code wins. Runs *after* the caller's own turbo/`package.json`
677
- * resolution (step 3b) — no per-linter turbo passthrough here, unlike the
678
- * old single `lint` task's eslint-only special case; the whole task is what
679
- * turbo/a package script would have already caught in steps 1–2.
1142
+ * The linter registry — one list of linter names (from `config.tasks`'
1143
+ * `{ name: "lint", linters: [...] }`, or auto-detected) maps to both the
1144
+ * CI super-linter `VALIDATE_*` env and the native local run. This table is
1145
+ * the single source of truth for that mapping; {@link superLinterConfig}
1146
+ * (CI) and the `holocron run lint` aggregate (local) both read it.
1147
+ *
1148
+ * Spec: `docs/wiki/specifications/tech-astromech-task-runner.spec.md` "Lint parity".
680
1149
  */
681
- function runLinterGroup(input, names, passthrough) {
682
- const { print, logger, listDir, lookPath, cwd, task, readFile, fileExists } = input;
683
- const dryRun = input.dryRun ?? false;
684
- const runOne = makeRunOne(input);
685
- let rootFiles;
686
- try {
687
- rootFiles = listDir(cwd);
688
- } catch {
689
- rootFiles = [];
690
- }
691
- const resolved = resolveLinters({
692
- explicit: names,
693
- rootFiles
694
- });
695
- const reports = [];
696
- let anyHasLocalBin = false;
697
- for (const { name, def } of resolved) {
698
- const bin = def.localBin;
699
- if (!bin) {
700
- print(`· ${name} (CI only)`);
701
- continue;
702
- }
703
- anyHasLocalBin = true;
704
- const found = lookPath(cwd, bin);
705
- if (!found) {
706
- print(`! ${name} — ${bin} not on PATH${def.installHint ? `. ${def.installHint}` : ""} (enforced in CI)`);
707
- continue;
708
- }
709
- const configFlags = resolveToolConfig(name, cwd, {
710
- readFile,
711
- fileExists
712
- });
713
- reports.push(runOne(found, [
714
- ...configFlags,
715
- ...def.localArgs ?? [],
716
- ...passthrough
717
- ]));
1150
+ /**
1151
+ * Known linters, in execution order. `always` entries are the current
1152
+ * hard-coded super-linter baseline; `prettier` is always-on because the org
1153
+ * applies it universally (super-linter only lints files that exist).
1154
+ */
1155
+ const LINTERS = {
1156
+ eslint: {
1157
+ validate: ["VALIDATE_JAVASCRIPT_ES", "VALIDATE_TYPESCRIPT_ES"],
1158
+ localBin: "eslint",
1159
+ localArgs: ["."],
1160
+ detect: [
1161
+ "eslint.config.ts",
1162
+ "eslint.config.js",
1163
+ "eslint.config.mjs",
1164
+ "eslint.config.cjs",
1165
+ ".eslintrc",
1166
+ ".eslintrc.json",
1167
+ ".eslintrc.yml",
1168
+ ".eslintrc.yaml",
1169
+ ".eslintrc.cjs"
1170
+ ],
1171
+ configInput: "eslint-config"
1172
+ },
1173
+ prettier: {
1174
+ validate: [
1175
+ "VALIDATE_JAVASCRIPT_PRETTIER",
1176
+ "VALIDATE_JSX_PRETTIER",
1177
+ "VALIDATE_TYPESCRIPT_PRETTIER",
1178
+ "VALIDATE_TSX",
1179
+ "VALIDATE_MARKDOWN_PRETTIER"
1180
+ ],
1181
+ fix: [
1182
+ "FIX_JAVASCRIPT_PRETTIER",
1183
+ "FIX_JSX_PRETTIER",
1184
+ "FIX_TYPESCRIPT_PRETTIER",
1185
+ "FIX_TSX",
1186
+ "FIX_MARKDOWN_PRETTIER"
1187
+ ],
1188
+ always: true,
1189
+ localBin: "prettier",
1190
+ localArgs: ["--check", "."],
1191
+ configInput: "prettier-config"
1192
+ },
1193
+ yamllint: {
1194
+ validate: ["VALIDATE_YAML"],
1195
+ always: true,
1196
+ localBin: "yamllint",
1197
+ localArgs: ["."],
1198
+ installHint: "brew install yamllint",
1199
+ configInput: "yaml-config"
1200
+ },
1201
+ actionlint: {
1202
+ validate: ["VALIDATE_GITHUB_ACTIONS"],
1203
+ always: true,
1204
+ localBin: "actionlint",
1205
+ localArgs: [],
1206
+ installHint: "brew install actionlint"
1207
+ },
1208
+ gitleaks: {
1209
+ validate: ["VALIDATE_GITLEAKS"],
1210
+ always: true,
1211
+ localBin: "gitleaks",
1212
+ localArgs: [
1213
+ "dir",
1214
+ "--no-banner",
1215
+ "--config",
1216
+ ".gitleaks.toml"
1217
+ ],
1218
+ installHint: "brew install gitleaks"
1219
+ },
1220
+ editorconfig: {
1221
+ validate: ["VALIDATE_EDITORCONFIG"],
1222
+ always: true,
1223
+ localBin: "editorconfig-checker",
1224
+ localArgs: [],
1225
+ installHint: "brew install editorconfig-checker"
1226
+ },
1227
+ commitlint: {
1228
+ validate: ["VALIDATE_GIT_COMMITLINT"],
1229
+ always: true
1230
+ },
1231
+ "git-merge-conflict-markers": {
1232
+ validate: ["VALIDATE_GIT_MERGE_CONFLICT_MARKERS"],
1233
+ always: true
1234
+ },
1235
+ markdownlint: {
1236
+ validate: ["VALIDATE_MARKDOWN"],
1237
+ localBin: "markdownlint-cli2",
1238
+ localArgs: ["**/*.md"],
1239
+ detect: [
1240
+ ".markdownlint.json",
1241
+ ".markdownlint.jsonc",
1242
+ ".markdownlint.yaml",
1243
+ ".markdownlint.yml",
1244
+ ".markdownlint-cli2.jsonc",
1245
+ ".markdownlint-cli2.yaml",
1246
+ ".markdownlint-cli2.mjs"
1247
+ ]
718
1248
  }
719
- if (reports.length === 0) {
720
- const required = input.required && anyHasLocalBin;
721
- const msg = `no tooling available locally for ${task} — every linter in this group is CI-only here`;
722
- print(required ? `✗ ${msg} (required)` : `· ${msg}`);
723
- logger[required ? "warn" : "debug"]({
724
- task,
725
- status: required ? "fail" : "skip"
726
- }, `run: ${task}`);
727
- return {
728
- status: required ? "fail" : "skip",
729
- message: msg
730
- };
1249
+ };
1250
+ /** Every linter name the registry knows. */
1251
+ const LINTER_NAMES = new Set(Object.keys(LINTERS));
1252
+ /**
1253
+ * Resolve the linter set for a repo. An `explicit` list (from `config.tasks`)
1254
+ * picks the candidate set; otherwise the candidates are every `always` linter
1255
+ * plus every `detect`-gated linter whose filenames are present at the repo
1256
+ * root, OR whose mapped `@theholocron/*-config` package resolves per
1257
+ * {@link sharedConfigAvailable} — a repo can't run eslint with genuinely no
1258
+ * config anywhere (super-linter FATALs if you ask it to,
1259
+ * theholocron/holocron#654), but a *shared* one it has installed counts
1260
+ * (holocron#795: proving `clients`' eslint.config.ts files reduce to zero
1261
+ * remaining content is only half the story — the file itself was still load-
1262
+ * bearing here as the sole "eslint is even configured" signal until this
1263
+ * fallback existed). `always` linters are unconditional. Result is ordered
1264
+ * by {@link LINTERS} declaration order.
1265
+ *
1266
+ * `sharedConfigAvailable` is caller-computed (`run.ts`'s `runLinterGroup`
1267
+ * already resolves each linter's `resolveToolConfig()` per member to build
1268
+ * its actual command — reusing that instead of duplicating the package-
1269
+ * resolution logic here keeps this function itself pure and file-I/O-free).
1270
+ *
1271
+ * @throws when an `explicit` name is not in the registry — a typo is a
1272
+ * config bug, not a linter to silently skip.
1273
+ */
1274
+ function resolveLinters(opts) {
1275
+ const order = Object.keys(LINTERS);
1276
+ const present = new Set(opts.rootFiles);
1277
+ const shared = opts.sharedConfigAvailable ?? /* @__PURE__ */ new Set();
1278
+ const configPresent = (name, def) => def.always === true || (def.detect ?? []).some((f) => present.has(f)) || shared.has(name);
1279
+ if (opts.explicit && opts.explicit.length > 0) {
1280
+ const unknown = opts.explicit.filter((n) => !LINTER_NAMES.has(n));
1281
+ if (unknown.length > 0) throw new Error(`unknown linter${unknown.length > 1 ? "s" : ""} ${unknown.map((n) => `"${n}"`).join(", ")} — known: ${order.join(", ")}`);
1282
+ const wanted = new Set(opts.explicit);
1283
+ return order.filter((n) => wanted.has(n) && configPresent(n, LINTERS[n])).map((name) => ({
1284
+ name,
1285
+ def: LINTERS[name]
1286
+ }));
731
1287
  }
732
- const command = reports.map((r) => r.command).filter((c) => Boolean(c)).join(" && ");
733
- if (dryRun) return {
734
- status: "dry-run",
735
- command
736
- };
737
- return reports.some((r) => r.status === "fail") ? {
738
- status: "fail",
739
- command,
740
- message: "one or more linters failed"
741
- } : {
742
- status: "ok",
743
- command
744
- };
1288
+ return order.filter((name) => configPresent(name, LINTERS[name])).map((name) => ({
1289
+ name,
1290
+ def: LINTERS[name]
1291
+ }));
1292
+ }
1293
+ //#endregion
1294
+ //#region src/tasks/run/resolve-bin/resolve-bin.ts
1295
+ /** `<cwd>/node_modules/.bin/<tool>` when it exists, else the bare `tool` name (left to `PATH`). */
1296
+ function resolveBin(cwd, tool, fileExists) {
1297
+ const local = join(cwd, "node_modules", ".bin", tool);
1298
+ return fileExists(local) ? local : tool;
745
1299
  }
1300
+ //#endregion
1301
+ //#region src/tasks/run/resolve-runner/resolve-runner.ts
746
1302
  const mkRunner = (tool, args = []) => ({
747
1303
  tool,
748
1304
  args
749
1305
  });
750
1306
  /**
751
- * Resolve `{ tool, args }` for a runner, applying `detect[]` against repo
752
- * files. Only called for `tool` / `detect` runners — the caller handles
753
- * `command` runners itself, so `local.detect` is present whenever
754
- * `local.tool` is not.
1307
+ * Resolve `{ tool, args }` for a runner, applying `detect[]` against repo
1308
+ * files, falling back (#750) to each candidate's `tool` name against
1309
+ * `package.json`'s own declared dependencies when neither matches by
1310
+ * filename. Only called for `tool` / `detect` runners — the caller handles
1311
+ * `command` runners itself, so `local.detect` is present whenever
1312
+ * `local.tool` is not.
1313
+ *
1314
+ * The dependency fallback matters once a Bucket A migration (#680) deletes
1315
+ * a build tool's own config file (`tsdown.config.ts`, `vite.config.ts`, …):
1316
+ * that file was the *only* signal `detect[]` had to offer, so removing it
1317
+ * used to make `delivery.build` report "no task for this repo" and silently
1318
+ * skip the build, even though the tool is still very much in use — a
1319
+ * package can't actually build with `tsdown` without it in
1320
+ * `dependencies`/`devDependencies`, so that's a signal at least as reliable
1321
+ * as the config file, and one deleting the file doesn't remove.
1322
+ *
1323
+ * **Per-candidate, not two full passes** (#750 follow-up — the first
1324
+ * shipped version got this wrong): `delivery.build`'s own `detect[]` ends
1325
+ * with `{ when: /^tsconfig\.json$/, tool: "tsc" }` as its last-resort
1326
+ * fallback, and `tsconfig.json` exists in essentially every package
1327
+ * regardless of build tool. A "check every candidate's filename, *then*
1328
+ * check every candidate's dependency" two-pass design would let that
1329
+ * always-present fallback file win over a real `tsdown`/`vite`/`rollup`
1330
+ * dependency match, the moment the earlier candidates' own config files are
1331
+ * gone — exactly backwards. Checking `(file match) || (dependency match)`
1332
+ * together for each candidate before moving to the next preserves the
1333
+ * intended precedence: a real signal for an *earlier* candidate, by either
1334
+ * means, still wins over a later candidate's mere filename presence.
1335
+ */
1336
+ function resolveRunner(local, cwd, listDir, readFile) {
1337
+ if (local.tool) return mkRunner(local.tool, local.args);
1338
+ let files;
1339
+ try {
1340
+ files = listDir(cwd);
1341
+ } catch {
1342
+ files = [];
1343
+ }
1344
+ const deps = dependencyNames(readPackageJsonSync(cwd, { readFile }));
1345
+ for (const candidate of local.detect) if (files.some((f) => candidate.when.test(f)) || deps.has(candidate.tool)) return mkRunner(candidate.tool, candidate.args);
1346
+ }
1347
+ //#endregion
1348
+ //#region src/tasks/run/resolve-export-entry/resolve-export-entry.ts
1349
+ /**
1350
+ * Unwrap a `package.json#exports` subpath entry down to its file path —
1351
+ * either a bare string, or an object with condition keys (`import`,
1352
+ * `default`, `require`, …) that may themselves nest one level (`{ import: {
1353
+ * types, default } }`, the shape every `@theholocron/*-config` package
1354
+ * actually uses). Prefers `import` (this org ships ESM), then `default`,
1355
+ * then `require` as a last resort.
1356
+ */
1357
+ function resolveExportEntry(entry) {
1358
+ if (typeof entry === "string") return entry;
1359
+ if (entry === null || typeof entry !== "object") return void 0;
1360
+ const conditions = entry;
1361
+ const value = conditions.import ?? conditions.default ?? conditions.require;
1362
+ if (typeof value === "string") return value;
1363
+ if (typeof value === "object" && value !== null) return resolveExportEntry(value);
1364
+ }
1365
+ //#endregion
1366
+ //#region src/tasks/run/tool-configs/tool-configs.ts
1367
+ /**
1368
+ * `library` is every mapped tool's org-wide default variant today — every
1369
+ * currently-migrated repo (`holocron`, `clients`, `utils`, `configs`,
1370
+ * `themes`, `observability`, `skills`) is a publishable library package, and
1371
+ * `eslint-config`/`vitest-config`/`tsdown-config` all name their
1372
+ * general-purpose preset exactly that. Picking a different variant per repo
1373
+ * (an app-type template, say) needs new `holocron.config.ts` surface this
1374
+ * doesn't add yet — deliberately out of scope until a real consumer needs it.
1375
+ */
1376
+ const TOOL_CONFIGS = {
1377
+ eslint: {
1378
+ package: "@theholocron/eslint-config",
1379
+ exportPath: "./bundles/library",
1380
+ flag: ["--config"]
1381
+ },
1382
+ prettier: {
1383
+ package: "@theholocron/prettier-config",
1384
+ exportPath: ".",
1385
+ flag: ["--config"]
1386
+ },
1387
+ vitest: {
1388
+ package: "@theholocron/vitest-config",
1389
+ exportPath: "./bundles/library",
1390
+ flag: ["--config"]
1391
+ },
1392
+ tsdown: {
1393
+ package: "@theholocron/tsdown-config",
1394
+ exportPath: "./presets/library",
1395
+ flag: ["--config"]
1396
+ },
1397
+ commitlint: {
1398
+ package: "@theholocron/commitlint-config",
1399
+ exportPath: ".",
1400
+ flag: ["--config"]
1401
+ }
1402
+ };
1403
+ /**
1404
+ * Every filename a tool's own auto-discovery would pick up from `cwd` —
1405
+ * same extension set `registry.ts`'s `turbo.inputs`/`local.detect` entries
1406
+ * already use for these tools, kept in sync by hand (no shared source of
1407
+ * truth between the two today). Presence of any one of these means the
1408
+ * package has its own local config — customized or not, `resolveToolConfig`
1409
+ * can't tell the difference from the filename alone, so it defers to it
1410
+ * either way (#749).
755
1411
  */
756
- function resolveRunner(local, cwd, listDir) {
757
- if (local.tool) return mkRunner(local.tool, local.args);
758
- let files;
1412
+ const LOCAL_CONFIG_FILES = {
1413
+ eslint: [
1414
+ "eslint.config.ts",
1415
+ "eslint.config.js",
1416
+ "eslint.config.mjs",
1417
+ "eslint.config.cjs"
1418
+ ],
1419
+ prettier: [
1420
+ "prettier.config.ts",
1421
+ "prettier.config.js",
1422
+ "prettier.config.mjs",
1423
+ "prettier.config.cjs"
1424
+ ],
1425
+ vitest: [
1426
+ "vitest.config.ts",
1427
+ "vitest.config.js",
1428
+ "vitest.config.mjs"
1429
+ ],
1430
+ tsdown: [
1431
+ "tsdown.config.ts",
1432
+ "tsdown.config.js",
1433
+ "tsdown.config.mjs",
1434
+ "tsdown.config.cjs"
1435
+ ],
1436
+ commitlint: [
1437
+ "commitlint.config.ts",
1438
+ "commitlint.config.js",
1439
+ "commitlint.config.mjs",
1440
+ "commitlint.config.cjs"
1441
+ ]
1442
+ };
1443
+ //#endregion
1444
+ //#region src/tasks/run/resolve-tool-config/resolve-tool-config.ts
1445
+ /**
1446
+ * Resolves the absolute path to a Bucket A tool's shared `@theholocron/*-config`
1447
+ * entry point, installed in the consuming repo's own `node_modules` — and the
1448
+ * CLI flag(s) to hand it via (`--config`, `--extends`, …). `run.ts` splices the
1449
+ * result into the command it builds for a tool/detect runner or a linter-group
1450
+ * entry, right after the tool's own args.
1451
+ *
1452
+ * Every mapped package's resolved entry point already carries a ready-to-use
1453
+ * default export (a plain re-export for `prettier`/`commitlint`; an invoked,
1454
+ * zero-arg preset for `eslint`/`vitest`/`tsdown` — verified end-to-end against
1455
+ * each tool's real `--config`/`--extends` loader, not just the file's shape).
1456
+ * A repo doesn't need this package installed at all — every lookup degrades to
1457
+ * `[]` (no flag added, the tool falls back to its own auto-discovery of a
1458
+ * local file, unchanged from today) rather than throwing.
1459
+ *
1460
+ * **Local file wins.** A `<tool>.config.*` sitting in `cwd` (whether it's
1461
+ * pure boilerplate or has real per-package customization on top of the
1462
+ * shared bundle, e.g. `mergeConfig(base, { test: { coverage: { exclude }
1463
+ * } })`) always skips the splice, even when the shared package is also
1464
+ * installed — an explicit `--config <shared path>` flag beats a tool's own
1465
+ * auto-discovery every time, so splicing it unconditionally would silently
1466
+ * discard that customization the file is still sitting right there on disk.
1467
+ * The shared bundle is only ever the fallback for a package with *no* local
1468
+ * file at all — that's still the "delete the file to go fully shared" path
1469
+ * this was designed for (#676); it just isn't unconditional (#749).
1470
+ *
1471
+ * `semantic-release`, `devmoji`, `editorconfig-checker`, and `knip` are
1472
+ * deliberately absent — `semantic-release-config`'s `defineConfig()` needs
1473
+ * real per-repo data (branches, npm options) a static `--extends <path>` can't
1474
+ * carry; `devmoji` runs through a git hook template, not `holocron run
1475
+ * <task>`; `editorconfig-checker` has no shared-config package yet; `knip` is
1476
+ * repo-specific by nature, never a shared-config candidate. See
1477
+ * `.notes/tech-config-resolution.spec.md` (#676).
1478
+ */
1479
+ /**
1480
+ * `<flag> <absolute path>` for `tool`, resolved against `cwd`'s
1481
+ * `node_modules` — or `[]` when the tool isn't mapped, `cwd` already has its
1482
+ * own local config file for it (#749 — local always wins, customized or
1483
+ * not), the shared package isn't installed, its `package.json` doesn't
1484
+ * declare the export subpath this needs, or the resolved file doesn't
1485
+ * actually exist on disk (a stale install, or a package version that
1486
+ * predates the export existing).
1487
+ */
1488
+ function resolveToolConfig(tool, cwd, deps) {
1489
+ const spec = TOOL_CONFIGS[tool];
1490
+ if (!spec) return [];
1491
+ if ((LOCAL_CONFIG_FILES[tool] ?? []).some((name) => deps.fileExists(join(cwd, name)))) return [];
1492
+ const pkgDir = join(cwd, "node_modules", ...spec.package.split("/"));
1493
+ const pkgJsonPath = join(pkgDir, "package.json");
1494
+ if (!deps.fileExists(pkgJsonPath)) return [];
1495
+ let pkg;
759
1496
  try {
760
- files = listDir(cwd);
1497
+ pkg = JSON.parse(deps.readFile(pkgJsonPath));
761
1498
  } catch {
762
- return;
1499
+ return [];
763
1500
  }
764
- for (const candidate of local.detect) if (files.some((f) => candidate.when.test(f))) return mkRunner(candidate.tool, candidate.args);
765
- }
766
- function resolveBin(cwd, tool, fileExists) {
767
- const local = join(cwd, "node_modules", ".bin", tool);
768
- return fileExists(local) ? local : tool;
769
- }
770
- function packageManager(cwd, readFile, fileExists) {
771
- try {
772
- const pkg = JSON.parse(readFile(join(cwd, "package.json")));
773
- if (typeof pkg.packageManager === "string") return pkg.packageManager.split("@")[0];
774
- } catch {}
775
- if (fileExists(join(cwd, "pnpm-lock.yaml"))) return "pnpm";
776
- if (fileExists(join(cwd, "bun.lockb"))) return "bun";
777
- if (fileExists(join(cwd, "yarn.lock"))) return "yarn";
778
- if (fileExists(join(cwd, "package-lock.json"))) return "npm";
779
- return "pnpm";
1501
+ const relative = resolveExportEntry(pkg.exports?.[spec.exportPath]);
1502
+ if (!relative) return [];
1503
+ const absolute = join(pkgDir, relative);
1504
+ if (!deps.fileExists(absolute)) return [];
1505
+ return [...spec.flag, absolute];
780
1506
  }
1507
+ //#endregion
1508
+ //#region src/tasks/run/turbo-defines-task/turbo-defines-task.ts
1509
+ /** `true` when `<cwd>/turbo.json` declares `task` (under `tasks`, or turbo 1.x's `pipeline`). */
781
1510
  function turboDefinesTask(cwd, task, readFile, fileExists) {
782
1511
  if (!fileExists(join(cwd, "turbo.json"))) return false;
783
1512
  try {
@@ -787,302 +1516,343 @@ function turboDefinesTask(cwd, task, readFile, fileExists) {
787
1516
  return false;
788
1517
  }
789
1518
  }
790
- function packageJsonScript(cwd, task, readFile, fileExists) {
791
- if (!fileExists(join(cwd, "package.json"))) return void 0;
792
- try {
793
- return JSON.parse(readFile(join(cwd, "package.json"))).scripts?.[task];
794
- } catch {
795
- return;
796
- }
797
- }
798
1519
  //#endregion
799
- //#region src/thin-callers.ts
1520
+ //#region src/tasks/run/run.ts
800
1521
  /**
801
- * Workflow templates + thin-caller generation.
1522
+ * `holocron run <task> [job] [-- <passthrough>]` — run a registry task (or one
1523
+ * of its sub-jobs) locally.
802
1524
  *
803
- * `WORKFLOW_TEMPLATES` holds each reusable `.github/workflows/<name>.yml`
804
- * (synced to `theholocron/.github`); `generateThinCallerContent` wraps one
805
- * into the thin caller `holocron setup` / `holocron sync` write locally.
806
- */
807
- const WORKFLOW_TEMPLATES = {
808
- "verification.unitTests": "name: Verification\n\non: # yamllint disable-line rule:truthy\n push:\n branches: [main, alpha]\n pull_request:\n\nconcurrency:\n group: test-${{ github.ref }}\n cancel-in-progress: true\n\npermissions:\n contents: read\n id-token: write\n statuses: write\n\njobs:\n test:\n name: Test\n uses: theholocron/.github/.github/workflows/verification.unitTests.yml@main\n with:\n run-unit: true\n secrets: inherit\n",
809
- "verification.typeSafety": "name: Verification\n\non: # yamllint disable-line rule:truthy\n push:\n branches: [main, alpha]\n pull_request:\n\nconcurrency:\n group: typecheck-${{ github.ref }}\n cancel-in-progress: true\n\npermissions:\n contents: read\n\njobs:\n typecheck:\n name: Typecheck\n uses: theholocron/.github/.github/workflows/verification.typeSafety.yml@main\n secrets: inherit\n",
810
- "verification.performance": "name: Verification\n\non: # yamllint disable-line rule:truthy\n push:\n branches: [main, alpha]\n pull_request:\n\npermissions:\n contents: read\n\njobs:\n performance:\n name: Audit the Performance\n uses: theholocron/.github/.github/workflows/verification.performance.yml@main\n secrets: inherit\n",
811
- "sourceQuality.staticAnalysis": "name: Source Quality\n\non: # yamllint disable-line rule:truthy\n push:\n branches: [main, alpha]\n pull_request:\n\nconcurrency:\n group: source-quality-static-analysis-${{ github.ref }}\n cancel-in-progress: true\n\npermissions:\n contents: write\n statuses: write\n\njobs:\n static-analysis:\n name: Static Analysis\n uses: theholocron/.github/.github/workflows/sourceQuality.staticAnalysis.yml@main\n secrets: inherit\n",
812
- "sourceQuality.formatting": "name: Source Quality\n\non: # yamllint disable-line rule:truthy\n push:\n branches: [main, alpha]\n pull_request:\n\nconcurrency:\n group: source-quality-formatting-${{ github.ref }}\n cancel-in-progress: true\n\npermissions:\n contents: write\n statuses: write\n\njobs:\n formatting:\n name: Formatting\n uses: theholocron/.github/.github/workflows/sourceQuality.formatting.yml@main\n secrets: inherit\n",
813
- "sourceQuality.structuredDataValidation": "name: Source Quality\n\non: # yamllint disable-line rule:truthy\n push:\n branches: [main, alpha]\n pull_request:\n\nconcurrency:\n group: source-quality-structured-data-validation-${{ github.ref }}\n cancel-in-progress: true\n\npermissions:\n contents: read\n\njobs:\n structured-data-validation:\n name: Structured Data Validation\n uses: theholocron/.github/.github/workflows/sourceQuality.structuredDataValidation.yml@main\n secrets: inherit\n",
814
- "sourceQuality.deadCodeAnalysis": "name: Source Quality\n\non: # yamllint disable-line rule:truthy\n push:\n branches: [main, alpha]\n pull_request:\n\npermissions:\n contents: read\n\njobs:\n dead-code-analysis:\n name: Dead Code Analysis\n uses: theholocron/.github/.github/workflows/sourceQuality.deadCodeAnalysis.yml@main\n secrets: inherit\n",
815
- "security.secretDetection": "name: Security\n\non: # yamllint disable-line rule:truthy\n push:\n branches: [main, alpha]\n pull_request:\n\nconcurrency:\n group: security-secret-detection-${{ github.ref }}\n cancel-in-progress: true\n\npermissions:\n contents: read\n\njobs:\n secret-detection:\n name: Secret Detection\n uses: theholocron/.github/.github/workflows/security.secretDetection.yml@main\n secrets: inherit\n",
816
- "security.codeScanning": "name: Security\n\non: # yamllint disable-line rule:truthy\n push:\n branches:\n - main\n pull_request:\n branches:\n - main\n schedule:\n - cron: \"0 0 * * 1\"\n\npermissions:\n actions: read\n contents: read\n security-events: write\n\njobs:\n security:\n name: Code Scanning\n uses: theholocron/.github/.github/workflows/security.codeScanning.yml@main\n secrets: inherit\n",
817
- "delivery.build": "name: Delivery\n\non: # yamllint disable-line rule:truthy\n push:\n branches-ignore:\n - main # delivery.publish builds before publishing\n pull_request:\n branches:\n - main\n\nconcurrency:\n group: delivery.build-${{ github.ref }}\n cancel-in-progress: true\n\npermissions:\n contents: read\n\njobs:\n build:\n name: Build\n uses: theholocron/.github/.github/workflows/delivery.build.yml@main\n secrets: inherit\n",
818
- "delivery.publish": "name: Delivery\n\non: # yamllint disable-line rule:truthy\n push:\n branches:\n - main\n - alpha\n workflow_dispatch:\n inputs:\n dry_run:\n description: >\n Dry run — analyze commits and preview the release without git writes\n or publish. Push-triggered runs always run fully; this only applies\n to manual workflow_dispatch triggers.\n required: false\n default: true\n type: boolean\n\npermissions:\n contents: write\n id-token: write\n issues: write\n pull-requests: write\n\nconcurrency:\n group: ${{ github.workflow }}-${{ github.ref }}\n cancel-in-progress: false\n\njobs:\n release:\n name: Release\n uses: theholocron/.github/.github/workflows/delivery.publish.yml@main\n with:\n dry-run: ${{ inputs.dry_run == true }}\n secrets: inherit\n",
819
- "delivery.deploy": "name: Delivery\n\non: # yamllint disable-line rule:truthy\n push:\n branches: [main]\n workflow_dispatch:\n\nconcurrency:\n group: pages\n cancel-in-progress: false\n\npermissions:\n contents: read\n pages: write\n id-token: write\n\njobs:\n deploy:\n name: Deploy\n uses: theholocron/.github/.github/workflows/delivery.deploy.yml@main\n secrets: inherit\n",
820
- "delivery.bundleSize": "name: Delivery\n\non: # yamllint disable-line rule:truthy\n push:\n branches: [main, alpha]\n pull_request:\n\npermissions:\n contents: read\n\njobs:\n bundle-size:\n name: Audit the Bundle Size\n uses: theholocron/.github/.github/workflows/delivery.bundleSize.yml@main\n secrets: inherit\n",
821
- "platform.repoSync": "name: Platform\n\non: # yamllint disable-line rule:truthy\n push:\n branches: [main]\n paths:\n - holocron.config.ts\n - package.json\n - pnpm-workspace.yaml\n workflow_dispatch:\n inputs:\n steps:\n description: \"Sync steps to run (default: all)\"\n type: string\n required: false\n\nconcurrency:\n group: sync-${{ github.ref }}\n cancel-in-progress: true\n\npermissions:\n contents: write\n pull-requests: write\n\njobs:\n sync:\n name: Sync\n uses: theholocron/.github/.github/workflows/platform.repoSync.yml@main\n with:\n steps: ${{ inputs.steps }}\n secrets: inherit\n",
822
- "platform.commitStandards": "name: Platform\n\non: # yamllint disable-line rule:truthy\n pull_request:\n\nconcurrency:\n group: platform-commit-standards-${{ github.ref }}\n cancel-in-progress: true\n\npermissions:\n contents: read\n\njobs:\n commit-standards:\n name: Commit Standards\n uses: theholocron/.github/.github/workflows/platform.commitStandards.yml@main\n secrets: inherit\n",
823
- "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
- "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
- 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: 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
- stale: "name: Stale\n\non: # yamllint disable-line rule:truthy\n schedule:\n - cron: \"30 1 * * *\"\n\npermissions:\n contents: write\n issues: write\n pull-requests: write\n\njobs:\n stale:\n uses: theholocron/.github/.github/workflows/stale.yml@main\n with:\n exempt-issue-labels: \"in-progress,wip\"\n exempt-all-issue-milestones: true\n exempt-all-issue-projects: true\n exempt-all-pr-projects: true\n secrets: inherit\n",
828
- greetings: "name: Platform\n\non: # yamllint disable-line rule:truthy\n pull_request:\n issues:\n\npermissions:\n issues: write\n pull-requests: write\n\njobs:\n greetings:\n name: Greetings\n uses: theholocron/.github/.github/workflows/greetings.yml@main\n secrets: inherit\n",
829
- dependencies: "name: Platform\n\non: # yamllint disable-line rule:truthy\n pull_request:\n\npermissions:\n contents: write\n pull-requests: write\n\njobs:\n dependencies:\n name: Dependencies\n uses: theholocron/.github/.github/workflows/dependencies.yml@main\n secrets: inherit\n",
830
- bookkeeping: "name: Platform\n\non: # yamllint disable-line rule:truthy\n pull_request:\n types:\n - opened\n - edited\n\npermissions:\n contents: read\n issues: write\n pull-requests: write\n\njobs:\n bookkeeping:\n name: Bookkeeping\n uses: theholocron/.github/.github/workflows/bookkeeping.yml@main\n secrets: inherit\n"
831
- };
832
- const KNOWN_WORKFLOWS = new Set(Object.keys(WORKFLOW_TEMPLATES));
833
- /**
834
- * The GitHub status-check context a `required` task contributes to branch
835
- * protection. Format: `"{workflow name} / {job name}"`.
1525
+ * A `job` argument only applies to tasks that declare `jobs`; for any other
1526
+ * task it is folded back into the passthrough (`holocron run delivery.build
1527
+ * src/`).
836
1528
  *
837
- * Most of these are single-job workflows now (D3's decomposition split what
838
- * used to be multi-job `lint`/`audit` into one task per concern) — the
839
- * context names that job directly, no `Conclusion` aggregator needed; a
840
- * single job's own conclusion already *is* the workflow's conclusion.
841
- * `Conclusion` fan-in jobs are kept only where a task genuinely has several
842
- * conditionally-run jobs feeding one required check:
843
- * `verification.unitTests` (unit / Storybook / Chromatic / interaction /
844
- * user-flow, each gated by its own `run-*` input) and
845
- * `platform.repoValidation` (its three script jobs all must pass). Only
846
- * merge-gating workflows are listed. `astromech.requiredChecks()` reads this
847
- * for every `required` task.
848
- */
849
- const WORKFLOW_CHECK_CONTEXTS = {
850
- "delivery.build": "Build / Build every workspace",
851
- "verification.unitTests": "Test / Conclusion",
852
- "verification.typeSafety": "Typecheck / Run tsc --noEmit",
853
- "verification.performance": "Audit the Performance / Run Lighthouse CI",
854
- "sourceQuality.staticAnalysis": "Static Analysis / Run eslint and actionlint",
855
- "sourceQuality.formatting": "Formatting / Run prettier, editorconfig-checker, markdownlint",
856
- "sourceQuality.structuredDataValidation": "Structured Data Validation / Run yamllint",
857
- "sourceQuality.deadCodeAnalysis": "Dead Code Analysis / Run knip",
858
- "security.secretDetection": "Secret Detection / Run gitleaks",
859
- "platform.commitStandards": "Commit Standards / Run commitlint",
860
- "platform.repoValidation": "Repo Validation / Conclusion",
861
- "delivery.bundleSize": "Audit the Bundle Size / Upload bundle stats to Codecov"
862
- };
863
- /**
864
- * Generate the thin caller content for a workflow, optionally injecting or
865
- * merging `with:` overrides into the jobs block.
1529
+ * Resolution:
866
1530
  *
867
- * Two strategies are used depending on the template:
868
- * - Templates that already have a `with:` block (e.g. lint):
869
- * the override entries are merged in, replacing existing keys and appending
870
- * new ones.
871
- * - Templates that end with ` secrets: inherit`: a new `with:` block is
872
- * injected immediately before `secrets: inherit`.
873
- * If neither pattern matches the template, a warning is emitted and the
874
- * base template is returned unchanged.
1531
+ * J. job given → TASKS[task].jobs[job].local (unknown job → exit 1)
1532
+ * 1. turbo.json defines the task → `turbo run <task>`
1533
+ * 2. package.json has a `<task>` script → `<pm> run <task>`
1534
+ * (unless it's the `holocron run …` thin caller — that recurses)
1535
+ * 3. TASKS[task].local resolves → `<tool> <args> <org-flags> <passthrough>`
1536
+ * 3b. TASKS[task].linterGroup → run each resolved linter natively (see below)
1537
+ * 4. TASKS[task].jobs has entries → run each job in declared order
1538
+ * 5. TASKS[task].local === null → "enforced in CI" (skip, even with --required)
1539
+ * 5b. known task, nothing resolved → "no <task> task" (exit 0, or 1 with --required)
1540
+ * 6. unknown task → "unknown task" (exit 1)
1541
+ *
1542
+ * A `linterGroup` task (`sourceQuality.staticAnalysis`, etc.) runs its fixed
1543
+ * set of `linters.ts` entries, each still gated by that linter's own
1544
+ * `detect`/`always` rule — every entry runs its `localBin` natively when
1545
+ * found on PATH. Missing tools are flagged; the exit code is the worst of
1546
+ * the lot. Unlike the old single `lint` task, this check runs *after*
1547
+ * turbo/`package.json` resolution (step 3b, not step 0) — a `linterGroup`
1548
+ * task is a normal turbo-delegatable task like any other.
875
1549
  */
876
- function generateThinCallerContent(name, withOverrides, additionalPaths, logger, comments) {
877
- const base = WORKFLOW_TEMPLATES[name];
878
- if (!base) return "";
879
- const yamlScalar = (v) => {
880
- if (v === true) return "true";
881
- if (v === false) return "false";
882
- const s = String(v);
883
- return s.startsWith("[") || s.startsWith("{") ? `'${s}'` : s;
884
- };
885
- const fmt = (k, v) => {
886
- const line = ` ${k}: ${yamlScalar(v)}`;
887
- return comments?.[k] ? ` # ${comments[k]}\n${line}` : line;
1550
+ /** The per-command runner — `runTask` and `runLintAggregate` share it. */
1551
+ function makeRunOne(input) {
1552
+ const { print, logger, exec, task, cwd } = input;
1553
+ const dryRun = input.dryRun ?? false;
1554
+ return (cmd, args) => {
1555
+ const command = [cmd, ...args].join(" ");
1556
+ if (dryRun) {
1557
+ print(`would run: ${command}`);
1558
+ logger.debug({
1559
+ task,
1560
+ command,
1561
+ status: "dry-run"
1562
+ }, `run: ${task}`);
1563
+ return {
1564
+ status: "dry-run",
1565
+ command
1566
+ };
1567
+ }
1568
+ print(`→ ${command}`);
1569
+ const { exitCode } = exec(cmd, args, { cwd });
1570
+ const status = exitCode === 0 ? "ok" : "fail";
1571
+ logger[status === "fail" ? "warn" : "debug"]({
1572
+ task,
1573
+ command,
1574
+ exitCode,
1575
+ status
1576
+ }, `run: ${task}`);
1577
+ return {
1578
+ status,
1579
+ command,
1580
+ ...status === "fail" ? { message: `\`${command}\` exited ${exitCode}` } : {}
1581
+ };
888
1582
  };
889
- let result = base;
890
- if (additionalPaths && additionalPaths.length > 0) {
891
- const pathsBlockRe = /( {4}paths:\n)((?:[ ]{6}- [^\n]+\n)+)/;
892
- if (pathsBlockRe.test(result)) result = result.replace(pathsBlockRe, (_, header, existing) => {
893
- const existingPaths = new Set([...existing.matchAll(/- (.+)/g)].map((m) => m[1]));
894
- const newEntries = additionalPaths.filter((p) => !existingPaths.has(p)).map((p) => ` - ${p}\n`).join("");
895
- return header + existing + newEntries;
896
- });
897
- else {
898
- const pathsBlock = ` paths:\n${additionalPaths.map((p) => ` - ${p}\n`).join("")}`;
899
- result = result.replace(/( {4}branches: \[main\]\n)/, `$1${pathsBlock}`);
1583
+ }
1584
+ function runTask(input) {
1585
+ const { print, logger, readFile, fileExists, listDir, task, cwd } = input;
1586
+ const def = TASKS[task];
1587
+ let job = input.job;
1588
+ let passthrough = input.passthrough ?? [];
1589
+ if (job !== void 0 && !def?.jobs) {
1590
+ passthrough = [job, ...passthrough];
1591
+ job = void 0;
1592
+ }
1593
+ if (job !== void 0) {
1594
+ const jobDef = def.jobs[job];
1595
+ if (!jobDef) {
1596
+ const known = Object.keys(def.jobs).join(", ");
1597
+ print(`✗ unknown job "${job}" for "${task}" (known: ${known})`);
1598
+ logger.warn({
1599
+ task,
1600
+ job,
1601
+ status: "unknown"
1602
+ }, `run: ${task} ${job}`);
1603
+ return {
1604
+ status: "unknown",
1605
+ message: `unknown job "${task} ${job}"`
1606
+ };
1607
+ }
1608
+ const label = `${task} ${job}`;
1609
+ return runUnit(input, label, jobDef.local, makeRunOne({
1610
+ ...input,
1611
+ task: label
1612
+ }), passthrough);
1613
+ }
1614
+ const run = makeRunOne(input);
1615
+ if (turboDefinesTask(cwd, task, readFile, fileExists)) {
1616
+ const filterArg = input.filter ? [`--filter=${input.filter}`] : [];
1617
+ const forwarded = [...def?.local && !def.local.command && def.local.tool ? def.flags?.[def.local.tool] ?? [] : [], ...passthrough];
1618
+ const args = [
1619
+ "run",
1620
+ task,
1621
+ ...filterArg,
1622
+ ...forwarded.length ? ["--", ...forwarded] : []
1623
+ ];
1624
+ return run(resolveBin(cwd, "turbo", fileExists), args);
1625
+ }
1626
+ const script = readPackageJsonSync(cwd, { readFile })?.scripts?.[task];
1627
+ if (script && !/^holocron run\b/.test(script.trim())) return run(detectPackageManager(cwd, {
1628
+ readFile,
1629
+ fileExists
1630
+ }), [
1631
+ "run",
1632
+ task,
1633
+ ...passthrough.length ? ["--", ...passthrough] : []
1634
+ ]);
1635
+ if (def?.local) {
1636
+ if (def.local.command) return run(process.execPath, [
1637
+ process.argv[1],
1638
+ ...def.local.command.split(" "),
1639
+ ...passthrough
1640
+ ]);
1641
+ const runner = resolveRunner(def.local, cwd, listDir, readFile);
1642
+ if (runner) {
1643
+ const flags = (def.flags?.[runner.tool] ?? []).filter((f) => !passthrough.includes(f));
1644
+ const configFlags = resolveToolConfig(runner.tool, cwd, {
1645
+ readFile,
1646
+ fileExists
1647
+ });
1648
+ return run(resolveBin(cwd, runner.tool, fileExists), [
1649
+ ...runner.args,
1650
+ ...configFlags,
1651
+ ...flags,
1652
+ ...passthrough
1653
+ ]);
900
1654
  }
901
1655
  }
902
- if (!withOverrides || Object.keys(withOverrides).length === 0) return result;
903
- const withBlockRe = /( {4}with:\n)((?:[ ]{6}[^\n]+\n)*)/;
904
- const existingMatch = result.match(withBlockRe);
905
- if (existingMatch) {
906
- const existingEntries = new Map(existingMatch[2].split("\n").filter(Boolean).filter((line) => !line.trim().startsWith("#")).map((line) => {
907
- const m = line.match(/^ {6}([^:]+):\s*(.*)/);
908
- return m ? [m[1].trim(), m[2].trim()] : null;
909
- }).filter((e) => e !== null));
910
- for (const [k, v] of Object.entries(withOverrides)) existingEntries.set(k, yamlScalar(v));
911
- const merged = [...existingEntries.entries()].map(([k, v]) => ` ${k}: ${v}`).join("\n");
912
- return result.replace(withBlockRe, ` with:\n${merged}\n`);
1656
+ if (def?.linterGroup) return runLinterGroup(input, def.linterGroup, passthrough);
1657
+ if (def?.jobs && Object.keys(def.jobs).length > 0) return runAllJobs(input, def.jobs, passthrough);
1658
+ if (def?.local === null) {
1659
+ const msg = `no local equivalent for ${task} — enforced in CI`;
1660
+ print(`· ${msg}`);
1661
+ logger.debug({
1662
+ task,
1663
+ status: "skip"
1664
+ }, `run: ${task}`);
1665
+ return {
1666
+ status: "skip",
1667
+ message: msg
1668
+ };
913
1669
  }
914
- const withBlock = Object.entries(withOverrides).map(([k, v]) => fmt(k, v)).join("\n");
915
- const injected = result.replace(/ {4}secrets: inherit\n$/, ` with:\n${withBlock}\n secrets: inherit\n`);
916
- if (injected === result) logger?.warn({ template: name }, "generateThinCallerContent: could not inject `with:` overrides");
917
- return injected;
918
- }
919
- /**
920
- * Extract the Cloudflare Pages preview config from a deploy workflow's `with:` object.
921
- *
922
- * Accepts three forms:
923
- * - `preview: true` — derive both project and domain from org context
924
- * - `preview: { project: "..." }` — explicit project; domain derived from context if omitted
925
- * - `preview: { project: "...", domain: "..." }` — fully explicit
926
- *
927
- * Returns null when `preview:` is absent, false, or can't be resolved.
928
- */
929
- function extractPreviewConfig(raw, ctx = {}) {
930
- const preview = raw["preview"];
931
- if (!preview) return null;
932
- if (preview === true) {
933
- const project = ctx.org ? `${ctx.org}-preview` : null;
934
- const domain = ctx.domain ? `preview.${ctx.domain}` : void 0;
935
- if (!project) return null;
1670
+ if (KNOWN_TASKS.has(task)) {
1671
+ const msg = `no ${task} task for this repo`;
1672
+ print(input.required ? `✗ ${msg} (required)` : `· ${msg}`);
1673
+ logger[input.required ? "warn" : "debug"]({
1674
+ task,
1675
+ status: input.required ? "fail" : "skip"
1676
+ }, `run: ${task}`);
936
1677
  return {
937
- project,
938
- ...domain ? { domain } : {}
1678
+ status: input.required ? "fail" : "skip",
1679
+ message: msg
939
1680
  };
940
1681
  }
941
- if (typeof preview !== "object") return null;
942
- const p = preview;
943
- const project = typeof p["project"] === "string" && p["project"] ? p["project"] : ctx.org ? `${ctx.org}-preview` : null;
944
- if (!project) return null;
945
- const domain = typeof p["domain"] === "string" && p["domain"] ? p["domain"] : ctx.domain ? `preview.${ctx.domain}` : void 0;
1682
+ print(`✗ unknown task "${task}"`);
1683
+ logger.warn({
1684
+ task,
1685
+ status: "unknown"
1686
+ }, `run: ${task}`);
946
1687
  return {
947
- project,
948
- ...domain ? { domain } : {}
1688
+ status: "unknown",
1689
+ message: `unknown task "${task}"`
949
1690
  };
950
1691
  }
951
1692
  /**
952
- * Generate the full thin-caller YAML for a `deploy.yml` that handles both
953
- * production (push to main → GitHub Pages) and preview (pull_request →
954
- * Cloudflare Pages) in a single file.
1693
+ * Run one registry unit — a task's own runner or a single sub-job. Resolution
1694
+ * is registry-only (`turbo` / `package.json` scripts are keyed by task, not
1695
+ * `task/job`). `label` names the unit in messages (`audit performance`).
955
1696
  *
956
- * Both jobs receive the same docs/storybook `with:` inputs. If the per-repo
957
- * config supplies `cloudflare-project` it is forwarded; otherwise the reusable
958
- * falls back to the `CLOUDFLARE_PAGES_PROJECT` org variable — set that once and
959
- * all repos with a `deploy` workflow get previews without per-repo config.
1697
+ * `local: null` → CI-only by design, always a skip (never a failure, even with
1698
+ * `--required`). A declared runner with no matching repo file, or whose tool
1699
+ * isn't installed locally → a skip (`--required` turns it into a failure — the
1700
+ * repo claims a check it can't back). Mirrors the lint aggregate: run what's
1701
+ * here, flag the rest, let CI enforce.
960
1702
  */
961
- function generateCombinedDeployContent(deployWith, paths, preview) {
962
- const yamlScalar = (v) => {
963
- if (v === true) return "true";
964
- if (v === false) return "false";
965
- const s = String(v);
966
- return s.startsWith("[") || s.startsWith("{") ? `'${s}'` : s;
1703
+ function runUnit(input, label, local, run, passthrough) {
1704
+ const { print, logger, listDir, lookPath, cwd, readFile } = input;
1705
+ const skip = (msg) => {
1706
+ print(input.required ? `✗ ${msg} (required)` : `· ${msg}`);
1707
+ logger[input.required ? "warn" : "debug"]({
1708
+ task: label,
1709
+ status: input.required ? "fail" : "skip"
1710
+ }, `run: ${label}`);
1711
+ return {
1712
+ status: input.required ? "fail" : "skip",
1713
+ message: msg
1714
+ };
967
1715
  };
968
- const withLines = (entries) => Object.entries(entries).map(([k, v]) => ` ${k}: ${yamlScalar(v)}`).join("\n");
969
- const pathsBlock = paths.length > 0 ? ` paths:\n${paths.map((p) => ` - ${p}\n`).join("")}` : "";
970
- const previewWith = {
971
- ...deployWith,
972
- "cloudflare-project": preview.project
1716
+ if (local) {
1717
+ const runner = resolveRunner(local, cwd, listDir, readFile);
1718
+ if (!runner) return skip(`no ${label} runner for this repo`);
1719
+ const found = lookPath(cwd, runner.tool);
1720
+ if (!found) return skip(`${runner.tool} not installed locally for ${label} — enforced in CI`);
1721
+ return run(found, [...runner.args, ...passthrough]);
1722
+ }
1723
+ const msg = `no local equivalent for ${label} — enforced in CI`;
1724
+ print(`· ${msg}`);
1725
+ logger.debug({
1726
+ task: label,
1727
+ status: "skip"
1728
+ }, `run: ${label}`);
1729
+ return {
1730
+ status: "skip",
1731
+ message: msg
973
1732
  };
974
- const deployWithBlock = Object.keys(deployWith).length > 0 ? ` with:\n${withLines(deployWith)}\n` : "";
975
- const previewWithBlock = ` with:\n${withLines(previewWith)}\n`;
976
- return [
977
- `name: Deploy`,
978
- ``,
979
- `on: # yamllint disable-line rule:truthy`,
980
- ` push:`,
981
- ` branches: [main]`,
982
- ...pathsBlock ? [`${pathsBlock}`] : [],
983
- ` pull_request:`,
984
- ` branches: [main]`,
985
- ` types: [opened, synchronize, reopened, closed]`,
986
- ...pathsBlock ? [`${pathsBlock}`] : [],
987
- ` workflow_dispatch:`,
988
- ``,
989
- `concurrency:`,
990
- ` group: $\{{ github.event_name == 'pull_request' && format('preview-{0}', github.event.pull_request.number) || 'pages' }}`,
991
- ` cancel-in-progress: $\{{ github.event_name == 'pull_request' && github.event.action != 'closed' }}`,
992
- ``,
993
- `permissions:`,
994
- ` contents: read`,
995
- ` deployments: write`,
996
- ` pages: write`,
997
- ` id-token: write`,
998
- ` pull-requests: write`,
999
- ``,
1000
- `jobs:`,
1001
- ` deploy:`,
1002
- ` name: Deploy`,
1003
- ` if: \${{ github.event_name != 'pull_request' }}`,
1004
- ` uses: theholocron/.github/.github/workflows/delivery.deploy.yml@main`,
1005
- ...deployWithBlock ? [deployWithBlock.trimEnd()] : [],
1006
- ` secrets: inherit`,
1007
- ``,
1008
- ` preview:`,
1009
- ` name: Preview`,
1010
- ` if: \${{ github.event_name == 'pull_request' }}`,
1011
- ` uses: theholocron/.github/.github/workflows/preview.yml@main`,
1012
- previewWithBlock.trimEnd(),
1013
- ` secrets: inherit`,
1014
- ``
1015
- ].join("\n");
1016
1733
  }
1017
1734
  /**
1018
- * Expand structured with-values to flat GitHub Actions inputs before
1019
- * generating the thin caller. Handles:
1020
- * - deploy shorthand: docs/storybook → type + storybook-projects
1021
- * - preview: stripped (handled separately via extractPreviewConfig)
1022
- * - run-chromatic object → run-chromatic: true + chromatic-projects
1023
- * - plain arrays → JSON-stringified for YAML scalar quoting
1024
- *
1025
- * Used by both `holocron setup` and `sync-workflow-templates`.
1735
+ * `holocron run <task>` for a task that is a container of sub-jobs (`audit`) —
1736
+ * run each job in declared order. Status precedence: any `fail` → `fail`; else
1737
+ * any `ok` → `ok`; else any `dry-run` → `dry-run`; else `skip` (nothing ran).
1026
1738
  */
1027
- function normalizeWorkflowWith(raw) {
1028
- const result = { ...raw };
1029
- delete result["preview"];
1030
- const hasDocs = raw["docs"] === true || raw["docs"] !== null && typeof raw["docs"] === "object";
1031
- const storybookProjects = raw["storybook"];
1032
- if (hasDocs) {
1033
- result["type"] = "docs";
1034
- delete result["docs"];
1035
- }
1036
- if (Array.isArray(storybookProjects)) {
1037
- if (!hasDocs) result["type"] = "storybook";
1038
- result["storybook-projects"] = JSON.stringify(storybookProjects.map(({ name, path = "." }) => ({
1039
- name,
1040
- workingDir: path
1041
- })));
1042
- delete result["storybook"];
1043
- }
1044
- const runChromatic = raw["run-chromatic"];
1045
- if (runChromatic !== null && typeof runChromatic === "object" && "projects" in runChromatic) {
1046
- result["run-chromatic"] = true;
1047
- const projects = runChromatic.projects.map((p) => ({
1048
- ...p,
1049
- ...Array.isArray(p.untraced) ? { untraced: p.untraced.join("\n") } : {}
1050
- }));
1051
- result["chromatic-projects"] = JSON.stringify(projects);
1739
+ function runAllJobs(input, jobs, passthrough) {
1740
+ const { print, task } = input;
1741
+ const perJob = {
1742
+ ...input,
1743
+ required: false
1744
+ };
1745
+ const reports = [];
1746
+ for (const [name, jobDef] of Object.entries(jobs)) {
1747
+ const label = `${task} ${name}`;
1748
+ print(`▶ ${jobDef.checkContext}`);
1749
+ reports.push(runUnit(perJob, label, jobDef.local, makeRunOne({
1750
+ ...perJob,
1751
+ task: label
1752
+ }), passthrough));
1052
1753
  }
1053
- for (const [k, v] of Object.entries(result)) if (Array.isArray(v)) result[k] = JSON.stringify(v);
1054
- return result;
1754
+ const command = reports.map((r) => r.command).filter((c) => Boolean(c)).join(" && ");
1755
+ const statuses = new Set(reports.map((r) => r.status));
1756
+ if (statuses.has("fail")) return {
1757
+ status: "fail",
1758
+ command,
1759
+ message: `one or more ${task} jobs failed`
1760
+ };
1761
+ if (statuses.has("ok")) return {
1762
+ status: "ok",
1763
+ command
1764
+ };
1765
+ if (statuses.has("dry-run")) return {
1766
+ status: "dry-run",
1767
+ command
1768
+ };
1769
+ return {
1770
+ status: "skip",
1771
+ message: `no ${task} job has a local equivalent — enforced in CI`
1772
+ };
1055
1773
  }
1056
1774
  /**
1057
- * Derive on.push.paths entries from the deploy with: shorthand.
1058
- * Used by both `holocron setup` and `sync-workflow-templates`.
1059
- */
1060
- function deriveDeployPaths(raw) {
1061
- const paths = [];
1062
- const docs = raw["docs"];
1063
- if (docs === true) {
1064
- paths.push("docs/**");
1065
- paths.push("astro.config.ts");
1066
- paths.push("pnpm-workspace.yaml");
1067
- paths.push("pnpm-lock.yaml");
1068
- } else if (docs !== null && typeof docs === "object" && "path" in docs) {
1069
- const p = docs.path;
1070
- if (p && p !== ".") paths.push(`${p}/**`);
1775
+ * `holocron run <task>` for a `linterGroup` task — the task's fixed linter
1776
+ * set, each still gated by its own `detect`/`always` rule, run natively.
1777
+ * Worst exit code wins. Runs *after* the caller's own turbo/`package.json`
1778
+ * resolution (step 3b) — no per-linter turbo passthrough here, unlike the
1779
+ * old single `lint` task's eslint-only special case; the whole task is what
1780
+ * turbo/a package script would have already caught in steps 1–2.
1781
+ */
1782
+ function runLinterGroup(input, names, passthrough) {
1783
+ const { print, logger, listDir, lookPath, cwd, task, readFile, fileExists } = input;
1784
+ const dryRun = input.dryRun ?? false;
1785
+ const runOne = makeRunOne(input);
1786
+ let rootFiles;
1787
+ try {
1788
+ rootFiles = listDir(cwd);
1789
+ } catch {
1790
+ rootFiles = [];
1071
1791
  }
1072
- const storybookProjects = raw["storybook"];
1073
- if (Array.isArray(storybookProjects)) for (const s of storybookProjects) {
1074
- const p = s.path || ".";
1075
- if (p === ".") {
1076
- paths.push("src/**");
1077
- paths.push(".storybook/**");
1078
- } else paths.push(`${p}/**`);
1792
+ const sharedConfigAvailable = new Set(names.filter((name) => resolveToolConfig(name, cwd, {
1793
+ readFile,
1794
+ fileExists
1795
+ }).length > 0));
1796
+ const resolved = resolveLinters({
1797
+ explicit: names,
1798
+ rootFiles,
1799
+ sharedConfigAvailable
1800
+ });
1801
+ const reports = [];
1802
+ let anyHasLocalBin = false;
1803
+ for (const { name, def } of resolved) {
1804
+ const bin = def.localBin;
1805
+ if (!bin) {
1806
+ print(`· ${name} (CI only)`);
1807
+ continue;
1808
+ }
1809
+ anyHasLocalBin = true;
1810
+ const found = lookPath(cwd, bin);
1811
+ if (!found) {
1812
+ print(`! ${name} — ${bin} not on PATH${def.installHint ? `. ${def.installHint}` : ""} (enforced in CI)`);
1813
+ continue;
1814
+ }
1815
+ const configFlags = resolveToolConfig(name, cwd, {
1816
+ readFile,
1817
+ fileExists
1818
+ });
1819
+ reports.push(runOne(found, [
1820
+ ...configFlags,
1821
+ ...def.localArgs ?? [],
1822
+ ...passthrough
1823
+ ]));
1079
1824
  }
1080
- return paths;
1825
+ if (reports.length === 0) {
1826
+ const required = input.required && anyHasLocalBin;
1827
+ const msg = `no tooling available locally for ${task} — every linter in this group is CI-only here`;
1828
+ print(required ? `✗ ${msg} (required)` : `· ${msg}`);
1829
+ logger[required ? "warn" : "debug"]({
1830
+ task,
1831
+ status: required ? "fail" : "skip"
1832
+ }, `run: ${task}`);
1833
+ return {
1834
+ status: required ? "fail" : "skip",
1835
+ message: msg
1836
+ };
1837
+ }
1838
+ const command = reports.map((r) => r.command).filter((c) => Boolean(c)).join(" && ");
1839
+ if (dryRun) return {
1840
+ status: "dry-run",
1841
+ command
1842
+ };
1843
+ return reports.some((r) => r.status === "fail") ? {
1844
+ status: "fail",
1845
+ command,
1846
+ message: "one or more linters failed"
1847
+ } : {
1848
+ status: "ok",
1849
+ command
1850
+ };
1081
1851
  }
1082
1852
  //#endregion
1083
- //#region src/ci.ts
1853
+ //#region src/tasks/ci/ci.ts
1084
1854
  /**
1085
- * `holocron ci` / `astromech.ci()` — run the merge-gating checks locally, in CI
1855
+ * `holocron run ci` / `astromech.ci()` — run the merge-gating checks locally, in CI
1086
1856
  * order, and exit non-zero on the first failure. The "will CI pass?" command a
1087
1857
  * `pre-push` hook and the agent skills point at.
1088
1858
  *
@@ -1104,7 +1874,7 @@ function runCi(input) {
1104
1874
  const fellBack = input.scope !== "all" && selected.length === 0 && all.length > 0;
1105
1875
  if (fellBack) selected = all;
1106
1876
  const ordered = [...selected].sort((a, b) => CI_ORDER.indexOf(a.name) - CI_ORDER.indexOf(b.name));
1107
- print(`holocron ci — ${input.scope === "all" ? "all CI checks" : "required checks"} (${ordered.length})`);
1877
+ print(`holocron run ci — ${input.scope === "all" ? "all CI checks" : "required checks"} (${ordered.length})`);
1108
1878
  if (fellBack) print(" (no required tasks in the manifest — running every CI task)");
1109
1879
  print("");
1110
1880
  const jobs = [];
@@ -1138,8 +1908,8 @@ function runCi(input) {
1138
1908
  const passed = jobs.filter((j) => j.status === "ok" || j.status === "dry-run").length;
1139
1909
  const skipped = jobs.filter((j) => j.status === "skip").length;
1140
1910
  print("");
1141
- if (ordered.length === 0) print("holocron ci — nothing to run");
1142
- else if (failed > 0) print(`✗ ${failed} failed, ${passed} passed${skipped ? `, ${skipped} skipped` : ""}`);
1911
+ if (ordered.length === 0) print("holocron run ci — nothing to run");
1912
+ else if (failed > 0) print(`✗ ${passed} passed, ${failed} failed${skipped ? `, ${skipped} skipped` : ""}`);
1143
1913
  else print(`✓ ${passed} passed${skipped ? `, ${skipped} skipped` : ""}${input.dryRun ? " (plan only)" : ""}`);
1144
1914
  return {
1145
1915
  status: failed > 0 ? "fail" : "ok",
@@ -1150,364 +1920,474 @@ function taskName(item) {
1150
1920
  return typeof item === "string" ? item : item.name;
1151
1921
  }
1152
1922
  //#endregion
1153
- //#region src/templates/codecov/codecov.yml
1154
- var codecov_default = "codecov:\n require_ci_to_pass: true\n\ncoverage:\n precision: 2\n round: down\n status:\n project:\n default:\n target: auto\n threshold: 2%\n # Infra-only PRs (dep bumps, config, lockfile) touch no coverable code,\n # so codecov produces no report to compare. Pass instead of blocking\n # forever on a required check that will never post.\n if_not_found: success\n patch:\n default:\n target: 80%\n if_not_found: success\n\ncomment:\n layout: \"reach,diff,flags,components\"\n behavior: default\n require_changes: true\n\ncomponent_management:\n default_rules:\n statuses:\n - type: patch\n target: 80%\n if_not_found: success\n individual_components:\n";
1155
- //#endregion
1156
- //#region src/codecov.ts
1157
- /**
1158
- * `codecov.yml` generation — a manifest-derived artifact (component `paths`
1159
- * come from workspace packages; the status targets track the `test` task's
1160
- * coverage setup), the same category as the reusable `test.yml` this package
1161
- * also generates. Moved out of `@theholocron/cli` (theholocron/holocron#650)
1162
- * — `holocron setup`'s codecov step now calls {@link codecovConfig} instead
1163
- * of importing this logic directly.
1164
- */
1165
- const INDIVIDUAL_COMPONENTS_MARKER = " individual_components:";
1166
- function scaffoldHeader() {
1167
- return [
1168
- `# Scaffolded by holocron setup — edit this file freely.`,
1169
- `# Source: theholocron/holocron · packages/astromech/src/codecov.ts`,
1170
- ``
1171
- ].join("\n");
1172
- }
1173
- function codecovComponentBlock(packages) {
1174
- if (packages.length === 0) return "\n []\n";
1175
- return "\n" + packages.flatMap(({ slug, dir }) => [
1176
- ` - component_id: ${slug}`,
1177
- ` name: "${slug}"`,
1178
- ` paths:`,
1179
- ` - ${dir ?? "packages"}/${slug}/**`,
1180
- ``
1181
- ]).join("\n");
1182
- }
1923
+ //#region src/utils/create-header/create-header.ts
1183
1924
  /**
1184
- * Ensure every codecov status default carries `if_not_found: success`, so an
1185
- * infra-only PR (dep bump, config, lockfile — no coverable change) passes
1186
- * instead of hanging forever on a required check codecov never posts.
1925
+ * Factory that binds `source` and `tool` once and returns all header
1926
+ * constructors for auto-generated files. Prefer this over passing source
1927
+ * as a positional argument at every call site.
1187
1928
  *
1188
- * Injects the key on the line after each `target:` that sits inside a status
1189
- * block (`coverage.status.*.default` or a `statuses:` list item — indent ≥ 8,
1190
- * or ≥ 8 for a `- type:` list entry), when the immediate sibling lines do not
1191
- * already declare it.
1929
+ * @example
1930
+ * const { workflowHeader, scaffoldHeader } = createHeader({
1931
+ * source: "packages/astromech/src/templates/configs/editorconfig/create-config.ts",
1932
+ * });
1933
+ *
1934
+ * export function createConfig(): string {
1935
+ * return `${workflowHeader()}${body}`;
1936
+ * }
1937
+ *
1938
+ * ## Which header to use
1939
+ *
1940
+ * | Function | When to use | User can edit? |
1941
+ * |-------------------|----------------------------------------------------|----------------|
1942
+ * | `workflowHeader` | Files overwritten on every setup / sync-github run | No — "AUTO-GENERATED — do not edit" |
1943
+ * | `scaffoldHeader` | Files written once; user takes ownership after | Yes — "Scaffolded — edit this file freely" |
1944
+ *
1945
+ * ## `workflowHeader` formats
1946
+ *
1947
+ * | Format | Comment style | Used for |
1948
+ * |---------------------|------------------------|-------------------------------------------------------|
1949
+ * | `"yaml"` (default) | `# …` lines | YAML workflows, ignore files, `.editorconfig`, labeler|
1950
+ * | `"cjs"` | `/* … *\/` block | CommonJS config files (`.cjs`) |
1951
+ * | `"shebang"` | `#!/bin/sh` + `# …` | Shell scripts (`prepare-commit-msg`, hooks) |
1192
1952
  */
1193
- function ensureIfNotFound(yaml) {
1194
- const lines = yaml.split("\n");
1195
- const out = [];
1196
- for (let i = 0; i < lines.length; i++) {
1197
- const line = lines[i];
1198
- out.push(line);
1199
- const m = /^(\s{8,})target:\s+\S+\s*$/.exec(line);
1200
- if (!m) continue;
1201
- const indent = m[1];
1202
- let alreadySet = false;
1203
- for (let j = i + 1; j < lines.length; j++) {
1204
- const sib = lines[j];
1205
- if (sib.trim() === "") continue;
1206
- const sibIndent = sib.match(/^\s*/)[0].length;
1207
- if (sibIndent < indent.length) break;
1208
- if (sibIndent === indent.length && /^\s*if_not_found\s*:/.test(sib)) {
1209
- alreadySet = true;
1210
- break;
1211
- }
1212
- if (sibIndent === indent.length && /^\s*(- |\w)/.test(sib) && !/^\s*(threshold|flags|paths|branches)\s*:/.test(sib)) break;
1953
+ function createHeader(options) {
1954
+ const { source, tool = "holocron repo setup", forPrimary = false } = options;
1955
+ const doNotEdit = forPrimary ? `AUTO-GENERATED — do not edit in theholocron/.github directly.` : `AUTO-GENERATED — do not edit directly.`;
1956
+ return {
1957
+ workflowHeader(format = "yaml") {
1958
+ if (format === "cjs") return [
1959
+ `/* ${doNotEdit}`,
1960
+ ` * Source: theholocron/holocron · ${source}`,
1961
+ ` * Tool: ${tool}`,
1962
+ ` * Changes: edit source in theholocron/holocron`,
1963
+ ` */`,
1964
+ ``
1965
+ ].join("\n");
1966
+ const yamlLines = [
1967
+ `# ${doNotEdit}`,
1968
+ `# Source: theholocron/holocron · ${source}`,
1969
+ `# Tool: ${tool}`,
1970
+ `# Changes: edit source in theholocron/holocron`,
1971
+ ``
1972
+ ].join("\n");
1973
+ if (format === "shebang") return `#!/bin/sh\n\n${yamlLines}`;
1974
+ return yamlLines;
1975
+ },
1976
+ scaffoldHeader() {
1977
+ return [
1978
+ `# Scaffolded by holocron repo setup — edit this file freely.`,
1979
+ `# Source: theholocron/holocron · ${source}`,
1980
+ ``
1981
+ ].join("\n");
1213
1982
  }
1214
- if (!alreadySet) out.push(`${indent}if_not_found: success`);
1215
- }
1216
- return out.join("\n");
1217
- }
1218
- function mergeCodecovComponents(existing, packages) {
1219
- const idx = existing.indexOf(INDIVIDUAL_COMPONENTS_MARKER);
1220
- if (idx === -1) return ensureIfNotFound(existing);
1221
- return ensureIfNotFound(existing.slice(0, idx + 24)) + codecovComponentBlock(packages);
1222
- }
1223
- function readWorkspaceDir(repoRoot, dir) {
1224
- const dirPath = join(repoRoot, dir);
1225
- if (!existsSync(dirPath)) return [];
1226
- const packages = [];
1227
- for (const entry of readdirSync(dirPath, { withFileTypes: true })) {
1228
- if (!entry.isDirectory()) continue;
1229
- try {
1230
- const raw = readFileSync(join(dirPath, entry.name, "package.json"), "utf8");
1231
- const pkg = JSON.parse(raw);
1232
- if (typeof pkg.name === "string") packages.push({
1233
- slug: entry.name,
1234
- name: pkg.name,
1235
- dir
1236
- });
1237
- } catch {}
1238
- }
1239
- return packages;
1983
+ };
1240
1984
  }
1985
+ //#endregion
1986
+ //#region src/utils/default-list-dirs/default-list-dirs.ts
1987
+ const defaultListDirs = (path) => readdirSync(path, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
1988
+ //#endregion
1989
+ //#region src/utils/default-read-json/default-read-json.ts
1990
+ const defaultReadJson = (path) => JSON.parse(readFileSync(path, "utf8"));
1991
+ //#endregion
1992
+ //#region src/utils/default-run/default-run.ts
1993
+ const defaultRun = (command, args, opts) => spawnSync(command, args, {
1994
+ encoding: "utf8",
1995
+ ...opts
1996
+ });
1997
+ //#endregion
1998
+ //#region src/utils/default-sleep/default-sleep.ts
1999
+ const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
2000
+ //#endregion
2001
+ //#region src/utils/exec/exec.ts
2002
+ /** Runs `cmd` with the parent's stdio; a signal-killed or unspawnable process reports `-1`. */
2003
+ const realExec = (cmd, args, opts) => {
2004
+ return { exitCode: spawnSync(cmd, args, {
2005
+ cwd: opts.cwd,
2006
+ stdio: "inherit"
2007
+ }).status ?? -1 };
2008
+ };
2009
+ //#endregion
2010
+ //#region src/utils/look-path/look-path.ts
2011
+ /** `node_modules/.bin/<bin>`, else the first `PATH` entry that has it, else `null`. */
2012
+ const realLookPath = (cwd, bin) => findExecutableSync(bin, { path: join(cwd, "node_modules", ".bin") }) ?? findExecutableSync(bin) ?? null;
2013
+ //#endregion
2014
+ //#region src/utils/noop-logger/noop-logger.ts
2015
+ /** The `RunLogger` used when the caller injects none: drops every record. */
2016
+ const noopLogger = {
2017
+ debug() {},
2018
+ warn() {}
2019
+ };
2020
+ //#endregion
2021
+ //#region src/tasks/deploy-on-release/constants.ts
2022
+ /** A stable release (no prerelease identifier) is on `main`, the default channel for a task. */
2023
+ const MAIN_CHANNEL = "main";
2024
+ const DEPLOY_TASK = "delivery.deploy";
2025
+ /** How long to wait for npm to serve the release's versions, and how often to ask. */
2026
+ const WAIT_TIMEOUT_MS = 3e5;
2027
+ const WAIT_INTERVAL_MS = 1e4;
2028
+ //#endregion
2029
+ //#region src/tasks/deploy-on-release/workspace-dependencies/workspace-dependencies.ts
1241
2030
  /**
1242
- * Every public `packages/*` and `apps/*` workspace, sorted by slug. `[]`
1243
- * when neither directory exists. Monorepo templates (Next.js, React, …)
1244
- * ship user-facing code under `apps/` alongside library code under
1245
- * `packages/` — both need a codecov component so `codecov/patch/<slug>`
1246
- * actually posts for each.
2031
+ * `pkg`'s direct `workspace:*` dependencies -- `dependencies`, plus
2032
+ * `devDependencies` when asked. `pkg.devDependencies` defaults to `{}`:
2033
+ * it's optional on `WorkspacePackage` precisely so a caller holding the
2034
+ * previous (dev-dependency-less) shape doesn't crash here.
1247
2035
  */
1248
- function readWorkspacePackages(repoRoot) {
1249
- return [...readWorkspaceDir(repoRoot, "packages"), ...readWorkspaceDir(repoRoot, "apps")].sort((a, b) => a.slug.localeCompare(b.slug));
1250
- }
1251
- /** A brand-new `codecov.yml` — the scaffold header + the base template + this repo's component list. */
1252
- function createCodecovConfig(packages) {
1253
- return `${scaffoldHeader()}${codecov_default.trimEnd()}${codecovComponentBlock(packages)}`;
2036
+ function directWorkspaceDependencies(pkg, all, includeDevDependencies) {
2037
+ const entries = includeDevDependencies ? [...Object.entries(pkg.dependencies), ...Object.entries(pkg.devDependencies ?? {})] : Object.entries(pkg.dependencies);
2038
+ const names = new Set(entries.filter(([, spec]) => String(spec).startsWith("workspace:")).map(([name]) => name));
2039
+ return all.filter((candidate) => names.has(candidate.name));
1254
2040
  }
1255
2041
  /**
1256
- * The full pipeline `holocron setup` needs: read this repo's public
1257
- * `packages/*`, then either merge them into an `existing` `codecov.yml` or
1258
- * scaffold a new one.
2042
+ * Every `workspace:*` dependency of `pkg`, transitively (holocron#1071) —
2043
+ * its own direct dependencies, and theirs, and so on — as workspace
2044
+ * packages (with their current version).
2045
+ *
2046
+ * `includeDevDependencies` defaults to `false`, matching the pre-#1071
2047
+ * behavior: a shared dev/build-time workspace package (a tsconfig, a
2048
+ * lint config, a build script) counts only when a caller opts in.
2049
+ * `defaultPaths` (computing paths that should trigger a rebuild) opts in;
2050
+ * `deployOnRelease`'s publish-wait list (waiting for npm to serve a
2051
+ * package's RUNTIME dependencies before deploying it) does not -- an
2052
+ * unpublished, possibly `private`, dev-only package would never satisfy
2053
+ * that wait and the deploy would time out (holocron#1071 review).
2054
+ *
2055
+ * A diamond (two packages depending on the same third one) contributes
2056
+ * that third package once, not twice — tracked by a visited set, seeded
2057
+ * with `pkg` itself so a dependency cycle (A -> B -> A) can't loop
2058
+ * forever either, even though a real workspace graph normally can't have
2059
+ * one.
1259
2060
  */
1260
- function codecovConfig(repoRoot, existing) {
1261
- const packages = readWorkspacePackages(repoRoot);
1262
- return existing != null ? mergeCodecovComponents(existing, packages) : createCodecovConfig(packages);
2061
+ function workspaceDependencies(pkg, all, options) {
2062
+ const includeDevDependencies = options?.includeDevDependencies ?? false;
2063
+ const visited = /* @__PURE__ */ new Set([pkg.name]);
2064
+ const result = [];
2065
+ let frontier = directWorkspaceDependencies(pkg, all, includeDevDependencies);
2066
+ while (frontier.length > 0) {
2067
+ const next = [];
2068
+ for (const dep of frontier) {
2069
+ if (visited.has(dep.name)) continue;
2070
+ visited.add(dep.name);
2071
+ result.push(dep);
2072
+ next.push(...directWorkspaceDependencies(dep, all, includeDevDependencies));
2073
+ }
2074
+ frontier = next;
2075
+ }
2076
+ return result;
1263
2077
  }
1264
2078
  //#endregion
1265
- //#region src/required-checks.ts
2079
+ //#region src/tasks/deploy-on-release/default-paths/default-paths.ts
1266
2080
  /**
1267
- * `requiredChecks(config)` — the branch-protection required-status-check list,
1268
- * derived from the task manifest. Every `required: true` task contributes its
1269
- * {@link WORKFLOW_CHECK_CONTEXTS} entry; `config.extraRequiredChecks` adds
1270
- * contexts not backed by a task (codecov, DCO is prepended by the caller).
2081
+ * The paths a release must touch to redeploy `pkg` when its manifest doesn't
2082
+ * list them: its own folder plus the folder of every
2083
+ * {@link workspaceDependencies} entry. Derived rather than listed so a new or
2084
+ * dropped workspace dependency needs no second edit.
1271
2085
  *
1272
- * `@theholocron/cli`'s `holocron setup` calls this instead of the old
1273
- * hand-maintained `repo.requiredChecks` array. Policy-free — it only knows the
1274
- * manifest.
2086
+ * @returns Repo-relative prefixes, each ending in `/`.
1275
2087
  */
1276
- /** Ordered (task contexts in {@link CI_ORDER}, then extras), de-duplicated. */
1277
- function requiredChecks(config) {
1278
- const entries = (config.tasks ?? []).map(normalizeTaskEntry);
1279
- const seen = /* @__PURE__ */ new Set();
1280
- const out = [];
1281
- for (const name of CI_ORDER) {
1282
- const context = entries.some((e) => e.name === name && e.required === true) ? WORKFLOW_CHECK_CONTEXTS[name] : void 0;
1283
- if (context && !seen.has(context)) {
1284
- seen.add(context);
1285
- out.push(context);
1286
- }
1287
- }
1288
- for (const context of config.extraRequiredChecks ?? []) if (!seen.has(context)) {
1289
- seen.add(context);
1290
- out.push(context);
1291
- }
1292
- return out;
2088
+ function defaultPaths(repoRoot, pkg, all) {
2089
+ return [pkg, ...workspaceDependencies(pkg, all, { includeDevDependencies: true })].map(({ dir }) => `${relative(repoRoot, dir).replace(/\\/g, "/")}/`);
1293
2090
  }
1294
2091
  //#endregion
1295
- //#region src/templates/reusable/actions/auto-commit.yml
1296
- 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
- //#endregion
1298
- //#region src/templates/reusable/actions/holocron.yml
1299
- var holocron_default = "name: Holocron\ndescription: >\n Run the Holocron CLI — `holocron run <task> [job]` by default, or any other\n subcommand via `command:` (`sync`, `sync-github`, …). Resolves\n `@theholocron/cli` from `node_modules` (published tarball) or builds it from\n source when it resolves to an unbuilt workspace checkout (the holocron repo\n itself). Requires dependencies to be installed already (run the setup action\n first).\n\ninputs:\n command:\n description: >\n Holocron subcommand. Default `run` — then `task` (and optional `job`)\n name the manifest task. Set to `sync` / `sync-github` / … to run that\n subcommand directly; `args` becomes everything after it.\n required: false\n default: run\n task:\n description: >\n Manifest task to run when command is run — typecheck, test, build, audit.\n required: false\n default: \"\"\n job:\n description: Optional sub-job within the task (e.g. knip, performance for audit).\n required: false\n default: \"\"\n args:\n description: >\n For `command: run`, extra arguments forwarded to the tool after `--`.\n For any other command, the arguments passed straight to it.\n required: false\n default: \"\"\n\nruns:\n using: composite\n\n steps:\n - name: Build the workspace when the CLI resolves to an unbuilt checkout\n shell: bash\n # The holocron repo carries @theholocron/cli as `workspace:*`; its bin\n # (dist/cli.mjs) does not exist on a fresh checkout. `pnpm build` is a\n # turbo run — cached and cheap once warm. A no-op everywhere else, where\n # the published tarball already ships dist/.\n #\n # pnpm creates every workspace package's node_modules/.bin/holocron shim\n # once, during the setup action's earlier `pnpm install` — before this\n # build has run, so every one of them is silently skipped (\"Failed to\n # create bin ... ENOENT\"), not just @theholocron/cli's own. That's a\n # one-time decision: a later build doesn't retroactively fix a shim that\n # was never created. Re-running `pnpm install` here (fast — the lockfile\n # hasn't changed, so it only re-links `.bin`) is what actually fixes it,\n # confirmed against a real cold install: `holocron` stays unresolvable\n # from inside any package until this second install runs.\n run: |\n if [ ! -f node_modules/@theholocron/cli/dist/cli.mjs ]; then\n echo \"→ @theholocron/cli dist/ missing — building the workspace from source\" >&2\n pnpm build\n echo \"→ re-linking node_modules/.bin — pnpm skipped it pre-build\" >&2\n pnpm install --frozen-lockfile\n fi\n\n - name: holocron ${{ inputs.command }} ${{ inputs.task }}\n shell: bash\n env:\n HOLOCRON_COMMAND: ${{ inputs.command }}\n HOLOCRON_TASK: ${{ inputs.task }}\n HOLOCRON_JOB: ${{ inputs.job }}\n HOLOCRON_ARGS: ${{ inputs.args }}\n run: |\n if [ \"$HOLOCRON_COMMAND\" = \"run\" ]; then\n set -- run \"$HOLOCRON_TASK\"\n [ -n \"$HOLOCRON_JOB\" ] && set -- \"$@\" \"$HOLOCRON_JOB\"\n # shellcheck disable=SC2086\n [ -n \"$HOLOCRON_ARGS\" ] && set -- \"$@\" -- $HOLOCRON_ARGS\n else\n # shellcheck disable=SC2086\n set -- \"$HOLOCRON_COMMAND\" $HOLOCRON_ARGS\n fi\n # Invoke the built entry directly. `pnpm exec holocron` relies on a\n # node_modules/.bin/holocron shim that pnpm does not create for a\n # workspace package whose dist/ was absent at install time (the holocron\n # repo itself). Resolving the package's main export gives dist/index.mjs;\n # the bin sits next to it as dist/cli.mjs. This is `require.resolve` from\n # node_modules — it works in the holocron monorepo AND in every consumer\n # repo (theholocron/holocron#655), unlike a hard-coded `packages/cli/…`.\n cli=\"$(dirname \"$(node -e 'process.stdout.write(require.resolve(\"@theholocron/cli\"))')\")/cli.mjs\"\n if [ ! -f \"$cli\" ]; then\n echo \"::error::@theholocron/cli entry not found at $cli — is the workspace built?\" >&2\n exit 1\n fi\n echo \"→ $cli $*\" >&2\n node \"$cli\" \"$@\"\n";
1300
- //#endregion
1301
- //#region src/templates/reusable/actions/install.yml
1302
- var install_default = "name: Install dependencies\ndescription: Install project dependencies with pnpm frozen lockfile.\n\nruns:\n using: composite\n\n steps:\n - name: Install dependencies\n if: ${{ hashFiles('pnpm-lock.yaml') != '' }}\n shell: bash\n run: pnpm install --frozen-lockfile\n";
1303
- //#endregion
1304
- //#region src/templates/reusable/actions/setup.yml
1305
- var setup_default = "name: Setup\ndescription: Prepare the environment and install project dependencies.\n\ninputs:\n node-version:\n description: Node.js version\n required: false\n default: \"22.x\"\n\nruns:\n using: composite\n\n steps:\n - uses: theholocron/.github/.github/actions/setup-node@main\n with:\n node-version: ${{ inputs.node-version }}\n\n - uses: theholocron/.github/.github/actions/install@main\n\n # `@theholocron/cli`'s `dist/cli.mjs` doesn't exist yet at this point, so\n # pnpm's workspace bin-linking can't create `node_modules/.bin/holocron`\n # (silent `WARN ... ENOENT`, not a hard failure). Every package whose\n # scripts delegate via `holocron run <task> --` then fails at run time\n # with `holocron: not found` (#698). Same fix the `holocron` composite\n # action already applies for its own direct invocation (see its \"Build\n # the workspace when the CLI resolves to an unbuilt checkout\" step) —\n # mirrored here so it also covers *nested* `holocron run <task> --`\n # calls from inside package.json scripts, which that action's own guard\n # doesn't reach. `pnpm build` (not a scoped `--filter`) so the CLI's own\n # workspace dependencies (e.g. rollup-plugin-transform-template) build\n # in topological order too — a scoped build skips them and fails.\n # No-op in every consumer repo, where the published tarball already\n # ships `dist/`.\n - name: Build the Holocron CLI\n shell: bash\n run: |\n if [ ! -f node_modules/@theholocron/cli/dist/cli.mjs ]; then\n echo \"→ @theholocron/cli dist/ missing — building the workspace from source\" >&2\n pnpm build\n echo \"→ re-linking node_modules/.bin — pnpm skipped it pre-build\" >&2\n pnpm install --frozen-lockfile\n fi\n\n - name: Cache Turbo artifacts\n if: ${{ hashFiles('turbo.json') != '' }}\n uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0\n with:\n path: .turbo\n key: turbo-${{ runner.os }}-${{ github.sha }}\n restore-keys: |\n turbo-${{ runner.os }}-\n";
1306
- //#endregion
1307
- //#region src/templates/reusable/actions/setup-node.yml
1308
- var setup_node_default = "name: Setup Node\ndescription: Install pnpm and Node.js with pnpm dependency caching.\n\ninputs:\n node-version:\n description: Node.js version\n required: false\n default: \"22.x\"\nruns:\n using: composite\n\n steps:\n - name: Setup pnpm\n if: ${{ hashFiles('pnpm-lock.yaml') != '' }}\n uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4\n\n - name: Setup Node.js\n uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0\n with:\n node-version: ${{ hashFiles('.node-version') != '' && '' || inputs.node-version }}\n node-version-file: ${{ hashFiles('.node-version') != '' && '.node-version' || '' }}\n cache: ${{ hashFiles('pnpm-lock.yaml') != '' && 'pnpm' || '' }}\n # Do NOT add registry-url here. setup-node writes .npmrc with\n # _authToken=${NODE_AUTH_TOKEN} and sets NPM_CONFIG_USERCONFIG to it.\n # pnpm reads that file, fails env-var substitution when the token is\n # empty, and loses auth entirely. Without registry-url, pnpm 10.15+\n # handles Trusted Publishing via its own native OIDC exchange.\n\n - name: Add node_modules/.bin to PATH\n shell: bash\n run: echo \"$GITHUB_WORKSPACE/node_modules/.bin\" >> $GITHUB_PATH\n";
1309
- //#endregion
1310
- //#region src/reusable.ts
2092
+ //#region src/tasks/deploy-on-release/deploy-targets/deploy-targets.ts
1311
2093
  /**
1312
- * The **reusable** GitHub Actions surface pushed to `theholocron/.github` by
1313
- * `holocron sync-github` — the `workflow_call` implementations and composite
1314
- * actions that every repo's thin callers delegate to.
1315
- *
1316
- * `theholocron/.github` is a **pure sync target**: `.github/workflows/*`,
1317
- * `.github/actions/*` and `workflow-templates/*` there are generated from the
1318
- * `.yml` files in `src/templates/reusable/` and pushed verbatim (with a
1319
- * "do not edit" header). Never hand-edit them in `.github`.
1320
- *
1321
- * Not to be confused with `WORKFLOW_TEMPLATES` in `thin-callers.ts` — those are
1322
- * the *thin-caller bases* written into each consumer repo's `.github/workflows/`.
2094
+ * The packages whose manifest declares a release-time `delivery.deploy`
2095
+ * (`with.on === "release"`), with the task's options resolved.
1323
2096
  */
1324
- /** `workflow_call` implementations → `.github/workflows/<name>.yml`. */
1325
- 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/test-results-action@0fa95f0e1eeaafde2c782583b36b28ad0d8c77d3 # v1\n name: Upload test results to Codecov\n if: ${{ !cancelled() }}\n with:\n use_oidc: true\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 - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0\n name: Upload coverage to Codecov\n with:\n use_oidc: true\n\n - uses: codecov/test-results-action@0fa95f0e1eeaafde2c782583b36b28ad0d8c77d3 # v1\n name: Upload test results to Codecov\n if: ${{ !cancelled() }}\n with:\n use_oidc: true\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
- "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
- "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
- "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",
1330
- "sourceQuality.formatting": "name: Formatting\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n enable-auto-commit:\n description: >\n Auto-commit prettier/editorconfig/markdownlint --fix results as a\n verified commit via a GitHub App (same App used across every\n formatting-capable task — D12).\n type: boolean\n required: false\n default: false\n secrets:\n SUPER_LINTER_APP_ID:\n required: false\n SUPER_LINTER_PRIVATE_KEY:\n required: false\n TURBO_TOKEN:\n required: false\n\njobs:\n formatting:\n name: Run prettier, editorconfig-checker, markdownlint\n permissions:\n contents: write\n statuses: write\n runs-on: ubuntu-latest\n timeout-minutes: 15\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n APP_ID_SET: ${{ secrets.SUPER_LINTER_APP_ID != '' }}\n steps:\n - name: Generate GitHub App token\n id: app-token\n if: >\n inputs.enable-auto-commit == true &&\n github.event.pull_request != null &&\n github.event.pull_request.head.repo.full_name == github.repository &&\n github.ref_name != github.event.repository.default_branch &&\n env.APP_ID_SET == 'true'\n uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0\n with:\n app-id: ${{ secrets.SUPER_LINTER_APP_ID }}\n private-key: ${{ secrets.SUPER_LINTER_PRIVATE_KEY }}\n\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n with:\n fetch-depth: 0\n token: ${{ steps.app-token.outputs.token || github.token }}\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n # editorconfig-checker has no npm distribution — install the prebuilt\n # binary from its release tarball (v4.0.1+ ships the binary at the\n # tarball root as `editorconfig-checker`, not `bin/ec-<os>-<arch>` —\n # confirmed against a live CI run; the old asset name/path 404s now).\n - name: Install editorconfig-checker\n run: |\n curl -sSL https://github.com/editorconfig-checker/editorconfig-checker/releases/latest/download/editorconfig-checker-linux-amd64.tar.gz \\\n | tar xz -C /usr/local/bin editorconfig-checker\n\n - uses: theholocron/.github/.github/actions/holocron@main\n name: Run prettier, editorconfig-checker, markdownlint\n with:\n task: sourceQuality.formatting\n\n - uses: theholocron/.github/.github/actions/auto-commit@main\n name: Commit and push formatting fixes\n if: >\n inputs.enable-auto-commit == true &&\n github.event.pull_request != null &&\n github.event.pull_request.head.repo.full_name == github.repository &&\n github.ref_name != github.event.repository.default_branch &&\n env.APP_ID_SET == 'true' &&\n steps.app-token.conclusion == 'success'\n with:\n token: ${{ steps.app-token.outputs.token }}\n branch: ${{ github.event.pull_request.head.ref || github.head_ref || github.ref }}\n commit-message: \"chore: fix formatting issues\"\n commit-options: \"--no-verify\"\n",
1331
- "sourceQuality.structuredDataValidation": "name: Structured Data Validation\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n secrets:\n TURBO_TOKEN:\n required: false\n\njobs:\n structured-data-validation:\n name: Run yamllint\n permissions:\n contents: read\n runs-on: ubuntu-latest\n timeout-minutes: 10\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n # yamllint has no npm distribution — Python ships preinstalled on\n # ubuntu-latest runners.\n - name: Install yamllint\n run: pip install --user yamllint\n\n - uses: theholocron/.github/.github/actions/holocron@main\n name: Run yamllint\n with:\n task: sourceQuality.structuredDataValidation\n",
1332
- "sourceQuality.deadCodeAnalysis": "name: Dead Code Analysis\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n secrets:\n TURBO_TOKEN:\n required: false\n\njobs:\n dead-code-analysis:\n name: Run knip\n permissions:\n contents: read\n runs-on: ubuntu-latest\n timeout-minutes: 10\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n with:\n persist-credentials: false\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - uses: theholocron/.github/.github/actions/holocron@main\n name: Run knip\n with:\n task: sourceQuality.deadCodeAnalysis\n",
1333
- "security.secretDetection": "name: Secret Detection\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n secrets:\n TURBO_TOKEN:\n required: false\n\njobs:\n secret-detection:\n name: Run gitleaks\n permissions:\n contents: read\n runs-on: ubuntu-latest\n timeout-minutes: 15\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n with:\n fetch-depth: 0\n\n # gitleaks has no npm distribution — install the prebuilt binary via\n # the maintainer's own documented install script.\n # NOTE: unverified against a live CI run at authoring time (D12) — flag\n # here if this needs adjustment.\n - name: Install gitleaks\n run: |\n curl -sSfL https://raw.githubusercontent.com/gitleaks/gitleaks/master/install.sh | sh -s -- -b /usr/local/bin\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - uses: theholocron/.github/.github/actions/holocron@main\n name: Run gitleaks\n with:\n task: security.secretDetection\n",
1334
- "security.codeScanning": "name: Security\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n language:\n description: CodeQL language to analyze\n type: string\n required: false\n default: javascript-typescript\n\njobs:\n analyze:\n name: Analyze (${{ inputs.language }})\n permissions:\n actions: read\n contents: read\n security-events: write\n runs-on: ubuntu-latest\n timeout-minutes: 45\n # Do not cancel in-progress security scans.\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0\n name: Initialize CodeQL\n with:\n languages: ${{ inputs.language }}\n\n - uses: github/codeql-action/autobuild@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0\n name: Autobuild\n\n - uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0\n name: Analyze\n with:\n category: /language:${{ inputs.language }}\n",
1335
- "delivery.build": "name: Build\n\n# Sanity check that every workspace actually compiles to dist/ — doesn't\n# upload or publish anything. delivery.publish builds again before\n# publishing; this just answers \"does it build?\" on every push/PR.\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n secrets:\n TURBO_TOKEN:\n required: false\n\njobs:\n build:\n name: Build every workspace\n permissions:\n contents: read\n runs-on: ubuntu-latest\n timeout-minutes: 10\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - uses: theholocron/.github/.github/actions/holocron@main\n name: Build\n with:\n task: delivery.build\n",
1336
- "delivery.publish": "name: Release\n\n# Semantic-release with OIDC Trusted Publishing.\n# actions/setup-node writes a default NODE_AUTH_TOKEN=${{ github.token }}\n# which shadows OIDC auth. We explicitly clear it so npm falls through to\n# the Trusted Publisher OIDC exchange.\n# The calling repo must have a .releaserc.json that configures branches,\n# plugins, and any publish options. npm@11+ is installed to support OIDC.\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n dry-run:\n description: >\n When true, runs semantic-release --dry-run: analyzes commits and\n previews the next release without git writes or publish. Only\n meaningful for workflow_dispatch triggers; push-triggered runs\n always run fully.\n type: boolean\n required: false\n default: false\n post-release:\n description: >\n When true, dispatches a readme sync broadcast after a successful\n release. Enable for packages consumed by other repos via the\n registry-doc system so downstream README installation blocks\n stay current.\n type: boolean\n required: false\n default: false\n run-build:\n description: Run `pnpm build` before releasing\n type: boolean\n required: false\n default: true\n sentry-project:\n description: >\n Sentry project slug for sourcemap upload and release creation after\n publishing. Omit to skip the Sentry release step entirely.\n type: string\n required: false\n default: \"\"\n secrets:\n HOLOCRON_RELEASE_TOKEN:\n description: >\n Fine-grained PAT (Contents + Issues + Pull requests: write) owned by\n an admin. Required when the default branch is protected by a ruleset —\n github.token cannot push through rulesets, but an admin PAT can.\n Takes priority over HOLOCRON_SYNC_TOKEN. Falls back to github.token.\n required: false\n HOLOCRON_SYNC_TOKEN:\n description: >\n Legacy alias for HOLOCRON_RELEASE_TOKEN — kept for backward compatibility.\n Prefer HOLOCRON_RELEASE_TOKEN for new repos.\n required: false\n HOLOCRON_READ_TOKEN:\n description: >\n Fine-grained PAT for read-only GitHub API calls (e.g. resolving git\n committer identity via `gh api user`). Falls back to github.token.\n required: false\n GH_TOKEN:\n description: >\n Generic GitHub token fallback for `gh` CLI calls. Used when\n HOLOCRON_READ_TOKEN is not set.\n required: false\n SENTRY_AUTH_TOKEN:\n description: >\n Sentry auth token for sourcemap upload and release creation.\n Required when sentry-project is set. Use the org-level secret.\n required: false\n TURBO_TOKEN:\n required: false\n\njobs:\n release:\n name: Semantic release\n permissions:\n contents: write\n id-token: write\n issues: write\n pull-requests: write\n runs-on: ubuntu-latest\n timeout-minutes: 30\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n # Do not cancel in-progress releases — a partial release is worse than a slow one.\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n with:\n fetch-depth: 0\n persist-credentials: false\n # Use HOLOCRON_RELEASE_TOKEN when available — git push (tags, release commits)\n # uses the checkout credential, not GITHUB_TOKEN env var. The\n # built-in github.token cannot push through branch protection rulesets.\n token: ${{ secrets.HOLOCRON_RELEASE_TOKEN || secrets.HOLOCRON_SYNC_TOKEN || github.token }}\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - name: Configure git identity\n run: |\n GIT_NAME=$(gh api user --jq .name 2>/dev/null || echo \"github-actions[bot]\")\n GIT_EMAIL=$(gh api user --jq '\"\\(.id)+\\(.login)@users.noreply.github.com\"' 2>/dev/null || echo \"41898282+github-actions[bot]@users.noreply.github.com\")\n git config --global user.name \"$GIT_NAME\"\n git config --global user.email \"$GIT_EMAIL\"\n git config --global format.signoff true\n env:\n GH_TOKEN: ${{ secrets.HOLOCRON_READ_TOKEN || secrets.GH_TOKEN || github.token }}\n\n - name: Upgrade npm for OIDC support\n run: npm install -g npm@11 sigstore\n # sigstore is required by libnpmpublish/provenance.js at module parse\n # time — before any config takes effect. Some npm 11.x builds stopped\n # bundling it; installing it globally into the same prefix ensures it\n # resolves regardless of npm version. (Discovered 2026-07-09.)\n\n - run: pnpm build\n name: Build\n if: ${{ inputs.run-build == true }}\n\n - name: Release\n id: release\n run: |\n BEFORE=$(git rev-parse HEAD)\n if [ \"$DRY_RUN\" = \"true\" ]; then\n npx semantic-release --dry-run\n else\n npx semantic-release\n fi\n AFTER=$(git rev-parse HEAD)\n # semantic-release only commits (`chore(release): … [skip ci]`) when it\n # actually publishes. That commit gets no CI (and so no coverage\n # upload), which makes the next PR's `codecov/project` compare a full\n # report against a stale ancestor — a phantom drop (holocron#644).\n # Emit the new SHA so the steps below upload coverage for it.\n if [ \"$BEFORE\" != \"$AFTER\" ]; then echo \"commit=$AFTER\" >> \"$GITHUB_OUTPUT\"; fi\n env:\n DRY_RUN: ${{ inputs.dry-run }}\n # Prefer HOLOCRON_RELEASE_TOKEN (fine-grained PAT, Contents+Issues+PRs write,\n # owned by an admin with ruleset bypass) so @semantic-release/git can\n # push the version-bump commit through branch protection. Falls back to\n # HOLOCRON_SYNC_TOKEN (legacy) then github.token for unprotected repos.\n GITHUB_TOKEN: ${{ secrets.HOLOCRON_RELEASE_TOKEN || secrets.HOLOCRON_SYNC_TOKEN || github.token }}\n HUSKY: \"0\"\n NPM_CONFIG_PROVENANCE: true\n\n - uses: theholocron/.github/.github/actions/holocron@main\n name: Coverage for the release commit\n if: ${{ steps.release.outputs.commit != '' }}\n with:\n task: verification.unitTests\n\n - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0\n name: Upload release-commit coverage to Codecov\n if: ${{ steps.release.outputs.commit != '' }}\n with:\n use_oidc: true\n override_commit: ${{ steps.release.outputs.commit }}\n override_branch: ${{ github.ref_name }}\n\n - name: Get release version\n id: release_version\n if: ${{ inputs.sentry-project != '' && inputs.dry-run != true }}\n env:\n SENTRY_PROJECT: ${{ inputs.sentry-project }}\n run: |\n TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo \"\")\n if [ -n \"$TAG\" ]; then\n echo \"release=${SENTRY_PROJECT}@${TAG#v}\" >> \"$GITHUB_OUTPUT\"\n fi\n\n - name: Create Sentry release\n if: ${{ inputs.sentry-project != '' && steps.release_version.outputs.release != '' && inputs.dry-run != true }}\n uses: getsentry/action-release@ff07929a6537bac57790c3451cf4d364aca38528 # v3\n env:\n SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}\n SENTRY_ORG: theholocron\n SENTRY_PROJECT: ${{ inputs.sentry-project }}\n with:\n environment: production\n version: ${{ steps.release_version.outputs.release }}\n sourcemaps: \"**/dist\"\n\n sync-readme:\n name: Post-release readme sync\n needs: release\n if: ${{ inputs.post-release == true && inputs.dry-run != true }}\n runs-on: ubuntu-latest\n timeout-minutes: 5\n steps:\n - name: Trigger broadcast readme sync\n run: |\n gh workflow run sync-dispatch.yml \\\n --repo theholocron/.github \\\n --field \"steps=readme\"\n env:\n GH_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN }}\n",
1337
- "delivery.deploy": "name: Deploy\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n type:\n description: \"Type of deployment: docs or storybook\"\n required: true\n type: string\n storybook-projects:\n description: >\n JSON array of { \"name\"?, \"workingDir\", \"outputDir\"? } objects for storybook deploys.\n Each is built via `pnpm -C <workingDir> build:storybook`. If \"name\" is provided the\n output is placed under `sandbox/<name>/`; omit \"name\" for single-repo deploys and the\n output lands directly in `sandbox/`.\n type: string\n required: false\n default: \"[]\"\n build-script:\n description: pnpm script that builds the Storybook static output (single storybook, type:storybook only)\n type: string\n required: false\n default: build:storybook\n output-dir:\n description: Directory where Storybook writes its static output (single storybook, type:storybook only)\n type: string\n required: false\n default: storybook-static\n\njobs:\n deploy:\n name: Deploy\n runs-on: ubuntu-latest\n permissions:\n contents: read\n pages: write\n id-token: write\n environment:\n name: github-pages\n url: ${{ steps.deployment.outputs.page_url }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - name: Build docs site\n if: ${{ inputs.type == 'docs' }}\n run: pnpm -C docs build\n\n - name: Build Storybook projects\n if: ${{ inputs.storybook-projects != '[]' }}\n env:\n PROJECTS: ${{ inputs.storybook-projects }}\n run: |\n echo \"$PROJECTS\" | jq -c '.[]' | while IFS= read -r project; do\n workingDir=$(echo \"$project\" | jq -r '.workingDir')\n pnpm -C \"$workingDir\" build:storybook\n done\n\n - name: Build Storybook\n if: ${{ inputs.type == 'storybook' && inputs.storybook-projects == '[]' }}\n env:\n BUILD_SCRIPT: ${{ inputs.build-script }}\n run: pnpm run \"$BUILD_SCRIPT\"\n\n - name: Assemble site\n env:\n DEPLOY_TYPE: ${{ inputs.type }}\n PROJECTS: ${{ inputs.storybook-projects }}\n STORYBOOK_OUTPUT_DIR: ${{ inputs.output-dir }}\n run: |\n mkdir -p _site\n if [ \"$DEPLOY_TYPE\" = \"docs\" ]; then\n cp -r docs/dist/. _site/\n fi\n if [ \"$PROJECTS\" != \"[]\" ]; then\n echo \"$PROJECTS\" | jq -c '.[]' | while IFS= read -r project; do\n name=$(echo \"$project\" | jq -r '.name // \"\"')\n workingDir=$(echo \"$project\" | jq -r '.workingDir')\n outputDir=$(echo \"$project\" | jq -r '.outputDir // \"storybook-static\"')\n if [ -n \"$name\" ]; then\n target=\"_site/sandbox/${name}\"\n else\n target=\"_site/sandbox\"\n fi\n mkdir -p \"$target\"\n cp -r \"${workingDir}/${outputDir}/.\" \"$target/\"\n done\n elif [ \"$DEPLOY_TYPE\" = \"storybook\" ]; then\n mkdir -p _site/sandbox\n cp -r \"${STORYBOOK_OUTPUT_DIR}/.\" _site/sandbox/\n fi\n\n - uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3.0.1\n name: Upload pages artifact\n with:\n path: _site\n\n - uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5\n id: deployment\n name: Deploy to GitHub Pages\n",
1338
- "delivery.bundleSize": "name: Audit the Bundle Size\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n secrets:\n CODECOV_TOKEN:\n required: false\n TURBO_TOKEN:\n required: false\n\njobs:\n bundle-size:\n name: Upload bundle stats to Codecov\n permissions:\n contents: read\n runs-on: ubuntu-latest\n timeout-minutes: 15\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n # Read by the bundle-stats uploader inside `holocron run delivery.build`.\n CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - uses: theholocron/.github/.github/actions/holocron@main\n name: Build and upload bundle stats\n with:\n task: delivery.build\n",
1339
- "platform.repoSync": "name: Sync\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n steps:\n description: >\n Sync steps to run (default: all). Valid values:\n labels, properties, teams, topics, keywords, description, homepage, readme, workflows, wiki.\n Pass a space-separated list to run a subset, e.g. \"readme\" or \"readme wiki\".\n type: string\n required: false\n secrets:\n HOLOCRON_ADMIN_TOKEN:\n description: Fine-grained PAT with admin scopes (labels, properties, teams).\n required: false\n HOLOCRON_AXIOM_TOKEN:\n description: >\n Axiom API token. When set alongside the HOLOCRON_AXIOM_DATASET\n repo/org variable, the CLI ships this run's structured logs to Axiom.\n Falls back to the vendor-native AXIOM_TOKEN secret.\n required: false\n AXIOM_TOKEN:\n description: Vendor-native fallback for HOLOCRON_AXIOM_TOKEN.\n required: false\n HOLOCRON_DEPLOY_TOKEN:\n description: Fine-grained PAT for GitHub Pages configuration.\n required: false\n HOLOCRON_ISSUES_TOKEN:\n description: Fine-grained PAT for issue management.\n required: false\n HOLOCRON_ORG_TOKEN:\n description: Org-scoped fine-grained PAT for team sync and org properties.\n required: false\n HOLOCRON_READ_TOKEN:\n description: Fine-grained PAT for read-only GitHub API calls.\n required: false\n HOLOCRON_SYNC_TOKEN:\n required: false\n GH_TOKEN:\n description: >\n Generic GitHub token fallback for gh CLI calls. Used when\n HOLOCRON_SYNC_TOKEN is not set.\n required: false\n\njobs:\n sync:\n name: Run holocron sync\n runs-on: ubuntu-latest\n timeout-minutes: 10\n permissions:\n contents: write\n pull-requests: write\n # Job-level so the `holocron` composite action's steps inherit them.\n env:\n HOLOCRON_ADMIN_TOKEN: ${{ secrets.HOLOCRON_ADMIN_TOKEN }}\n HOLOCRON_AXIOM_TOKEN: ${{ secrets.HOLOCRON_AXIOM_TOKEN || secrets.AXIOM_TOKEN }}\n HOLOCRON_AXIOM_DATASET: ${{ vars.HOLOCRON_AXIOM_DATASET || vars.AXIOM_DATASET }}\n POSTHOG_PROJECT_TOKEN: ${{ vars.POSTHOG_PROJECT_TOKEN }}\n HOLOCRON_DEPLOY_TOKEN: ${{ secrets.HOLOCRON_DEPLOY_TOKEN }}\n HOLOCRON_ISSUES_TOKEN: ${{ secrets.HOLOCRON_ISSUES_TOKEN }}\n HOLOCRON_ORG_TOKEN: ${{ secrets.HOLOCRON_ORG_TOKEN }}\n HOLOCRON_READ_TOKEN: ${{ secrets.HOLOCRON_READ_TOKEN }}\n HOLOCRON_SYNC_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n with:\n token: ${{ secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN || github.token }}\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - name: Run holocron sync\n # The `holocron` action resolves @theholocron/cli from node_modules (or\n # builds the workspace when it's an unbuilt checkout — the holocron repo\n # itself). `inputs.steps` is a space-separated list or empty; the\n # `--steps <list>` arg is omitted when empty.\n uses: theholocron/.github/.github/actions/holocron@main\n with:\n command: sync\n args: ${{ inputs.steps != '' && format('--steps {0}', inputs.steps) || '' }}\n\n - name: Format generated files\n run: |\n pnpm exec prettier --write README.md docs/src/content/docs/index.mdx 2>/dev/null || true\n # `holocron sync` writes package.json fields (keywords, description,\n # homepage) with a plain assignment, which appends new keys at the\n # end — re-apply the canonical order the lint config enforces.\n pnpm exec eslint --fix --no-warn-ignored package.json 2>/dev/null || true\n\n - uses: theholocron/.github/.github/actions/auto-commit@main\n id: auto-commit\n name: Commit sync changes\n with:\n token: ${{ secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN || github.token }}\n branch: chore/auto-sync\n commit-message: \"chore: sync from holocron.config\"\n commit-options: \"--no-verify\"\n\n - name: Open PR if changes were committed\n if: steps.auto-commit.outputs.changes-detected == 'true'\n run: |\n gh pr create \\\n --title \"chore: sync README and repo metadata\" \\\n --body \"Automated sync triggered by changes to config or package files. Merge to apply.\" \\\n --base main \\\n --head chore/auto-sync \\\n || echo \"PR already open — branch updated.\"\n env:\n GH_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN || github.token }}\n\n - name: Broadcast wiki sync if navbar changed\n if: >-\n steps.auto-commit.outputs.changes-detected == 'true' &&\n github.event_name == 'push' &&\n (inputs.steps == '' || contains(inputs.steps, 'wiki'))\n run: |\n if git diff HEAD~1 --name-only | grep -q 'fern/docs.yml'; then\n gh workflow run sync-dispatch.yml \\\n --repo theholocron/.github \\\n --field \"steps=wiki\" \\\n || echo \"skipping broadcast — insufficient permissions\"\n fi\n env:\n GH_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN || github.token }}\n",
1340
- "platform.commitStandards": "name: Commit Standards\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n secrets:\n TURBO_TOKEN:\n required: false\n\njobs:\n commit-standards:\n name: Run commitlint\n permissions:\n contents: read\n runs-on: ubuntu-latest\n timeout-minutes: 10\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n with:\n fetch-depth: 0\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n # No local equivalent (linters.ts: commitlint has no localBin — enforced\n # only at commit time via the commit-msg hook, and here in CI over the\n # PR's commit range). Not run through the holocron composite action.\n #\n # --config <resolved shared path> (config-resolution workstream, #676)\n # — falls back to commitlint's own auto-discovery of a local\n # commitlint.config.ts when the shared package's built dist/ isn't\n # present, same guard as the commit-msg hook.\n - name: Run commitlint over the PR range\n if: github.event_name == 'pull_request'\n run: |\n COMMITLINT_CONFIG=\"node_modules/@theholocron/commitlint-config/dist/index.js\"\n if [ -f \"$COMMITLINT_CONFIG\" ]; then\n pnpm exec commitlint --config \"$COMMITLINT_CONFIG\" --from \"${{ github.event.pull_request.base.sha }}\" --to \"${{ github.sha }}\"\n else\n pnpm exec commitlint --from \"${{ github.event.pull_request.base.sha }}\" --to \"${{ github.sha }}\"\n fi\n",
1341
- "platform.repoValidation": "name: Repo Validation\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n secrets:\n TURBO_TOKEN:\n required: false\n\njobs:\n adrs:\n name: Validate ADRs and specs\n permissions:\n contents: read\n runs-on: ubuntu-latest\n timeout-minutes: 10\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n with:\n fetch-depth: 0\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - uses: theholocron/.github/.github/actions/holocron@main\n name: Validate ADR and spec frontmatter\n with:\n task: platform.repoValidation\n job: adrs\n\n registry:\n name: Validate registry consistency\n permissions:\n contents: read\n runs-on: ubuntu-latest\n timeout-minutes: 10\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - uses: theholocron/.github/.github/actions/holocron@main\n name: Validate registry consistency\n with:\n task: platform.repoValidation\n job: registry\n\n docs-presence:\n name: Validate docs presence\n permissions:\n contents: read\n runs-on: ubuntu-latest\n timeout-minutes: 10\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n with:\n fetch-depth: 0\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - uses: theholocron/.github/.github/actions/holocron@main\n name: Validate docs presence for new packages\n with:\n task: platform.repoValidation\n job: docsPresence\n env:\n BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}\n\n conclusion:\n name: Conclusion\n runs-on: ubuntu-latest\n if: always()\n needs: [adrs, registry, docs-presence]\n steps:\n - name: Check job statuses\n run: |\n if [[ \"$RESULTS\" == *\"failure\"* ]] || [[ \"$RESULTS\" == *\"cancelled\"* ]]; then\n exit 1\n fi\n env:\n RESULTS: ${{ join(needs.*.result, ',') }}\n",
1342
- "knowledge.wiki": "name: Wiki\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n fern-version:\n description: >\n Fern CLI version to install. Pin this to avoid breaking changes when\n Fern updates their config schema.\n type: string\n required: false\n default: \"5.114.1\"\n preview:\n description: >\n When true, publishes a preview instead of production.\n Requires preview-id to be set.\n type: boolean\n required: false\n default: false\n preview-id:\n description: >\n Stable ID for the preview build. Use the PR number (e.g. \"pr-123\")\n so the same preview URL is reused on every push to the branch.\n type: string\n required: false\n default: \"\"\n fern-org:\n description: >\n DEPRECATED — the preview URL is now read from Fern's own output.\n Kept as a fallback for building the URL when that parse fails, and so\n existing callers do not error.\n type: string\n required: false\n default: \"\"\n base-path:\n description: >\n DEPRECATED — see fern-org. Basepath appended to the fallback preview\n URL (e.g. \"holocron\" for multi-source routing).\n type: string\n required: false\n default: \"\"\n secrets:\n HOLOCRON_FERN_TOKEN:\n required: false\n FERN_TOKEN:\n required: false\n\njobs:\n publish:\n name: Publish to Fern\n runs-on: ubuntu-latest\n timeout-minutes: 10\n permissions:\n contents: read\n deployments: write\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - name: Install Fern CLI\n run: npm install -g \"fern-api@$FERN_VERSION\"\n env:\n FERN_VERSION: ${{ inputs.fern-version }}\n\n - name: Publish docs\n if: ${{ !inputs.preview }}\n run: fern generate --docs\n env:\n FERN_TOKEN: ${{ secrets.HOLOCRON_FERN_TOKEN || secrets.FERN_TOKEN }}\n\n - name: Preview docs\n id: preview\n if: ${{ inputs.preview && inputs.preview-id != '' }}\n env:\n FERN_TOKEN: ${{ secrets.HOLOCRON_FERN_TOKEN || secrets.FERN_TOKEN }}\n PREVIEW_ID: ${{ inputs.preview-id }}\n run: |\n out=$(fern generate --docs --preview --id \"$PREVIEW_ID\" 2>&1) || { printf '%s\\n' \"$out\"; exit 1; }\n printf '%s\\n' \"$out\"\n # Fern prints \"Published docs to https://<org>-preview-<id>.docs.buildwithfern.com/<path>\"\n url=$(printf '%s\\n' \"$out\" | grep -oiE 'https://[a-z0-9.-]+\\.docs\\.buildwithfern\\.com[^ )]*' | head -n1)\n echo \"url=$url\" >> \"$GITHUB_OUTPUT\"\n\n - name: Report preview URL\n if: ${{ inputs.preview && inputs.preview-id != '' }}\n env:\n GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n CAPTURED_URL: ${{ steps.preview.outputs.url }}\n FERN_ORG: ${{ inputs.fern-org }}\n PREVIEW_ID: ${{ inputs.preview-id }}\n BASE_PATH: ${{ inputs.base-path }}\n run: |\n PREVIEW_URL=\"$CAPTURED_URL\"\n if [ -z \"$PREVIEW_URL\" ] && [ -n \"$FERN_ORG\" ]; then\n PREVIEW_URL=\"https://${FERN_ORG}-preview-${PREVIEW_ID}.docs.buildwithfern.com${BASE_PATH:+/${BASE_PATH}}\"\n fi\n if [ -z \"$PREVIEW_URL\" ]; then\n echo \"::warning::no preview URL parsed from Fern output and no fern-org fallback — skipping the deployment widget\"\n exit 0\n fi\n PAYLOAD=$(printf '{\"ref\":\"%s\",\"environment\":\"wiki (Preview)\",\"description\":\"Wiki\",\"production_environment\":false,\"auto_merge\":false,\"required_contexts\":[]}' \\\n \"$GITHUB_HEAD_REF\")\n DEPLOY_ID=$(printf '%s\\n' \"$PAYLOAD\" | gh api \"repos/${GITHUB_REPOSITORY}/deployments\" \\\n --method POST --input - | jq -r '.id')\n gh api \"repos/${GITHUB_REPOSITORY}/deployments/${DEPLOY_ID}/statuses\" \\\n --method POST --field state=success --field environment_url=\"$PREVIEW_URL\"\n",
1343
- bookkeeping: "name: Bookkeeping\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n configuration-path:\n description: Path to the labeler configuration file in the calling repo\n type: string\n required: false\n default: .github/labeler.yml\n\njobs:\n label:\n name: Apply Labels\n permissions:\n contents: read\n issues: write\n pull-requests: write\n runs-on: ubuntu-latest\n timeout-minutes: 5\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n with:\n sparse-checkout: ${{ inputs.configuration-path || '.github/labeler.yml' }}\n sparse-checkout-cone-mode: false\n\n - uses: github/issue-labeler@c1b0f9f52a63158c4adc09425e858e87b32e9685 # v3.4\n if: ${{ github.event_name == 'pull_request' && hashFiles(inputs.configuration-path || '.github/labeler.yml') != '' }}\n # v3.4 bundles Node 20; allow it to run under Actions' current default.\n env:\n ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true\n with:\n # Fall back to default path when triggered directly (not via workflow_call)\n # because inputs.* defaults only apply on workflow_call events.\n configuration-path: ${{ inputs.configuration-path || '.github/labeler.yml' }}\n include-title: 1\n include-body: 0\n sync-labels: 1\n enable-versioned-regex: 0\n repo-token: ${{ github.token }}\n",
1344
- dependencies: "name: Dependencies\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n secrets:\n merge-token:\n description: >\n Optional privileged token for auto-merge. Falls back to GITHUB_TOKEN.\n Required when branch protection enforces required reviews — GITHUB_TOKEN\n cannot approve its own PRs.\n required: false\n\njobs:\n dependabot:\n name: Update the dependencies\n permissions:\n contents: write\n pull-requests: write\n runs-on: ubuntu-latest\n timeout-minutes: 5\n if: github.event.pull_request.user.login == 'dependabot[bot]'\n steps:\n - uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3.1.0\n name: Fetch Dependabot metadata\n id: metadata\n\n - run: gh pr merge --auto --squash \"$PR_URL\"\n # --squash is intentional: repo protection sets allow_merge_commit: false,\n # so --merge would fail on any repo using the standard preset.\n name: Enable auto-merge for Dependabot PRs\n if: steps.metadata.outputs.update-type == 'version-update:semver-patch'\n env:\n PR_URL: ${{ github.event.pull_request.html_url }}\n GH_TOKEN: ${{ secrets.merge-token || github.token }}\n",
1345
- preview: "name: Preview\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n type:\n description: \"Type of deployment: docs or storybook\"\n required: true\n type: string\n name:\n description: Repo name prefix used to filter the docs site package (<name>-site); omit to run pnpm -C docs build\n required: false\n type: string\n default: \"\"\n storybook-projects:\n description: >\n JSON array of { \"name\"?, \"workingDir\", \"outputDir\"? } objects for storybook deploys.\n Each is built via `pnpm -C <workingDir> build:storybook`. If \"name\" is provided the\n output is placed under `sandbox/<name>/`; omit \"name\" for single-repo deploys and the\n output lands directly in `sandbox/`.\n type: string\n required: false\n default: \"[]\"\n build-script:\n description: pnpm script that builds the Storybook static output (single storybook, type:storybook only)\n type: string\n required: false\n default: build:storybook\n output-dir:\n description: Directory where Storybook writes its static output (single storybook, type:storybook only)\n type: string\n required: false\n default: storybook-static\n cloudflare-project:\n description: >\n Cloudflare Pages project name. Falls back to the CLOUDFLARE_PAGES_PROJECT\n org variable when omitted — set that variable once and all repos get previews\n without per-repo config.\n required: false\n type: string\n default: \"\"\n\njobs:\n deploy-preview:\n name: Preview\n if: ${{ github.event.action != 'closed' }}\n runs-on: ubuntu-latest\n permissions:\n contents: read\n deployments: write\n pull-requests: write\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - name: Build workspace dependencies\n # The docs site imports workspace packages (e.g. a shared theme) via\n # `workspace:*` — its exports map points at dist/, which doesn't\n # exist until something builds it. Nothing else in this job does,\n # so without this step Vite fails to resolve the import entirely.\n if: ${{ inputs.type == 'docs' }}\n run: pnpm run build\n\n - name: Build docs site\n if: ${{ inputs.type == 'docs' && inputs.name != '' }}\n env:\n SITE_NAME: ${{ inputs.name }}\n run: pnpm --filter @theholocron/\"$SITE_NAME\"-site build\n\n - name: Build docs site\n if: ${{ inputs.type == 'docs' && inputs.name == '' }}\n run: pnpm -C docs build\n\n - name: Build Storybook projects\n if: ${{ inputs.storybook-projects != '[]' }}\n env:\n PROJECTS: ${{ inputs.storybook-projects }}\n run: |\n echo \"$PROJECTS\" | jq -c '.[]' | while IFS= read -r project; do\n workingDir=$(echo \"$project\" | jq -r '.workingDir')\n pnpm -C \"$workingDir\" build:storybook\n done\n\n - name: Build Storybook\n if: ${{ inputs.type == 'storybook' && inputs.storybook-projects == '[]' }}\n env:\n BUILD_SCRIPT: ${{ inputs.build-script }}\n run: pnpm run \"$BUILD_SCRIPT\"\n\n - name: Assemble site\n env:\n DEPLOY_TYPE: ${{ inputs.type }}\n SITE_NAME: ${{ inputs.name }}\n PROJECTS: ${{ inputs.storybook-projects }}\n STORYBOOK_OUTPUT_DIR: ${{ inputs.output-dir }}\n run: |\n mkdir -p _site\n if [ \"$DEPLOY_TYPE\" = \"docs\" ]; then\n if [ -n \"$SITE_NAME\" ]; then\n # Site built with base: /<name>/ — nest under _site/<name>/ so asset\n # paths match (/name/_astro/... → served at /name/_astro/...).\n mkdir -p \"_site/${SITE_NAME}\"\n cp -r docs/dist/. \"_site/${SITE_NAME}/\"\n # Redirect root to the base path so the preview URL lands correctly.\n printf \"/ /%s/ 301\\n\" \"$SITE_NAME\" > _site/_redirects\n else\n cp -r docs/dist/. _site/\n fi\n fi\n if [ \"$PROJECTS\" != \"[]\" ]; then\n echo \"$PROJECTS\" | jq -c '.[]' | while IFS= read -r project; do\n name=$(echo \"$project\" | jq -r '.name // \"\"')\n workingDir=$(echo \"$project\" | jq -r '.workingDir')\n outputDir=$(echo \"$project\" | jq -r '.outputDir // \"storybook-static\"')\n if [ -n \"$name\" ]; then\n target=\"_site/sandbox/${name}\"\n else\n target=\"_site/sandbox\"\n fi\n mkdir -p \"$target\"\n cp -r \"${workingDir}/${outputDir}/.\" \"$target/\"\n done\n elif [ \"$DEPLOY_TYPE\" = \"storybook\" ]; then\n mkdir -p _site/sandbox\n cp -r \"${STORYBOOK_OUTPUT_DIR}/.\" _site/sandbox/\n fi\n\n - name: Install Wrangler\n # The action's install step runs npm/pnpm in the repo root; pnpm fails\n # because it rejects adding to a workspace root without -w, and npm fails\n # because package.json contains pnpm catalog: references it cannot parse.\n # Pre-installing from /tmp (no package.json) lets the action find wrangler\n # already on PATH and skip its own install entirely.\n working-directory: /tmp\n run: npm install -g wrangler@4\n\n - uses: cloudflare/wrangler-action@ebbaa1584979971c8614a24965b4405ff95890e0 # v4.0.0\n id: deploy\n name: Deploy to Cloudflare Pages\n # Skip when neither the per-repo input nor the org variable is set.\n if: ${{ inputs.cloudflare-project != '' || vars.CLOUDFLARE_PAGES_PROJECT != '' }}\n with:\n apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}\n accountId: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}\n packageManager: npm\n command: >-\n pages deploy _site\n --project-name ${{ inputs.cloudflare-project || vars.CLOUDFLARE_PAGES_PROJECT }}\n --branch ${{ github.event.repository.name }}-pr-${{ github.event.pull_request.number }}\n --commit-dirty=true\n\n - name: Report preview URL\n if: ${{ steps.deploy.outputs.pages-deployment-alias-url != '' }}\n env:\n GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n ALIAS: ${{ steps.deploy.outputs.pages-deployment-alias-url }}\n SITE_NAME: ${{ inputs.name }}\n PROJECT: ${{ inputs.cloudflare-project || vars.CLOUDFLARE_PAGES_PROJECT }}\n run: |\n PREVIEW_URL=\"${ALIAS%/}${SITE_NAME:+/${SITE_NAME}}/\"\n # GITHUB_HEAD_REF is the PR branch name — links the deployment to this PR's sidebar widget.\n PAYLOAD=$(printf '{\"ref\":\"%s\",\"environment\":\"%s\",\"description\":\"Cloudflare Pages\",\"production_environment\":false,\"auto_merge\":false,\"required_contexts\":[]}' \\\n \"$GITHUB_HEAD_REF\" \"${PROJECT} (Preview)\")\n DEPLOY_ID=$(echo \"$PAYLOAD\" | gh api \"repos/${GITHUB_REPOSITORY}/deployments\" \\\n --method POST --input - | jq -r '.id')\n gh api \"repos/${GITHUB_REPOSITORY}/deployments/${DEPLOY_ID}/statuses\" \\\n --method POST --field state=success --field environment_url=\"${PREVIEW_URL}\"\n\n cleanup:\n name: Clean up Preview\n if: ${{ github.event.action == 'closed' }}\n runs-on: ubuntu-latest\n permissions:\n contents: read\n deployments: write\n pull-requests: write\n steps:\n - name: Delete Cloudflare Pages deployments for branch\n if: ${{ inputs.cloudflare-project != '' || vars.CLOUDFLARE_PAGES_PROJECT != '' }}\n env:\n CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}\n CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}\n PROJECT: ${{ inputs.cloudflare-project || vars.CLOUDFLARE_PAGES_PROJECT }}\n BRANCH: ${{ github.event.repository.name }}-pr-${{ github.event.pull_request.number }}\n GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n run: |\n DEPLOYMENTS=$(curl -s \\\n \"https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/pages/projects/${PROJECT}/deployments\" \\\n -H \"Authorization: Bearer ${CLOUDFLARE_API_TOKEN}\" \\\n | jq -r --arg b \"$BRANCH\" \\\n '.result[] | select(.deployment_trigger.metadata.branch == $b) | .id')\n\n if [ -z \"$DEPLOYMENTS\" ]; then\n echo \"No deployments found for branch ${BRANCH} — nothing to clean up.\"\n exit 0\n fi\n\n for id in $DEPLOYMENTS; do\n echo \"Deleting CF Pages deployment: $id\"\n curl -s -X DELETE \\\n \"https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/pages/projects/${PROJECT}/deployments/${id}?force=true\" \\\n -H \"Authorization: Bearer ${CLOUDFLARE_API_TOKEN}\" | jq -r 'if .success then \" ✓ deleted\" else \" ✗ \\(.errors[0].message)\" end'\n done\n\n # Mark the GitHub Deployment environment as inactive.\n ENV_NAME=\"${PROJECT} (Preview)\"\n gh api \"repos/${GITHUB_REPOSITORY}/deployments\" \\\n | jq -r \".[] | select(.environment == \\\"${ENV_NAME}\\\") | .id\" \\\n | while read -r deploy_id; do\n gh api \"repos/${GITHUB_REPOSITORY}/deployments/${deploy_id}/statuses\" \\\n --method POST --field state=inactive 2>/dev/null || true\n done\n",
1346
- greetings: "name: Greetings\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n\njobs:\n greeting:\n name: Greet first-time contributors\n permissions:\n issues: write\n pull-requests: write\n runs-on: ubuntu-latest\n timeout-minutes: 5\n # Group by the issue/PR number so duplicate events don't race each other.\n steps:\n - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0\n name: Greet on first contribution\n with:\n script: |\n // Only greet on the initial open — ignore synchronize, reopened, etc.\n if (context.payload.action !== 'opened') return;\n\n const actor = context.actor;\n const { owner, repo } = context.repo;\n\n // Payload inspection is more reliable than context.eventName for detecting\n // whether this is an issue vs. PR event — works regardless of how GitHub\n // propagates event names through workflow_call chains.\n const isIssue = !!context.payload.issue && !context.payload.pull_request;\n // listForRepo returns both issues and PRs (GitHub treats PRs as issues),\n // sorted newest-first. Filter by type to track first-issue and first-PR\n // independently, and avoid search-index eventual-consistency lag.\n const { data: recent } = await github.rest.issues.listForRepo({\n owner, repo,\n creator: actor,\n state: 'all',\n per_page: 100\n });\n\n const sameType = recent.filter(item =>\n isIssue ? !item.pull_request : !!item.pull_request\n );\n\n if (sameType.length !== 1) return;\n const body = isIssue\n ? `Hey @${actor}!\\n\\nWe really appreciate you taking the time to report an issue. The collaborators on this project attempt to help as many people as possible, but we are a limited number of volunteers, so it is possible that this will not be addressed as swiftly.\\n\\nYour patience is much appreciated and we will get back to you as quickly as possible.`\n : `Hey @${actor}!\\n\\nWe really appreciate you taking the time to help out with this PR. The collaborators on this project attempt to help as many people as possible, but we are a limited number of volunteers, so it is possible that this will not be addressed as swiftly.\\n\\nYour patience is much appreciated and we will get back to you as quickly as possible.`;\n\n await github.rest.issues.createComment({\n owner,\n repo,\n issue_number: context.issue.number,\n body\n });\n",
1347
- review: "name: Review\n\n# ReviewDog is the annotation layer — posts inline PR diff annotations.\n# Runs on pull_request only: inline annotations require PR context,\n# and branch protection ensures all changes go through PRs anyway.\n# security.secretDetection.yml / sourceQuality.structuredDataValidation.yml\n# are the CI gates covering push + PR events. Gitleaks and YAML are\n# intentionally duplicated: those gate merges; ReviewDog surfaces exact\n# line annotations in the PR diff.\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n\njobs:\n reviewdog:\n name: Post inline lint annotations\n runs-on: ubuntu-latest\n timeout-minutes: 20\n permissions:\n contents: read\n pull-requests: write\n\n steps:\n - name: Checkout repository\n uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n with:\n fetch-depth: 0\n\n - name: Setup\n if: ${{ hashFiles('pnpm-lock.yaml') != '' }}\n uses: theholocron/.github/.github/actions/setup@main\n\n - name: Install ReviewDog\n uses: reviewdog/action-setup@d8a7baabd7f3e8544ee4dbde3ee41d0011c3a93f # v1\n with:\n reviewdog_version: latest\n\n # Detect which tools are relevant for this repo, excluding node_modules.\n # hashFiles('**/*') recurses into node_modules/.pnpm and produces false\n # positives for repos that don't own those file types.\n # -print -quit stops find after the first match without a pipe, avoiding\n # the SIGPIPE/pipefail exit-141 that find|head-1 triggers under\n # GitHub Actions' default bash --noprofile --norc -e -o pipefail mode.\n - name: Detect project features\n id: detect\n shell: bash\n run: |\n has() { find . -not -path '*/node_modules/*' -name \"$1\" -print -quit 2>/dev/null | grep -q .; }\n has_ext() { find . -not -path '*/node_modules/*' -name \"$1\" -print -quit 2>/dev/null | grep -q .; }\n { { has 'eslint.config.js' || has 'eslint.config.mjs' || has 'eslint.config.cjs' || \\\n has 'eslint.config.ts' || has '.eslintrc' || has '.eslintrc.js' || \\\n has '.eslintrc.cjs' || has '.eslintrc.json' || has '.eslintrc.yaml' || \\\n has '.eslintrc.yml'; } && grep -qF '\"eslint\":' package.json 2>/dev/null; } && echo \"eslint=true\" >> \"$GITHUB_OUTPUT\" || echo \"eslint=false\" >> \"$GITHUB_OUTPUT\"\n { has 'tsconfig.json' && grep -qF '\"typescript\":' package.json 2>/dev/null; } && echo \"tsconfig=true\" >> \"$GITHUB_OUTPUT\" || echo \"tsconfig=false\" >> \"$GITHUB_OUTPUT\"\n has_ext '*.sh' && echo \"shell=true\" >> \"$GITHUB_OUTPUT\" || echo \"shell=false\" >> \"$GITHUB_OUTPUT\"\n has 'Dockerfile' || has_ext '*.Dockerfile' || has 'Containerfile' && \\\n echo \"docker=true\" >> \"$GITHUB_OUTPUT\" || echo \"docker=false\" >> \"$GITHUB_OUTPUT\"\n has_ext '.env*' && echo \"dotenv=true\" >> \"$GITHUB_OUTPUT\" || echo \"dotenv=false\" >> \"$GITHUB_OUTPUT\"\n has_ext '*.md' && echo \"markdown=true\" >> \"$GITHUB_OUTPUT\" || echo \"markdown=false\" >> \"$GITHUB_OUTPUT\"\n\n #\n # Always applicable\n #\n\n - name: Gitleaks (secrets)\n uses: reviewdog/action-gitleaks@2b7b5685e3e3eecddab5d30cfa04f18123031421 # v1\n with:\n reporter: github-pr-annotations\n tool_name: \"Review / gitleaks\"\n fail_level: error\n gitleaks_flags: --log-opts=${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}\n\n - name: YamlLint\n if: ${{ hashFiles('.yamllint.yml') != '' }}\n uses: reviewdog/action-yamllint@b5f7217d8c815ae374d1d55840d5e569d82f01f0 # v1\n with:\n reporter: github-pr-annotations\n tool_name: \"Review / yamllint\"\n fail_level: error\n yamllint_flags: -c ${{ github.workspace }}/.yamllint.yml ${{ github.workspace }}\n\n - name: ActionLint (GitHub Actions)\n if: ${{ hashFiles('.github/workflows/*.yml', '.github/workflows/*.yaml') != '' }}\n uses: reviewdog/action-actionlint@6fb7acc99f4a1008869fa8a0f09cfca740837d9d # v1\n with:\n reporter: github-pr-annotations\n tool_name: \"Review / actionlint\"\n fail_level: error\n\n #\n # TypeScript / JavaScript\n #\n\n - name: ESLint\n if: steps.detect.outputs.eslint == 'true'\n uses: reviewdog/action-eslint@556a3fdaf8b4201d4d74d406013386aa4f7dab96 # v1.34.0\n with:\n reporter: github-pr-annotations\n tool_name: \"Review / eslint\"\n fail_level: error\n eslint_flags: .\n\n - name: TypeScript\n if: steps.detect.outputs.tsconfig == 'true'\n uses: EPMatt/reviewdog-action-tsc@63d923a3c5b4497671940b8874f58a404e2351b5 # v1\n with:\n reporter: github-pr-annotations\n tool_name: \"Review / tsc\"\n fail_level: error\n\n #\n # Shell\n #\n\n - name: ShellCheck\n if: steps.detect.outputs.shell == 'true'\n uses: reviewdog/action-shellcheck@4c07458293ac342d477251099501a718ae5ef86e # v1\n with:\n reporter: github-pr-annotations\n tool_name: \"Review / shellcheck\"\n fail_level: none\n\n #\n # Docker\n #\n\n - name: Hadolint\n if: steps.detect.outputs.docker == 'true'\n uses: reviewdog/action-hadolint@1b2cfa6ba72072ad35158d7ff3aa49bbdc03506d # v1\n with:\n reporter: github-pr-annotations\n tool_name: \"Review / hadolint\"\n fail_level: none\n\n #\n # Environment files\n #\n\n - name: dotenv-linter\n if: steps.detect.outputs.dotenv == 'true'\n uses: dotenv-linter/action-dotenv-linter@afde61cfda2ecffe7bea35837b6f20b956c88689 # v3.0.0\n with:\n reporter: github-code-suggestions\n\n #\n # Documentation\n #\n\n - name: Alex (inclusive language)\n if: steps.detect.outputs.markdown == 'true'\n uses: reviewdog/action-alex@347481655add010a2ae302df34b57c9bcfa0d6e4 # v1\n with:\n reporter: github-pr-annotations\n tool_name: \"Review / alex\"\n",
1348
- stale: "name: Stale\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n days-before-stale:\n description: Days of inactivity before an issue or PR is marked stale (applies to both unless type-specific value is set)\n type: number\n required: false\n default: 30\n days-before-close:\n description: Days after stale label before closing (applies to both unless type-specific value is set)\n type: number\n required: false\n default: 5\n days-before-issue-stale:\n description: Days of inactivity before an issue is marked stale; overrides days-before-stale when set to a non-negative value\n type: number\n required: false\n default: -1\n days-before-issue-close:\n description: Days after stale label before closing an issue; overrides days-before-close when set to a non-negative value\n type: number\n required: false\n default: -1\n days-before-pr-stale:\n description: Days of inactivity before a PR is marked stale; overrides days-before-stale when set to a non-negative value\n type: number\n required: false\n default: -1\n days-before-pr-close:\n description: Days after stale label before closing a PR; overrides days-before-close when set to a non-negative value\n type: number\n required: false\n default: -1\n exempt-issue-labels:\n description: Comma-separated labels that exempt an issue from being marked stale\n type: string\n required: false\n default: \"in-progress,wip\"\n exempt-pr-labels:\n description: Comma-separated labels that exempt a PR from being marked stale\n type: string\n required: false\n default: \"\"\n exempt-all-issue-milestones:\n description: Issues assigned to any milestone are never marked stale\n type: boolean\n required: false\n default: true\n exempt-all-pr-milestones:\n description: PRs assigned to any milestone are never marked stale\n type: boolean\n required: false\n default: true\n exempt-all-issue-projects:\n description: Issues assigned to any project are never marked stale\n type: boolean\n required: false\n default: true\n exempt-all-pr-projects:\n description: PRs assigned to any project are never marked stale\n type: boolean\n required: false\n default: true\n\njobs:\n stale:\n name: Mark stale issues and pull requests\n permissions:\n contents: write\n issues: write\n pull-requests: write\n runs-on: ubuntu-latest\n timeout-minutes: 10\n steps:\n - uses: actions/stale@1e223db275d687790206a7acac4d1a11bd6fe629 # v10.4.0\n name: Run Stale\n with:\n close-issue-message: >\n This issue was closed because it has been stalled for\n ${{ inputs.days-before-close }} days with no activity.\n days-before-close: ${{ inputs.days-before-close }}\n days-before-issue-close: ${{ inputs.days-before-issue-close }}\n days-before-issue-stale: ${{ inputs.days-before-issue-stale }}\n days-before-pr-close: ${{ inputs.days-before-pr-close }}\n days-before-pr-stale: ${{ inputs.days-before-pr-stale }}\n days-before-stale: ${{ inputs.days-before-stale }}\n exempt-all-issue-milestones: ${{ inputs.exempt-all-issue-milestones }}\n exempt-all-issue-projects: ${{ inputs.exempt-all-issue-projects }}\n exempt-all-pr-milestones: ${{ inputs.exempt-all-pr-milestones }}\n exempt-all-pr-projects: ${{ inputs.exempt-all-pr-projects }}\n exempt-issue-labels: ${{ inputs.exempt-issue-labels }}\n exempt-pr-labels: ${{ inputs.exempt-pr-labels }}\n stale-issue-label: wontfix\n stale-issue-message: >\n This issue is stale because it has been open ${{ inputs.days-before-stale }}\n days with no activity. Remove the stale label or comment, or this will be\n closed in ${{ inputs.days-before-close }} days.\n stale-pr-label: wontfix\n stale-pr-message: >\n This PR is stale because it has been open ${{ inputs.days-before-stale }}\n days with no activity. Remove the stale label or comment, or this will be\n closed in ${{ inputs.days-before-close }} days.\n",
1349
- "sync-dispatch": "name: Sync Dispatch\n\non: # yamllint disable-line rule:truthy\n workflow_dispatch:\n inputs:\n steps:\n description: >\n Sync steps to pass to each repo's sync.yml. Default is \"readme\"\n (only README marker blocks are updated).\n type: string\n required: false\n default: readme\n\npermissions:\n contents: read\n\njobs:\n broadcast:\n name: Broadcast sync to all repos\n runs-on: ubuntu-latest\n timeout-minutes: 15\n steps:\n - name: Dispatch sync to all repos with sync.yml\n run: |\n gh api /orgs/theholocron/repos --paginate --jq '.[].name' \\\n | while IFS= read -r repo; do\n gh api \"/repos/theholocron/$repo/contents/.github/workflows/sync.yml\" --silent 2>/dev/null || continue\n echo \"Dispatching sync to theholocron/$repo\"\n gh workflow run sync.yml \\\n --repo \"theholocron/$repo\" \\\n --field \"steps=$STEPS\" \\\n || echo \"Warning: could not dispatch to theholocron/$repo — skipping\"\n done\n env:\n GH_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN }}\n STEPS: ${{ inputs.steps }}\n",
1350
- tag: "name: Tag\n\n# Release Please — fully automated tag and GitHub Release from Conventional Commits.\n# No package.json or npm publishing required. Operates in \"simple\" mode by default:\n# analyzes commits since the last tag, maintains a rolling Release PR, and creates\n# a tag + GitHub Release when that PR is merged.\n#\n# The calling repo must have two files at the root:\n# release-please-config.json — declares packages and release-type\n# .release-please-manifest.json — tracks the current version\n\non: # yamllint disable-line rule:truthy\n # Self-trigger: when this workflow lives in theholocron/.github itself,\n # push to main runs Release Please for that repo's own releases.\n push:\n branches:\n - main\n workflow_call:\n inputs:\n release-type:\n description: Release Please release type (simple, node, python, etc.)\n type: string\n required: false\n default: simple\n config-file:\n description: Path to release-please-config.json\n type: string\n required: false\n default: release-please-config.json\n manifest-file:\n description: Path to .release-please-manifest.json\n type: string\n required: false\n default: .release-please-manifest.json\n\njobs:\n tag:\n name: Tag release\n permissions:\n contents: write\n pull-requests: write\n runs-on: ubuntu-latest\n timeout-minutes: 10\n steps:\n - uses: google-github-actions/release-please-action@e4dc86ba9405554aeba3c6bb2d169500e7d3b4ee # v4.1.1\n name: Run Release Please\n with:\n release-type: ${{ inputs.release-type }}\n config-file: ${{ inputs.config-file }}\n manifest-file: ${{ inputs.manifest-file }}\n",
1351
- "sync-github": "name: Sync workflow templates\n\n# Builds the holocron CLI from source and pushes updated workflow templates\n# and composite actions to downstream .github repos. Runs whenever the\n# template source files change on main or alpha.\n#\n# Secrets required:\n# HOLOCRON_SYNC_TOKEN — fine-grained PAT (resource owner: org) with:\n# Actions: Read and write (dispatch workflow runs via gh workflow run)\n# Contents: Read and write (git trees, blobs, refs)\n# Pull requests: Read and write (open sync PR)\n# Workflows: Read and write (write .github/workflows/*.yml)\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n primary-repo:\n description: >\n Primary .github repo — receives composite actions, reusable workflows,\n and thin-caller templates. Requires a PR (branch protection assumed).\n type: string\n required: false\n default: theholocron/.github\n secondary-repos:\n description: >\n Space-separated list of secondary repos (reusable workflows + thin\n callers only, no composite actions). Changes are delivered via pull\n request, same as the primary repo.\n type: string\n required: false\n default: \"\"\n sync-branch:\n description: Branch name used for the primary and secondary repo PRs\n type: string\n required: false\n default: chore/sync-templates\n secrets:\n HOLOCRON_SYNC_TOKEN:\n required: true\n HOLOCRON_READ_TOKEN:\n description: >\n Fine-grained PAT for read-only GitHub API calls (e.g. resolving git\n committer identity via `gh api user`). Falls back to HOLOCRON_SYNC_TOKEN.\n required: false\n GH_TOKEN:\n description: >\n Generic GitHub token fallback for `gh` CLI calls. Used when neither\n HOLOCRON_READ_TOKEN nor HOLOCRON_SYNC_TOKEN is set.\n required: false\n HOLOCRON_AXIOM_TOKEN:\n description: >\n Axiom API token. When set alongside the HOLOCRON_AXIOM_DATASET\n repo/org variable, the CLI ships this run's structured logs to Axiom.\n Falls back to the vendor-native AXIOM_TOKEN secret.\n required: false\n AXIOM_TOKEN:\n description: Vendor-native fallback for HOLOCRON_AXIOM_TOKEN.\n required: false\n TURBO_TOKEN:\n required: false\n\njobs:\n sync:\n name: Sync templates\n runs-on: ubuntu-latest\n timeout-minutes: 15\n permissions:\n contents: read\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n HOLOCRON_AXIOM_TOKEN: ${{ secrets.HOLOCRON_AXIOM_TOKEN || secrets.AXIOM_TOKEN }}\n HOLOCRON_AXIOM_DATASET: ${{ vars.HOLOCRON_AXIOM_DATASET || vars.AXIOM_DATASET }}\n POSTHOG_PROJECT_TOKEN: ${{ vars.POSTHOG_PROJECT_TOKEN }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - run: pnpm build\n name: Build CLI\n\n - name: Cache actionlint\n id: cache-actionlint\n uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0\n with:\n path: /tmp/actionlint\n key: actionlint-v1.7.7-linux-amd64\n\n - name: Download actionlint\n if: steps.cache-actionlint.outputs.cache-hit != 'true'\n run: |\n curl -fsSL https://github.com/rhysd/actionlint/releases/download/v1.7.7/actionlint_1.7.7_linux_amd64.tar.gz \\\n | tar -xz -C /tmp actionlint\n\n - name: Validate generated workflows\n run: |\n node packages/cli/dist/cli.mjs sync-github \\\n --repo \"$PRIMARY_REPO\" \\\n --output-dir /tmp/sync-validate\n /tmp/actionlint /tmp/sync-validate/.github/workflows/*.yml\n env:\n PRIMARY_REPO: ${{ inputs.primary-repo }}\n\n - name: Sync primary repo (PR)\n run: |\n GIT_NAME=$(gh api user --jq .name 2>/dev/null || echo \"github-actions[bot]\")\n GIT_EMAIL=$(gh api user --jq '\"\\(.id)+\\(.login)@users.noreply.github.com\"' 2>/dev/null || echo \"41898282+github-actions[bot]@users.noreply.github.com\")\n COMMIT_MSG=\"chore: sync from theholocron/holocron\"$'\\n\\n'\"Signed-off-by: $GIT_NAME <$GIT_EMAIL>\"\n node packages/cli/dist/cli.mjs sync-github \\\n --repo \"$PRIMARY_REPO\" \\\n --branch \"$SYNC_BRANCH\" \\\n --pr \\\n --message \"$COMMIT_MSG\"\n GH_TOKEN=\"$HOLOCRON_SYNC_TOKEN\" gh pr merge --auto --squash \\\n --repo \"$PRIMARY_REPO\" \"$SYNC_BRANCH\" 2>/dev/null || true\n env:\n GH_TOKEN: ${{ secrets.HOLOCRON_READ_TOKEN || secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN }}\n HOLOCRON_SYNC_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN }}\n PRIMARY_REPO: ${{ inputs.primary-repo }}\n SYNC_BRANCH: ${{ inputs.sync-branch }}\n\n - name: Sync secondary repos (PR)\n if: ${{ inputs.secondary-repos != '' }}\n run: |\n GIT_NAME=$(gh api user --jq .name 2>/dev/null || echo \"github-actions[bot]\")\n GIT_EMAIL=$(gh api user --jq '\"\\(.id)+\\(.login)@users.noreply.github.com\"' 2>/dev/null || echo \"41898282+github-actions[bot]@users.noreply.github.com\")\n COMMIT_MSG=\"chore: sync from theholocron/holocron\"$'\\n\\n'\"Signed-off-by: $GIT_NAME <$GIT_EMAIL>\"\n for repo in $SECONDARY_REPOS; do\n node packages/cli/dist/cli.mjs sync-github \\\n --repo \"$repo\" \\\n --branch \"$SYNC_BRANCH\" \\\n --pr \\\n --message \"$COMMIT_MSG\"\n GH_TOKEN=\"$HOLOCRON_SYNC_TOKEN\" gh pr merge --auto --squash \\\n --repo \"$repo\" \"$SYNC_BRANCH\" 2>/dev/null || true\n done\n env:\n GH_TOKEN: ${{ secrets.HOLOCRON_READ_TOKEN || secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN }}\n HOLOCRON_SYNC_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN }}\n SECONDARY_REPOS: ${{ inputs.secondary-repos }}\n SYNC_BRANCH: ${{ inputs.sync-branch }}\n"
1352
- };
1353
- /** Composite actions → `.github/actions/<name>/action.yml` (key includes `/action`). */
1354
- const REUSABLE_ACTIONS = {
1355
- "auto-commit/action": auto_commit_default,
1356
- "holocron/action": holocron_default,
1357
- "install/action": install_default,
1358
- "setup/action": setup_default,
1359
- "setup-node/action": setup_node_default
1360
- };
1361
- /** GitHub starter-workflow metadata → `workflow-templates/<name>.properties.json`. */
1362
- const WORKFLOW_TEMPLATE_PROPERTIES = { bookkeeping: JSON.stringify({
1363
- name: "Bookkeeping",
1364
- description: "Label and track issues and pull requests.",
1365
- iconName: "octicon tag"
1366
- }, null, 2) };
1367
- const SOURCE = "packages/astromech/src/reusable.ts";
2097
+ function deployTargets(packages, manifests) {
2098
+ return packages.flatMap((pkg) => (manifests.get(pkg.dir)?.tasks ?? []).flatMap((item) => {
2099
+ const entry = normalizeTaskEntry(item);
2100
+ if (entry.name !== "delivery.deploy" || entry.with?.["on"] !== "release") return [];
2101
+ const { channel, paths } = entry.with;
2102
+ return [{
2103
+ pkg,
2104
+ channel: typeof channel === "string" ? channel : MAIN_CHANNEL,
2105
+ paths: Array.isArray(paths) ? paths.map(String) : void 0
2106
+ }];
2107
+ }));
2108
+ }
2109
+ //#endregion
2110
+ //#region src/tasks/deploy-on-release/should-deploy/should-deploy.ts
1368
2111
  /**
1369
- * "AUTO-GENERATED — do not edit" header for the YAML pushed to
1370
- * `theholocron/.github`. Matches `createHeader().workflowHeader({ forPrimary: true })`
1371
- * field-for-field, minus any timestamp — a live `Synced:` line makes the content
1372
- * hash change on every run and defeats `sync-github`'s unchanged-file skip.
2112
+ * @param channel The release's channel: its prerelease identifier (`alpha`), or `main` for a stable release.
2113
+ * @param changedFiles `undefined` when there's no previous release to diff against.
1373
2114
  */
1374
- function reusableHeader() {
1375
- return [
1376
- `# AUTO-GENERATED — do not edit in theholocron/.github directly.`,
1377
- `# Source: theholocron/holocron · ${SOURCE}`,
1378
- `# Tool: holocron sync-github`,
1379
- `# Changes: edit source in theholocron/holocron`,
1380
- ``
1381
- ].join("\n");
2115
+ function shouldDeploy({ channel, configuredChannel, changedFiles, paths }) {
2116
+ if (channel !== configuredChannel) return {
2117
+ deploy: false,
2118
+ reason: `channel "${channel}" isn't "${configuredChannel}"`
2119
+ };
2120
+ if (changedFiles === void 0) return {
2121
+ deploy: true,
2122
+ reason: "no previous release to diff against"
2123
+ };
2124
+ const touched = changedFiles.find((file) => paths.some((path) => file.startsWith(path)));
2125
+ return touched ? {
2126
+ deploy: true,
2127
+ reason: `the release changes ${touched}`
2128
+ } : {
2129
+ deploy: false,
2130
+ reason: `the release changes nothing under ${paths.join(", ")}`
2131
+ };
1382
2132
  }
2133
+ //#endregion
2134
+ //#region src/tasks/deploy-on-release/wait-for-published/wait-for-published.ts
1383
2135
  /**
1384
- * The complete file batch `holocron sync-github` pushes to `theholocron/.github`:
1385
- * path → content. YAML gets the "do not edit" header; the starter-workflow
1386
- * `.properties.json` files are left bare (native JSON, not a commented file).
2136
+ * Waits until npm serves every `name@version` in `packages`, asking with
2137
+ * `npm view --prefer-online` (so npm's local cache can't answer for the
2138
+ * registry) every `intervalMs`, for up to `timeoutMs`.
2139
+ *
2140
+ * @returns The specs npm still doesn't serve; empty once all are available.
1387
2141
  */
1388
- function reusableTemplates() {
1389
- const out = /* @__PURE__ */ new Map();
1390
- const header = reusableHeader();
1391
- for (const [name, body] of Object.entries(REUSABLE_ACTIONS)) out.set(`.github/actions/${name}.yml`, `${header}${body}`);
1392
- for (const [name, body] of Object.entries(REUSABLE_WORKFLOWS)) out.set(`.github/workflows/${name}.yml`, `${header}${body}`);
1393
- for (const [name, body] of Object.entries(WORKFLOW_TEMPLATES)) {
1394
- out.set(`workflow-templates/${name}.yml`, `${header}${body}`);
1395
- const props = WORKFLOW_TEMPLATE_PROPERTIES[name];
1396
- if (props) out.set(`workflow-templates/${name}.properties.json`, props);
2142
+ async function waitForPublished({ packages, run, sleep, now, cwd, timeoutMs = WAIT_TIMEOUT_MS, intervalMs = WAIT_INTERVAL_MS }) {
2143
+ const deadline = now() + timeoutMs;
2144
+ let pending = packages.map(({ name, version }) => `${name}@${version}`);
2145
+ for (;;) {
2146
+ pending = pending.filter((spec) => {
2147
+ const version = spec.slice(spec.lastIndexOf("@") + 1);
2148
+ const result = run("npm", [
2149
+ "view",
2150
+ spec,
2151
+ "version",
2152
+ "--prefer-online"
2153
+ ], { cwd });
2154
+ return !(result.status === 0 && result.stdout.trim() === version);
2155
+ });
2156
+ if (pending.length === 0 || now() + intervalMs > deadline) return pending;
2157
+ await sleep(intervalMs);
1397
2158
  }
1398
- return out;
1399
2159
  }
1400
2160
  //#endregion
1401
- //#region src/turbo.ts
2161
+ //#region src/tasks/deploy-on-release/workspace-packages/workspace-packages.ts
1402
2162
  /**
1403
- * `turboConfig(config)` — the generated `turbo.json` for a repo's task
1404
- * manifest, epic #672 D9 (#681). Every workspace, monorepo or single-package
1405
- * (Turborepo's single-package mode), gets this from the same `tasks` array
1406
- * that already drives thin callers / required checks / package scripts —
1407
- * never hand-authored.
2163
+ * Every package under `<repoRoot>/packages/*`, `<repoRoot>/apps/*` and
2164
+ * `<repoRoot>/plugins/*` with a readable, named `package.json` — `holocron`'s
2165
+ * own layout splits vendor plugin packages into `plugins/*` and deployable
2166
+ * apps into `apps/*`, siblings to `packages/*`.
2167
+ */
2168
+ function workspacePackages(repoRoot, { readJson, listDirs }) {
2169
+ return listWorkspacePackagesSync(repoRoot, {
2170
+ roots: [
2171
+ "packages",
2172
+ "apps",
2173
+ "plugins"
2174
+ ],
2175
+ readFile: (path) => JSON.stringify(readJson(path)),
2176
+ listDirs: (path) => {
2177
+ try {
2178
+ return listDirs(path);
2179
+ } catch {
2180
+ throw Object.assign(/* @__PURE__ */ new Error(`ENOENT: ${path}`), { code: "ENOENT" });
2181
+ }
2182
+ }
2183
+ }).flatMap(({ name, dir, manifest }) => name ? [{
2184
+ name,
2185
+ dir,
2186
+ version: manifest.version,
2187
+ dependencies: manifest.dependencies ?? {},
2188
+ devDependencies: manifest.devDependencies ?? {}
2189
+ }] : []);
2190
+ }
2191
+ //#endregion
2192
+ //#region src/tasks/deploy-on-release/deploy-on-release.ts
2193
+ /**
2194
+ * Deploy workspace packages once a release has published (holocron#930).
1408
2195
  *
1409
- * Only tasks with a {@link TurboTaskConfig} in the registry (real per-
1410
- * workspace build/compile/test steps) get an entry. Whole-repo, single-run
1411
- * tools (prettier, gitleaks, commitlint, yamllint, …) have none — they stay
1412
- * off turbo.json entirely, same as today.
2196
+ * A package opts in with a `delivery.deploy` task in its own `holocron.config`
2197
+ * whose `with.on` is `"release"`:
1413
2198
  *
1414
- * A task entry's own `turbo.passThroughEnv` (schema.ts) appends extra env
1415
- * vars to the registry default's cache-key hashing — the one thing that's
1416
- * genuinely per-repo (e.g. a package tagging Sentry releases during its own
1417
- * build needs `SENTRY_AUTH_TOKEN` in its cache key; most repos don't).
2199
+ * ```ts
2200
+ * // packages/<app>/holocron.config.ts
2201
+ * tasks: [{ name: "delivery.deploy", with: { on: "release", channel: "alpha" } }];
2202
+ * ```
2203
+ *
2204
+ * `channel` defaults to `main` (a stable release); set a prerelease identifier
2205
+ * such as `alpha` to deploy those releases instead.
2206
+ *
2207
+ * The shared `delivery.publish` workflow runs this (`holocron deploy on-release`)
2208
+ * as its own `deploy` job after the release job, so a deploy is a separate,
2209
+ * re-runnable check rather than a step buried in the release log. Each package
2210
+ * with such a task deploys when the release is on its `channel` and changes a file under its paths: the package itself plus its
2211
+ * `workspace:*` dependencies ({@link defaultPaths}), or an explicit
2212
+ * `with.paths`. It first waits for npm to serve the release's new versions of
2213
+ * those dependencies ({@link waitForPublished}) — the registry can lag a version
2214
+ * it has just accepted, and the deploy installs them — then runs
2215
+ * `pnpm --filter <package> delivery.deploy`.
2216
+ *
2217
+ * The release is already published by then, so a failed deploy can't undo it;
2218
+ * it fails this job instead, loudly and re-runnable on its own. A task is
2219
+ * `skip`ped (not failed) only when it doesn't apply to this release.
2220
+ *
2221
+ * A `delivery.deploy` task with `on: "release"` generates no CI workflow.
1418
2222
  */
1419
- /** Filed once per repo, unconditionally — every generated turbo.json's root. */
1420
- const GLOBAL_DEPENDENCIES = ["pnpm-workspace.yaml", "tsconfig.json"];
1421
2223
  /**
1422
- * `turbo.json` content for this manifest, pretty-printed — write it
1423
- * directly, no merge (matches `thinCallers()` / `reusableTemplates()`: this
1424
- * file is generated, not hand-edited). `null` when no task in the manifest
1425
- * has turbo fan-out config (nothing to write).
2224
+ * Deploys every package whose manifest declares a release-time deploy that
2225
+ * applies to this release. Never throws for a deploy problem: each package's
2226
+ * outcome is in the report, and `status: "fail"` is the caller's exit code.
1426
2227
  */
1427
- function turboConfig(config) {
1428
- const entries = (config.tasks ?? []).map(normalizeTaskEntry);
1429
- const tasks = {};
1430
- for (const entry of entries) {
1431
- if (entry.local === false) continue;
1432
- const turbo = TASKS[entry.name]?.turbo;
1433
- if (!turbo) continue;
1434
- const extraEnv = entry.turbo?.passThroughEnv;
1435
- tasks[entry.name] = extraEnv && extraEnv.length > 0 ? {
1436
- ...turbo,
1437
- passThroughEnv: extraEnv
1438
- } : turbo;
2228
+ async function deployOnRelease(options, { run = defaultRun, readJson = defaultReadJson, listDirs = defaultListDirs, sleep = defaultSleep, now = Date.now, loadTasks = loadTasksConfig } = {}) {
2229
+ const { cwd, from, print } = options;
2230
+ const channel = options.channel || "main";
2231
+ const to = options.to ?? "HEAD";
2232
+ const packages = workspacePackages(cwd, {
2233
+ readJson,
2234
+ listDirs
2235
+ });
2236
+ const manifests = /* @__PURE__ */ new Map();
2237
+ for (const pkg of packages) try {
2238
+ manifests.set(pkg.dir, await loadTasks(pkg.dir));
2239
+ } catch (err) {
2240
+ print(`${pkg.name}: couldn't read its task manifest (${err.message}); not deploying it.`);
1439
2241
  }
1440
- if (Object.keys(tasks).length === 0) return null;
1441
- return `${JSON.stringify({
1442
- $schema: "https://turborepo.org/schema.json",
1443
- globalDependencies: GLOBAL_DEPENDENCIES,
1444
- tasks
1445
- }, null, 2)}\n`;
2242
+ const targets = deployTargets(packages, manifests);
2243
+ if (targets.length === 0) {
2244
+ print("No package declares a release-time delivery.deploy (with.on: release).");
2245
+ return {
2246
+ status: "skip",
2247
+ results: []
2248
+ };
2249
+ }
2250
+ let diff;
2251
+ const changedFiles = () => {
2252
+ if (diff) return diff;
2253
+ if (!from) return diff = { files: void 0 };
2254
+ const result = run("git", [
2255
+ "diff",
2256
+ "--name-only",
2257
+ from,
2258
+ to
2259
+ ], { cwd });
2260
+ return diff = result.status === 0 ? { files: result.stdout.split("\n").filter(Boolean) } : {
2261
+ files: [],
2262
+ error: `git diff ${from} ${to} failed: ${result.stderr || result.status}`
2263
+ };
2264
+ };
2265
+ const results = [];
2266
+ const record = (pkg, status, message) => {
2267
+ results.push({
2268
+ pkg,
2269
+ status,
2270
+ message
2271
+ });
2272
+ print(`${status === "fail" ? "✗ " : ""}${pkg}: ${message}`);
2273
+ };
2274
+ for (const target of targets) {
2275
+ const name = target.pkg.name;
2276
+ const { files, error } = target.channel === channel ? changedFiles() : {
2277
+ files: [],
2278
+ error: void 0
2279
+ };
2280
+ if (error) {
2281
+ record(name, "fail", error);
2282
+ continue;
2283
+ }
2284
+ const decision = shouldDeploy({
2285
+ channel,
2286
+ configuredChannel: target.channel,
2287
+ changedFiles: files,
2288
+ paths: target.paths ?? defaultPaths(cwd, target.pkg, packages)
2289
+ });
2290
+ if (!decision.deploy) {
2291
+ record(name, "skip", `not deploying: ${decision.reason}.`);
2292
+ continue;
2293
+ }
2294
+ if (options.dryRun) {
2295
+ record(name, "dry-run", `would deploy because ${decision.reason}.`);
2296
+ continue;
2297
+ }
2298
+ const dependencies = workspaceDependencies(target.pkg, packages).flatMap(({ name: dep, version }) => version ? [{
2299
+ name: dep,
2300
+ version
2301
+ }] : []);
2302
+ print(`${name}: waiting for npm to serve ${dependencies.map((d) => `${d.name}@${d.version}`).join(", ") || "nothing"}.`);
2303
+ const unpublished = await waitForPublished({
2304
+ packages: dependencies,
2305
+ run,
2306
+ sleep,
2307
+ now,
2308
+ cwd
2309
+ });
2310
+ if (unpublished.length > 0) {
2311
+ record(name, "fail", `npm still doesn't serve ${unpublished.join(", ")} after ${WAIT_TIMEOUT_MS / 6e4} minutes. Re-run this job once it does.`);
2312
+ continue;
2313
+ }
2314
+ print(`${name}: deploying because ${decision.reason}.`);
2315
+ const result = run("pnpm", [
2316
+ "--filter",
2317
+ name,
2318
+ DEPLOY_TASK
2319
+ ], {
2320
+ cwd,
2321
+ env: process.env,
2322
+ stdio: "inherit"
2323
+ });
2324
+ if (result.status === 0) record(name, "ok", "deployed.");
2325
+ else record(name, "fail", `the deploy failed (exit ${result.status}).`);
2326
+ }
2327
+ return {
2328
+ status: results.some((r) => r.status === "fail") ? "fail" : results.every((r) => r.status === "skip") ? "skip" : "ok",
2329
+ results
2330
+ };
1446
2331
  }
2332
+ //#endregion
2333
+ //#region src/tasks/match-namespace/match-namespace.ts
2334
+ /** Every configured task whose name starts with `"<namespace>."`, in manifest declaration order. */
2335
+ function matchNamespace(config, namespace) {
2336
+ const prefix = `${namespace}.`;
2337
+ return (config.tasks ?? []).map(normalizeTaskEntry).reduce((names, entry) => {
2338
+ if (entry.name.startsWith(prefix) && !names.includes(entry.name)) names.push(entry.name);
2339
+ return names;
2340
+ }, []);
2341
+ }
2342
+ //#endregion
2343
+ //#region src/tasks/group/group.ts
1447
2344
  /**
1448
- * Fixes #692: `turboConfig()` writes a `turbo.json` the moment *any* task in
1449
- * the manifest has fan-out config — with no check for whether the repo's own
1450
- * root package is actually covered by `pnpm-workspace.yaml`'s `packages:`
1451
- * list. A repo shaped like `observability` (the library lives at repo root;
1452
- * `packages:` lists only an unrelated `docs` site) silently breaks the
1453
- * instant `turbo.json` exists: `holocron run <task>` switches from running
1454
- * root's own script directly to `turbo run <task>`, which only sees declared
1455
- * workspace members — root vanishes, the task "succeeds" with zero real work
1456
- * done (confirmed empirically: `Packages in scope: docs`, root never
1457
- * mentioned, exit 0).
1458
- *
1459
- * Detection is narrow and specific: root only needs to be an explicit
1460
- * workspace member if its *own* `package.json` has a script literally named
1461
- * after one of the tasks turbo.json is about to fan out — a plain
1462
- * orchestrator root (`holocron`'s own `"build": "turbo run delivery.build"`,
1463
- * which has no `delivery.build` script itself) never trips this; only a root
1464
- * that's genuinely a directly-buildable package does. Confirmed the fix
1465
- * empirically too — adding `.` to `packages:` makes turbo pick root back up
1466
- * (`Packages in scope: docs, root-lib`) without disturbing the sibling
1467
- * package's own caching.
2345
+ * `holocron run <namespace>` — run every configured task under one
2346
+ * namespace prefix (`sourceQuality.*`, `verification.*`, …) in sequence.
2347
+ * `Astromech.run()` falls back here whenever the given name isn't an exact
2348
+ * `TASKS` entry but matches the prefix of one or more configured tasks —
2349
+ * same "aggregate several real tasks into one report" shape `runCi` already
2350
+ * established, scoped to one namespace instead of the merge-gating set.
1468
2351
  *
1469
- * A targeted line-based edit, not a full YAML parse/reserialize — same
1470
- * pattern `codecov.ts`'s `mergeCodecovComponents()` already uses for editing
1471
- * an existing generated file. `pnpm-workspace.yaml` routinely carries a
1472
- * `catalog:`/`catalogs:`/`overrides:` block after `packages:`; a real parser
1473
- * round-trip risks reformatting or reordering content nobody asked to touch.
1474
- * Only ever *adds* a line — never rewrites or reorders anything already
1475
- * there — and is a no-op (`changed: false`) whenever root doesn't need it,
1476
- * root is already listed, or `packages:` isn't in the plain block-list form
1477
- * every repo checked actually uses.
2352
+ * Every task in the namespace always runs — one task's failure never skips
2353
+ * the rest, same as `runCi`'s own loop — so a single invocation surfaces
2354
+ * every problem in the namespace at once instead of stopping at the first.
1478
2355
  */
1479
- function ensureRootWorkspaceMember(workspaceYaml, rootScripts, taskNames) {
1480
- if (!taskNames.some((t) => rootScripts.includes(t))) return {
1481
- content: workspaceYaml,
1482
- changed: false
1483
- };
1484
- const lines = workspaceYaml.split("\n");
1485
- const packagesLineIdx = lines.findIndex((l) => /^packages:\s*$/.test(l));
1486
- if (packagesLineIdx === -1) return {
1487
- content: workspaceYaml,
1488
- changed: false
2356
+ function runGroup(input) {
2357
+ const { print, namespace } = input;
2358
+ const names = matchNamespace(input.config, namespace);
2359
+ if (names.length === 0) return {
2360
+ status: "unknown",
2361
+ jobs: []
1489
2362
  };
1490
- const items = [];
1491
- let cursor = packagesLineIdx + 1;
1492
- while (cursor < lines.length && /^\s*-\s*/.test(lines[cursor])) {
1493
- items.push(lines[cursor]);
1494
- cursor++;
2363
+ print(`holocron run ${namespace} — ${names.length} task(s)`);
2364
+ print("");
2365
+ const jobs = [];
2366
+ for (const name of names) {
2367
+ print(`▶ ${name}`);
2368
+ const r = runTask({
2369
+ print: (line) => print(` ${line}`),
2370
+ logger: input.logger,
2371
+ exec: input.exec,
2372
+ readFile: input.readFile,
2373
+ fileExists: input.fileExists,
2374
+ listDir: input.listDir,
2375
+ lookPath: input.lookPath,
2376
+ task: name,
2377
+ cwd: input.cwd,
2378
+ dryRun: input.dryRun ?? false,
2379
+ ...input.filter ? { filter: input.filter } : {}
2380
+ });
2381
+ jobs.push({
2382
+ task: name,
2383
+ status: r.status,
2384
+ command: r.command,
2385
+ message: r.message
2386
+ });
1495
2387
  }
1496
- if (items.some((item) => {
1497
- const value = item.replace(/^\s*-\s*/, "").replace(/^["']|["']$/g, "").trim();
1498
- return value === "." || value === "";
1499
- })) return {
1500
- content: workspaceYaml,
1501
- changed: false
1502
- };
1503
- const newLine = `${items[0]?.match(/^(\s*-\s*)/)?.[1] ?? " - "}"."`;
1504
2388
  return {
1505
- content: [
1506
- ...lines.slice(0, packagesLineIdx + 1),
1507
- newLine,
1508
- ...lines.slice(packagesLineIdx + 1)
1509
- ].join("\n"),
1510
- changed: true
2389
+ status: jobs.some((j) => j.status === "fail") ? "fail" : jobs.every((j) => j.status === "dry-run") ? "dry-run" : "ok",
2390
+ jobs
1511
2391
  };
1512
2392
  }
1513
2393
  //#endregion
@@ -1517,23 +2397,6 @@ function ensureRootWorkspaceMember(workspaceYaml, rootScripts, taskNames) {
1517
2397
  * `@theholocron/cli` instantiates it once and delegates the `run` /
1518
2398
  * `ci` / workflow-generation commands to it (like `@theholocron/observability`).
1519
2399
  */
1520
- const noopLogger = {
1521
- debug() {},
1522
- warn() {}
1523
- };
1524
- const realExec = (cmd, args, opts) => {
1525
- return { exitCode: spawnSync(cmd, args, {
1526
- cwd: opts.cwd,
1527
- stdio: "inherit"
1528
- }).status ?? -1 };
1529
- };
1530
- /** `node_modules/.bin/<bin>`, else the first `PATH` entry that has it, else `null`. */
1531
- const realLookPath = (cwd, bin) => {
1532
- const local = join(cwd, "node_modules", ".bin", bin);
1533
- if (existsSync(local)) return local;
1534
- for (const dir of (process.env["PATH"] ?? "").split(":")) if (dir && existsSync(join(dir, bin))) return join(dir, bin);
1535
- return null;
1536
- };
1537
2400
  function createAstromech(options) {
1538
2401
  const deps = {
1539
2402
  print: options.print ?? ((line) => console.log(line)),
@@ -1545,17 +2408,46 @@ function createAstromech(options) {
1545
2408
  lookPath: options.lookPath ?? realLookPath
1546
2409
  };
1547
2410
  const items = () => (options.config?.tasks ?? []).map((i) => normalizeTaskEntry(i));
2411
+ /**
2412
+ * Task names this repo's manifest can actually run locally — shared by
2413
+ * {@link packageScripts} and by `holocron`'s own interactive task picker.
2414
+ * Deliberately independent of `syncScripts`/`holocronScript`/`hooks`:
2415
+ * those govern whether/how `package.json` scripts get *written*, not
2416
+ * which tasks exist to run. A repo with `syncScripts: false` (this repo
2417
+ * itself, among others) still has real runnable tasks — found live when
2418
+ * `packageScripts()`'s `{}` short-circuit silently emptied the picker too.
2419
+ */
2420
+ const runnableTaskNames = () => {
2421
+ const out = [];
2422
+ for (const entry of items()) {
2423
+ const def = TASKS[entry.name];
2424
+ const hasNoLocalRunner = def?.local === null && !def.linterGroup && !def.jobs;
2425
+ if (entry.local === false || !KNOWN_TASKS.has(entry.name) || hasNoLocalRunner) continue;
2426
+ out.push(entry.name);
2427
+ }
2428
+ return out;
2429
+ };
1548
2430
  return {
1549
- run: (task, opts = {}) => runTask({
1550
- ...deps,
1551
- task,
1552
- cwd: options.cwd,
1553
- ...opts.job !== void 0 ? { job: opts.job } : {},
1554
- passthrough: opts.passthrough ?? [],
1555
- dryRun: opts.dryRun ?? false,
1556
- required: opts.required ?? false,
1557
- ...opts.filter ? { filter: opts.filter } : {}
1558
- }),
2431
+ run: (task, opts = {}) => {
2432
+ if (!TASKS[task] && options.config && matchNamespace(options.config, task).length > 0) return runGroup({
2433
+ ...deps,
2434
+ cwd: options.cwd,
2435
+ config: options.config,
2436
+ namespace: task,
2437
+ dryRun: opts.dryRun ?? false,
2438
+ ...opts.filter ? { filter: opts.filter } : {}
2439
+ });
2440
+ return runTask({
2441
+ ...deps,
2442
+ task,
2443
+ cwd: options.cwd,
2444
+ ...opts.job !== void 0 ? { job: opts.job } : {},
2445
+ passthrough: opts.passthrough ?? [],
2446
+ dryRun: opts.dryRun ?? false,
2447
+ required: opts.required ?? false,
2448
+ ...opts.filter ? { filter: opts.filter } : {}
2449
+ });
2450
+ },
1559
2451
  ci: (opts = {}) => runCi({
1560
2452
  ...deps,
1561
2453
  cwd: options.cwd,
@@ -1569,6 +2461,7 @@ function createAstromech(options) {
1569
2461
  const isDocsSite = entry.name === "knowledge.docs";
1570
2462
  const isComponents = entry.name === "knowledge.components";
1571
2463
  if (entry.ci === false || !KNOWN_WORKFLOWS.has(entry.name) && !isDocsSite && !isComponents) continue;
2464
+ if (entry.name === "delivery.deploy" && entry.with?.["on"] === "release") continue;
1572
2465
  const rawWith = isDocsSite ? {
1573
2466
  preview: true,
1574
2467
  ...entry.with,
@@ -1599,51 +2492,168 @@ function createAstromech(options) {
1599
2492
  const out = {};
1600
2493
  if (!options.config || options.config.syncScripts === false) return out;
1601
2494
  out.holocron = options.config.holocronScript ?? "holocron";
1602
- for (const entry of items()) {
1603
- const def = TASKS[entry.name];
1604
- const hasNoLocalRunner = def?.local === null && !def.linterGroup && !def.jobs;
1605
- if (entry.local === false || !KNOWN_TASKS.has(entry.name) || hasNoLocalRunner) continue;
1606
- out[entry.name] = `holocron run ${entry.name} --`;
1607
- }
2495
+ for (const name of runnableTaskNames()) out[name] = `holocron run ${name} --`;
1608
2496
  const hooks = options.config.hooks;
1609
2497
  if (hooks === true || typeof hooks === "object" && hooks.prePush !== false) out.prepare = "husky";
1610
2498
  return out;
1611
2499
  },
2500
+ runnableTasks: () => runnableTaskNames(),
1612
2501
  reusableTemplates: () => reusableTemplates(),
1613
2502
  requiredChecks: () => requiredChecks(options.config ?? {}),
1614
2503
  codecovConfig: (existing) => codecovConfig(options.cwd, existing),
1615
2504
  turboConfig: () => turboConfig(options.config ?? {}),
1616
2505
  ensureRootWorkspaceMember: (workspaceYaml, rootScripts) => {
1617
2506
  return ensureRootWorkspaceMember(workspaceYaml, rootScripts, (options.config?.tasks ?? []).map(normalizeTaskEntry).filter((e) => e.local !== false).map((e) => e.name));
1618
- }
2507
+ },
2508
+ ensureTurboDependency: (packageJson, workspaceYaml) => ensureTurboDependency(packageJson, workspaceYaml)
1619
2509
  };
1620
2510
  }
1621
2511
  //#endregion
1622
- //#region src/tsconfig.ts
2512
+ //#region src/templates/configs/alexjs/alexignore
2513
+ var alexignore_default = ".github/*\nCHANGELOG.md\nLICENSE\n";
2514
+ //#endregion
2515
+ //#region src/templates/configs/alexjs/create-config.ts
2516
+ const { workflowHeader: workflowHeader$9 } = createHeader({ source: "packages/astromech/src/templates/configs/alexjs/create-config.ts" });
2517
+ function createRcConfig() {
2518
+ return stringifyJson(alexrc_default);
2519
+ }
2520
+ function createIgnoreConfig() {
2521
+ return `${workflowHeader$9()}${alexignore_default}`;
2522
+ }
2523
+ //#endregion
2524
+ //#region src/templates/configs/commit-msg/commit-msg
2525
+ var commit_msg_default = "__HOLOCRON_SCRIPT__ run lint commit-msg \"$1\"\n";
2526
+ //#endregion
2527
+ //#region src/templates/configs/commit-msg/create-config.ts
2528
+ const { workflowHeader: workflowHeader$8 } = createHeader({ source: "packages/astromech/src/templates/configs/commit-msg/create-config.ts" });
1623
2529
  /**
1624
- * A package-level `tsconfig.json` — the uniform shape confirmed across
1625
- * every package checked, parameterized only by the two fields that
1626
- * genuinely vary (`display`, `paths`). No scaffold/workflow header (matches
1627
- * `.alexrc.json`'s existing precedent for a strict-JSON Bucket B file —
1628
- * JSON has no comment syntax to carry one, and TypeScript's own tolerance
1629
- * for JSONC comments in `tsconfig.json` isn't worth relying on for a file
1630
- * this thin).
2530
+ * `.husky/commit-msg` — lints the commit message via `holocron run lint
2531
+ * commit-msg` (holocron#789), the real programmatic replacement for
2532
+ * shelling out to `commitlint --edit`. Installed by `holocron repo setup` when
2533
+ * hooks are enabled. Same `__HOLOCRON_SCRIPT__` substitution as `pre-push`.
2534
+ *
2535
+ * @param holocronScript - the command that invokes the CLI. Defaults to
2536
+ * `pnpm exec holocron`; the source repo passes `node packages/cli/dist/cli.mjs`.
1631
2537
  */
1632
- function createTsconfig(options) {
1633
- const variant = options.variant ?? "node-lts";
1634
- const compilerOptions = {
1635
- baseUrl: "./",
1636
- outDir: "./dist"
1637
- };
1638
- if (options.paths) compilerOptions.paths = { "@/*": ["./src/*"] };
1639
- const config = {
1640
- display: options.display,
1641
- extends: `@theholocron/tsconfig/${variant}`,
1642
- compilerOptions,
1643
- include: ["src/**/*.ts"],
1644
- exclude: ["node_modules", "dist"]
1645
- };
1646
- return JSON.stringify(config, null, 2) + "\n";
2538
+ function createConfig(holocronScript = "pnpm exec holocron") {
2539
+ return `${workflowHeader$8("shebang")}${commit_msg_default.replace("__HOLOCRON_SCRIPT__", holocronScript)}`;
2540
+ }
2541
+ //#endregion
2542
+ //#region src/templates/configs/dependabot/dependabot.yml
2543
+ var dependabot_default = "version: 2\nupdates:\n - package-ecosystem: npm\n directory: /\n schedule:\n interval: weekly\n commit-message:\n prefix: \"chore(deps)\"\n prefix-development: \"chore(deps-dev)\"\n groups:\n security-patches:\n applies-to: security-updates\n patterns:\n - \"*\"\n all-dependencies:\n update-types:\n - minor\n - patch\n\n - package-ecosystem: github-actions\n directory: /\n schedule:\n interval: weekly\n commit-message:\n prefix: \"chore(deps)\"\n groups:\n all-actions:\n patterns:\n - \"*\"\n";
2544
+ //#endregion
2545
+ //#region src/templates/configs/dependabot/create-config.ts
2546
+ const { workflowHeader: workflowHeader$7 } = createHeader({ source: "packages/astromech/src/templates/configs/dependabot/create-config.ts" });
2547
+ function createConfig$1() {
2548
+ return `${workflowHeader$7()}${dependabot_default}`;
2549
+ }
2550
+ //#endregion
2551
+ //#region src/templates/configs/devmoji/create-config.ts
2552
+ const { workflowHeader: workflowHeader$6 } = createHeader({ source: "packages/astromech/src/templates/configs/devmoji/create-config.ts" });
2553
+ function createConfig$2() {
2554
+ return [
2555
+ workflowHeader$6("cjs"),
2556
+ `/* eslint-disable */`,
2557
+ `const { defineConfig } = require("@theholocron/devmoji-config");`,
2558
+ `module.exports = defineConfig();`,
2559
+ ``
2560
+ ].join("\n");
2561
+ }
2562
+ //#endregion
2563
+ //#region src/templates/configs/editorconfig/editorconfig
2564
+ var editorconfig_default = "root = true\n\n[*]\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\nindent_style = tab\nindent_size = 4\n\n[.gitattributes]\nindent_style = space\nindent_size = 2\n\n[*.{json,yml,yaml}]\nindent_style = space\nindent_size = 2\n\n[*.{md,mdx}]\ntrim_trailing_whitespace = false\nindent_style = unset\n\n[.*{rc,ignore}]\nindent_style = space\nindent_size = 2\n";
2565
+ //#endregion
2566
+ //#region src/templates/configs/editorconfig/create-config.ts
2567
+ const { workflowHeader: workflowHeader$5 } = createHeader({ source: "packages/astromech/src/templates/configs/editorconfig/create-config.ts" });
2568
+ function createConfig$3() {
2569
+ return `${workflowHeader$5()}${editorconfig_default}`;
2570
+ }
2571
+ //#endregion
2572
+ //#region src/templates/configs/editorconfig-checker/create-config.ts
2573
+ /**
2574
+ * The generated `.editorconfig-checker.json`.
2575
+ *
2576
+ * Deliberately carries no `"Version"` key: editorconfig-checker treats it as a
2577
+ * hard gate and exits non-zero the moment the running binary's version differs
2578
+ * — which it does the instant a contributor's local install (Homebrew tracks
2579
+ * latest) drifts from the version super-linter bundles in CI, breaking
2580
+ * `holocron run lint` / the pre-push hook for everyone (#618). The config
2581
+ * options here (`Exclude`, `Disable`) are stable across releases, so pinning
2582
+ * buys nothing.
2583
+ */
2584
+ function createConfig$4() {
2585
+ return stringifyJson(editorconfig_checker_default);
2586
+ }
2587
+ //#endregion
2588
+ //#region src/templates/configs/labeler/labeler.yml
2589
+ var labeler_default = "bug:\n - \"^fix\"\n\nchore:\n - '^chore(?!\\(deps)'\n\nci:\n - \"^ci\"\n\ndependencies:\n - '^chore\\(deps'\n\ndocumentation:\n - \"^docs\"\n\nenhancement:\n - \"^feat\"\n\nperformance:\n - \"^perf\"\n\nrefactor:\n - \"^refactor\"\n\ntest:\n - \"^test\"\n";
2590
+ //#endregion
2591
+ //#region src/templates/configs/labeler/create-config.ts
2592
+ const { workflowHeader: workflowHeader$4 } = createHeader({ source: "packages/astromech/src/templates/configs/labeler/create-config.ts" });
2593
+ function createConfig$5() {
2594
+ return `${workflowHeader$4()}${labeler_default}`;
2595
+ }
2596
+ //#endregion
2597
+ //#region src/templates/configs/pre-commit/pre-commit
2598
+ var pre_commit_default = "# Run GitLeaks against staged changes only — a full `gitleaks git` history\n# scan re-flags every pre-existing (already-merged) finding on every commit,\n# permanently blocking the repo once any false positive lands in history.\n# CI's super-linter GITLEAKS validator is likewise scoped to changed files.\ngitleaks protect --staged --no-banner\n\n# Exit with non-zero if secrets are detected by GitLeaks\nif [ $? -ne 0 ]; then\n echo \"Secrets detected by GitLeaks, aborting commit!\"\n exit 1\nfi\n\n# Run lint-staged with a custom config\nnpx lint-staged --config @theholocron/lint-staged-config\n";
2599
+ //#endregion
2600
+ //#region src/templates/configs/pre-commit/create-config.ts
2601
+ const { workflowHeader: workflowHeader$3 } = createHeader({ source: "packages/astromech/src/templates/configs/pre-commit/create-config.ts" });
2602
+ /**
2603
+ * `.husky/pre-commit` — runs GitLeaks (scoped to staged changes) and
2604
+ * lint-staged before every commit. Installed by `holocron repo setup` when hooks
2605
+ * are enabled (default for `protection: "strict"`). `git commit --no-verify`
2606
+ * bypasses it.
2607
+ *
2608
+ * `gitleaks protect --staged` (not `gitleaks git`) is deliberate: the latter
2609
+ * scans the entire commit history on every invocation, so once any false
2610
+ * positive lands in a past commit — a fake test token, an example key in a
2611
+ * doc — every future commit is permanently blocked, regardless of what's
2612
+ * actually staged.
2613
+ */
2614
+ function createConfig$6() {
2615
+ return `${workflowHeader$3("shebang")}${pre_commit_default}`;
2616
+ }
2617
+ //#endregion
2618
+ //#region src/templates/configs/pre-push/pre-push
2619
+ var pre_push_default = "# Run the merge-gating checks locally before pushing — the same set CI runs.\n# Bypass once with: git push --no-verify\n\n# CI runners (semantic-release, super-linter auto-commit, bots) push without a\n# built dev environment — the hook has nothing to add there.\n[ -n \"$CI\" ] && exit 0\n\n__HOLOCRON_SCRIPT__ run ci\n";
2620
+ //#endregion
2621
+ //#region src/templates/configs/pre-push/create-config.ts
2622
+ const { workflowHeader: workflowHeader$2 } = createHeader({ source: "packages/astromech/src/templates/configs/pre-push/create-config.ts" });
2623
+ /**
2624
+ * `.husky/pre-push` — runs `holocron run ci` (the merge-gating checks, in CI order)
2625
+ * before every push. Installed by `holocron repo setup` when hooks are enabled
2626
+ * (default for `protection: "strict"`). `git push --no-verify` bypasses it.
2627
+ *
2628
+ * @param holocronScript - the command that invokes the CLI. Defaults to
2629
+ * `pnpm exec holocron`; the source repo passes `node packages/cli/dist/cli.mjs`.
2630
+ */
2631
+ function createConfig$7(holocronScript = "pnpm exec holocron") {
2632
+ return `${workflowHeader$2("shebang")}${pre_push_default.replace("__HOLOCRON_SCRIPT__", holocronScript)}`;
2633
+ }
2634
+ //#endregion
2635
+ //#region src/templates/configs/prepare-commit-msg/prepare-commit-msg
2636
+ var prepare_commit_msg_default = "NAME=$(git config user.name)\nEMAIL=$(git config user.email)\n\nif [ -z \"$NAME\" ]; then\n echo \"empty git config user.name\"\n exit 1\nfi\n\nif [ -z \"$EMAIL\" ]; then\n echo \"empty git config user.email\"\n exit 1\nfi\n\ngit interpret-trailers --if-exists doNothing --trailer \\\n \"Signed-off-by: $NAME <$EMAIL>\" \\\n --in-place \"$1\"\n\n# --config <resolved shared path> (config-resolution workstream, #676) — falls\n# back to devmoji's own auto-discovery of a local devmoji.config.cjs when the\n# shared package's built dist/ isn't present (not installed, or a stale\n# install predating this file).\nDEVMOJI_CONFIG=\"node_modules/@theholocron/devmoji-config/dist/index.cjs\"\nif [ -f \"$DEVMOJI_CONFIG\" ]; then\n npx devmoji --config \"$DEVMOJI_CONFIG\" -e\nelse\n npx devmoji -e\nfi\n";
2637
+ //#endregion
2638
+ //#region src/templates/configs/prepare-commit-msg/create-config.ts
2639
+ const { workflowHeader: workflowHeader$1 } = createHeader({ source: "packages/astromech/src/templates/configs/prepare-commit-msg/create-config.ts" });
2640
+ function createConfig$8() {
2641
+ return `${workflowHeader$1("shebang")}${prepare_commit_msg_default}`;
2642
+ }
2643
+ //#endregion
2644
+ //#region src/templates/configs/yamllint/.yamlignore
2645
+ var _yamlignore_default = "pnpm-lock.yaml\n";
2646
+ //#endregion
2647
+ //#region src/templates/configs/yamllint/.yamllint.yml
2648
+ var _yamllint_default = "---\nextends: default\n\nignore-from-file:\n - .gitignore\n - .yamlignore\n\nrules:\n comments:\n min-spaces-from-content: 1\n require-starting-space: true\n\n comments-indentation: {}\n\n document-start:\n ignore: |\n .github/*.yml\n\n indentation:\n spaces: consistent\n indent-sequences: consistent\n\n line-length: disable\n";
2649
+ //#endregion
2650
+ //#region src/templates/configs/yamllint/create-config.ts
2651
+ const { workflowHeader } = createHeader({ source: "packages/astromech/src/templates/configs/yamllint/create-config.ts" });
2652
+ function createConfig$9() {
2653
+ return `${workflowHeader()}${_yamllint_default}`;
2654
+ }
2655
+ function createIgnoreConfig$1() {
2656
+ return `${workflowHeader()}${_yamlignore_default}`;
1647
2657
  }
1648
2658
  //#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 };
2659
+ export { CI_ORDER, KNOWN_TASKS, KNOWN_WORKFLOWS, REUSABLE_ACTIONS, REUSABLE_WORKFLOWS, TASKS, WORKFLOW_CHECK_CONTEXTS, WORKFLOW_TEMPLATES, WORKFLOW_TEMPLATE_PROPERTIES, codecovComponentBlock, codecovConfig, createIgnoreConfig as createAlexignore, createRcConfig as createAlexrc, createAstromech, createCodecovConfig, createConfig as createCommitMsg, createConfig$1 as createDependabot, createConfig$2 as createDevmoji, createConfig$3 as createEditorconfig, createConfig$4 as createEditorconfigChecker, createHeader, createConfig$5 as createLabeler, createConfig$6 as createPreCommit, createConfig$7 as createPrePush, createConfig$8 as createPrepareCommitMsg, createTsconfig, createIgnoreConfig$1 as createYamlignore, createConfig$9 as createYamllint, defaultListDirs, defaultPaths, defaultReadJson, deployOnRelease, deriveDeployPaths, ensureIfNotFound, extractPreviewConfig, generateCombinedDeployContent, generateThinCallerContent, matchNamespace, mergeCodecovComponents, normalizeWorkflowWith, readWorkspacePackages, requiredChecks, reusableTemplates, runCi, runGroup, runTask, workspacePackages };