Skip to main content

CI/CD Pipeline

Overview

Two-tier CI with path-based filtering, local development mirror via Makefile, manual tag-triggered releases, and automated dependency updates.

WorkflowFileTriggerPurpose
CIci.ymlPR to main, dispatchTier 1 — lint + test on every PR
System CIci-system.ymlNightly 03:00 UTC, dispatch, callableTier 2 — full suite on GPU hardware
Releaserelease.ymlTag push v*.*.*, dispatchCreate GitHub Release
Docs syncsync-docs-to-hub.ymlPush to main (docs changes), dispatchSync docs/user/ to ds01-hub

All workflows support workflow_dispatch for manual triggering.

Tier 1: CI (ci.yml)

Runs on every PR to main. Uses dorny/paths-filter to skip irrelevant jobs — a docs-only PR runs nothing; a Python-only change skips shell checks.

Jobs:

JobConditionWhat it does~Time
Detect changesAlwaysPath filter → outputs python, shell, workflows7s
RuffPython changedruff format --check + ruff check via astral-sh/ruff-action7s
Shell formatShell changedshfmt -d -i 4 -ci -s via mfinelli/setup-shfmt4s
ShellcheckShell changedshellcheck -x -S warning on all scripts12s
TestsPython or shell changedpytest -m "not system" (unit + integration)19s
Lint workflowsWorkflows changedactionlint on workflow YAML13s
CIAlwaysGate job — passes if all above pass or skip3s

Branch protection requires the single CI gate job. Individual jobs can be skipped by path filtering without blocking the PR.

Concurrency: Cancels previous runs on the same PR branch (ci-${{ github.ref }}).

Path filter groups

GroupPatterns
python**/*.py, pyproject.toml
shellscripts/**, .shellcheckrc
workflows.github/workflows/**

Tier 2: System CI (ci-system.yml)

Runs nightly at 03:00 UTC on the self-hosted GPU runner ([self-hosted, linux, gpu]). Executes the full test suite with no marker filter — a superset of Tier 1 plus the 32 system tests requiring real Docker, GPU, and sudo.

sudo /home/datasciencelab/anaconda3/bin/python -m pytest . -v --tb=short

On failure: checks for an existing open issue before creating a new one. If one exists, adds a comment instead.

Also supports workflow_call for use as a release gate if needed.

Release (release.yml)

Manual tag-triggered releases. No automated semantic-release.

Release process

  1. Update the VERSION file with the new version (e.g. 1.5.0)
  2. Commit: chore: bump version to 1.5.0
  3. Tag and push:
    git tag v1.5.0
    git push --tags
  4. Workflow validates:
    • Tag matches semver format (vX.Y.Z)
    • VERSION file matches the tag
  5. Creates GitHub Release with auto-generated notes

Can also be triggered via dispatch (enter tag manually).

Dependabot (.github/dependabot.yml)

Monthly updates, grouped as single PRs:

EcosystemWhat it updatesSchedule
github-actionsAction versions in workflowsMonthly (Monday 06:00 CET)
pipPython dependencies (pytest, pyyaml, ruff)Monthly (Monday 06:00 CET)

Minor + patch updates are grouped into a single PR per ecosystem.

Local development

Makefile

The Makefile mirrors CI locally. All developers should run make check before pushing.

make help # Show all targets
make check # Full CI locally (lint + test)
make lint # lint-python + lint-shell
make fmt # Auto-format everything (ruff + shfmt)
make test # pytest -m "not system" (unit + integration)
make test-all # sudo pytest (all tiers including system)

Pre-commit hooks (.pre-commit-config.yaml)

Installed with pre-commit install --hook-type commit-msg. Runs automatically on git commit.

HookWhat it does
validate-commit-messageConventional commit format + blocks AI attribution
trailing-whitespaceRemoves trailing whitespace
end-of-file-fixerEnsures files end with newline
check-yamlValidates YAML syntax
check-added-large-filesBlocks files > 500KB
check-merge-conflictDetects merge conflict markers
check-executables-have-shebangsShell script validation
no-commit-to-branchPrevents direct commits to main
shellcheckShell script linting (-x flag)
shfmtShell formatting (-i 4 -ci -s)
ruffPython linting + auto-fix
ruff-formatPython formatting

Configuration files

FilePurpose
.shellcheckrcShellcheck suppressions (SC1090, SC1091, SC2154, SC2034, SC2155)
.github/actionlint.yamlDeclares gpu as valid self-hosted runner label
pyproject.tomlRuff config (line-length 100, py310, isort)

Shellcheck suppressions

CodeReason
SC1090Can't follow non-constant source (dynamic paths)
SC1091Not following sourced files (unavailable at check time)
SC2154Variable referenced but not assigned (set via sourced init scripts)
SC2034Variable appears unused (used by callers of sourced libraries)
SC2155Declare and assign separately (pervasive local var=$(cmd) pattern)

Test structure

Three tiers, mapped to CI:

TierDirectoryCountMarkerRuns in
Unittests/unit/648unitTier 1 + Tier 2
Integrationtests/integration/143integrationTier 1 + Tier 2
Systemtests/system/32systemTier 2 only

Tier 1 runs pytest -m "not system" (791 tests). Tier 2 runs everything (823 tests).

See tests/README.md for test details.

Troubleshooting

CI gate job fails but individual jobs passed

Check if any individual job was cancelled (not just skipped). The gate job treats both failure and cancelled as failures.

Shellcheck fails on new code

Run locally: make lint-shell. Shellcheck reads .shellcheckrc for suppressions. If you get SC2155 (declare and assign separately), it's suppressed — ensure .shellcheckrc is present.

shfmt fails

Run make fmt-shell to auto-format, then commit the changes.

Tests fail locally but pass in CI

CI runs on Ubuntu with Python 3.13. Locally you may be using conda. Ensure pytest and pyyaml are installed in your active Python environment.

Nightly system CI creates duplicate issues

It shouldn't — the workflow checks for existing open issues with "System CI failed" before creating new ones. If duplicates appear, check the gh issue list --search logic in ci-system.yml.