---
name: CI
on:
push:
branches:
- main
pull_request:
workflow_dispatch:
workflow_call:
permissions:
contents: read
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
CARGO_TERM_COLOR: always
jobs:
validation-plan:
name: Validation plan
runs-on: ubuntu-24.04
timeout-minutes: 5
outputs:
plan: ${{ steps.plan.outputs.plan }}
profile: ${{ steps.plan.outputs.profile }}
select_rust: ${{ steps.plan.outputs.select_rust }}
select_msrv: ${{ steps.plan.outputs.select_msrv }}
select_dependencies: ${{ steps.plan.outputs.select_dependencies }}
select_documentation: ${{ steps.plan.outputs.select_documentation }}
select_api: ${{ steps.plan.outputs.select_api }}
select_coverage: ${{ steps.plan.outputs.select_coverage }}
select_lockfile_release_age: ${{ steps.plan.outputs.select_lockfile_release_age }}
steps:
- name: Check out exact tested revision and comparison history
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 with:
fetch-depth: 0
persist-credentials: false
- name: Check out trusted PR base validation policy
if: github.event_name == 'pull_request'
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 with:
ref: ${{ github.event.pull_request.base.sha }}
path: .validation-base
persist-credentials: false
- name: Select exact-revision validation plan
id: plan
shell: bash
env:
EVENT_NAME: ${{ github.event_name }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
set -euo pipefail
if [[ "${EVENT_NAME}" == 'pull_request' ]]; then
if [[ -f .validation-base/scripts/validation-plan.py &&
-f .validation-base/scripts/validation-policy.json ]]; then
python3 .validation-base/scripts/validation-plan.py plan \
--repository-root "${GITHUB_WORKSPACE}" \
--policy-root .validation-base \
--event pull_request --base-sha "${BASE_SHA}" --head-sha "${HEAD_SHA}" \
--tested-sha "${GITHUB_SHA}" --github-output "${GITHUB_OUTPUT}"
else
# First rollout: never execute a PR-head classifier without trusted base policy.
python3 - <<'PY'
import json
import os
import re
jobs = "rust msrv dependencies api documentation coverage lockfile-release-age".split()
base, head, tested = (os.environ[key] for key in ("BASE_SHA", "HEAD_SHA", "GITHUB_SHA"))
if any(not re.fullmatch(r"[0-9a-f]{40}", value) for value in (base, head, tested)):
raise SystemExit("first-rollout comparison has an invalid SHA")
plan = {"schema": 2, "repository": "Strukturpiloten/compose-lens", "event": "pull_request",
"base_sha": base, "head_sha": head, "tested_sha": tested, "profile": "full",
"jobs": {job: True for job in jobs}, "reasons": ["trusted base has no classifier"],
"local_fingerprint": None}
with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as output:
output.write("plan=" + json.dumps(plan, sort_keys=True, separators=(",", ":")) + "\nprofile=full\n")
for job in jobs:
output.write("select_" + job.replace("-", "_") + "=true\n")
PY
fi
else
python3 scripts/validation-plan.py plan --event "${EVENT_NAME}" \
--tested-sha "${GITHUB_SHA}" --github-output "${GITHUB_OUTPUT}"
fi
rust:
name: Rust quality
runs-on: ubuntu-24.04
needs: validation-plan
if: needs.validation-plan.outputs.select_rust == 'true'
timeout-minutes: 30
steps:
- &checkout
name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 with:
persist-credentials: false
- name: Check pip-capable Python venv
run: bash .devcontainer/verify-tools.sh --check-python-venv
- name: Install configured Rust toolchain
run: rustup show active-toolchain
- name: Check formatting
run: cargo fmt --all -- --check
- name: Check all targets
run: cargo ci-check
- name: Check repository policies
run: cargo ci-policy
- name: Run Clippy
run: cargo ci-clippy
- name: Run tests
run: cargo ci-test
- name: Check real-application conformance contracts
run: cargo ci-application
- name: Run documentation tests
run: cargo ci-doctest
- name: Build documentation
env:
RUSTDOCFLAGS: -D warnings
run: cargo ci-doc
- name: Test provider Python bootstrap
run: bash scripts/test-provider-python-bootstrap.sh
- name: Verify release package
run: cargo ci-package
coverage:
name: Coverage ratchet
runs-on: ubuntu-24.04
needs: validation-plan
if: needs.validation-plan.outputs.select_coverage == 'true'
timeout-minutes: 30
steps:
- *checkout
- name: Install LLVM coverage tools
run: rustup component add llvm-tools-preview
- name: Install pinned cargo-llvm-cov
run: cargo install --locked --version 0.9.0 cargo-llvm-cov
- name: Clean coverage artifacts
run: cargo llvm-cov clean --locked
- name: Measure and enforce coarse coverage ratchet
run: >-
cargo llvm-cov --locked --no-clean --workspace --all-features --all-targets --summary-only
--fail-under-regions 88 --fail-under-functions 87 --fail-under-lines 89
msrv:
name: MSRV
runs-on: ubuntu-24.04
needs: validation-plan
if: needs.validation-plan.outputs.select_msrv == 'true'
timeout-minutes: 20
steps:
- *checkout
- name: Read the workspace MSRV
id: metadata
shell: bash
run: |
msrv="$(
cargo metadata --locked --no-deps --format-version 1 |
jq -er '[.packages[].rust_version] | unique | if length == 1 and .[0] != null then .[0] else error("workspace packages must declare one common rust-version") end'
)"
printf 'msrv=%s\n' "$msrv" >> "$GITHUB_OUTPUT"
- name: Install minimum supported Rust
env:
RUST_MSRV: ${{ steps.metadata.outputs.msrv }}
run: rustup toolchain install "${RUST_MSRV}" --profile minimal
- name: Check all targets with the MSRV
env:
RUST_MSRV: ${{ steps.metadata.outputs.msrv }}
run: cargo "+${RUST_MSRV}" ci-check
- name: Check repository policies with the MSRV
env:
RUST_MSRV: ${{ steps.metadata.outputs.msrv }}
run: cargo "+${RUST_MSRV}" ci-policy
dependencies:
name: Dependency and license policy
runs-on: ubuntu-24.04
needs: validation-plan
if: needs.validation-plan.outputs.select_dependencies == 'true'
timeout-minutes: 15
steps:
- *checkout
- name: Audit dependencies, licenses, bans, and sources
uses: EmbarkStudios/cargo-deny-action@3c6349835b2b7b196a839186cb8b78e02f7b5f25 with:
command: check
arguments: --all-features
api:
name: Public API compatibility
runs-on: ubuntu-24.04
needs: validation-plan
if: needs.validation-plan.outputs.select_api == 'true'
timeout-minutes: 20
steps:
- *checkout
- name: Compare with the latest published release
uses: obi1kenobi/cargo-semver-checks-action@6b69fcf40e9b5fb17adeb57e4b6ecd020649a239 with:
package: compose-lens
documentation:
name: Repository files and documentation
runs-on: ubuntu-24.04
needs: validation-plan
if: needs.validation-plan.outputs.select_documentation == 'true'
timeout-minutes: 15
steps:
- name: Check out repository with release history
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 with:
fetch-depth: 0
persist-credentials: false
- name: Install pinned Node.js toolchain
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 with:
node-version: 24.21.0
cache: npm
cache-dependency-path: package-lock.json
- name: Install locked Node file-quality tools
run: |
npm ci --ignore-scripts
printf '%s\n' "${GITHUB_WORKSPACE}/node_modules/.bin" >> "${GITHUB_PATH}"
- name: Install checksum-pinned native file-quality tools
run: sudo bash scripts/install-file-tools.sh /usr/local/bin
- name: Check non-Rust file formatting and lint
run: bash scripts/check-files.sh --check
- name: Check release metadata and changelog
run: bash scripts/check-release-metadata.sh
- name: Test validation-plan regressions
run: PYTHONDONTWRITEBYTECODE=1 python3 scripts/test-validation-plan.py
- name: Check local documentation links without network access
uses: lycheeverse/lychee-action@e7477775783ea5526144ba13e8db5eec57747ce8 with:
args: >-
--config lychee.toml --root-dir '${{ github.workspace }}' --offline
'./**/*.md'
fail: true
token: ${{ secrets.GITHUB_TOKEN }}
lockfile-release-age:
name: Lockfile release age
runs-on: ubuntu-24.04
needs: validation-plan
if: needs.validation-plan.outputs.select_lockfile_release_age == 'true'
timeout-minutes: 5
steps:
- name: Preserve the aggregate result outside pull requests
if: github.event_name != 'pull_request'
run: echo 'Lockfile release-age comparison applies to pull requests.'
- name: Check out the exact pull-request candidate and base history
if: github.event_name == 'pull_request'
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false
- name: Check out the shared lockfile policy
if: github.event_name == 'pull_request'
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 with:
repository: Strukturpiloten/.github
ref: e362e4564f8e3c1d9d113242e3bf34011f2d7cd7
path: .github/strukturpiloten-shared
persist-credentials: false
- name: Reject newly introduced dependencies younger than three days
if: github.event_name == 'pull_request'
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: >-
python3 .github/strukturpiloten-shared/scripts/lockfile_release_age.py
--repository-root "${GITHUB_WORKSPACE}"
--base "${BASE_SHA}"
--head "${HEAD_SHA}"
--minimum-age-hours 72
pr-gate:
name: Required CI
if: always()
needs:
[
validation-plan,
rust,
msrv,
dependencies,
api,
documentation,
coverage,
lockfile-release-age,
]
runs-on: ubuntu-24.04
steps:
- name: Check out candidate aggregate verifier
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 with:
persist-credentials: false
- name: Check out trusted PR base aggregate verifier
if: github.event_name == 'pull_request'
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 with:
ref: ${{ github.event.pull_request.base.sha }}
path: .validation-base
persist-credentials: false
- name: Require every selected job to succeed and every skip to be planned
shell: bash
env:
EVENT_NAME: ${{ github.event_name }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
PLAN_JSON: ${{ needs.validation-plan.outputs.plan }}
NEEDS_JSON: ${{ toJSON(needs) }}
run: |
set -euo pipefail
if [[ "${EVENT_NAME}" == 'pull_request' &&
( ! -f .validation-base/scripts/validation-plan.py ||
! -f .validation-base/scripts/validation-policy.json ) ]]; then
# Bootstrap has no trusted verifier. Require the literal complete plan and all jobs.
python3 - <<'PY' | tee -a "${GITHUB_STEP_SUMMARY}"
import json
import os
jobs = "rust msrv dependencies api documentation coverage lockfile-release-age".split()
plan = json.loads(os.environ["PLAN_JSON"])
needs = json.loads(os.environ["NEEDS_JSON"])
if (set(plan) != {"schema", "repository", "event", "base_sha", "head_sha", "tested_sha",
"profile", "jobs", "reasons", "local_fingerprint"}
or plan["schema"] != 2 or plan["repository"] != "Strukturpiloten/compose-lens"
or plan["event"] != "pull_request" or plan["profile"] != "full"
or plan["base_sha"] != os.environ["BASE_SHA"]
or plan["head_sha"] != os.environ["HEAD_SHA"]
or plan["tested_sha"] != os.environ["GITHUB_SHA"]
or plan["jobs"] != {job: True for job in jobs}
or set(needs) != {"validation-plan", *jobs}):
raise SystemExit("first-rollout complete validation contract is missing or inconsistent")
for job in ("validation-plan", *jobs):
if needs[job].get("result") != "success":
raise SystemExit(f"first-rollout required job {job} did not succeed")
print("First-rollout complete validation: every required job succeeded")
PY
else
verifier=scripts/validation-plan.py
policy_root=.
if [[ "${EVENT_NAME}" == 'pull_request' ]]; then
verifier=.validation-base/scripts/validation-plan.py
policy_root=.validation-base
fi
python3 "${verifier}" gate --policy-root "${policy_root}" \
--event "${EVENT_NAME}" --base-sha "${BASE_SHA}" --head-sha "${HEAD_SHA}" \
--tested-sha "${GITHUB_SHA}" \
--plan-json "${PLAN_JSON}" --needs-json "${NEEDS_JSON}" |
tee -a "${GITHUB_STEP_SUMMARY}"
fi