Nx CI — consolidated per-package workflows
SkillDev toolsLets your agent understand and modify a repo's consolidated pnpm+Nx CI pipelines, from adding packages to debugging stages.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Nx CI — consolidated per-package workflows skill
About this capability
Understand and modify the pnpm+Nx consolidated CI, the generic -nx.yml leaves driven by each package's project.json options.ci. Use when adding/changing a package's CI, onboarding a new package, generalising a bespoke per-package workflow into a leaf, editing the nx-project-matrix action or the on-
What this skill tells your AI
The instructions your AI receives, as published by tetherto/qvac in .cursor/skills/qv-nx-ci/SKILL.md and read by ahel’s review.
Per-package CI is consolidated into generic -nx.yml leaves. Each package declares its CI shape as data in packages/<pkg>/project.json under targets.<target>.options.ci; Nx computes the affected set and .github/actions/nx-project-matrix turns each affected package's options.ci into a GitHub Actions matrix that the leaf fans out over.
Full reference (wiring diagram, options.ci cheat-sheet, fork-safety model): docs/ci/nx-ci-consolidation.md. Workspace/tooling primer (pnpm config, Nx affected graph, local commands): docs/pnpm-nx-workspace.md. Read both first.
When to use
- Adding or changing a package's CI (prebuilds, cpp tests, integration, on-pr checks, benchmarks, on-merge publish).
- Onboarding a new package to the consolidated pipeline.
- Generalising a bespoke per-package workflow into a leaf.
- Editing
nx-project-matrixor any*-nx.ymlleaf. - Debugging why a stage ran / didn't run for a package.
Mental model: config is data, the leaf is generic
The matrix action flattens every field under options.ci into each matrix row generically ({package, workdir, ...ci} + platform). So a leaf reads a field as ${{ matrix.<field> }} and a new field needs no action change — only the project.json (produce it) and the leaf step (consume it). Keep behaviour in data; touch a .yml only when a genuinely new step/shape is needed.
Hard rules
- Config is read from the trusted default branch, never PR head.
nx-project-matrixdoesgit show <config-ref>:packages/<pkg>/project.json, and onpull_request_targetconfig-ref=default_branch. A new package'sproject.jsonmust land on the default branch before its CI runs. Never "fix" a pre-merge red by reading config from head. - Fork safety is non-negotiable.
on-pr-nx.ymlispull_request_target(base context, secrets). Base = control surface, head = code-under-test. Every privileged job hasfork-approvalfirst inneeds:and gates onauthorize.outputs.allowed. Never add a privileged job without both. Leaves build/testgithub.event.pull_request.head.sha— only after the gates pass. - Mobile stays as
main's. Do not consolidateintegration-mobile-test-<pkg>.ymlor add mobile to PRs beyond the existing asr/ttscoload-smoke-mobilemain-parity job. Never addrun-mobile-addon-testson PRs. hasPrebuildsis derived, not declared — it is true iffbuild.options.ciexists. Don't add it by hand.- Overrides are validated. The action's
overridesinput can only set a field a package already declares — a PR cannot inject new CI behaviour. Keep it that way. - Merge-gated jobs go through
qvac-merge-guard / validate-pronly. Never create a second required check. Use theqv-merge-guard-wireskill /docs/ci/MERGE-GUARD.md. - Never delete/skip/weaken a test. Fix the code or the config.
Recipe A — change an existing package's CI (data-only)
- Edit
packages/<pkg>/project.json→targets.<target>.options.ci. Adjust the existing field (e.g. add a platform row totest:cpp, flipincludeVulkanSdk, change a perf-report pattern). - Nothing else if the field is already consumed by the leaf. Validate (below).
Recipe B — add a NEW options.ci field (data + one leaf step)
- Add the field under the target's
options.ciin the package(s) that need it. Others simply won't have it (matrix row omits it →${{ matrix.<field> }}is empty). - Consume it in the leaf: reference
${{ matrix.<field> }}in the relevant step, or gate a step withif: ${{ matrix.<field> }}. For a whole flag-gated job, add a filtered sublist in thematrixjob'sfilterstep (jqselect(.<field> == true)) and drive the job offfromJSON(needs.matrix.outputs.<field>list). - Do not touch
nx-project-matrix— pass-through is generic.
Recipe C — onboard a new package
- Add
packages/<pkg>/project.jsonwith the targets it needs (build,on-pr,test:cpp,test:integration,benchmark,on-merge) and theiroptions.ci. Copy the closest existing package as a template. - Native addon: set
implicitDependencies: ["inference-addon-cpp", "lint-cpp"](and any real deps) so the graph and affected-detection are correct. - Land
project.jsonon the default branch first (rule 1) — until then, dispatch the leaf withconfig_ref_override/base-refpointed at your branch to test.
Recipe D — generalise a bespoke per-package workflow into a leaf
The consolidation move: fold an on-pr-<pkg>.yml (or prebuilds/cpp/integration/on-merge twin) into the generic leaf.
- Diff the bespoke workflow against a package already folded. What differs is either (a) a value → an
options.cifield, or (b) a genuinely different step → a conditional step in the leaf. - Decide fold vs carve-out:
- Fold when the flow is the leaf's shape with different values/toggles. Express every difference as
options.cifields (Recipe B) and delete the bespoke file. - Carve-out when steps genuinely diverge (bespoke integration/cpp flow, e.g. llm/asr/translation). Set
carveOut: trueon that target'soptions.ci, keep the bespoke reusable, and route to it from the orchestrator via thecarveoutsoutput (if: contains(fromJSON(needs.matrix.outputs.carveouts), '<pkg>')). The package still self-affects.
- Fold when the flow is the leaf's shape with different values/toggles. Express every difference as
- Map, don't reinvent: each bespoke
with:/env/matrix value becomes anoptions.cifield consumed by the existing leaf step. Prefer widening an existing field over adding a near-duplicate. - Delete the legacy file in the same change (or the stacked cleanup PR) so PRs don't run two pipelines. Confirm nothing references it:
git grep <workflow-basename> .github/workflows. - If the folded flow posts the
qvac/prebuild-<pkg>status or gates the merge guard, confirm the producer/trust path (scripts/prebuild-status/) still holds.
Validate before done
node .github/scripts/test/ci-trust-policy.test.mjs— fork-safety + trusted-ref policy (must stay green).node .github/scripts/test/prebuild-status.test.mjs— if you touched prebuild-status trust.actionlint <edited leaves>— only pre-existing shellcheck style is acceptable.pnpm exec nx show projects --affected -t <target> --base=<base> --head=<head>— confirm the package set is what you expect.- Standalone
gh workflow run <leaf>-nx.yml --ref <branch> -f base-ref=<x> -f head-ref=<y>— prove the matrix resolves and the leaf runs. git grep <deleted-workflow-basename> .github/workflowsreturns nothing after a Recipe D deletion.
Signals
- GitHub stars
- 612
- Forks
- 112
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
qv-nx-ci- Source
- github.com/tetherto/qvac