compose-lens 0.4.0

Source-aware Rust library for Docker Compose parsing, processing, validation, editing, and rendering
Documentation
---
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 # v7.0.1
        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 # v7.0.1
        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 # v7.0.1
        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
        # renovate: datasource=crate depName=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 # v2.1.1
        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 # v2.9
        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 # v7.0.1
        with:
          fetch-depth: 0
          persist-credentials: false
      - name: Install pinned Node.js toolchain
        uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
        with:
          # renovate: datasource=node-version depName=node
          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 # v2.9.0
        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 # v7.0.1
        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 # v7.0.1
        with:
          repository: Strukturpiloten/.github
          # renovate: datasource=github-digest depName=Strukturpiloten/.github currentValue=main
          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 # v7.0.1
        with:
          persist-credentials: false
      - name: Check out trusted PR base aggregate verifier
        if: github.event_name == 'pull_request'
        uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        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