Nightly Build & Package #204
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: Nightly Build & Package | |
| on: | |
| schedule: | |
| # Daily at 09:00 UTC (4am EST / 5am EDT) | |
| - cron: '0 9 * * *' | |
| workflow_dispatch: | |
| inputs: | |
| reason: | |
| description: "Why are you running this manually?" | |
| required: true | |
| default: "manual trigger" | |
| skip_tests: | |
| description: "Skip test-nightly-image job?" | |
| required: false | |
| default: "false" | |
| env: | |
| GO_VERSION: '1.26.4' | |
| NODE_VERSION: '24.17.0' | |
| GOTOOLCHAIN: local | |
| GHCR_REGISTRY: ghcr.io | |
| DOCKERHUB_REGISTRY: docker.io | |
| IMAGE_NAME: wikid82/charon | |
| permissions: | |
| contents: read | |
| jobs: | |
| sync-development-to-nightly: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| outputs: | |
| has_changes: ${{ steps.sync.outputs.has_changes }} | |
| steps: | |
| - name: Checkout nightly branch | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| ref: nightly | |
| fetch-depth: 0 | |
| token: ${{ secrets.CHARON_CI_TRIGGER_TOKEN || secrets.GITHUB_TOKEN }} | |
| - name: Configure Git | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| - name: Sync development to nightly | |
| id: sync | |
| env: | |
| HAS_TRIGGER_TOKEN: ${{ secrets.CHARON_CI_TRIGGER_TOKEN != '' }} | |
| run: | | |
| # Fetch both branches to ensure we have the latest remote state | |
| git fetch origin development | |
| git fetch origin nightly | |
| # Sync local nightly with remote nightly to prevent non-fast-forward errors | |
| echo "Syncing local nightly with remote nightly..." | |
| git reset --hard origin/nightly | |
| # Check if there are differences between remote branches | |
| if git diff --quiet origin/nightly origin/development; then | |
| echo "No changes to sync from development to nightly" | |
| echo "has_changes=false" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "Syncing changes from development to nightly" | |
| # Fast-forward merge development into nightly | |
| git merge origin/development --ff-only -m "chore: sync from development branch [skip ci]" || { | |
| # If fast-forward fails, force reset to development | |
| echo "Fast-forward not possible, resetting nightly to development" | |
| git reset --hard origin/development | |
| } | |
| if [[ "$HAS_TRIGGER_TOKEN" != "true" ]]; then | |
| echo "::warning title=Using GITHUB_TOKEN fallback::Set CHARON_CI_TRIGGER_TOKEN to ensure push-triggered workflows run on nightly." | |
| fi | |
| # Force push to handle cases where nightly diverged from development | |
| git push --force origin nightly | |
| echo "has_changes=true" >> "$GITHUB_OUTPUT" | |
| fi | |
| trigger-nightly-validation: | |
| name: Trigger Nightly Validation Workflows | |
| needs: sync-development-to-nightly | |
| if: needs.sync-development-to-nightly.outputs.has_changes == 'true' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| actions: write | |
| contents: read | |
| steps: | |
| - name: Dispatch Missing Nightly Validation Workflows | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 | |
| with: | |
| script: | | |
| const owner = context.repo.owner; | |
| const repo = context.repo.repo; | |
| const { data: nightlyBranch } = await github.rest.repos.getBranch({ | |
| owner, | |
| repo, | |
| branch: 'nightly', | |
| }); | |
| const nightlyHeadSha = nightlyBranch.commit.sha; | |
| core.info(`Current nightly HEAD: ${nightlyHeadSha}`); | |
| const workflows = [ | |
| { id: 'e2e-tests-split.yml' }, | |
| { id: 'codecov-upload.yml', inputs: { run_backend: 'true', run_frontend: 'true' } }, | |
| { id: 'supply-chain-verify.yml' }, | |
| { id: 'codeql.yml' }, | |
| ]; | |
| core.info('Skipping security-pr.yml: PR-only workflow intentionally excluded from nightly non-PR dispatch'); | |
| for (const workflow of workflows) { | |
| const { data: workflowRuns } = await github.rest.actions.listWorkflowRuns({ | |
| owner, | |
| repo, | |
| workflow_id: workflow.id, | |
| branch: 'nightly', | |
| per_page: 50, | |
| }); | |
| const hasRunForHead = workflowRuns.workflow_runs.some( | |
| (run) => run.head_sha === nightlyHeadSha, | |
| ); | |
| if (hasRunForHead) { | |
| core.info(`Skipping dispatch for ${workflow.id}; run already exists for nightly HEAD`); | |
| continue; | |
| } | |
| await github.rest.actions.createWorkflowDispatch({ | |
| owner, | |
| repo, | |
| workflow_id: workflow.id, | |
| ref: 'nightly', | |
| ...(workflow.inputs ? { inputs: workflow.inputs } : {}), | |
| }); | |
| core.info(`Dispatched ${workflow.id} on nightly (missing run for HEAD)`); | |
| } | |
| build-and-push-nightly: | |
| needs: sync-development-to-nightly | |
| runs-on: ubuntu-latest | |
| env: | |
| HAS_DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN != '' }} | |
| permissions: | |
| contents: read | |
| packages: write | |
| id-token: write | |
| outputs: | |
| version: ${{ steps.meta.outputs.version }} | |
| digest: ${{ steps.resolve_digest.outputs.digest }} | |
| steps: | |
| - name: Free disk space | |
| uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1 | |
| with: | |
| android: true | |
| dotnet: true | |
| haskell: true | |
| large-packages: true | |
| docker-images: false | |
| swap-storage: true | |
| tool-cache: false | |
| - name: Checkout nightly branch | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| ref: ${{ github.event_name == 'workflow_dispatch' && github.ref || 'nightly' }} | |
| fetch-depth: 0 | |
| - name: Set lowercase image name | |
| run: echo "IMAGE_NAME_LC=${IMAGE_NAME,,}" >> "$GITHUB_ENV" | |
| - name: Set up QEMU | |
| uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 | |
| - name: Resolve Alpine base image digest | |
| id: alpine | |
| run: | | |
| ALPINE_IMAGE_REF=$(grep -m1 'ARG ALPINE_IMAGE=' Dockerfile | cut -d'=' -f2-) | |
| if [[ -z "$ALPINE_IMAGE_REF" ]]; then | |
| echo "::error::Failed to parse ALPINE_IMAGE from Dockerfile" | |
| exit 1 | |
| fi | |
| echo "Resolved Alpine image: ${ALPINE_IMAGE_REF}" | |
| echo "image=${ALPINE_IMAGE_REF}" >> "$GITHUB_OUTPUT" | |
| - name: Log in to GitHub Container Registry | |
| uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 | |
| with: | |
| registry: ${{ env.GHCR_REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Log in to Docker Hub | |
| if: env.HAS_DOCKERHUB_TOKEN == 'true' | |
| uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 | |
| with: | |
| registry: docker.io | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: Extract metadata | |
| id: meta | |
| uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 | |
| with: | |
| images: | | |
| ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }} | |
| ${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }} | |
| tags: | | |
| type=raw,value=nightly | |
| type=raw,value=nightly-{{date 'YYYY-MM-DD'}} | |
| type=sha,prefix=nightly-,format=short | |
| labels: | | |
| org.opencontainers.image.title=Charon Nightly | |
| org.opencontainers.image.description=Nightly build of Charon | |
| - name: Build and push Docker image | |
| id: build | |
| uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 | |
| with: | |
| context: . | |
| platforms: linux/amd64,linux/arm64 | |
| push: true | |
| tags: ${{ steps.meta.outputs.tags }} | |
| labels: ${{ steps.meta.outputs.labels }} | |
| build-args: | | |
| VERSION=nightly-${{ github.sha }} | |
| VCS_REF=${{ github.sha }} | |
| BUILD_DATE=${{ github.event.repository.pushed_at }} | |
| ALPINE_IMAGE=${{ steps.alpine.outputs.image }} | |
| CROWDSEC_VERSION=1.7.8 | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| no-cache-filters: caddy-builder,crowdsec-builder | |
| provenance: true | |
| sbom: true | |
| - name: Resolve and export image digest | |
| id: resolve_digest | |
| run: | | |
| set -euo pipefail | |
| DIGEST="${{ steps.build.outputs.digest }}" | |
| if [[ -z "$DIGEST" ]]; then | |
| echo "Build action digest empty; querying GHCR registry API..." | |
| GHCR_TOKEN=$(curl -sf \ | |
| -u "${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}" \ | |
| "https://ghcr.io/token?scope=repository:${{ env.IMAGE_NAME }}:pull&service=ghcr.io" \ | |
| | jq -r '.token') | |
| DIGEST=$(curl -sfI \ | |
| -H "Authorization: Bearer ${GHCR_TOKEN}" \ | |
| -H "Accept: application/vnd.oci.image.index.v1+json,application/vnd.docker.distribution.manifest.list.v2+json,application/vnd.oci.image.manifest.v1+json" \ | |
| "https://ghcr.io/v2/${{ env.IMAGE_NAME }}/manifests/nightly" \ | |
| | grep -i '^docker-content-digest:' | awk '{print $2}' | tr -d '\r' || true) | |
| [[ -n "$DIGEST" ]] && echo "Resolved from GHCR API: ${DIGEST}" | |
| fi | |
| if [[ -z "$DIGEST" ]]; then | |
| echo "::error::Could not determine image digest from step output or GHCR registry API" | |
| exit 1 | |
| fi | |
| echo "RESOLVED_DIGEST=${DIGEST}" >> "$GITHUB_ENV" | |
| echo "digest=${DIGEST}" >> "$GITHUB_OUTPUT" | |
| echo "Exported digest: ${DIGEST}" | |
| - name: Record nightly image digest | |
| run: | | |
| echo "## 🧾 Nightly Image Digest" >> "$GITHUB_STEP_SUMMARY" | |
| echo "- ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:nightly@${{ steps.resolve_digest.outputs.digest }}" >> "$GITHUB_STEP_SUMMARY" | |
| - name: Generate SBOM | |
| id: sbom_primary | |
| continue-on-error: true | |
| uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0 | |
| with: | |
| image: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.resolve_digest.outputs.digest }} | |
| format: cyclonedx-json | |
| output-file: sbom-nightly.json | |
| syft-version: v1.45.1 | |
| - name: Generate SBOM fallback with pinned Syft | |
| if: always() | |
| run: | | |
| set -euo pipefail | |
| if [[ "${{ steps.sbom_primary.outcome }}" == "success" ]] && [[ -s sbom-nightly.json ]] && jq -e . sbom-nightly.json >/dev/null 2>&1; then | |
| echo "Primary SBOM generation succeeded with valid JSON; skipping fallback" | |
| exit 0 | |
| fi | |
| echo "Primary SBOM generation failed or produced missing/invalid output; using deterministic Syft fallback" | |
| SYFT_VERSION="v1.45.1" | |
| OS="$(uname -s | tr '[:upper:]' '[:lower:]')" | |
| ARCH="$(uname -m)" | |
| case "$ARCH" in | |
| x86_64) ARCH="amd64" ;; | |
| aarch64|arm64) ARCH="arm64" ;; | |
| *) echo "Unsupported architecture: $ARCH"; exit 1 ;; | |
| esac | |
| TARBALL="syft_${SYFT_VERSION#v}_${OS}_${ARCH}.tar.gz" | |
| BASE_URL="https://github.com/anchore/syft/releases/download/${SYFT_VERSION}" | |
| curl -fsSLo "$TARBALL" "${BASE_URL}/${TARBALL}" | |
| curl -fsSLo checksums.txt "${BASE_URL}/syft_${SYFT_VERSION#v}_checksums.txt" | |
| grep " ${TARBALL}$" checksums.txt > checksum_line.txt | |
| sha256sum -c checksum_line.txt | |
| tar -xzf "$TARBALL" syft | |
| chmod +x syft | |
| DIGEST="${{ steps.resolve_digest.outputs.digest }}" | |
| if [[ -z "$DIGEST" ]]; then | |
| echo "::error::Digest from resolve_digest step is empty; the digest-resolution step did not complete successfully" | |
| exit 1 | |
| fi | |
| ./syft "${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@${DIGEST}" -o cyclonedx-json=sbom-nightly.json | |
| - name: Verify SBOM artifact | |
| if: always() | |
| run: | | |
| set -euo pipefail | |
| test -s sbom-nightly.json | |
| jq -e . sbom-nightly.json >/dev/null | |
| jq -e ' | |
| .bomFormat == "CycloneDX" | |
| and (.specVersion | type == "string" and length > 0) | |
| and has("version") | |
| and has("metadata") | |
| and (.components | type == "array") | |
| ' sbom-nightly.json >/dev/null | |
| - name: Upload SBOM artifact | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: sbom-nightly | |
| path: sbom-nightly.json | |
| retention-days: 30 | |
| # Install Cosign for keyless signing | |
| - name: Install Cosign | |
| uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 | |
| # Sign GHCR image with keyless signing (Sigstore/Fulcio) | |
| # Retry up to 3 times to handle transient Fulcio/Rekor INTERNAL_ERROR (HTTP/2 stream errors) | |
| - name: Sign GHCR Image | |
| run: | | |
| echo "Signing GHCR nightly image with keyless signing..." | |
| for attempt in 1 2 3; do | |
| if cosign sign --yes "${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.resolve_digest.outputs.digest }}"; then | |
| echo "✅ GHCR nightly image signed successfully" | |
| break | |
| fi | |
| if [ "$attempt" -eq 3 ]; then | |
| echo "❌ GHCR signing failed after 3 attempts" | |
| exit 1 | |
| fi | |
| echo "⚠️ Attempt $attempt failed — retrying in 15s..." | |
| sleep 15 | |
| done | |
| # Sign Docker Hub image with keyless signing (Sigstore/Fulcio) | |
| # Retry up to 3 times to handle transient Fulcio/Rekor INTERNAL_ERROR (HTTP/2 stream errors) | |
| - name: Sign Docker Hub Image | |
| if: env.HAS_DOCKERHUB_TOKEN == 'true' | |
| run: | | |
| echo "Signing Docker Hub nightly image with keyless signing..." | |
| for attempt in 1 2 3; do | |
| if cosign sign --yes "${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.resolve_digest.outputs.digest }}"; then | |
| echo "✅ Docker Hub nightly image signed successfully" | |
| break | |
| fi | |
| if [ "$attempt" -eq 3 ]; then | |
| echo "❌ Docker Hub signing failed after 3 attempts" | |
| exit 1 | |
| fi | |
| echo "⚠️ Attempt $attempt failed — retrying in 15s..." | |
| sleep 15 | |
| done | |
| # Attach SBOM to Docker Hub image | |
| - name: Attach SBOM to Docker Hub | |
| if: env.HAS_DOCKERHUB_TOKEN == 'true' | |
| run: | | |
| echo "Attaching SBOM to Docker Hub nightly image..." | |
| cosign attach sbom --sbom sbom-nightly.json "${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.resolve_digest.outputs.digest }}" | |
| echo "✅ SBOM attached to Docker Hub nightly image" | |
| build-and-push-nightly-orthrus: | |
| needs: sync-development-to-nightly | |
| if: needs.sync-development-to-nightly.outputs.has_changes == 'true' || github.event_name == 'workflow_dispatch' | |
| runs-on: ubuntu-latest | |
| env: | |
| ORTHRUS_IMAGE_NAME: wikid82/orthrus | |
| HAS_DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN != '' }} | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| - name: Free disk space | |
| uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1 | |
| with: | |
| android: true | |
| dotnet: true | |
| haskell: true | |
| large-packages: true | |
| docker-images: false | |
| swap-storage: true | |
| tool-cache: false | |
| - name: Checkout nightly branch | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| ref: ${{ github.event_name == 'workflow_dispatch' && github.ref || 'nightly' }} | |
| fetch-depth: 0 | |
| - name: Set lowercase image name | |
| run: echo "ORTHRUS_IMAGE_NAME_LC=${ORTHRUS_IMAGE_NAME,,}" >> "$GITHUB_ENV" | |
| - name: Set up QEMU | |
| uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 | |
| - name: Log in to GitHub Container Registry | |
| uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 | |
| with: | |
| registry: ${{ env.GHCR_REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Log in to Docker Hub | |
| if: env.HAS_DOCKERHUB_TOKEN == 'true' | |
| uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 | |
| with: | |
| registry: docker.io | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: Extract metadata | |
| id: meta | |
| uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 | |
| with: | |
| images: | | |
| ${{ env.GHCR_REGISTRY }}/${{ env.ORTHRUS_IMAGE_NAME_LC }} | |
| ${{ env.DOCKERHUB_REGISTRY }}/${{ env.ORTHRUS_IMAGE_NAME_LC }} | |
| tags: | | |
| type=raw,value=nightly | |
| type=raw,value=nightly-{{date 'YYYY-MM-DD'}} | |
| type=sha,prefix=nightly-,format=short | |
| labels: | | |
| org.opencontainers.image.title=Orthrus Agent Nightly | |
| org.opencontainers.image.description=Nightly build of the Orthrus reverse-proxy agent | |
| - name: Build and push Orthrus nightly image | |
| uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 | |
| with: | |
| context: ./agent | |
| file: ./agent/Dockerfile | |
| platforms: linux/amd64,linux/arm64 | |
| push: true | |
| tags: ${{ steps.meta.outputs.tags }} | |
| labels: ${{ steps.meta.outputs.labels }} | |
| build-args: | | |
| VERSION=nightly-${{ github.sha }} | |
| GIT_COMMIT=${{ github.sha }} | |
| BUILD_DATE=${{ github.event.repository.pushed_at }} | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| - name: Record Orthrus nightly image tags | |
| run: | | |
| { | |
| echo "## 🐕 Orthrus Nightly Image Tags" | |
| echo '```' | |
| echo "${{ steps.meta.outputs.tags }}" | |
| echo '```' | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| test-nightly-image: | |
| needs: build-and-push-nightly | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| packages: read | |
| steps: | |
| - name: Checkout nightly branch | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| ref: ${{ github.event_name == 'workflow_dispatch' && github.ref || 'nightly' }} | |
| - name: Set lowercase image name | |
| run: echo "IMAGE_NAME_LC=${IMAGE_NAME,,}" >> "$GITHUB_ENV" | |
| - name: Log in to GitHub Container Registry | |
| uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 | |
| with: | |
| registry: ${{ env.GHCR_REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Pull nightly image | |
| run: docker pull "${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:nightly@${{ needs.build-and-push-nightly.outputs.digest }}" | |
| - name: Run container smoke test | |
| run: | | |
| IMAGE_REF="${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:nightly@${{ needs.build-and-push-nightly.outputs.digest }}" | |
| docker run --name charon-nightly -d \ | |
| -p 8080:8080 \ | |
| "${IMAGE_REF}" | |
| # Wait for container to become healthy | |
| echo "⏳ Waiting for Charon to be healthy..." | |
| MAX_ATTEMPTS=30 | |
| ATTEMPT=0 | |
| while [[ ${ATTEMPT} -lt ${MAX_ATTEMPTS} ]]; do | |
| ATTEMPT=$((ATTEMPT + 1)) | |
| echo "Attempt ${ATTEMPT}/${MAX_ATTEMPTS}..." | |
| if docker exec charon-nightly wget -qO- http://127.0.0.1:8080/health > /dev/null 2>&1; then | |
| echo "✅ Charon is healthy!" | |
| docker exec charon-nightly wget -qO- http://127.0.0.1:8080/health | |
| break | |
| fi | |
| sleep 2 | |
| done | |
| if [[ ${ATTEMPT} -ge ${MAX_ATTEMPTS} ]]; then | |
| echo "❌ Health check failed after ${MAX_ATTEMPTS} attempts" | |
| docker logs charon-nightly | |
| docker stop charon-nightly | |
| docker rm charon-nightly | |
| exit 1 | |
| fi | |
| # Cleanup | |
| docker stop charon-nightly | |
| docker rm charon-nightly | |
| # NOTE: Standalone binary builds removed - Charon uses Docker-only deployment | |
| # The build-nightly-release job that ran GoReleaser for Windows/macOS/Linux binaries | |
| # was removed because: | |
| # 1. Charon is distributed exclusively via Docker images | |
| # 2. Cross-compilation was failing due to Unix-specific syscalls | |
| # 3. No users download standalone binaries (all use Docker) | |
| # If standalone binaries are needed in the future, re-add the job with Linux-only targets | |
| verify-nightly-supply-chain: | |
| needs: build-and-push-nightly | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| packages: read | |
| security-events: write | |
| steps: | |
| - name: Checkout nightly branch | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| ref: ${{ github.event_name == 'workflow_dispatch' && github.ref || 'nightly' }} | |
| - name: Set lowercase image name | |
| run: echo "IMAGE_NAME_LC=${IMAGE_NAME,,}" >> "$GITHUB_ENV" | |
| - name: Download SBOM | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: sbom-nightly | |
| - name: Scan with Grype | |
| uses: anchore/scan-action@e1165082ffb1fe366ebaf02d8526e7c4989ea9d2 # v7.4.0 | |
| with: | |
| sbom: sbom-nightly.json | |
| fail-build: false | |
| severity-cutoff: high | |
| - name: Scan with Trivy | |
| uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 | |
| with: | |
| image-ref: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:nightly@${{ needs.build-and-push-nightly.outputs.digest }} | |
| format: 'sarif' | |
| output: 'trivy-nightly.sarif' | |
| version: 'v0.71.2' | |
| trivyignores: '.trivyignore' | |
| - name: Upload Trivy results | |
| uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 | |
| with: | |
| sarif_file: 'trivy-nightly.sarif' | |
| category: 'trivy-nightly' | |
| - name: Security severity policy summary | |
| run: | | |
| { | |
| echo "## 🔐 Nightly Supply Chain Severity Policy" | |
| echo "" | |
| echo "- Blocking: Critical, High" | |
| echo "- Medium: non-blocking by default (report + triage SLA)" | |
| echo "- Policy file: .github/security-severity-policy.yml" | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| - name: Check for Critical/High CVEs | |
| run: | | |
| set -euo pipefail | |
| jq -e . trivy-nightly.sarif >/dev/null | |
| CRITICAL_COUNT=$(jq -r ' | |
| [ | |
| .runs[] as $run | |
| | ($run.tool.driver.rules // []) as $rules | |
| | $run.results[]? | |
| | . as $result | |
| | ( | |
| ( | |
| if (($result.ruleIndex | type) == "number") then | |
| ($rules[$result.ruleIndex].properties["security-severity"] // empty) | |
| else | |
| empty | |
| end | |
| ) | |
| // ([ | |
| $rules[]? | |
| | select((.id // "") == ($result.ruleId // "")) | |
| | .properties["security-severity"] | |
| ][0] // empty) | |
| // empty | |
| ) as $securitySeverity | |
| | (try ($securitySeverity | tonumber) catch empty) as $score | |
| | select($score != null and $score >= 9.0) | |
| ] | length | |
| ' trivy-nightly.sarif) | |
| HIGH_COUNT=$(jq -r ' | |
| [ | |
| .runs[] as $run | |
| | ($run.tool.driver.rules // []) as $rules | |
| | $run.results[]? | |
| | . as $result | |
| | ( | |
| ( | |
| if (($result.ruleIndex | type) == "number") then | |
| ($rules[$result.ruleIndex].properties["security-severity"] // empty) | |
| else | |
| empty | |
| end | |
| ) | |
| // ([ | |
| $rules[]? | |
| | select((.id // "") == ($result.ruleId // "")) | |
| | .properties["security-severity"] | |
| ][0] // empty) | |
| // empty | |
| ) as $securitySeverity | |
| | (try ($securitySeverity | tonumber) catch empty) as $score | |
| | select($score != null and $score >= 7.0 and $score < 9.0) | |
| ] | length | |
| ' trivy-nightly.sarif) | |
| MEDIUM_COUNT=$(jq -r ' | |
| [ | |
| .runs[] as $run | |
| | ($run.tool.driver.rules // []) as $rules | |
| | $run.results[]? | |
| | . as $result | |
| | ( | |
| ( | |
| if (($result.ruleIndex | type) == "number") then | |
| ($rules[$result.ruleIndex].properties["security-severity"] // empty) | |
| else | |
| empty | |
| end | |
| ) | |
| // ([ | |
| $rules[]? | |
| | select((.id // "") == ($result.ruleId // "")) | |
| | .properties["security-severity"] | |
| ][0] // empty) | |
| // empty | |
| ) as $securitySeverity | |
| | (try ($securitySeverity | tonumber) catch empty) as $score | |
| | select($score != null and $score >= 4.0 and $score < 7.0) | |
| ] | length | |
| ' trivy-nightly.sarif) | |
| { | |
| echo "- Structured SARIF counts: CRITICAL=${CRITICAL_COUNT}, HIGH=${HIGH_COUNT}, MEDIUM=${MEDIUM_COUNT}" | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| # List all Critical/High/Medium findings with details for triage | |
| # shellcheck disable=SC2016 | |
| LIST_FINDINGS=' | |
| .runs[] as $run | |
| | ($run.tool.driver.rules // []) as $rules | |
| | $run.results[]? | |
| | . as $result | |
| | ( | |
| ( | |
| if (($result.ruleIndex | type) == "number") then | |
| ($rules[$result.ruleIndex] // {}) | |
| else | |
| {} | |
| end | |
| ) as $ruleByIndex | |
| | ( | |
| [$rules[]? | select((.id // "") == ($result.ruleId // ""))][0] // {} | |
| ) as $ruleById | |
| | ($ruleByIndex // $ruleById) as $rule | |
| | ($rule.properties["security-severity"] // null) as $sev | |
| | (try ($sev | tonumber) catch null) as $score | |
| | select($score != null and $score >= 4.0) | |
| | { | |
| id: ($result.ruleId // "unknown"), | |
| score: $score, | |
| severity: ( | |
| if $score >= 9.0 then "CRITICAL" | |
| elif $score >= 7.0 then "HIGH" | |
| else "MEDIUM" | |
| end | |
| ), | |
| message: ($result.message.text // $rule.shortDescription.text // "no description")[0:120] | |
| } | |
| ) | |
| ' | |
| echo "" | |
| echo "=== Vulnerability Details ===" | |
| jq -r "[ ${LIST_FINDINGS} ] | sort_by(-.score) | .[] | \"\\(.severity) (\\(.score)): \\(.id) — \\(.message)\"" trivy-nightly.sarif || true | |
| echo "=============================" | |
| echo "" | |
| if [ "$CRITICAL_COUNT" -gt 0 ]; then | |
| echo "❌ Critical vulnerabilities found in nightly build (${CRITICAL_COUNT})" | |
| { | |
| echo "" | |
| echo "### ❌ Critical CVEs blocking nightly" | |
| echo '```' | |
| jq -r "[ ${LIST_FINDINGS} | select(.severity == \"CRITICAL\") ] | sort_by(-.score) | .[] | \"\\(.id) (score: \\(.score)): \\(.message)\"" trivy-nightly.sarif || true | |
| echo '```' | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| exit 1 | |
| fi | |
| if [ "$HIGH_COUNT" -gt 0 ]; then | |
| echo "❌ High vulnerabilities found in nightly build (${HIGH_COUNT})" | |
| { | |
| echo "" | |
| echo "### ❌ High CVEs blocking nightly" | |
| echo '```' | |
| jq -r "[ ${LIST_FINDINGS} | select(.severity == \"HIGH\") ] | sort_by(-.score) | .[] | \"\\(.id) (score: \\(.score)): \\(.message)\"" trivy-nightly.sarif || true | |
| echo '```' | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| exit 1 | |
| fi | |
| if [ "$MEDIUM_COUNT" -gt 0 ]; then | |
| echo "::warning::Medium vulnerabilities found in nightly build (${MEDIUM_COUNT}). Non-blocking by policy; triage with SLA per .github/security-severity-policy.yml" | |
| { | |
| echo "" | |
| echo "### ⚠️ Medium CVEs (non-blocking)" | |
| echo '```' | |
| jq -r "[ ${LIST_FINDINGS} | select(.severity == \"MEDIUM\") ] | sort_by(-.score) | .[] | \"\\(.id) (score: \\(.score)): \\(.message)\"" trivy-nightly.sarif || true | |
| echo '```' | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| echo "✅ No Critical/High vulnerabilities found" |