Merge pull request #695 from OtowoOrg/dependabot/docker/lukemathwalke… #1130
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI/CD Pipeline | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| branches: [main] | |
| concurrency: | |
| group: ci-${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| env: | |
| CARGO_TERM_COLOR: always | |
| RUST_BACKTRACE: 1 | |
| SHELLCHECK_EXCLUDES: "" | |
| REGISTRY: ghcr.io | |
| IMAGE_NAME: ${{ github.repository }} | |
| # ────────────────────────────────────────────────────────────────────────────── | |
| # Pipeline architecture | |
| # ────────────────────────────────────────────────────────────────────────────── | |
| # changes ──┬── lint ──────────────────────────────────────────────────────────┐ | |
| # ├── security-audit ──────────────────────────────────────────────┐ │ | |
| # ├── helm-lint (helm changes only) │ │ | |
| # ├── examples-smoke-test (examples changes only) │ │ | |
| # └── api-docs (api_docs changes only) │ │ | |
| # ▼ ▼ | |
| # test ─── coverage | |
| # │ | |
| # build (main only) | |
| # │ | |
| # docker (main only) | |
| # │ | |
| # security-scan | |
| # ────────────────────────────────────────────────────────────────────────────── | |
| jobs: | |
| # ── 1. Change detection ───────────────────────────────────────────────────── | |
| changes: | |
| name: Detect Changes | |
| runs-on: ubuntu-latest | |
| outputs: | |
| docker: ${{ steps.detect.outputs.docker }} | |
| helm: ${{ steps.detect.outputs.helm }} | |
| api_docs: ${{ steps.detect.outputs.api_docs }} | |
| rust_core: ${{ steps.detect.outputs.rust_core }} | |
| deps: ${{ steps.detect.outputs.deps }} | |
| examples: ${{ steps.detect.outputs.examples }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Detect changed file groups | |
| id: detect | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if [[ "${{ github.event_name }}" == "pull_request" ]]; then | |
| BASE_SHA="${{ github.event.pull_request.base.sha }}" | |
| HEAD_SHA="${{ github.event.pull_request.head.sha }}" | |
| git fetch --no-tags --depth=1 origin "$BASE_SHA" | |
| git fetch --no-tags --depth=1 origin "$HEAD_SHA" | |
| else | |
| BASE_SHA="${{ github.event.before }}" | |
| HEAD_SHA="${{ github.sha }}" | |
| fi | |
| # First push on a new branch has no before SHA — treat as full change | |
| if [[ -z "$BASE_SHA" || "$BASE_SHA" == "0000000000000000000000000000000000000000" ]]; then | |
| for key in docker helm api_docs rust_core deps examples; do | |
| echo "${key}=true" >> "$GITHUB_OUTPUT" | |
| done | |
| exit 0 | |
| fi | |
| CHANGED=$(git diff --name-only "$BASE_SHA" "$HEAD_SHA" || true) | |
| echo "Changed files:" | |
| echo "$CHANGED" | |
| _match() { echo "$CHANGED" | grep -Eq "$1" && echo "true" || echo "false"; } | |
| echo "docker=$(_match \ | |
| '^(Dockerfile|\.dockerignore|Cargo\.toml|Cargo\.lock|src/|config/|Makefile)')" \ | |
| >> "$GITHUB_OUTPUT" | |
| echo "helm=$(_match \ | |
| '^(charts/|config/crd/|config/samples/|examples/|Makefile)')" \ | |
| >> "$GITHUB_OUTPUT" | |
| echo "api_docs=$(_match \ | |
| '^(docs/|scripts/generate-api-docs\.py|config/crd/|src/crd/|src/rest_api/|Makefile)')" \ | |
| >> "$GITHUB_OUTPUT" | |
| echo "rust_core=$(_match \ | |
| '^(src/|Cargo\.toml|Cargo\.lock|Makefile|build\.rs)')" \ | |
| >> "$GITHUB_OUTPUT" | |
| echo "deps=$(_match '^(Cargo\.toml|Cargo\.lock)')" >> "$GITHUB_OUTPUT" | |
| echo "examples=$(_match \ | |
| '^(examples/|config/crd/|config/samples/)')" \ | |
| >> "$GITHUB_OUTPUT" | |
| # ── 2. Version consistency (main push only) ────────────────────────────────── | |
| version-consistency: | |
| name: Version Consistency Check | |
| if: github.event_name != 'pull_request' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Setup Rust | |
| uses: ./.github/actions/setup-rust | |
| with: | |
| cache-key: "ci-version" | |
| - name: Verify Cargo.toml and binary version match | |
| run: | | |
| set -euo pipefail | |
| CARGO_VERSION=$(grep '^version = ' Cargo.toml | head -1 | cut -d'"' -f2) | |
| EXPECTED_VERSION="${EXPECTED_RELEASE_VERSION:-}" | |
| if [[ -z "$EXPECTED_VERSION" && "${GITHUB_REF:-}" =~ ^refs/tags/v ]]; then | |
| EXPECTED_VERSION="${GITHUB_REF#refs/tags/v}" | |
| fi | |
| EXPECTED_VERSION="${EXPECTED_VERSION:-$CARGO_VERSION}" | |
| K8S_OPENAPI_ENABLED_VERSION=1.30 cargo build --locked --bin stellar-operator | |
| BIN_VERSION=$(./target/debug/stellar-operator version \ | |
| | sed -n 's/^Stellar-K8s Operator v//p' | head -1) | |
| echo "Expected: $EXPECTED_VERSION | Cargo: $CARGO_VERSION | Binary: $BIN_VERSION" | |
| [[ "$CARGO_VERSION" == "$EXPECTED_VERSION" ]] || { | |
| echo "::error::Cargo.toml version ($CARGO_VERSION) != expected ($EXPECTED_VERSION)" | |
| exit 1 | |
| } | |
| [[ "$BIN_VERSION" == "$EXPECTED_VERSION" ]] || { | |
| echo "::error::Binary version ($BIN_VERSION) != expected ($EXPECTED_VERSION)" | |
| exit 1 | |
| } | |
| # ── 3. Lint & Format ───────────────────────────────────────────────────────── | |
| lint: | |
| name: Lint & Format | |
| runs-on: ubuntu-latest | |
| needs: [changes] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Setup Rust | |
| uses: ./.github/actions/setup-rust | |
| with: | |
| cache-key: "ci-lint" | |
| toolchain: "1.92" | |
| components: "rustfmt clippy" | |
| - name: Install shellcheck | |
| run: sudo apt-get install -y shellcheck | |
| - name: Run shellcheck | |
| run: | | |
| mapfile -t shell_files < <(find scripts -type f -name "*.sh" 2>/dev/null || true) | |
| if [[ "${#shell_files[@]}" -eq 0 ]]; then | |
| echo "No shell scripts found — skipping shellcheck" | |
| exit 0 | |
| fi | |
| shellcheck_cmd=(shellcheck -S error) | |
| [[ -n "${SHELLCHECK_EXCLUDES}" ]] && shellcheck_cmd+=(-e "${SHELLCHECK_EXCLUDES}") | |
| "${shellcheck_cmd[@]}" "${shell_files[@]}" | |
| - name: Check formatting | |
| if: needs.changes.outputs.rust_core == 'true' | |
| run: make fmt-check | |
| - name: Run clippy | |
| if: needs.changes.outputs.rust_core == 'true' | |
| run: make lint | |
| # ── 4. Security audit (only when deps change) ──────────────────────────────── | |
| security-audit: | |
| name: Security Audit | |
| if: needs.changes.outputs.deps == 'true' | |
| runs-on: ubuntu-latest | |
| needs: [changes] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Setup Rust | |
| uses: ./.github/actions/setup-rust | |
| with: | |
| cache-key: "ci-audit" | |
| - name: Run cargo audit | |
| run: | | |
| cargo install cargo-audit --locked | |
| cargo audit \ | |
| --ignore RUSTSEC-2020-0071 \ | |
| --ignore RUSTSEC-2024-0380 \ | |
| --ignore RUSTSEC-2024-0381 \ | |
| --ignore RUSTSEC-2026-0085 \ | |
| --ignore RUSTSEC-2026-0086 \ | |
| --ignore RUSTSEC-2026-0087 \ | |
| --ignore RUSTSEC-2026-0091 \ | |
| --ignore RUSTSEC-2026-0094 \ | |
| --ignore RUSTSEC-2026-0097 \ | |
| --ignore RUSTSEC-2026-0098 \ | |
| --ignore RUSTSEC-2026-0099 \ | |
| --ignore RUSTSEC-2026-0104 \ | |
| --ignore RUSTSEC-2026-0149 | |
| # ── 5. Helm lint (only when helm/chart files change) ───────────────────────── | |
| helm-lint: | |
| name: Helm Lint & Schema Validation | |
| if: needs.changes.outputs.helm == 'true' | |
| runs-on: ubuntu-latest | |
| needs: [changes] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Install Helm | |
| uses: azure/setup-helm@v4 | |
| with: | |
| version: "3.14.0" | |
| - name: Lint chart (default values) | |
| run: make helm-lint | |
| - name: Lint chart (strict) | |
| run: helm lint charts/stellar-operator --strict | |
| - name: Schema rejects invalid logLevel | |
| run: | | |
| printf 'operator:\n logLevel: verbose\n' > /tmp/bad-loglevel.yaml | |
| helm lint charts/stellar-operator -f /tmp/bad-loglevel.yaml 2>&1 \ | |
| | grep -q "must be one of" \ | |
| && echo "Schema correctly rejected invalid logLevel" \ | |
| || { echo "ERROR: Schema did not reject invalid logLevel"; exit 1; } | |
| - name: Schema rejects invalid service type | |
| run: | | |
| printf 'service:\n type: ExternalName\n' > /tmp/bad-service.yaml | |
| helm lint charts/stellar-operator -f /tmp/bad-service.yaml 2>&1 \ | |
| | grep -q "must be one of" \ | |
| && echo "Schema correctly rejected invalid service type" \ | |
| || { echo "ERROR: Schema did not reject invalid service type"; exit 1; } | |
| - name: Schema rejects invalid pullPolicy | |
| run: | | |
| printf 'image:\n pullPolicy: Whenever\n' > /tmp/bad-pullpolicy.yaml | |
| helm lint charts/stellar-operator -f /tmp/bad-pullpolicy.yaml 2>&1 \ | |
| | grep -q "must be one of" \ | |
| && echo "Schema correctly rejected invalid pullPolicy" \ | |
| || { echo "ERROR: Schema did not reject invalid pullPolicy"; exit 1; } | |
| # ── 6. Helm unit tests ──────────────────────────────────────────────────────── | |
| helm-test: | |
| name: Helm Unit Tests | |
| if: needs.changes.outputs.helm == 'true' | |
| runs-on: ubuntu-latest | |
| needs: [changes, lint] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Install Helm | |
| uses: azure/setup-helm@v4 | |
| with: | |
| version: v3.14.0 | |
| - name: Install helm-unittest plugin | |
| run: helm plugin install https://github.com/helm-unittest/helm-unittest.git --version v0.5.1 | |
| - name: Run Helm unit tests | |
| run: helm unittest charts/stellar-operator --strict --color | |
| # ── 7. API docs drift check ─────────────────────────────────────────────────── | |
| api-docs: | |
| name: API Docs Drift Check | |
| if: needs.changes.outputs.api_docs == 'true' | |
| runs-on: ubuntu-latest | |
| needs: [changes, lint] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - name: Install PyYAML | |
| run: pip install pyyaml | |
| - name: Regenerate API docs | |
| run: python3 scripts/generate-api-docs.py | |
| - name: Fail if docs are out of date | |
| run: | | |
| if ! git diff --exit-code docs/api-reference.md; then | |
| echo "ERROR: docs/api-reference.md is out of date." | |
| echo "Run 'make generate-api-docs' and commit the result." | |
| exit 1 | |
| fi | |
| echo "OK: docs/api-reference.md is up to date." | |
| # ── 8. Examples smoke test ──────────────────────────────────────────────────── | |
| examples-smoke-test: | |
| name: Examples Smoke Test | |
| if: needs.changes.outputs.examples == 'true' | |
| runs-on: ubuntu-latest | |
| needs: [changes] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Create kind cluster | |
| uses: helm/kind-action@v1.14.0 | |
| with: | |
| cluster_name: examples-test | |
| - name: Apply StellarNode CRD | |
| run: kubectl apply -f config/crd/stellarnode-crd.yaml | |
| - name: Run smoke test on examples | |
| run: | | |
| for ns in stellar stellar-nodes stellar-system metallb-system; do | |
| kubectl create namespace "$ns" --dry-run=client -o yaml | kubectl apply -f - | |
| done | |
| for f in validator-mainnet validator-testnet horizon soroban-rpc dr-setup; do | |
| kubectl apply --dry-run=server -f "examples/${f}.yaml" | |
| done | |
| # ── 9. Tests (unit + doc) ───────────────────────────────────────────────────── | |
| # security-audit is conditional (only runs when deps change). We use | |
| # `if: always()` + an explicit check so test/coverage still run when | |
| # security-audit was skipped, but are blocked when it actually failed. | |
| test: | |
| name: Test | |
| runs-on: ubuntu-latest | |
| needs: [security-audit, lint] | |
| if: >- | |
| always() && | |
| needs.lint.result == 'success' && | |
| (needs.security-audit.result == 'success' || needs.security-audit.result == 'skipped') | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Setup Rust | |
| uses: ./.github/actions/setup-rust | |
| with: | |
| cache-key: "ci-test" | |
| - name: Run tests | |
| run: make test | |
| # ── 10. Code coverage ───────────────────────────────────────────────────────── | |
| coverage: | |
| name: Code Coverage | |
| runs-on: ubuntu-latest | |
| needs: [security-audit, lint] | |
| if: >- | |
| always() && | |
| needs.lint.result == 'success' && | |
| (needs.security-audit.result == 'success' || needs.security-audit.result == 'skipped') | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Setup Rust | |
| uses: ./.github/actions/setup-rust | |
| with: | |
| cache-key: "ci-coverage" | |
| - name: Install tarpaulin | |
| run: cargo install cargo-tarpaulin | |
| - name: Generate coverage | |
| run: | | |
| cargo tarpaulin \ | |
| --out Xml \ | |
| --timeout 300 \ | |
| --exclude-files tests/* \ | |
| --ignore-panics \ | |
| --ignore-tests | |
| - name: Upload coverage to Codecov | |
| uses: codecov/codecov-action@v4 | |
| with: | |
| files: ./cobertura.xml | |
| flags: unittests | |
| name: codecov-umbrella | |
| fail_ci_if_error: false | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| # ── 11. Release build (main push only) ─────────────────────────────────────── | |
| build: | |
| name: Build | |
| if: github.event_name != 'pull_request' | |
| runs-on: ubuntu-latest | |
| needs: test | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Setup Rust | |
| uses: ./.github/actions/setup-rust | |
| with: | |
| cache-key: "ci-build" | |
| - name: Build release | |
| run: make build | |
| - name: Upload binary artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: stellar-k8s-binaries | |
| path: | | |
| target/release/stellar-operator | |
| target/release/kubectl-stellar | |
| retention-days: 7 | |
| # ── 11. Docker build & push (main push only) ────────────────────────────────── | |
| docker: | |
| name: Docker Build | |
| if: >- | |
| github.event_name != 'pull_request' && | |
| needs.changes.outputs.docker == 'true' && | |
| (needs.helm-test.result == 'success' || needs.helm-test.result == 'skipped') && | |
| (needs.api-docs.result == 'success' || needs.api-docs.result == 'skipped') | |
| runs-on: ubuntu-latest | |
| needs: [changes, build, helm-test, api-docs] | |
| env: | |
| DOCKER_PLATFORMS: linux/amd64 | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Lowercase IMAGE_NAME | |
| run: echo "IMAGE_NAME=$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_ENV" | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Log in to GHCR | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Extract metadata | |
| id: meta | |
| uses: docker/metadata-action@v5 | |
| with: | |
| images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} | |
| tags: | | |
| type=ref,event=branch | |
| type=sha,prefix=sha-,format=long | |
| type=raw,value=latest,enable={{is_default_branch}} | |
| - name: Build and push | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: . | |
| target: runtime | |
| platforms: ${{ env.DOCKER_PLATFORMS }} | |
| push: true | |
| tags: ${{ steps.meta.outputs.tags }} | |
| labels: ${{ steps.meta.outputs.labels }} | |
| cache-from: type=gha,scope=stellar-k8s-docker | |
| cache-to: type=gha,mode=max,scope=stellar-k8s-docker | |
| provenance: false | |
| sbom: false | |
| build-args: | | |
| BUILDKIT_INLINE_CACHE=1 | |
| # ── 12. Security scan (main push only) ─────────────────────────────────────── | |
| security-scan: | |
| name: Security Scan | |
| runs-on: ubuntu-latest | |
| needs: docker | |
| if: github.event_name != 'pull_request' | |
| permissions: | |
| contents: read | |
| security-events: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Lowercase IMAGE_NAME | |
| run: echo "IMAGE_NAME=$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_ENV" | |
| - name: Run Trivy | |
| uses: aquasecurity/trivy-action@0.35.0 | |
| with: | |
| image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ github.sha }} | |
| format: sarif | |
| output: trivy-results.sarif | |
| severity: CRITICAL,HIGH | |
| - name: Upload Trivy results | |
| uses: github/codeql-action/upload-sarif@v3 | |
| if: always() | |
| with: | |
| sarif_file: trivy-results.sarif | |
| # ── 13. Upgrade workflow test ───────────────────────────────────────────────── | |
| upgrade-test: | |
| name: Upgrade Workflow Test | |
| runs-on: ubuntu-latest | |
| needs: [test] | |
| if: >- | |
| always() && | |
| needs.test.result == 'success' | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Setup Rust | |
| uses: ./.github/actions/setup-rust | |
| with: | |
| cache-key: "ci-upgrade-test" | |
| - name: Run upgrade orchestrator unit tests | |
| run: cargo test --locked upgrade_orchestrator -- --nocapture | |
| - name: Run PVC autoscaler unit tests | |
| run: cargo test --locked pvc_autoscaler -- --nocapture |