@theholocron/astromech 5.0.0-alpha.171 → 5.0.0-alpha.173

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.d.mts CHANGED
@@ -383,6 +383,15 @@ interface WorkspacePackage {
383
383
  dir: string;
384
384
  version: string | undefined;
385
385
  dependencies: Record<string, string>;
386
+ /**
387
+ * A workspace:* devDependency can optionally count too (holocron#1071,
388
+ * workspaceDependencies' includeDevDependencies option). Optional so a
389
+ * caller holding the previous, dev-dependency-less shape (a plain
390
+ * object literal, or a value from outside this package's own version)
391
+ * keeps compiling and working -- a missing value is treated the same
392
+ * as `{}`.
393
+ */
394
+ devDependencies?: Record<string, string>;
386
395
  }
387
396
  interface Tree {
388
397
  readJson: (path: string) => unknown;
package/dist/index.mjs CHANGED
@@ -143,7 +143,7 @@ var delivery_deploy_default$1 = "name: Deploy\n\non: # yamllint disable-line rul
143
143
  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";
144
144
  //#endregion
145
145
  //#region src/templates/reusable/delivery.previewDeploy.yml
146
- 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. That skip is silent to the fork\n # contributor -- no preview, no explanation why -- tracked\n # as #1027 rather than fixed here (the real fix needs either\n # a status comment or a maintainer-gated trusted-deploy path,\n # not a quick addition to this step).\n echo \"available=$([ -n \"$VERCEL_TOKEN\" ] && echo true || echo false)\" >> \"$GITHUB_OUTPUT\"\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";
146
+ 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";
147
147
  //#endregion
148
148
  //#region src/templates/reusable/delivery.publish.yml
149
149
  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";
@@ -2017,11 +2017,54 @@ const WAIT_TIMEOUT_MS = 3e5;
2017
2017
  const WAIT_INTERVAL_MS = 1e4;
2018
2018
  //#endregion
2019
2019
  //#region src/tasks/deploy-on-release/workspace-dependencies/workspace-dependencies.ts
2020
- /** The `workspace:*` dependencies of `pkg`, as workspace packages (with their current version). */
2021
- function workspaceDependencies(pkg, all) {
2022
- const names = new Set(Object.entries(pkg.dependencies).filter(([, spec]) => String(spec).startsWith("workspace:")).map(([name]) => name));
2020
+ /**
2021
+ * `pkg`'s direct `workspace:*` dependencies -- `dependencies`, plus
2022
+ * `devDependencies` when asked. `pkg.devDependencies` defaults to `{}`:
2023
+ * it's optional on `WorkspacePackage` precisely so a caller holding the
2024
+ * previous (dev-dependency-less) shape doesn't crash here.
2025
+ */
2026
+ function directWorkspaceDependencies(pkg, all, includeDevDependencies) {
2027
+ const entries = includeDevDependencies ? [...Object.entries(pkg.dependencies), ...Object.entries(pkg.devDependencies ?? {})] : Object.entries(pkg.dependencies);
2028
+ const names = new Set(entries.filter(([, spec]) => String(spec).startsWith("workspace:")).map(([name]) => name));
2023
2029
  return all.filter((candidate) => names.has(candidate.name));
2024
2030
  }
2031
+ /**
2032
+ * Every `workspace:*` dependency of `pkg`, transitively (holocron#1071) —
2033
+ * its own direct dependencies, and theirs, and so on — as workspace
2034
+ * packages (with their current version).
2035
+ *
2036
+ * `includeDevDependencies` defaults to `false`, matching the pre-#1071
2037
+ * behavior: a shared dev/build-time workspace package (a tsconfig, a
2038
+ * lint config, a build script) counts only when a caller opts in.
2039
+ * `defaultPaths` (computing paths that should trigger a rebuild) opts in;
2040
+ * `deployOnRelease`'s publish-wait list (waiting for npm to serve a
2041
+ * package's RUNTIME dependencies before deploying it) does not -- an
2042
+ * unpublished, possibly `private`, dev-only package would never satisfy
2043
+ * that wait and the deploy would time out (holocron#1071 review).
2044
+ *
2045
+ * A diamond (two packages depending on the same third one) contributes
2046
+ * that third package once, not twice — tracked by a visited set, seeded
2047
+ * with `pkg` itself so a dependency cycle (A -> B -> A) can't loop
2048
+ * forever either, even though a real workspace graph normally can't have
2049
+ * one.
2050
+ */
2051
+ function workspaceDependencies(pkg, all, options) {
2052
+ const includeDevDependencies = options?.includeDevDependencies ?? false;
2053
+ const visited = /* @__PURE__ */ new Set([pkg.name]);
2054
+ const result = [];
2055
+ let frontier = directWorkspaceDependencies(pkg, all, includeDevDependencies);
2056
+ while (frontier.length > 0) {
2057
+ const next = [];
2058
+ for (const dep of frontier) {
2059
+ if (visited.has(dep.name)) continue;
2060
+ visited.add(dep.name);
2061
+ result.push(dep);
2062
+ next.push(...directWorkspaceDependencies(dep, all, includeDevDependencies));
2063
+ }
2064
+ frontier = next;
2065
+ }
2066
+ return result;
2067
+ }
2025
2068
  //#endregion
2026
2069
  //#region src/tasks/deploy-on-release/default-paths/default-paths.ts
2027
2070
  /**
@@ -2033,7 +2076,7 @@ function workspaceDependencies(pkg, all) {
2033
2076
  * @returns Repo-relative prefixes, each ending in `/`.
2034
2077
  */
2035
2078
  function defaultPaths(repoRoot, pkg, all) {
2036
- return [pkg, ...workspaceDependencies(pkg, all)].map(({ dir }) => `${relative(repoRoot, dir).replace(/\\/g, "/")}/`);
2079
+ return [pkg, ...workspaceDependencies(pkg, all, { includeDevDependencies: true })].map(({ dir }) => `${relative(repoRoot, dir).replace(/\\/g, "/")}/`);
2037
2080
  }
2038
2081
  //#endregion
2039
2082
  //#region src/tasks/deploy-on-release/deploy-targets/deploy-targets.ts
@@ -2131,7 +2174,8 @@ function workspacePackages(repoRoot, { readJson, listDirs }) {
2131
2174
  name,
2132
2175
  dir,
2133
2176
  version: manifest.version,
2134
- dependencies: manifest.dependencies ?? {}
2177
+ dependencies: manifest.dependencies ?? {},
2178
+ devDependencies: manifest.devDependencies ?? {}
2135
2179
  }] : []);
2136
2180
  }
2137
2181
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theholocron/astromech",
3
- "version": "5.0.0-alpha.171",
3
+ "version": "5.0.0-alpha.173",
4
4
  "description": "The Holocron task runner — one task manifest drives `holocron run`, `holocron run ci`, the CI workflows, package.json scripts, linters, and required checks.",
5
5
  "keywords": [
6
6
  "ci",
@@ -48,7 +48,7 @@
48
48
  "@theholocron/fs-utils": "^1.16.0",
49
49
  "@theholocron/json-utils": "^1.16.0",
50
50
  "@theholocron/pkg-utils": "^1.16.0",
51
- "@theholocron/datapad": "5.0.0-alpha.171"
51
+ "@theholocron/datapad": "5.0.0-alpha.173"
52
52
  },
53
53
  "devDependencies": {
54
54
  "@commitlint/config-conventional": "^21.2.3",