ReleaseBoard ships with a production-grade .gitlab-ci.yml pipeline that lints, tests, builds, and publishes the project on every push and merge request.
- Pipeline Overview
- Stage Diagram
- Stages in Detail
- Configuration Variables
- Caching
- Artifacts
- Coverage Badges
- Merge Request Integration
- Customization
- Troubleshooting
The .gitlab-ci.yml at the repository root defines a four-stage pipeline:
| Stage | Job | Image | Purpose |
|---|---|---|---|
| lint | ruff-lint |
python:3.12-slim |
Static analysis with ruff — fails fast on code quality issues |
| test | test (matrix) |
python:3.12-slim, python:3.13-slim |
Runs the full pytest suite with coverage on Python 3.12 and 3.13 |
| build | build-wheel |
python:3.12-slim |
Builds the sdist + wheel and validates the CLI against an example config |
| pages | pages |
python:3.12-slim |
Deploys docs/ to GitLab Pages (main branch only) |
The pipeline triggers on merge request events and pushes to the default branch via workflow: rules.
┌─────────┐ ┌──────────────────┐ ┌─────────────┐ ┌───────┐
│ lint │────▶│ test │────▶│ build │────▶│ pages │
│ │ │ (3.12 + 3.13) │ │ │ │ │
│ ruff │ │ pytest + cov │ │ wheel + CLI │ │ docs │
│ check │ │ │ │ validation │ │ deploy│
└─────────┘ └──────────────────┘ └─────────────┘ └───────┘
main only
ruff-lint:
stage: lint
image: python:3.12-slim
before_script:
- pip install --quiet ruff
script:
- ruff check src/ tests/- Runs
ruff checkagainst source and test directories. - Fails fast — no point running tests if the code doesn't pass linting.
- Uses the project's
[tool.ruff]configuration frompyproject.toml.
test:
stage: test
image: python:${PYTHON_VERSION}-slim
parallel:
matrix:
- PYTHON_VERSION: ["3.12", "3.13"]
before_script:
- pip install --quiet -e ".[dev]"
script:
- pytest --cov=releaseboard --cov-report=term-missing
--cov-report=xml:coverage.xml --junitxml=report.xml -q- Matrix build across Python 3.12 and 3.13.
- The main test suite runs without ReleasePilot installed. An optional
test-releasepilotjob (withallow_failure: true) tests the ReleasePilot integration separately — see the pipeline definition for details. - Produces
coverage.xml(Cobertura format) andreport.xml(JUnit format). - GitLab automatically parses both reports for the merge request UI.
- The
coverageregex extracts the total percentage for the pipeline badge. - Artifacts expire after 30 days.
build-wheel:
stage: build
image: python:3.12-slim
before_script:
- pip install --quiet build
- pip install --quiet -e .
script:
- python -m build
- releaseboard validate --config examples/config.json- Builds both sdist and wheel via PEP 517.
- Validates the installed CLI works by running
releaseboard validateagainst the example config. dist/artifacts are retained for 90 days.
pages:
stage: pages
image: python:3.12-slim
script:
- mkdir -p public
- cp -r docs/* public/
artifacts:
paths:
- public
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH- Copies
docs/intopublic/for GitLab Pages. - Only runs on the default branch (typically
main). - After deployment, docs are accessible at your project's GitLab Pages URL.
The pipeline defines these variables globally:
| Variable | Value | Purpose |
|---|---|---|
PYTHONDONTWRITEBYTECODE |
1 |
Prevents .pyc file creation in containers |
PIP_CACHE_DIR |
$CI_PROJECT_DIR/.pip-cache |
Keeps pip downloads inside the project for caching |
PYTHONPATH |
src |
Ensures src/ layout is importable without install |
You can override any variable in Settings → CI/CD → Variables or in your fork's .gitlab-ci.yml.
default:
cache:
key: pip-${CI_COMMIT_REF_SLUG}
paths:
- .pip-cache/
policy: pull-push- Per-branch cache key — avoids cache pollution between branches.
- The
.pip-cache/directory stores downloaded pip packages. pull-pushpolicy means every job reads from and writes to the cache.
| Job | Artifact | Retention | Format |
|---|---|---|---|
test |
coverage.xml |
30 days | Cobertura XML |
test |
report.xml |
30 days | JUnit XML |
build-wheel |
dist/ |
90 days | sdist + wheel |
pages |
public/ |
— | GitLab Pages |
Test artifacts are uploaded even on failure (when: always) so you can debug failing runs.
To enable a coverage badge in GitLab:
-
Go to Settings → CI/CD → General pipelines
-
Under Test coverage parsing, set the regex to:
TOTAL.*\s(\d+%)$ -
Save. The badge will appear on the project page and can be embedded in your README:

-
For the pipeline status badge:

Note: The
testjob already includes thecoverage:key with the regex, so GitLab parses it automatically.
On merge requests the pipeline runs all four stages (except pages). GitLab will:
- Show ruff lint results inline if the job fails.
- Display the JUnit test report in the MR widget with pass/fail counts.
- Show Cobertura coverage diffs inline on changed files.
- Display the coverage percentage change between the MR and target branch.
To require the pipeline to pass before merging, enable Merge checks → Pipelines must succeed in project settings.
To deploy the built wheel to a PyPI registry, add a stage after build:
stages:
- lint
- test
- build
- deploy # ← add
- pages
deploy-pypi:
stage: deploy
image: python:3.12-slim
dependencies:
- build-wheel
before_script:
- pip install --quiet twine
script:
- twine upload dist/*
rules:
- if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/
variables:
TWINE_USERNAME: __token__
TWINE_PASSWORD: $PYPI_TOKENNote: Store
PYPI_TOKENas a masked CI/CD variable.
For nightly readiness reports, create a scheduled pipeline:
-
Navigate to CI/CD → Schedules in your GitLab project.
-
Create a new schedule (e.g., daily at 06:00 UTC).
-
Add
workflow: rulesforscheduleif needed:workflow: rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH - if: $CI_PIPELINE_SOURCE == "schedule" # ← add
To add Python 3.14 (or any future version):
test:
parallel:
matrix:
- PYTHON_VERSION: ["3.12", "3.13", "3.14"]To allow a new Python version to fail without blocking the pipeline:
test-py314:
extends: test
variables:
PYTHON_VERSION: "3.14"
allow_failure: true| Issue | Solution |
|---|---|
ruff not found |
The ruff-lint job installs ruff in before_script. Ensure the runner has internet access. |
| Test failures on 3.13 only | Check for version-specific API changes. Run python3.13 -m pytest locally to reproduce. |
| Cache not speeding up jobs | Verify .pip-cache/ exists. Check CI/CD → Pipelines → Job for cache hit/miss messages. |
| Coverage not showing in MR | Ensure the regex TOTAL.*\s(\d+%)$ is set in Settings → CI/CD → General pipelines. |
| Pages not updating | The pages job only runs on the default branch. Check that the public/ artifact is produced. |
Build fails on releaseboard validate |
Ensure examples/config.json exists and is a valid configuration file. See configuration.md. |
| Pip install timeouts | Increase pip timeout: add PIP_TIMEOUT: "120" to variables. |
| Duplicate pipelines (MR + push) | The workflow: rules block prevents this — MR pipelines take precedence when both apply. |
Note: For detailed configuration options, see configuration.md. For schema reference, see schema.md.