CI/CD
Orbitron uses GitHub Actions for automated testing, building, and releasing. This section explains what the workflows do and how to use them.
13.1 What CI/CD Does
CI (Continuous Integration): Every time you push code or someone opens a pull request, GitHub automatically: - Checks the Rust workspace on Linux and Windows - Runs tests to catch regressions - Checks code formatting (cargo fmt) - Runs linters (cargo clippy)
CI deliberately produces no artifacts. release.yml owns the binaries and builds all three platforms on a v* tag (see §13.2 item 3).
Platform coverage is conditional. Every push, pull request, tag and weekly Monday run uses the same two legs, linux-self and windows-self. A manual run with the macos input adds a macOS leg; a manual run with windows_gui replaces the whole matrix with a Windows leg running in a desktop session, for the GUI start-up test. Trigger a manual run with macos before landing anything macOS-sensitive.
CD (Continuous Delivery): When you create a Git tag (e.g., v1.0.0), GitHub automatically: - Builds release binaries for all platforms - Creates a GitHub release with downloadable archives - Attaches binaries to the release page
Why it matters: CI/CD catches bugs before users see them, ensures code works on all platforms, and makes releases easy without manual builds.
13.2 Workflows Overview
The repository includes seven workflows:
.github/workflows/ci.yml- Main CI workflow- Triggers: Pushes to
main,v*tags, all pull requests, the weekly Monday schedule, and manual dispatch. Pushes and pull requests carrypaths-ignorefor**/*.md,notes/**,docs/**,website/**, and.claude/**, so a documentation-only change runs no CI job at all (docs-lint.ymlpicks those commits up instead). Tag pushes ignore path filters entirely, so the pre-release matrix still runs. - Platforms: Linux and Windows on every push, pull request, tag and weekly schedule; macOS only on manual dispatch with the
macosinput. The weekly and dispatch runs also run the ignored gate tests (the InChI corpus ratchet, the PubChem load, the 10k-atom XYZ), and those runs and av*tag run the whole suite a second time in release on the Linux leg.release.ymlwill not publish until CI has passed on the tagged commit. - Jobs:
build-viewer-assetsbuilds the generatedviewer_assets/bundle once and saves an exact-commit cross-platform cache.checksrestores it before compilingviewer/web-page, which embeds those files for both the desktop and the CLI (the build substitutes placeholders without them, but then the Web View export tests cannot run).wasm-smokeindependently runs the browser suite so a browser failure does not prevent the Rust checks from reporting.render-parityruns the strict six-scene golden comparison under Mesa lavapipe inside.github/render-parity/Dockerfile, which pins the base image, the Mesa package and the toolchain. It runs on the Linux runner onv*tags, weekly, and on dispatch withrender_parity. The container is given no GPU, so it cannot compare against the runner’s GPU by accident. - Steps (the
checksjob):- Format check (
cargo fmt --all -- --check) - Linting (
./scripts/check-rust-lints.sh) - Tests (
cargo nextest run --workspace --locked --profile cion every platform) - The same in release (
--release), Linux only, on tags, weekly and on dispatch - Doctests (
cargo test --workspace --doc --locked, Linux only — nextest runs none) - GUI/TUI smoke tests (Linux only)
- Security audit (
cargo denyon Linux) - Benchmark compilation check (Linux only)
linux-selfleg — those steps are gated onrunner.os == 'Linux'. The Xvfb and X11 library install (and the apt, disk-cleanup and system-linker steps) are still gated onmatrix.id == 'ubuntu-hosted', an id the matrix no longer produces, so they never run. The GUI smoke test therefore runs only if the runner has a display (orORBITRON_FORCE_GUI_TESTSis set); otherwisegui_available()inui/shell/tests/viewer_starts.rsskips it. - Format check (
- HDF5 Setup: every leg runs in a conda environment built from
environment.yml. On Linux, the “Use the machine’s conda environment” step setsHDF5_DIRto its prefix sohdf5-metnolinks that libhdf5 instead of compiling its own, and puts$prefix/libon the loader path, without which nextest dies before the first test. On Windows it sets noHDF5_DIR, and the bundled HDF5 source is built and linked statically, as in a release. DIRAC checkpoint support is enabled on all of them.
- Triggers: Pushes to
.github/workflows/python-tests.yml- Python bridge tests- Triggers: Pushes and PRs on
mainordevelopthat touchextensions/python-bridge/**,core/**,io/pipelines/**, or the workflow file - Platforms: Linux; Python 3.10 and 3.13
- Steps:
- Build Python package through the PEP 517 wrapper
- Run the unit tests, named individually rather than
pytest tests/, because the notebook file needs its own-m "not slow". A file left off that line is a suite nothing runs:test_configuration.py,test_viewer_server.pyandtest_widget_server.pysat uncollected until 2026-09-04, andtest_molecular_surface.pyandtest_study.pyuntil 2026-09-24. Add new files to the invocation in the same commit. - Run Jupyter notebook tests
- Execute tutorial notebook
- Build an asset-bearing package and run embedding tests in a separate job
- HDF5 Setup: Every job runs on Linux, and the build links conda’s HDF5 through
HDF5_DIRrather than compiling the bundled source. There is no macOS or Windows job.
- Triggers: Pushes and PRs on
.github/workflows/release.yml- Automated releases- Triggers: Git tags matching
v*(e.g.,v1.0.0,v0.2.1), and a manual dispatch, which is a dry run that builds and checks everything and publishes nothing - Platforms: Linux (in a manylinux_2_28 container, so the binaries run on glibc 2.28 and newer), macOS, Windows
- Steps:
- Generate the licence notices (
scripts/generate-third-party-notices.sh) and put them in every artifact - Build release binaries for all platforms, and refuse a Linux binary that needs more than glibc 2.28
- Sign and notarize the macOS app, DMG and CLI; on a tag, missing secrets or any signing failure fails the release
- Build
cp310-abi3wheels and versioned web-viewer artifacts - Check the asset set, write
SHA256SUMS, and attach everything to a GitHub release named for the tag
- Generate the licence notices (
- Manual control: You create the tag when ready; CI builds and publishes automatically
- Triggers: Git tags matching
.github/workflows/docs-lint.yml- Documentation integrity- Checks local links and paths (lychee), repository paths cited in Rust doc comments (
scripts/check_rust_doc_paths.py), and the removed-token denylist in.docs-lint, on documentation changes.
- Checks local links and paths (lychee), repository paths cited in Rust doc comments (
.github/workflows/macos-signing-check.yml- Signing readiness- Manually verifies that the configured Apple signing identity is available before a release tag depends on it.
.github/workflows/runner-health.yml- Runner pickup check- Runs daily and on demand.
- Fails when a workflow has remained queued for more than 45 minutes.
.github/workflows/main-health.yml- Red-main monitor- Runs every two hours, plus
workflow_dispatch. - Opens a GitHub issue while a workflow on
mainis failing, and closes it when main recovers. Deliberately an issue rather than a red check: a red check is what was already there and already missed — it scrolls off the runs list and its email lands beside every other Actions mail — while an open issue stays until the thing is fixed and clears itself without anyone tidying up. - It exists because CI failed on seven consecutive runs over eleven hours and nobody noticed, and the failure was not cosmetic:
orbitron render --annotateand--label-residueswere drawing no text at all on Linux or Windows, because the font lookup searched macOS paths only. - The
drilldispatch input reports a fake failure. The alerting half of a monitor otherwise only runs when something is already wrong, so it needs a way to be fired deliberately — run it once with the input on to watch the issue appear, once with it off to watch it close.
- Runs every two hours, plus
13.3 Checking CI Status
On GitHub: - Go to the “Actions” tab in your repository - Click on a workflow run to see detailed logs - Green checkmark = all tests passed - Red X = tests failed (click to see which step failed)
In Pull Requests: - CI status appears at the bottom of the PR page - All checks must pass before merging (if branch protection is enabled) - Click “Details” next to a check to see logs
Local testing before pushing — the same commands CI runs, in order:
cargo fmt --all -- --check
./scripts/check-rust-lints.sh
cargo nextest run --workspace --locked --profile ci
cargo test --workspace --doc --locked
cargo deny checkLocally, drop --profile ci so .config/nextest.toml caps the thread count, and prefix each command with conda run -n orbitron-dev (see §11.3).
13.4 Creating Releases
Manual release process (recommended for first-time open source):
Bump both package-version fields:
- Edit
versionunder[workspace.package]in the rootCargo.toml. Every Rust workspace member inherits it viaversion.workspace = true. - Edit
project.versioninextensions/python-bridge/pyproject.toml. Installed Python packages expose that metadata asorbitron.__version__. - Run
python scripts/check_version_sync.py --tag vX.Y.Z. CI checks the two fields on every push, and the release workflow checks them against the tag. - Update
CHANGELOG.mdwith release notes.
- Edit
Create and push a Git tag:
git tag -a vX.Y.Z-rc.1 -m "Orbitron vX.Y.Z-rc.1" git push origin vX.Y.Z-rc.1GitHub Actions (
release.yml) automatically:- Builds desktop apps (macOS/Linux/Windows), CLI binaries, the web viewer, and Python wheels (Linux manylinux + macOS arm64 + Windows, all cp310-abi3)
- Gates the release on CI: the
ci-gatejob polls the tag’s ownci.ymlrun (the push run whose branch name is the tag, at the tagged commit) for up to two hours and fails if it concludes anything but success, never starts, or does not finish. The Create Release job lists it inneeds, so a red or missing CI run on the tag publishes nothing. - Creates a GitHub release and attaches all of them, with
SHA256SUMS - Signs and notarizes the macOS app, DMG and CLI, and fails the release if the Apple secrets are missing or any signing or notarization step fails. A dispatched dry run may build unsigned instead (see
RELEASING.md).
Verify the release candidate before the final tag:
- Go to “Releases” in your GitHub repository
- Check that binaries are attached
- Follow the artifact and platform checks in
RELEASING.md.
Pre-release tags (for testing): - Use tags like v1.0.0-beta.1 or v1.0.0-rc.1 - The workflow marks these as “pre-release” automatically - Test the binaries before creating the final release tag
13.5 Troubleshooting CI Failures
Common issues:
- HDF5 build failures:
- The Windows leg of
ci.ymland everyrelease.ymlbuild compile the source bundled byhdf5-metnoand link it statically: check that CMake is installed, thatHDF5_DIRandHDF5_ROOTare unset, and inspect thehdf5-metno-srcbuild output.expected &usize, found &i64in hdf5-metno’sglobals.rsis the signature of anHDF5_DIRthat came back on Windows. - The Linux leg links conda’s libhdf5 through
HDF5_DIRinstead, so a failure there is a conda-prefix or loader-path problem rather than a build one;libhdf5.so.320: cannot open shared object fileis the signature.
- The Windows leg of
- Tests failing on one platform:
- Check platform-specific differences (file paths, line endings, etc.)
- Some tests may be platform-specific (GUI tests require X11/Wayland on Linux)
- Format/clippy failures:
- Run
cargo fmt --alllocally to fix formatting - Run
./scripts/check-rust-lints.shto see clippy suggestions
- Run
- Missing dependencies:
- Ensure all dependencies are declared in
Cargo.toml - Check that optional dependencies are properly gated behind feature flags
- Ensure all dependencies are declared in
- Windows tests dying with
0xc00000fd(stack overflow):- This looks like a platform bug and is not one. The debug profile’s clap derive builds the command tree in one stack frame per level, so the CLI parser tests (
cli::tests,command_round_trip) need about 2 MiB, which is all a Rust test thread gets, and MSVC’s frames are larger..cargo/config.tomlsetsRUST_MIN_STACK = "8388608"in its[env]table to give those threads 8 MiB; aRUST_MIN_STACKalready in the environment wins, and the release CLI needs under 384 KB of main stack. A red Windows job after an unrelated change may be a new subcommand tipping the parser tests over, so check the failing test names before suspecting the change. Measure a stack need by shrinkingRUST_MIN_STACKon a built test binary.
- This looks like a platform bug and is not one. The debug profile’s clap derive builds the command tree in one stack frame per level, so the CLI parser tests (
Getting help: - Check workflow logs for detailed error messages - Run the same commands locally to reproduce issues - See §15 Debugging Playbook for more troubleshooting tips
13.6 Customizing CI/CD
Adding new checks: - Edit .github/workflows/ci.yml to add new steps - Use the same HDF5 setup steps if your check needs HDF5
Changing release behavior: - Edit .github/workflows/release.yml to modify release creation - Configure signing/notarization credentials for macOS releases (see §14.2) - Add code signing for Windows (requires certificates)
Platform-specific builds: - The matrix strategy in ci.yml makes it easy to add new platforms - Ensure HDF5 setup is added for new platforms if needed