An example infrastructure repository for developing custom UAVs with simulated mission-based CI/CD.
This repository demonstrates how to:
- Develop custom aircraft models (novel propeller configurations, powertrains, etc.)
- Create custom PX4 firmware support for new vehicle types
- Extend QGroundControl with custom GUI integrations
- Test everything through automated mission scenarios in simulation
- Maintain software CI/CD across the entire development stack
Key Capabilities:
- Infrastructure as Code: Container-based development environments (DevContainers) with dependency management
- Simulation First: Test flight behavior before hardware exists
- Mission-Based Testing: Define scenarios as code (
tests/scenarios/), validate automatically - Hybrid Workflows: CLI for automation, GUI for visual debugging and manual control
- Full Stack Integration: From firmware to ground station, tested together
- Continuous Validation: Every code change triggers automated flight tests
GUI running in devcontainer: (30-second time-lapse):
Screen.Recording.2026-01-19.145036_fast_30s.mp4
The following diagrams break down the custom UAV development lifecycle into focused views suitable for presentation:
This diagram shows the main phases of the development lifecycle:
graph LR
Start[🚀 Fork px4-sim-suite] --> Develop[📐 Development Phase]
Develop --> CI[🔄 CI/CD Pipeline]
CI --> Local[💻 Local Testing & Iteration]
Local -.iterate.-> Develop
CI --> Deploy[🎯 Deployment Ready]
style Start fill:#4a9eff,stroke:#2d5f99,color:#fff
style Develop fill:#f9f,stroke:#d0d,color:#000
style CI fill:#9f9,stroke:#0a0,color:#000
style Local fill:#ff9,stroke:#990,color:#000
style Deploy fill:#f99,stroke:#900,color:#fff
This diagram shows the parallel development streams for custom UAV projects:
graph TB
Model[📐 Design Custom Aircraft<br/>• Novel propeller layout<br/>• Custom powertrain<br/>• Unique airframe geometry]
GazeboModel[🎨 Gazebo Simulation Model<br/>• SDF/URDF definition<br/>• Physics properties<br/>• Visual meshes]
PX4Custom[⚙️ PX4 Firmware Customization<br/>• Mixer configuration<br/>• Flight controller logic<br/>• Parameter definitions<br/>• Sensor integrations]
QGCCustom[🖥️ QGroundControl Extensions<br/>• Custom UI widgets<br/>• Vehicle-specific parameters<br/>• Telemetry displays<br/>• Mission planning features]
TestScenarios[📝 Test Scenarios<br/>tests/scenarios/<br/>• Takeoff/landing<br/>• Hover stability<br/>• Waypoint missions<br/>• Failure modes]
Model --> GazeboModel
Model --> PX4Custom
Model --> QGCCustom
GazeboModel --> TestScenarios
PX4Custom --> TestScenarios
QGCCustom --> TestScenarios
style Model fill:#64b5f6,stroke:#1976d2,color:#000
style GazeboModel fill:#ffb74d,stroke:#e65100,color:#000
style PX4Custom fill:#ba68c8,stroke:#4a148c,color:#000
style QGCCustom fill:#81c784,stroke:#1b5e20,color:#000
style TestScenarios fill:#fff176,stroke:#f57f00,color:#000
This diagram shows the automated testing pipeline that runs on every commit:
graph TB
Commit[git push] --> GHActions[GitHub Actions Trigger]
GHActions --> Build[🔨 Build Phase<br/>• Compile PX4 firmware<br/>• Build QGC binary<br/>• Validate models]
Build --> Sim[🎮 Simulation Phase<br/>• Headless Gazebo<br/>• Launch custom aircraft<br/>• Run test scenarios]
Sim --> Validate[✅ Validation<br/>• Flight logs analysis<br/>• Mission success criteria<br/>• Performance metrics]
Validate --> Artifacts[📦 Artifacts<br/>• ULog files<br/>• Test reports<br/>• Build binaries]
Artifacts --> Pass{Tests Pass?}
Pass -->|Yes| Success[✅ CI Success]
Pass -->|No| Fail[❌ CI Failure]
style Build fill:#64b5f6,stroke:#0d47a1,color:#000
style Sim fill:#81c784,stroke:#1b5e20,color:#000
style Validate fill:#fff176,stroke:#f57f00,color:#000
style Artifacts fill:#dce775,stroke:#827717,color:#000
style Success fill:#66bb6a,stroke:#1b5e20,color:#000
style Fail fill:#ef5350,stroke:#b71c1c,color:#fff
CI/CD Artifacts Example:
The pipeline generates detailed flight reports showing mission performance and telemetry data:
Example HTML flight report generated by GitHub Actions CI showing altitude, velocity, and mission event timeline
This diagram shows the different modes for local testing and debugging:
graph TB
Dev[Developer Workstation] --> Choice{Choose Testing Mode}
Choice --> CLI[🖥️ Headless CLI Testing<br/>./tools/simtest run<br/><br/>Default DevContainer<br/>• Fast automated testing<br/>• CI-equivalent environment<br/>• No GUI overhead]
Choice --> GUI[🎮 Visual Debugging<br/>./tools/run_sim_with_gui.sh<br/><br/>WSL GUI DevContainer<br/>• See Gazebo simulation<br/>• Debug vehicle behavior<br/>• Automated scenarios with visuals]
Choice --> Manual[✈️ Manual Flying<br/>./tools/run_sim_with_qgc.sh<br/><br/>WSL GUI DevContainer<br/>• Full QGroundControl integration<br/>• Interactive mission planning<br/>• Manual flight testing]
CLI --> Iterate[Iterate on Design]
GUI --> Iterate
Manual --> Iterate
Iterate -.-> Dev
style CLI fill:#64b5f6,stroke:#0d47a1,color:#000
style GUI fill:#ffb74d,stroke:#e65100,color:#000
style Manual fill:#ba68c8,stroke:#4a148c,color:#000
style Iterate fill:#81c784,stroke:#1b5e20,color:#000
This comprehensive diagram illustrates the complete custom UAV development lifecycle enabled by this infrastructure:
graph TB
Start[🚀 px4-sim-suite<br/>Infrastructure Repo] --> Fork[Fork Repository<br/>for Custom UAV Project]
Fork --> Develop[Development Phase]
subgraph Develop[" "]
direction TB
Model[📐 Design Custom Aircraft<br/>• Novel propeller layout<br/>• Custom powertrain<br/>• Unique airframe geometry]
GazeboModel[🎨 Create Gazebo Model<br/>• SDF/URDF definition<br/>• Physics properties<br/>• Visual meshes]
PX4Custom[⚙️ Customize PX4 Firmware<br/>• Mixer configuration<br/>• Flight controller logic<br/>• Parameter definitions<br/>• Sensor integrations]
QGCCustom[🖥️ Extend QGroundControl<br/>• Custom UI widgets<br/>• Vehicle-specific parameters<br/>• Telemetry displays<br/>• Mission planning features]
TestScenarios[📝 Define Test Scenarios<br/>tests/scenarios/<br/>• Takeoff/landing<br/>• Hover stability<br/>• Waypoint missions<br/>• Failure modes]
Model --> GazeboModel
Model --> PX4Custom
Model --> QGCCustom
GazeboModel --> TestScenarios
PX4Custom --> TestScenarios
QGCCustom --> TestScenarios
end
Develop --> CI[🔄 CI/CD Pipeline]
subgraph CI[" "]
direction TB
Commit[git push] --> GHActions[GitHub Actions Trigger]
GHActions --> Build[🔨 Build Phase<br/>• Compile PX4 firmware<br/>• Build QGC binary<br/>• Validate models]
Build --> Sim[🎮 Simulation Phase<br/>• Headless Gazebo<br/>• Launch custom aircraft<br/>• Run test scenarios]
Sim --> Validate[✅ Validation<br/>• Flight logs analysis<br/>• Mission success criteria<br/>• Performance metrics]
Validate --> Artifacts[📦 Artifacts<br/>• ULog files<br/>• Test reports<br/>• Build binaries]
end
CI --> Local[💻 Local Development]
subgraph Local[" "]
direction TB
CLI[Headless Testing<br/>./tools/simtest run<br/>Default DevContainer]
GUI[Visual Debugging<br/>./tools/run_sim_with_gui.sh<br/>WSL GUI DevContainer]
Manual[Manual Flying<br/>./tools/run_sim_with_qgc.sh<br/>WSL GUI DevContainer]
CLI --> Iterate
GUI --> Iterate
Manual --> Iterate[Iterate on Design]
end
Iterate -.-> Develop
CI --> Deploy[🎯 Deployment Ready]
subgraph Deploy[" "]
direction LR
FirmwareBin[PX4 Firmware<br/>for Hardware]
QGCBin[Custom QGC<br/>Ground Station]
Models[Validated<br/>Simulation Models]
Docs[Test Reports<br/>& Documentation]
end
style Start fill:#4a9eff,stroke:#2d5f99,color:#fff
style Develop fill:#f9f,stroke:#d0d,color:#000
style CI fill:#9f9,stroke:#0a0,color:#000
style Local fill:#ff9,stroke:#990,color:#000
style Deploy fill:#f99,stroke:#900,color:#fff
This is an orchestration and integration layer that wraps PX4, Gazebo, and QGroundControl to enable:
- Portable, container-friendly execution model
- Mission-level scenario testing as a first-class concept
- Artifact contracts (logs, reports) suitable for CI
- Clear separation between "engine" and "product/system testing"
- Agentic-AI-friendly contribution boundaries
- Hybrid human + agentic AI development workflows
- Portability across Ubuntu 24.04, WSL2, GitHub Codespaces, and headless CI
This repository is not a fork of PX4. It treats PX4, QGC, and Gazebo models as submodules, allowing you to customize them while maintaining upstream compatibility.
px4-sim-suite/
├── px4/ # PX4-Autopilot (fork, submodule)
├── qgroundcontrol/ # QGroundControl (fork, submodule)
├── px4-gazebo-models/ # Gazebo models (fork or upstream, submodule)
├── tools/ # Orchestration, runners, CI glue (owned here)
├── tests/ # Scenario / mission definitions (owned here)
├── docs/ # Design notes, references
├── AGENTS.md # Rules and procedures for agentic AI
└── README.md
The px4/ directory already contains extensive simulation infrastructure (Gazebo, FlightGear, jMAVSim, etc.) via PX4’s own submodules.
This repo intentionally does not duplicate that functionality, and instead layers testing, automation, and workflow management around PX4.
PX4 already functions as a self-contained firmware + simulation engine. However, PX4 alone does not provide:
- A portable, container-friendly execution model
- Mission-level scenario testing as a first-class concept
- Artifact contracts (logs, reports) suitable for CI
- Clear separation between “engine” and “product/system testing”
- Agentic-AI-friendly contribution boundaries
This repository exists to fill those gaps without modifying PX4’s internal structure unless necessary.
Key architectural principle:
PX4 is treated as a vendor engine. This repository owns orchestration, scenarios, CI, and workflow.
This repository uses git submodules for large upstream projects that we intentionally fork and track:
| Component | Location | Ownership |
|---|---|---|
| PX4 Autopilot | px4/ |
Fork maintained by repo owner |
| QGroundControl | qgroundcontrol/ |
Fork maintained by repo owner |
| PX4 Gazebo Models | px4-gazebo-models/ |
Fork or upstream mirror |
Each fork has:
- an
originremote (our fork) - an
upstreamremote (canonical project)
Upstream merges are intentional and explicit.
See AGENTS.md for the exact rules governing submodules and how changes are proposed and applied.
This is a hybrid-managed repository:
-
Humans:
- Own repo structure
- Own submodule configuration
- Perform upstream merges
- Apply cross-repo changes
-
Agentic AI (Codex, Copilot, etc.):
- Propose changes
- Modify code in-place where allowed
- Leave structured instructions or patches when blocked by permissions
This division is intentional and documented in AGENTS.md.
These projects provide the underlying capabilities used here:
- PX4 Autopilot: https://github.com/PX4/PX4-Autopilot
- PX4 Simulation docs: https://docs.px4.io/main/en/simulation/
- PX4 Gazebo models: https://github.com/PX4/PX4-gazebo-models
- QGroundControl: https://github.com/mavlink/qgroundcontrol
- MAVLink: https://github.com/mavlink/mavlink
PX4 already vendors many simulation components internally via submodules; this repo does not attempt to replace that system.
This repository:
- ✔ Wraps PX4 for testing and automation
- ✔ Supports Gazebo-based simulation
- ✔ Supports human-in-the-loop and headless execution
- ✔ Supports agent-assisted development
This repository does not:
- ❌ Replace PX4’s internal simulation system
- ❌ Vendor PX4 dependencies manually
- ❌ Treat QGroundControl as a CI dependency
- ❌ Assume a single-developer workflow
If you are an automated agent or a human working with one:
👉 Read AGENTS.md before making changes.
That file defines:
- What can and cannot be modified directly
- How submodule changes are proposed
- How permissions and limitations are handled
- How work is handed off between agents and humans
Looking to bring up PX4 SITL on Ubuntu 24.04/WSL2 for the MVP? Follow the runbook in docs/stage1-sitl.md for dependency setup, headless launch, and a manual takeoff/land smoke test.
The unified CLI entry point for the simulation pipeline is provided as a Stage 1 stub at tools/simtest.
It is POSIX-shell-friendly and intended to run the same way on Ubuntu, WSL2, GitHub Actions, Codespaces, or a mounted Docker workspace.
Usage: simtest [build|run|collect|all|--help]
build Build PX4, models, dependencies
run Run the Gazebo simulation
collect Fetch artifacts (logs, flight results)
all Execute build + run + collect
The build subcommand now runs the PX4 SITL CMake flow for Gazebo Classic (non-ROS) targeting the default quadrotor airframe:
- Executes CMake from within
px4/and configuresbuild/px4_sitl_default - Uses the Unix Makefiles generator with
make -j$(nproc) - Expects
cmakeandmaketo be installed; otherwise exits with a clear error
Troubleshooting tips:
- Ensure the
px4/submodule is present (rungit submodule update --init --recursiveif needed) - Install
cmakeandmakevia your system package manager before runningsimtest build
Examples:
sh tools/simtest build
sh tools/simtest all
The run subcommand now launches a headless PX4 SITL + Gazebo Harmonic session using the built firmware:
- Ensures the PX4 build exists (invokes
buildautomatically if missing) - Uses the modern Gazebo Harmonic (
gzCLI) flow, invokingmake px4_sitl gz_<model>frompx4/ - Defaults to the
x500quadrotor (override withPX4_SIM_MODEL) and extendsPX4_GZ_MODEL_PATH/GZ_SIM_RESOURCE_PATHwithpx4-gazebo-models - Runs for a bounded duration (
SIM_DURATIONseconds; defaults to 45) before shutting down - Executes the default Stage 5 scenario (
tests/scenarios/takeoff_land.py) that arms, climbs to ~3 m, holds briefly, and lands via MAVLink commands
You can customize the duration or model:
SIM_DURATION=30 PX4_SIM_MODEL=x500 sh tools/simtest runUse sh tools/simtest all to run both build and simulation in sequence.
If you want to disable automated flight control (for interactive debugging or manual testing), set SIMTEST_SCENARIO=none before invoking simtest run. To plug in a different scripted mission, drop a Python helper under tests/scenarios/ and set SIMTEST_SCENARIO=<name>.
Scenario selection examples:
SIMTEST_SCENARIO=intercept_lock_bootstrap ./tools/simtest run
SIMTEST_SCENARIO=vision_lock_static PX4_SIM_MODEL=x500_mono_cam_down PX4_GZ_WORLD=aruco ./tools/simtest run
SIMTEST_SCENARIO=none ./tools/simtest runintercept_lock_bootstrap is a non-flight bootstrap scenario that only maintains MAVLink GCS heartbeats and writes a JSON summary to SIMTEST_SCENARIO_RESULT, so downstream artifact/report tooling keeps working without changing contracts.
vision_lock_static is a non-chase lock-quality scenario that feeds a static target through the tracker and emits lock acquisition/hold metrics (time_to_first_track_s, time_to_lock_s, lock_hold_ratio, max_gap_s) to vision_lock_static_summary.json.
For pre-Task-4 vision prototyping, run the integrated orchestration entrypoint:
./tools/simtest visionCI/devcontainer mode can include the same loop by setting SIMTEST_ENABLE_VISION=1:
SIMTEST_ENABLE_VISION=1 ./tools/run_ci.sh --inside-devcontainerIn GitHub Actions (.github/workflows/simtest-build.yml), vision is now enabled by default (SIMTEST_ENABLE_VISION=1) and can be turned off by setting the repository variable SIMTEST_ENABLE_VISION=0. The workflow runCmd also exports the same default before invoking run_ci.sh, so the vision flag survives container execution boundaries.
run_ci.sh now records explicit vision status in artifacts/simtest-report.txt for both paths:
- Vision enabled:
vision_enabled=1,vision_seconds=<n>,vision_pipeline_exit_code=<n>, plus avision_feedback_summary_*block with checker status, lock metrics, and advisory latency min/p50/p95/max. - Vision disabled:
vision_enabled=0andvision_skipped_reason=SIMTEST_ENABLE_VISION_not_set_to_1(without creating a placeholdervision-pipeline.log).
If the vision checker reports FAIL, run_ci.sh now preserves artifacts and continues (with warning output) so downstream review can still happen; hard failures are still raised for missing/empty required artifacts or non-checker vision crashes.
When vision is enabled, run_ci.sh enforces non-empty required feedback artifacts and fails fast with explicit file names if any are missing:
artifacts/vision-pipeline.logartifacts/check_vision_lock_metrics.logartifacts/intercept_tracker_tracks.jsonlartifacts/guidance_advisory.jsonl
You can still use the tracker’s lower-level regression and replay entry points when needed:
python3 tools/intercept_tracker.py --simulate-stream --clear-output
python3 tools/intercept_tracker.py --input-jsonl artifacts/intercept_camera_frames.jsonl --clear-outputEach run persists its telemetry and summary artifacts under artifacts/ (override with SIMTEST_ARTIFACT_DIR):
<scenario>.log— live scenario transcript (for default behavior this istakeoff_land.log)<scenario>_summary.json— machine-readable JSON produced by the selected scenario viaSIMTEST_SCENARIO_RESULT<timestamp>.ulg— copy of the most recent PX4 flight log for post-flight analysisvision_lock_static_summary.json(or<scenario>_summary.json) — vision scenario summaryintercept_tracker_tracks.jsonl— tracker output streamintercept_tracker_events.jsonl— tracker event streamguidance_advisory.jsonl— bounded advisory output streamcheck_vision_lock_metrics.log— deterministic PASS/FAIL checker outputcamera_ingest_adapter.log,intercept_tracker.log,guidance_advisory.log— per-process pipeline logs
Use sh tools/simtest collect to list the files produced in the selected artifact directory.
If vision gating fails, check in this order:
artifacts/check_vision_lock_metrics.logfor exact missing artifacts or failed threshold messages.artifacts/<scenario>.logfor scenario runtime or summary generation issues.artifacts/intercept_tracker.log/artifacts/guidance_advisory.logfor stream contract or idle/timeout exits.
The run flow launches a lightweight MAVLink heartbeat helper implemented with pymavlink so PX4 no longer reports a missing GCS on startup. The dev container installs this dependency automatically; native environments should ensure pymavlink is available (for example via pip install --user pymavlink).
tools/simtest also provides a Stage 8 scaffold for exercising QGroundControl without leaving the repo:
-
Fast mode (default): leaves
SIMTEST_ENABLE_QGC=0, so CI/local./tools/run_ci.sh --inside-devcontaineronly executes PX4 build + Gazebo headless scenario. -
Full mode: set
SIMTEST_ENABLE_QGC=1to include the additionalsimtest qgc build,simtest qgc test,simtest qgc stub, and Stage 5 follow-up checks in the same pipeline. -
sh tools/simtest qgc buildconfigures QGC withQGC_BUILD_TESTING=ONby invoking the Qt toolchain declared intools/environment_manifest.jsonand produces both the desktop binary and AppImage target insidebuild/qgc-simtest/. -
sh tools/simtest qgc testruns the CTest suite headlessly (xvfb-runwhen available, otherwiseQT_QPA_PLATFORM=offscreen). -
sh tools/simtest qgc stublaunches the--simple-boot-testflow under Xvfb (if present) and drives a small MAVLink stub defined intools/qgc_virtual_px4.py; artifacts are written toartifacts/qgc/. -
./tools/run_ci.sh --inside-devcontaineralways readsSIMTEST_ENABLE_QGCfrom the current environment (including the workflow/job env in GitHub Actions) and appends timing data toartifacts/simtest-report.txtalongside any enabled QGC logs.- Set
SIMTEST_QGC_SKIP_PARAM_CHECK=1when you need the stub to succeed without a parameter request (useful for ad-hoc debugging).
- Set
A VS Code-compatible dev container is defined in .devcontainer/devcontainer.json to provide a consistent Ubuntu 24.04 base with CMake, Make, Python tooling, and PX4’s own Ubuntu setup script preinstalled. The container automatically initializes all submodules recursively, runs PX4’s Tools/setup/ubuntu.sh --no-nuttx to install SITL dependencies (including the Gazebo Harmonic toolchain via the gz CLI), installs the gz-harmonic meta-package explicitly, and mounts the repository at /workspaces/<repo>, matching the default Dev Containers layout so commands like the update hook run in the right place. The post-create hook now installs pymavlink alongside the existing PX4 Python tooling so the heartbeat helper is available everywhere.
For a single cross-platform entry point, use tools/run_ci.sh. Without arguments it builds (or updates) the dev container using the local devcontainer CLI and then runs the standard build-and-run sequence inside the container. GitHub Actions calls the same script with the --inside-devcontainer flag so both CI and local developers share identical orchestration. The workflow publishes three artifacts for traceability:
artifacts/simtest-build.log— full build outputartifacts/simtest-run.log— full headless run outputartifacts/simtest-report.txt— build and run timing summary (in seconds)
These artifacts help triage build and runtime regressions across platforms while keeping the single simtest entry point consistent locally and in CI.
This repository provides two devcontainer configurations to support different workflows:
Purpose: Headless CI/CD and command-line development
Use cases:
- GitHub Actions CI pipeline
- Headless automated testing
- Command-line development
- GitHub Codespaces (without GUI)
Features:
- Ubuntu 24.04 base
- All build dependencies pre-installed
- Python 3.10 with pymavlink
- PX4 Ubuntu setup (
--no-nuttx) - Gazebo Harmonic toolchain
- Minimal resource footprint
Launch:
# Opens automatically in VS Code with Dev Containers extension
code .Purpose: Visual simulation and manual control with QGroundControl
Use cases:
- Running Gazebo with visible GUI
- Manual flying with QGroundControl
- Visual debugging of flight tests
- Interactive development on Windows WSL2
Additional features (vs default):
- X11/Wayland forwarding for GUI support
- WSLg integration (
/mnt/wslg,/tmp/.X11-unix) - Host networking for display
- Environment variables:
DISPLAY,WAYLAND_DISPLAY,XDG_RUNTIME_DIR,PULSE_SERVER
Launch:
# From VS Code: Select "WSL GUI" configuration
# Or manually specify the config file
code --folder-uri vscode-remote://dev-container+/path/to/repo/.devcontainer/wsl-gui/devcontainer.jsonBoth devcontainers share the same installation infrastructure:
Common:
- Base image:
mcr.microsoft.com/devcontainers/base:ubuntu-24.04 - Dependency manifest:
tools/environment_manifest.json - Installation script:
tools/env_requirements.py install - PX4 setup:
px4/Tools/setup/ubuntu.sh --no-nuttx - Submodule initialization:
git submodule update --init --recursive
Divergence points:
- WSL GUI adds display mounts and environment variables
- WSL GUI uses
--net=hostfor X11 forwarding - Default has no GUI-specific configuration
| Workflow | DevContainer | Command Example |
|---|---|---|
| CI/CD pipeline | Default | ./tools/simtest run |
| Headless testing | Default | ./tools/simtest run |
| GUI + automation | WSL GUI | ./tools/run_sim_with_gui.sh |
| QGC manual control | WSL GUI | ./tools/run_sim_with_qgc.sh |
| Development (no GUI) | Default | Any CLI workflow |
| Development (with GUI) | WSL GUI | Any GUI workflow |
The following tools require the WSL GUI devcontainer:
./tools/run_sim_with_gui.sh- Visual Gazebo with automated scenario./tools/run_sim_with_qgc.sh- Gazebo + QGroundControl./tools/launch_qgc.sh- QGroundControl standalone
These tools will fail in the default devcontainer with:
Error: cannot open display: :0
Both devcontainers use the same installation flow:
# 1. Environment manifest defines dependencies
tools/environment_manifest.json
# 2. Installation script reads manifest
python3 tools/env_requirements.py install
# 3. PX4 setup script (Gazebo, MAVLink, etc.)
bash px4/Tools/setup/ubuntu.sh --no-nuttxThis ensures:
- No duplicate package lists
- Single source of truth for dependencies
- Consistent versions across CI and local development
- Easy updates (edit manifest, rebuild container)
