Skip to content

Merge pull request #695 from OtowoOrg/dependabot/docker/lukemathwalke… #1130

Merge pull request #695 from OtowoOrg/dependabot/docker/lukemathwalke…

Merge pull request #695 from OtowoOrg/dependabot/docker/lukemathwalke… #1130

Workflow file for this run

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