758 lines
26 KiB
YAML
758 lines
26 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
|
|
run: |
|
|
SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7)
|
|
DEFAULT_TAG="sha-${SHORT_SHA}"
|
|
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=$(echo "$sanitized" | sed 's/[^a-z0-9-]/-/g' | sed 's/--*/-/g')
|
|
sanitized=$(echo "$sanitized" | sed 's/^[^a-z0-9]*//' | sed 's/[^a-z0-9-]*$//')
|
|
|
|
if [ -z "$sanitized" ]; then
|
|
sanitized="branch"
|
|
fi
|
|
|
|
sanitized=$(echo "$sanitized" | cut -c1-"$max_len")
|
|
|
|
sanitized=$(echo "$sanitized" | sed 's/^[^a-z0-9]*//')
|
|
if [ -z "$sanitized" ]; then
|
|
sanitized="branch"
|
|
fi
|
|
|
|
echo "$sanitized"
|
|
}
|
|
|
|
SANITIZED_BRANCH=$(sanitize_tag "${{ github.ref_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 "${{ github.ref_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_SHA_TAG}")
|
|
TAGS+=("${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${BRANCH_SHA_TAG}")
|
|
|
|
if [[ "${{ github.ref_name }}" == feature/* ]]; then
|
|
TAGS+=("${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:${BRANCH_TAG}")
|
|
TAGS+=("${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${BRANCH_TAG}")
|
|
fi
|
|
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
|
|
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
|
|
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.push_image == 'true' && 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.push_image == 'true' && 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.push_image == 'true' && 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.push_image == 'true' && 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
|
|
if: always()
|
|
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.outputs.push_image == 'true'
|
|
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
|
|
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
|
|
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
|
|
if: always()
|
|
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:
|
|
name: e2e-coverage
|
|
path: coverage/e2e
|
|
|
|
- name: Upload coverage to Codecov
|
|
uses: codecov/codecov-action@7f9fc5e3cf521e84e0c9a667b0f6c6ad08c94b82 # v5.1.3
|
|
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@7f9fc5e3cf521e84e0c9a667b0f6c6ad08c94b82 # v5.1.3
|
|
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@7f9fc5e3cf521e84e0c9a667b0f6c6ad08c94b82 # v5.1.3
|
|
with:
|
|
token: ${{ secrets.CODECOV_TOKEN }}
|
|
flags: e2e
|
|
files: coverage/e2e/lcov.info
|
|
fail_ci_if_error: false
|
|
|
|
codecov-gate:
|
|
name: Codecov Gate
|
|
runs-on: ubuntu-latest
|
|
needs:
|
|
- codecov-upload
|
|
if: always()
|
|
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.outputs.push_image == 'true'
|
|
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.outputs.push_image == 'true'
|
|
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
|
|
if: always()
|
|
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
|