On PR → main:
feature-pull-request.yml
├─→ reusable-code-quality.yml
├─→ reusable-build.yml (push=false)
└─→ reusable-check.yml
On workflow_dispatch (Pre Release, any branch):
pre-release.yml
├─→ reusable-code-quality.yml
├─→ reusable-build.yml (push=true)
├─→ reusable-check.yml
└─→ reusable-release.yml (no GitHub Release)
├─→ push-dockerhub (independent)
├─→ push-ghcr (independent)
├─→ publish-pypi (TestPyPI, independent)
└─→ create-release-tag
On workflow_dispatch (Release, main only):
release.yml
├─→ reusable-code-quality.yml
├─→ reusable-build.yml (push=true)
├─→ reusable-check.yml
└─→ reusable-release.yml (creates GitHub Release)
├─→ push-dockerhub (independent)
├─→ push-ghcr (independent)
├─→ publish-pypi (PyPI, independent)
├─→ create-release-tag
└─→ github-release (runs if tag created, regardless of upload outcomes)
On workflow_dispatch (Helm Chart Release):
helm-release.yml
└─→ package + publish chart
On push feature/* or schedule:
codeql.yml
└─→ CodeQL analysis (Python + Actions)
| File | Purpose |
|---|---|
reusable-code-quality.yml |
Ruff, Pylint, MyPy, Bandit, pytest |
reusable-build.yml |
Version calc, Docker build, SBOM generation, Python package build |
reusable-check.yml |
Licence scan, Trivy vuln scan, container structure tests |
reusable-release.yml |
Independent upload jobs (DockerHub, GHCR, PyPI) + Git tag + GitHub Release |
-
Bash for version calculation — GitVersion (a .NET tool) was considered but rejected because it introduces a heavyweight external dependency. A short bash script that finds the latest semver tag in the appropriate namespace (
docker-v*,chart-v*), parses it, and applies the bump handles this transparently with zero external dependencies. -
YYYYMMDDhhmmssas ordering prefix — A fixed-length 14-character timestamp satisfies the spec's lexical monotone requirement because its fixed length guarantees that lexical sort equals chronological sort. The prefix is stripped from the human-readable GitHub Release name. -
Draft → Publish pattern for immutable releases — The spec requires asset attachment even when repository release immutability is enabled. The only way to satisfy both constraints is to create the release as
draft: truefirst (asset uploads are allowed on drafts), then finalize it with a separategh api PATCH draft=falsecall. This applies to bothreusable-release.ymlandhelm-release.yml. -
Feature branch releases always create a Git tag — Even when no GitHub Release entry is produced, a Git tag is required so the version calculator always has a valid baseline for the next run. The suffix order
rc.{branch-label}.{run_number}groups all builds from the same branch together and sorts them chronologically within that group. -
Transfer artifact pattern —
reusable-build.ymlsaves the Docker image and SBOM as GitHub Actions artifacts; downstream jobs (reusable-check.yml,reusable-release.yml) download them rather than rebuilding. This guarantees that the image that passed the check phase is byte-for-byte identical to the one released. -
Check phase: three independent job groups —
licence-check(Trivy licence scanner),security-check(Trivy vuln scan- SARIF upload), and
container-structure-testsrun in parallel. All three must succeed before any release job is triggered.
- SARIF upload), and
-
Bandit pass/fail via JSON post-processing — Bandit is invoked with
-f jsonso all findings are captured regardless of severity. A post-processing step logs everything (including LOW) but exits non-zero only for MEDIUM/HIGH. This avoids the-llflag, which would silently suppress LOW findings entirely. -
CodeQL exclusion via config file —
dev_environment/is excluded through.github/codeql/codeql-config.ymlrather than inlinepaths-ignorein the workflow, keeping the exclusion auditable alongside other tool configs. -
Helm chart version injected at package time —
Chart.yamlis never modified by CI. The authoritative version is the Git tag, injected viahelm package --versionat build time. This mirrors the hatch-vcs pattern used for Python packages. -
Change detection uses
dorny/paths-filter— Relevant paths:middleware/**,pyproject.toml,docker/**,scripts/**,.github/workflows/**. PRs that touch only docs, specs, or Helm YAML skip all CI jobs without consuming runner minutes. -
Required checks always produce a status via step-level
skipinput — GitHub required status checks block PR merges when the job is absent or skipped. The solution is askip: booleaninput onreusable-code-quality.ymlandreusable-check.yml. Whenskip: true, each job in those workflows runs but all substantive steps are guarded byif: ${{ !inputs.skip }}; only a single no-op echo step executes. The job completes with success and GitHub records the status. Non-required jobs (licence-check,security-check,build) retain their existingif:guards and may be skipped entirely.feature-pull-request.ymlalways calls both required-check workflows and passesskip: ${{ needs.detect-changes.outputs.code != 'true' }}. -
Upload jobs: no cross-dependency —
push-dockerhub,push-ghcr, andpublish-pypihave noneedsdependency on each other; they run in parallel.github-releaseusesif: always() && needs.create-release-tag.result == 'success'so the GitHub Release is created regardless of which uploads succeeded. The release body is generated dynamically from the individual job results. -
Python package distribution names differ from uv workspace names — The uv workspace uses short internal identifiers (
shared,api_client). The PyPI distribution names (fairagro-middleware-shared,fairagro-middleware-api-client) are globally namespaced for uniqueness. The import path (middleware.shared,middleware.api_client) is unaffected because it is controlled separately by[tool.hatch.build.targets.wheel] packagesin eachpyproject.toml. -
PEP 440 parallel version for Python packages — Docker semver pre-release format (
1.2.3-rc.branch.42) is not valid PEP 440. The build phase computes a parallelpep440_versionin the format1.2.3.dev42and injects it viaSETUPTOOLS_SCM_PRETEND_VERSIONto override hatch-vcs version discovery, so Docker and Python packages share the same numeric baseline. — This simple.devNformat was chosen for maximum compatibility with both hatchling and PyPI, using a global run number for uniqueness across all branches. -
Python packages built once in the build phase, reused in release —
reusable-build.ymlincludes apython-buildjob that produces wheels and sdists for both publishable packages and uploads them as the artifactpython-packages-{version}. This mirrors the Docker transfer-artifact pattern (Decision 5): the artifact that passed the check phase is the one that gets published.