Files
Charon/.github/workflows/ci-pipeline.yml
GitHub Actions ef227a316b fix: unblock pipeline by removing push_image gate from downstream jobs
Integration, E2E, and security jobs were being skipped on PR builds because
they required push_image == 'true'. Since the build succeeded and images were
available, these jobs should run regardless of push policy.

Changed conditions to depend on build success and image availability rather
than registry push status. This allows comprehensive testing on all builds
while still optimizing resource usage where needed.
2026-02-08 18:34:23 +00:00

773 lines
27 KiB
YAML

name: CI Pipeline
on:
pull_request:
workflow_dispatch:
inputs:
image_tag_override:
description: 'Optional image tag to use for build outputs'
required: false
type: string
run_coverage:
description: 'Run backend/frontend coverage jobs'
required: false
default: true
type: boolean
run_security_scans:
description: 'Run CodeQL, Trivy, and supply-chain checks'
required: false
default: true
type: boolean
run_integration:
description: 'Run integration test jobs'
required: false
default: true
type: boolean
run_e2e:
description: 'Run Playwright E2E tests'
required: false
default: true
type: boolean
permissions:
contents: read
env:
GO_VERSION: '1.25.7'
NODE_VERSION: '24.12.0'
GOTOOLCHAIN: auto
GHCR_REGISTRY: ghcr.io
DOCKERHUB_REGISTRY: docker.io
IMAGE_NAME: wikid82/charon
IS_FORK: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true && github.repository != github.event.pull_request.head.repo.full_name }}
jobs:
lint:
name: Lint and Repo Health
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Repo health check
run: bash scripts/repo_health_check.sh
- name: Run Hadolint
uses: hadolint/hadolint-action@2332a7b74a6de0dda2e2221d575162eba76ba5e5 # v3.3.0
with:
dockerfile: Dockerfile
config: .hadolint.yaml
failure-threshold: warning
- name: GORM Security Scanner
run: |
chmod +x scripts/scan-gorm-security.sh
./scripts/scan-gorm-security.sh --check
- name: Set up Go
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6
with:
go-version: ${{ env.GO_VERSION }}
cache-dependency-path: backend/go.sum
- name: Run golangci-lint (fast)
uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0
with:
version: v2.8.0
working-directory: backend
args: --config=.golangci-fast.yml --timeout=2m
- name: Check frontend lockfile
id: frontend-lockfile
run: |
if [ -f frontend/package-lock.json ]; then
echo "present=true" >> "$GITHUB_OUTPUT"
else
echo "present=false" >> "$GITHUB_OUTPUT"
fi
- name: Set up Node.js (with cache)
if: steps.frontend-lockfile.outputs.present == 'true'
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
- name: Set up Node.js
if: steps.frontend-lockfile.outputs.present != 'true'
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
with:
node-version: ${{ env.NODE_VERSION }}
- name: Install frontend dependencies
working-directory: frontend
run: npm ci
- name: Run frontend lint
working-directory: frontend
run: npm run lint
build-image:
name: Build and Publish Image
runs-on: ubuntu-latest
needs: lint
concurrency:
group: ci-build-image-${{ github.workflow }}-${{ github.ref_name }}
cancel-in-progress: true
permissions:
contents: read
packages: write
outputs:
image_digest: ${{ steps.build.outputs.digest }}
image_ref: ${{ steps.outputs.outputs.image_ref_dockerhub }}
image_ref_dockerhub: ${{ steps.outputs.outputs.image_ref_dockerhub }}
image_ref_ghcr: ${{ steps.outputs.outputs.image_ref_ghcr }}
image_tag: ${{ steps.outputs.outputs.image_tag }}
push_image: ${{ steps.image-policy.outputs.push }}
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Normalize image name
run: |
IMAGE_NAME=$(echo "${{ env.IMAGE_NAME }}" | tr '[:upper:]' '[:lower:]')
echo "IMAGE_NAME=${IMAGE_NAME}" >> "$GITHUB_ENV"
- name: Determine image push policy
id: image-policy
run: |
PUSH_IMAGE=true
if [ "${{ github.event_name }}" = "pull_request" ]; then
if [ "${{ github.event.pull_request.head.repo.fork }}" = "true" ] && \
[ "${{ github.repository }}" != "${{ github.event.pull_request.head.repo.full_name }}" ]; then
PUSH_IMAGE=false
fi
fi
echo "push=${PUSH_IMAGE}" >> "$GITHUB_OUTPUT"
- name: Compute image tags
id: tags
env:
PR_HEAD_REF: ${{ github.head_ref }}
run: |
SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7)
DEFAULT_TAG="sha-${SHORT_SHA}"
BRANCH_NAME="${{ github.ref_name }}"
if [ "${{ github.event_name }}" = "pull_request" ]; then
BRANCH_NAME="${PR_HEAD_REF}"
fi
if [ -n "${{ inputs.image_tag_override }}" ]; then
DEFAULT_TAG="${{ inputs.image_tag_override }}"
elif [ "${{ github.event_name }}" = "pull_request" ]; then
PR_NUMBER="${{ github.event.pull_request.number }}"
if [ -n "${PR_NUMBER}" ]; then
DEFAULT_TAG="pr-${PR_NUMBER}-${SHORT_SHA}"
fi
fi
sanitize_tag() {
local raw="$1"
local max_len="$2"
local sanitized
sanitized=$(echo "$raw" | tr '[:upper:]' '[:lower:]')
sanitized=${sanitized//[^a-z0-9-]/-}
while [[ "$sanitized" == *"--"* ]]; do
sanitized=${sanitized//--/-}
done
sanitized=${sanitized##[^a-z0-9]*}
sanitized=${sanitized%%[^a-z0-9-]*}
if [ -z "$sanitized" ]; then
sanitized="branch"
fi
sanitized=$(echo "$sanitized" | cut -c1-"$max_len")
sanitized=${sanitized##[^a-z0-9]*}
if [ -z "$sanitized" ]; then
sanitized="branch"
fi
echo "$sanitized"
}
SANITIZED_BRANCH=$(sanitize_tag "${BRANCH_NAME}" 128)
BRANCH_TAG="${SANITIZED_BRANCH}"
BRANCH_SHA_TAG="${SANITIZED_BRANCH}-$(sanitize_tag "${SHORT_SHA}" 7)"
if [ "${#SANITIZED_BRANCH}" -gt 120 ]; then
SANITIZED_BRANCH=$(sanitize_tag "${BRANCH_NAME}" 120)
BRANCH_SHA_TAG="${SANITIZED_BRANCH}-${SHORT_SHA}"
fi
TAGS=()
TAGS+=("${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:${DEFAULT_TAG}")
TAGS+=("${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${DEFAULT_TAG}")
if [ "${{ github.event_name }}" = "pull_request" ]; then
TAGS+=("${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:${BRANCH_TAG}")
TAGS+=("${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${BRANCH_TAG}")
else
TAGS+=("${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:${BRANCH_SHA_TAG}")
TAGS+=("${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${BRANCH_SHA_TAG}")
case "${{ github.ref_name }}" in
feature/*)
TAGS+=("${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:${BRANCH_TAG}")
TAGS+=("${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${BRANCH_TAG}")
;;
esac
fi
if [ "${{ github.event_name }}" != "pull_request" ] && \
{ [ "${{ github.ref_name }}" = "main" ] || [ "${{ github.ref_name }}" = "development" ] || [ "${{ github.ref_name }}" = "nightly" ]; }; then
TAGS+=("${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:${SHORT_SHA}")
TAGS+=("${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${SHORT_SHA}")
fi
if [ "${{ github.ref_name }}" = "main" ]; then
TAGS+=("${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:latest")
TAGS+=("${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest")
fi
if [ "${{ github.ref_name }}" = "development" ]; then
TAGS+=("${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:dev")
TAGS+=("${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:dev")
fi
if [ "${{ github.ref_name }}" = "nightly" ]; then
TAGS+=("${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:nightly")
TAGS+=("${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:nightly")
fi
{
echo "tags<<EOF"
printf '%s\n' "${TAGS[@]}"
echo "EOF"
} >> "$GITHUB_OUTPUT"
echo "image_tag=${DEFAULT_TAG}" >> "$GITHUB_OUTPUT"
- name: Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Log in to GitHub Container Registry
if: ${{ steps.image-policy.outputs.push == 'true' }}
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
registry: ${{ env.GHCR_REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Log in to Docker Hub
if: steps.image-policy.outputs.push == 'true'
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
registry: ${{ env.DOCKERHUB_REGISTRY }}
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Docker image
id: build
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
push: ${{ steps.image-policy.outputs.push == 'true' }}
load: ${{ steps.image-policy.outputs.push != 'true' }}
tags: ${{ steps.tags.outputs.tags }}
labels: |
org.opencontainers.image.revision=${{ github.sha }}
- name: Emit image outputs
id: outputs
run: |
DIGEST="${{ steps.build.outputs.digest }}"
if [ -z "${DIGEST}" ]; then
echo "image_ref_dockerhub=" >> "$GITHUB_OUTPUT"
echo "image_ref_ghcr=" >> "$GITHUB_OUTPUT"
else
IMAGE_REF_DOCKERHUB="${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}@${DIGEST}"
IMAGE_REF_GHCR="${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@${DIGEST}"
echo "image_ref_dockerhub=${IMAGE_REF_DOCKERHUB}" >> "$GITHUB_OUTPUT"
echo "image_ref_ghcr=${IMAGE_REF_GHCR}" >> "$GITHUB_OUTPUT"
echo "::add-mask::${IMAGE_REF_DOCKERHUB}"
echo "::add-mask::${IMAGE_REF_GHCR}"
fi
echo "image_tag=${{ steps.tags.outputs.image_tag }}" >> "$GITHUB_OUTPUT"
integration-cerberus:
name: Integration - Cerberus
runs-on: ubuntu-latest
needs: build-image
if: needs.build-image.result == 'success' && needs.build-image.outputs.image_ref_dockerhub != '' && (github.event_name != 'workflow_dispatch' || inputs.run_integration != false)
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Log in to Docker Hub
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
registry: ${{ env.DOCKERHUB_REGISTRY }}
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Pull shared image
run: |
docker pull "${{ needs.build-image.outputs.image_ref_dockerhub }}"
docker tag "${{ needs.build-image.outputs.image_ref_dockerhub }}" charon:local
- name: Run Cerberus integration tests
run: |
chmod +x scripts/cerberus_integration.sh
scripts/cerberus_integration.sh
integration-crowdsec:
name: Integration - CrowdSec
runs-on: ubuntu-latest
needs: build-image
if: needs.build-image.result == 'success' && needs.build-image.outputs.image_ref_dockerhub != '' && (github.event_name != 'workflow_dispatch' || inputs.run_integration != false)
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Log in to Docker Hub
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
registry: ${{ env.DOCKERHUB_REGISTRY }}
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Pull shared image
run: |
docker pull "${{ needs.build-image.outputs.image_ref_dockerhub }}"
docker tag "${{ needs.build-image.outputs.image_ref_dockerhub }}" charon:local
- name: Run CrowdSec integration tests
run: |
chmod +x .github/skills/scripts/skill-runner.sh
.github/skills/scripts/skill-runner.sh integration-test-crowdsec
.github/skills/scripts/skill-runner.sh integration-test-crowdsec-startup
integration-waf:
name: Integration - WAF
runs-on: ubuntu-latest
needs: build-image
if: needs.build-image.result == 'success' && needs.build-image.outputs.image_ref_dockerhub != '' && (github.event_name != 'workflow_dispatch' || inputs.run_integration != false)
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Log in to Docker Hub
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
registry: ${{ env.DOCKERHUB_REGISTRY }}
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Pull shared image
run: |
docker pull "${{ needs.build-image.outputs.image_ref_dockerhub }}"
docker tag "${{ needs.build-image.outputs.image_ref_dockerhub }}" charon:local
- name: Run WAF integration tests
run: |
chmod +x scripts/coraza_integration.sh
scripts/coraza_integration.sh
integration-ratelimit:
name: Integration - Rate Limit
runs-on: ubuntu-latest
needs: build-image
if: needs.build-image.result == 'success' && needs.build-image.outputs.image_ref_dockerhub != '' && (github.event_name != 'workflow_dispatch' || inputs.run_integration != false)
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Log in to Docker Hub
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
registry: ${{ env.DOCKERHUB_REGISTRY }}
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Pull shared image
run: |
docker pull "${{ needs.build-image.outputs.image_ref_dockerhub }}"
docker tag "${{ needs.build-image.outputs.image_ref_dockerhub }}" charon:local
- name: Run rate limit integration tests
run: |
chmod +x scripts/rate_limit_integration.sh
scripts/rate_limit_integration.sh
integration-gate:
name: Integration Gate
runs-on: ubuntu-latest
needs:
- build-image
- integration-cerberus
- integration-crowdsec
- integration-waf
- integration-ratelimit
steps:
- name: Evaluate integration results
run: |
if [ "${{ inputs.run_integration }}" = "false" ]; then
echo "Integration stage skipped."
exit 0
fi
if [ "${{ needs.build-image.result }}" != "success" ] || [ "${{ needs.build-image.outputs.push_image }}" != "true" ]; then
echo "Integration stage skipped due to build-image state or push policy."
exit 0
fi
RESULTS=(
"${{ needs.integration-cerberus.result }}"
"${{ needs.integration-crowdsec.result }}"
"${{ needs.integration-waf.result }}"
"${{ needs.integration-ratelimit.result }}"
)
for RESULT in "${RESULTS[@]}"; do
if [ "$RESULT" = "failure" ] || [ "$RESULT" = "cancelled" ]; then
echo "Integration stage failed: $RESULT"
exit 1
fi
done
e2e:
name: E2E Tests with Coverage
needs:
- build-image
- integration-gate
if: (github.event_name != 'workflow_dispatch' || inputs.run_e2e != false) && needs.build-image.result == 'success'
uses: ./.github/workflows/e2e-tests-split.yml
with:
browser: all
test_category: all
image_ref: ${{ needs.build-image.outputs.image_ref_dockerhub }}
image_tag: charon:e2e-test
playwright_coverage: true
secrets: inherit
coverage-backend:
name: Coverage - Backend
runs-on: ubuntu-latest
needs:
- build-image
- integration-gate
if: github.event_name != 'workflow_dispatch' || inputs.run_coverage != false
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6
with:
go-version: ${{ env.GO_VERSION }}
cache-dependency-path: backend/go.sum
- name: Run Go tests with coverage
env:
CGO_ENABLED: 1
run: |
bash scripts/go-test-coverage.sh 2>&1 | tee backend/test-output.txt
exit "${PIPESTATUS[0]}"
- name: Upload coverage artifact
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: backend-coverage
path: backend/coverage.txt
retention-days: 1
coverage-frontend:
name: Coverage - Frontend
runs-on: ubuntu-latest
needs:
- build-image
- integration-gate
if: github.event_name != 'workflow_dispatch' || inputs.run_coverage != false
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
- name: Set up Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
- name: Install dependencies
working-directory: frontend
run: npm ci
- name: Run frontend tests and coverage
run: |
bash scripts/frontend-test-coverage.sh 2>&1 | tee frontend/test-output.txt
exit "${PIPESTATUS[0]}"
- name: Upload coverage artifact
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: frontend-coverage
path: frontend/coverage
retention-days: 1
coverage-gate:
name: Coverage Gate
runs-on: ubuntu-latest
needs:
- coverage-backend
- coverage-frontend
- e2e
steps:
- name: Evaluate coverage results
run: |
if [ "${{ inputs.run_coverage }}" = "false" ]; then
echo "Coverage stage skipped."
exit 0
fi
RESULTS=(
"${{ needs.coverage-backend.result }}"
"${{ needs.coverage-frontend.result }}"
"${{ needs.e2e.result }}"
)
for RESULT in "${RESULTS[@]}"; do
if [ "$RESULT" = "failure" ] || [ "$RESULT" = "cancelled" ]; then
echo "Coverage stage failed: $RESULT"
exit 1
fi
done
codecov-upload:
name: Codecov Upload
runs-on: ubuntu-latest
needs:
- coverage-gate
if: github.event_name != 'workflow_dispatch' || inputs.run_coverage != false
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Download backend coverage artifact
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
name: backend-coverage
path: backend/
- name: Download frontend coverage artifact
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
name: frontend-coverage
path: frontend/coverage
- name: Download E2E coverage artifact
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
pattern: e2e-coverage-*
path: coverage/e2e-shards
merge-multiple: false
- name: Upload coverage to Codecov
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
flags: backend
files: backend/coverage.txt
fail_ci_if_error: false
- name: Upload frontend coverage to Codecov
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
flags: frontend
files: frontend/coverage/lcov.info
fail_ci_if_error: false
- name: Upload E2E coverage to Codecov
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
flags: e2e
files: coverage/e2e-shards/**/lcov.info
fail_ci_if_error: false
codecov-gate:
name: Codecov Gate
runs-on: ubuntu-latest
needs:
- codecov-upload
steps:
- name: Evaluate Codecov upload results
run: |
if [ "${{ inputs.run_coverage }}" = "false" ]; then
echo "Codecov upload stage skipped."
exit 0
fi
if [ "${{ needs.codecov-upload.result }}" = "failure" ] || [ "${{ needs.codecov-upload.result }}" = "cancelled" ]; then
echo "Codecov upload failed: ${{ needs.codecov-upload.result }}"
exit 1
fi
security-codeql:
name: Security - CodeQL
runs-on: ubuntu-latest
needs:
- codecov-gate
if: (github.event_name != 'workflow_dispatch' || inputs.run_security_scans != false) && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork != true)
permissions:
contents: read
security-events: write
actions: read
pull-requests: read
strategy:
fail-fast: false
matrix:
language: ['go', 'javascript-typescript']
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Initialize CodeQL
uses: github/codeql-action/init@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4
with:
languages: ${{ matrix.language }}
config-file: ./.github/codeql/codeql-config.yml
- name: Setup Go
if: matrix.language == 'go'
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6
with:
go-version: ${{ env.GO_VERSION }}
cache-dependency-path: backend/go.sum
- name: Autobuild
uses: github/codeql-action/autobuild@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4
with:
category: "/language:${{ matrix.language }}"
security-trivy:
name: Security - Trivy Image Scan
runs-on: ubuntu-latest
needs:
- build-image
- codecov-gate
if: (github.event_name != 'workflow_dispatch' || inputs.run_security_scans != false) && needs.build-image.result == 'success'
permissions:
contents: read
security-events: write
steps:
- name: Log in to Docker Hub
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
registry: ${{ env.DOCKERHUB_REGISTRY }}
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Run Trivy image scan (SARIF)
uses: aquasecurity/trivy-action@22438a435773de8c97dc0958cc0b823c45b064ac
with:
scan-type: image
image-ref: ${{ needs.build-image.outputs.image_ref_dockerhub }}
format: sarif
output: trivy-image-results.sarif
severity: 'CRITICAL,HIGH,MEDIUM'
continue-on-error: true
- name: Upload Trivy SARIF to GitHub Security
uses: github/codeql-action/upload-sarif@b13d724d35ff0a814e21683638ed68ed34cf53d1
with:
sarif_file: trivy-image-results.sarif
category: trivy-image
continue-on-error: true
- name: Run Trivy image scan (fail on CRITICAL/HIGH)
uses: aquasecurity/trivy-action@22438a435773de8c97dc0958cc0b823c45b064ac
with:
scan-type: image
image-ref: ${{ needs.build-image.outputs.image_ref_dockerhub }}
format: table
severity: 'CRITICAL,HIGH'
exit-code: '1'
security-supply-chain:
name: Security - Supply Chain
runs-on: ubuntu-latest
needs:
- build-image
- codecov-gate
if: (github.event_name != 'workflow_dispatch' || inputs.run_security_scans != false) && needs.build-image.result == 'success'
permissions:
contents: read
security-events: write
steps:
- name: Log in to Docker Hub
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
registry: ${{ env.DOCKERHUB_REGISTRY }}
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Generate SBOM
uses: anchore/sbom-action@28d71544de8eaf1b958d335707167c5f783590ad # v0.22.2
with:
image: ${{ needs.build-image.outputs.image_ref_dockerhub }}
format: cyclonedx-json
output-file: sbom.cyclonedx.json
- name: Scan SBOM for vulnerabilities
uses: anchore/scan-action@7037fa011853d5a11690026fb85feee79f4c946c # v7.3.2
with:
sbom: sbom.cyclonedx.json
fail-build: false
output-format: json
pipeline-gate:
name: Pipeline Gate
runs-on: ubuntu-latest
needs:
- lint
- build-image
- integration-gate
- coverage-gate
- codecov-gate
- security-codeql
- security-trivy
- security-supply-chain
steps:
- name: Evaluate pipeline results
run: |
RESULTS=(
"${{ needs.lint.result }}"
"${{ needs.build-image.result }}"
"${{ needs.integration-gate.result }}"
"${{ needs.coverage-gate.result }}"
"${{ needs.codecov-gate.result }}"
"${{ needs.security-codeql.result }}"
"${{ needs.security-trivy.result }}"
"${{ needs.security-supply-chain.result }}"
)
for RESULT in "${RESULTS[@]}"; do
if [ "$RESULT" = "failure" ] || [ "$RESULT" = "cancelled" ]; then
echo "Pipeline failed: $RESULT"
exit 1
fi
done