Skip to content

Instantly share code, notes, and snippets.

@mrpollo
Created February 18, 2026 22:00
Show Gist options
  • Select an option

  • Save mrpollo/600dca5ca2d70c48fd66681983b682e0 to your computer and use it in GitHub Desktop.

Select an option

Save mrpollo/600dca5ca2d70c48fd66681983b682e0 to your computer and use it in GitHub Desktop.
PX4 Fork-Friendly CI Orchestrator: Research Synthesis (2026-02-18)

Fork-Friendly CI Orchestrator: Research Synthesis

The Problem in One Sentence

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.


What We Learned

From the Competing Projects Study

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.


The Three Complementary Tools

All three approaches are complementary, not competing. The recommended architecture uses all three together.

1. Config File (.github/ci-config.yml)

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:

  • yq is 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 an if: fromJSON(...) != '[]' guard
  • uses: clauses cannot be dynamic — you can't swap action versions via config, only run: steps

2. Reusable Workflows (.github/workflows/_tier*.yml)

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.

3. Composite Actions (.github/actions/)

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 lack post hooks)

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.


Recommended Architecture

.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

How a Fork Uses It

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: false

That'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

Implementation Phases (Ordered by Impact/Effort)

Phase 1 — Config file + loader job (lowest effort, immediate value)

  • Add load-config job that reads .github/ci-config.yml with hardcoded defaults
  • Wire if: guards and fromJSON() runner labels into each job
  • Forks immediately get a single file to customize

Phase 2 — Composite actions (medium effort, big DRY win)

  • Extract setup-px4-container, setup-ccache, save-ccache
  • Workflow shrinks from 1,255 → ~850 lines
  • ccache config lives in one place instead of 12

Phase 3 — Reusable tier workflows (higher effort, enables cross-repo reference)

  • Extract T2 and T3 into _tier2-builds.yml and _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

Phase 4 — NuttX-style label filtering (optional, nice for PX4 contributors too)

  • PR labels like Vehicle: Copter, Board: fmu-v6x, Scope: EKF
  • A test-plan job reads labels and outputs a filtered SITL matrix
  • Skip irrelevant SITL tests on PRs, run full suite on push to main

The "Fork and Go" Promise

With Phases 1+2 done:

  1. Fork the repo
  2. Edit .github/ci-config.yml (single file, well-documented)
  3. CI works immediately on GitHub-hosted runners at no extra cost
  4. 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment