| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191 |
- # Supply-chain gate (task 46). Coordinates with task 42's `ci.yml`.
- #
- # Per-PR legs: `manifest-lint` (kustomize build + scripts/check_manifests.py,
- # including the L7-vs-L4 mTLS pairing check).
- # Per-push-to-master legs: multi-arch `docker buildx bake` with SBOM +
- # provenance, trivy/grype gating HIGH/CRITICAL, keyless cosign signing, and
- # digest pinning of `deploy/k8s/kustomization.yaml` (strict lint re-run).
- # Release tags (`v*`): the same signed multi-arch build is published under
- # semver tags (X.Y.Z, X.Y, X, `latest`; a pre-release tag gets only X.Y.Z) and
- # a GitHub Release is created. `pin-digest` does not run on a tag (it tracks
- # master).
- # Scheduled legs (weekly rebuild, Monday 04:00 UTC): rebuild + rescan + resign
- # so base-image digests advance even without code changes.
- #
- # The L7 mTLS constraint is DOCUMENTED (docs/DEPLOYMENT.md) and lint-checked
- # here; it is explicitly NOT exercised end-to-end in CI.
- name: supply-chain
- on:
- push:
- branches: ["master"]
- tags: ["v*"]
- pull_request: {}
- schedule:
- # Weekly rebuild (Monday 04:00 UTC).
- - cron: "0 4 * * 1"
- workflow_dispatch: {}
- permissions:
- contents: read
- env:
- REGISTRY_IMAGE: ghcr.io/syavne/d-oauth-server
- SQLX_OFFLINE: "true"
- jobs:
- manifest-lint:
- name: manifest-lint
- runs-on: ubuntu-24.04
- steps:
- - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
- - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
- with:
- python-version: "3.x"
- - name: Install pyyaml + kustomize
- run: |
- pip install pyyaml
- curl -sSfL https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize%2Fv5.4.3/kustomize_v5.4.3_linux_amd64.tar.gz \
- | tar -xzf - kustomize && sudo mv kustomize /usr/local/bin/
- - name: Kustomize build
- run: kustomize build deploy/k8s
- - name: Manifest policy lint (probes, PDB, no-baked-secrets, L7-vs-L4 mTLS)
- run: python3 scripts/check_manifests.py
- build:
- name: build
- if: github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
- runs-on: ubuntu-24.04
- permissions:
- contents: read
- packages: write
- id-token: write
- outputs:
- digest: ${{ steps.bake.outputs.digest }}
- steps:
- - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
- - uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3
- - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
- - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
- with:
- registry: ghcr.io
- username: ${{ github.actor }}
- password: ${{ secrets.GITHUB_TOKEN }}
- - name: Multi-arch bake with SBOM + provenance
- id: bake
- run: |
- set -euo pipefail
- if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then
- version="${GITHUB_REF_NAME#v}"
- if ! [[ "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then
- echo "::error::release tag '${GITHUB_REF_NAME}' is not semver (expected vX.Y.Z or vX.Y.Z-pre)"
- exit 1
- fi
- tags="${REGISTRY_IMAGE}:${version}"
- if [[ "${version}" != *-* ]]; then
- major="${version%%.*}"
- minor="${version#*.}"; minor="${minor%%.*}"
- tags+=",${REGISTRY_IMAGE}:${major}.${minor},${REGISTRY_IMAGE}:${major},${REGISTRY_IMAGE}:latest"
- fi
- tags+=",${REGISTRY_IMAGE}:sha-${GITHUB_SHA::12}"
- else
- tags="${REGISTRY_IMAGE}:sha-${GITHUB_SHA::12}"
- fi
- echo "tags=${tags}" >> "${GITHUB_OUTPUT}"
- IMAGE_TAGS="${tags}" docker buildx bake image \
- --set image.output="type=registry" \
- --sbom=true \
- --provenance=mode=max \
- --metadata-file /tmp/bake-metadata.json
- python3 -c "
- import json
- meta = json.load(open('/tmp/bake-metadata.json'))
- node = meta['image']['containerimage.digest']
- open('${GITHUB_OUTPUT}', 'a').write(f'digest={node}\n')
- "
- - name: SBOM (syft, committed as artifact)
- uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0
- with:
- image: ${{ env.REGISTRY_IMAGE }}@${{ steps.bake.outputs.digest }}
- artifact-name: sbom.spdx.json
- - name: Scan (trivy, gate HIGH/CRITICAL)
- uses: aquasecurity/trivy-action@6e7b7d1fd3e4fef0c5fa8cce1229c54b2c9bd0d8 # v0.24.0
- with:
- image-ref: ${{ env.REGISTRY_IMAGE }}@${{ steps.bake.outputs.digest }}
- format: sarif
- output: trivy.sarif
- severity: HIGH,CRITICAL
- exit-code: "1"
- - name: Scan (grype, gate high/critical)
- uses: anchore/scan-action@64a33b277ea7a1215a3c142735a1091341939ff5 # v4
- with:
- image: ${{ env.REGISTRY_IMAGE }}@${{ steps.bake.outputs.digest }}
- fail-build: true
- severity-cutoff: high
- - uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac # v3
- - name: Keyless sign (cosign, OIDC)
- env:
- COSIGN_YES: "true"
- run: |
- cosign sign "${REGISTRY_IMAGE}@${{ steps.bake.outputs.digest }}"
- cosign attest --predicate /tmp/bake-metadata.json \
- "${REGISTRY_IMAGE}@${{ steps.bake.outputs.digest }}"
- pin-digest:
- name: pin-digest
- needs: [build]
- # Only master/scheduled builds advance the committed digest; a release tag
- # must not rewrite kustomization.
- if: github.event_name != 'push' || !startsWith(github.ref, 'refs/tags/')
- runs-on: ubuntu-24.04
- steps:
- - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
- - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
- with:
- python-version: "3.x"
- - name: Pin kustomization to the built digest + strict lint
- env:
- DIGEST: ${{ needs.build.outputs.digest }}
- run: |
- pip install pyyaml
- python3 - "${DIGEST}" <<'EOF'
- import sys
- from pathlib import Path
- import yaml
- digest = sys.argv[1]
- assert digest.startswith("sha256:"), f"bad digest: {digest}"
- path = Path("deploy/k8s/kustomization.yaml")
- doc = yaml.safe_load(path.read_text())
- for img in doc.get("images", []):
- if img.get("name") == "ghcr.io/syavne/d-oauth-server":
- img.pop("newTag", None)
- img["digest"] = digest
- path.write_text(yaml.safe_dump(doc, sort_keys=False))
- EOF
- python3 scripts/check_manifests.py --strict-digest
- release:
- name: release
- needs: [build]
- if: startsWith(github.ref, 'refs/tags/v')
- runs-on: ubuntu-24.04
- permissions:
- contents: write
- steps:
- - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
- - name: Create GitHub Release
- env:
- GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- DIGEST: ${{ needs.build.outputs.digest }}
- run: |
- flags=()
- if [[ "${GITHUB_REF_NAME}" == *-* ]]; then
- flags+=(--prerelease)
- fi
- gh release create "${GITHUB_REF_NAME}" \
- --verify-tag \
- --generate-notes \
- --title "${GITHUB_REF_NAME}" \
- "${flags[@]}" \
- --notes "Container image \`${REGISTRY_IMAGE}@${DIGEST}\` (linux/amd64 + linux/arm64), signed with cosign; the SBOM and provenance attestations are attached to the image."
|