The current 1,255-line monolithic workflow is a binary proposition for forks: take it all or fork-and-diverge. Every upstream sync creates merge conflicts, every customization requires editing the sacred 1,200-line file, and there is zero out-of-the-box experience.
| Project | Approach | Fork-Friendly? |
|---|---|---|
| ArduPilot | 27 separate workflow files, no toggles | ❌ Worst case — exactly what to avoid |
| Zephyr | Owner gating + dynamic matrix from git-diff script | ✅ Good |
| NuttX | PR label filtering + reusable matrix generator | ✅ Very good |
| Home Assistant | info job outputs control signals for all downstream jobs |
✅ Excellent pattern |
| Flutter | ci/builders/*.json + .ci.yaml config files |
✅ Most complete config-as-code |
| PX4 (current) | Monolithic, no toggles, no config | ❌ Same class as ArduPilot |
Critical GitHub limitation confirmed: Repository vars.* are NOT accessible from fork PRs — so a config file committed in .github/ is the only reliable mechanism. This rules out the "set a repo variable in your fork" approach.
All three approaches are complementary, not competing. The recommended architecture uses all three together.
The primary fork customization surface. A fork edits this one file instead of the 1,200-line workflow.
What it controls (job-level concerns that can't go in composite actions):
- Tier toggles (
tiers.t2: false) - Runner label abstraction (
runners.linux_4cpu: '["ubuntu-latest"]') - Matrix overrides (
matrices.sitl_models,matrices.ubuntu_builds) - Feature flags (
jobs.macos_build: false,jobs.flash_comment: false)
Critical details from research:
yqis pre-installed on GitHub-hosted Ubuntu runners — no setup step needed- The upstream PX4 repo needs NO config file — defaults are hardcoded in the loader job's bash
- Empty matrix (
[]) causes a workflow crash — every matrix-based job needs anif: fromJSON(...) != '[]'guard uses:clauses cannot be dynamic — you can't swap action versions via config, onlyrun:steps
The tier-level modularity. Forks replace a 30-50 line orchestrator instead of 1,200+ lines.
The critical tradeoff (confirmed by research): Making T1 a reusable workflow introduces coarse-grained needs — all of T1 must finish before any T2 job starts. Currently, build-sitl starts as soon as its specific T1 deps finish.
Best hybrid (research recommendation): Keep T1 inline, make T2 and T3 reusable. Forks get:
- Fine-grained T1→T2 dependency (no latency regression)
- A thin orchestrator that
uses:T2 and T3 tier files - Full control over T2/T3 via inputs (runner labels, matrices, toggles)
Cross-repo reference is also viable — forks can uses: PX4/PX4-Autopilot/.github/workflows/_tier2-builds.yml@v1.15.0 and pin to a release tag, auto-inheriting upstream improvements.
The DRY layer. Eliminates 32% of the workflow (≈400 lines of duplication).
Three actions cover virtually all repetition:
.github/actions/setup-px4-container/— RunsOn setup + git safe directory (absorbed from every job).github/actions/setup-ccache/— cache restore + ccache.conf configuration (replaces ~20 lines × 12 jobs).github/actions/save-ccache/— stats + cache save (needed separately because composite actions lackposthooks)
Key limitation: Composite actions can't set runs-on, container, or strategy — those stay in the workflow YAML. This is actually correct separation of concerns.
.github/
ci-config.yml ← NEW: fork edits this one file
actions/
setup-px4-container/action.yml ← NEW: RunsOn + git safe dir
setup-ccache/action.yml ← NEW: cache restore + configure
save-ccache/action.yml ← NEW: stats + cache save
workflows/
ci-orchestrator.yml ← MODIFIED: thin orchestrator
_tier2-builds.yml ← NEW: reusable T2
_tier3-integration.yml ← NEW: reusable T3
build_all_targets.yml ← unchanged
Minimal fork (GitHub-hosted runners, skip T3):
# .github/ci-config.yml
tiers:
t3: false
runners:
linux_4cpu: '["ubuntu-latest"]'
linux_8cpu: '["ubuntu-latest"]'
linux_16cpu: '["ubuntu-22.04"]'
linux_1cpu: '["ubuntu-latest"]'
jobs:
flash_comment: false
macos_build: falseThat's it. The fork doesn't touch the workflow files at all.
Quadcopter-only fork:
# .github/ci-config.yml
matrices:
sitl_models:
- {model: "iris", latitude: "59.617693", longitude: "-151.145316", altitude: "48"}
# sitl: tailsitter and standard_vtol omitted → those jobs don't run
mavros_tests:
- {name: "Mission", test_file: "mavros_posix_test_mission.test", params: "mission:=MC_mission_box vehicle:=iris"}
# Offboard omitted
jobs:
flash_analysis: false # not relevant to this board- Add
load-configjob that reads.github/ci-config.ymlwith hardcoded defaults - Wire
if:guards andfromJSON()runner labels into each job - Forks immediately get a single file to customize
- Extract
setup-px4-container,setup-ccache,save-ccache - Workflow shrinks from 1,255 → ~850 lines
- ccache config lives in one place instead of 12
- Extract T2 and T3 into
_tier2-builds.ymland_tier3-integration.yml - Keep T1 inline to preserve fine-grained dependency timing
- Orchestrator becomes ~100 lines; forks replace just that file
- Opens the door to
uses: PX4/PX4-Autopilot/.github/workflows/_tier2-builds.yml@v1.15.0
- PR labels like
Vehicle: Copter,Board: fmu-v6x,Scope: EKF - A
test-planjob reads labels and outputs a filtered SITL matrix - Skip irrelevant SITL tests on PRs, run full suite on push to main
With Phases 1+2 done:
- Fork the repo
- Edit
.github/ci-config.yml(single file, well-documented) - CI works immediately on GitHub-hosted runners at no extra cost
- Later, opt into T3 or self-hosted runners by updating the config
No workflow YAML editing required. No merge conflict surface beyond one well-structured config file.
Research conducted 2026-02-18. Based on analysis of ArduPilot, Zephyr RTOS, NuttX, ROS 2/industrial_ci, Yocto Autobuilder, Kubernetes/Prow, Home Assistant, Flutter, and Android/AOSP CI patterns. GitHub Actions technical limits verified against official docs and community discussions.