chore: Refactor CI workflows for pipeline consolidation and manual dispatch triggers

- Updated quality-checks.yml to support manual dispatch with frontend checks.
- Modified rate-limit-integration.yml to remove workflow_run triggers and adjust conditions for execution.
- Removed pull request triggers from repo-health.yml, retaining only scheduled and manual dispatch.
- Adjusted security-pr.yml and supply-chain-pr.yml to eliminate workflow_run dependencies and refine execution conditions.
- Cleaned up supply-chain-verify.yml by removing workflow_run triggers and ensuring proper execution conditions.
- Updated waf-integration.yml to remove workflow_run triggers, allowing manual dispatch only.
- Revised current_spec.md to reflect the consolidation of CI workflows into a single pipeline, detailing objectives, research findings, and implementation plans.
This commit is contained in:
GitHub Actions
2026-02-08 05:36:29 +00:00
parent ac030cc54e
commit e7f791044d
18 changed files with 1222 additions and 389 deletions

View File

@@ -1,9 +1,6 @@
name: Go Benchmark
on:
workflow_run:
workflows: ["Docker Build, Publish & Test"]
types: [completed]
workflow_dispatch:
concurrency:

View File

@@ -3,11 +3,6 @@ name: Cerberus Integration
# Phase 2-3: Build Once, Test Many - Use registry image instead of building
# This workflow now waits for docker-build.yml to complete and pulls the built image
on:
workflow_run:
workflows: ["Docker Build, Publish & Test"]
types: [completed]
branches: [main, development, 'feature/**', 'hotfix/**']
# Allow manual trigger for debugging
workflow_dispatch:
inputs:
image_tag:
@@ -27,7 +22,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 20
# Only run if docker-build.yml succeeded, or if manually triggered
if: ${{ github.event_name == 'workflow_dispatch' || (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success') }}
if: ${{ github.event_name == 'workflow_dispatch' || (github.event_name == 'workflow_run' && (github.event.workflow_run.status != 'completed' || github.event.workflow_run.conclusion == 'success')) }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
@@ -57,9 +52,10 @@ jobs:
# Extract 7-character short SHA
SHORT_SHA=$(echo "$SHA" | cut -c1-7)
if [[ "$EVENT" == "pull_request" ]]; then
# Use native pull_requests array (no API calls needed)
PR_NUM=$(echo '${{ toJson(github.event.workflow_run.pull_requests) }}' | jq -r '.[0].number')
# Use native pull_requests array (no API calls needed)
PR_NUM=$(echo '${{ toJson(github.event.workflow_run.pull_requests) }}' | jq -r '.[0].number // empty')
if [[ "$EVENT" == "pull_request" || -n "$PR_NUM" ]]; then
# Fallback for direct PR trigger
if [[ -z "$PR_NUM" || "$PR_NUM" == "null" ]]; then

697
.github/workflows/ci-pipeline.yml vendored Normal file
View File

@@ -0,0 +1,697 @@
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
concurrency:
group: ci-manual-pipeline-${{ github.ref_name }}-${{ github.run_id }}
cancel-in-progress: true
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: 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
uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0
with:
version: latest
working-directory: backend
args: --timeout=5m
continue-on-error: true
- name: GORM Security Scanner
run: |
chmod +x scripts/scan-gorm-security.sh
./scripts/scan-gorm-security.sh --check
- 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 frontend dependencies
working-directory: frontend
run: npm ci
- name: Run frontend lint
working-directory: frontend
run: npm run lint
continue-on-error: true
build-image:
name: Build and Publish Image
runs-on: ubuntu-latest
needs: lint
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
TAGS=()
TAGS+=("${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:${DEFAULT_TAG}")
TAGS+=("${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${DEFAULT_TAG}")
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' && secrets.DOCKERHUB_TOKEN != '' }}
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: inputs.run_integration != false && needs.build-image.outputs.push_image == 'true'
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Log in to Docker Hub
if: ${{ secrets.DOCKERHUB_TOKEN != '' }}
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: inputs.run_integration != false && needs.build-image.outputs.push_image == 'true'
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Log in to Docker Hub
if: ${{ secrets.DOCKERHUB_TOKEN != '' }}
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: inputs.run_integration != false && needs.build-image.outputs.push_image == 'true'
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Log in to Docker Hub
if: ${{ secrets.DOCKERHUB_TOKEN != '' }}
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: inputs.run_integration != false && needs.build-image.outputs.push_image == 'true'
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Log in to Docker Hub
if: ${{ secrets.DOCKERHUB_TOKEN != '' }}
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:
- 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
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: 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: 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@ea165f2524e81b1a7f1f18e1bdb77f0840c18dd9 # v4
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: 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@ea165f2524e81b1a7f1f18e1bdb77f0840c18dd9 # v4
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: inputs.run_coverage != false
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Download backend coverage artifact
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4
with:
name: backend-coverage
path: backend/
- name: Download frontend coverage artifact
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4
with:
name: frontend-coverage
path: frontend/coverage
- name: Download E2E coverage artifact
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4
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: inputs.run_security_scans != false && env.IS_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: 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
if: ${{ secrets.DOCKERHUB_TOKEN != '' }}
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: 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
if: ${{ secrets.DOCKERHUB_TOKEN != '' }}
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

View File

@@ -1,12 +1,21 @@
name: Upload Coverage to Codecov
on:
workflow_run:
workflows: ["Docker Build, Publish & Test"]
types: [completed]
workflow_dispatch:
inputs:
run_backend:
description: 'Run backend coverage upload'
required: false
default: true
type: boolean
run_frontend:
description: 'Run frontend coverage upload'
required: false
default: true
type: boolean
concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.workflow_run.head_branch || github.head_ref || github.ref_name }}
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.run_id }}
cancel-in-progress: true
env:
@@ -22,13 +31,13 @@ jobs:
name: Backend Codecov Upload
runs-on: ubuntu-latest
timeout-minutes: 15
if: ${{ github.event.workflow_run.conclusion == 'success' }}
if: ${{ inputs.run_backend != false }}
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
ref: ${{ github.sha }}
- name: Set up Go
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6
@@ -56,13 +65,13 @@ jobs:
name: Frontend Codecov Upload
runs-on: ubuntu-latest
timeout-minutes: 15
if: ${{ github.event.workflow_run.conclusion == 'success' }}
if: ${{ inputs.run_frontend != false }}
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
ref: ${{ github.sha }}
- name: Set up Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6

View File

@@ -1,11 +1,9 @@
name: CodeQL - Analyze
on:
workflow_run:
workflows: ["Docker Build, Publish & Test"]
types: [completed]
workflow_dispatch:
schedule:
- cron: '0 3 * * 1'
- cron: '0 3 * * 1' # Mondays 03:00 UTC
concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.workflow_run.head_branch || github.head_ref || github.ref_name }}
@@ -27,7 +25,7 @@ jobs:
runs-on: ubuntu-latest
# Skip forked PRs where CHARON_TOKEN lacks security-events permissions
if: >-
(github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success')
(github.event_name != 'workflow_run' || github.event.workflow_run.status != 'completed' || github.event.workflow_run.conclusion == 'success')
permissions:
contents: read
security-events: write

View File

@@ -3,11 +3,6 @@ name: CrowdSec Integration
# Phase 2-3: Build Once, Test Many - Use registry image instead of building
# This workflow now waits for docker-build.yml to complete and pulls the built image
on:
workflow_run:
workflows: ["Docker Build, Publish & Test"]
types: [completed]
branches: [main, development, 'feature/**', 'hotfix/**']
# Allow manual trigger for debugging
workflow_dispatch:
inputs:
image_tag:
@@ -27,7 +22,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 15
# Only run if docker-build.yml succeeded, or if manually triggered
if: ${{ github.event_name == 'workflow_dispatch' || (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success') }}
if: ${{ github.event_name == 'workflow_dispatch' || (github.event_name == 'workflow_run' && (github.event.workflow_run.status != 'completed' || github.event.workflow_run.conclusion == 'success')) }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
@@ -57,9 +52,10 @@ jobs:
# Extract 7-character short SHA
SHORT_SHA=$(echo "$SHA" | cut -c1-7)
if [[ "$EVENT" == "pull_request" ]]; then
# Use native pull_requests array (no API calls needed)
PR_NUM=$(echo '${{ toJson(github.event.workflow_run.pull_requests) }}' | jq -r '.[0].number')
# Use native pull_requests array (no API calls needed)
PR_NUM=$(echo '${{ toJson(github.event.workflow_run.pull_requests) }}' | jq -r '.[0].number // empty')
if [[ "$EVENT" == "pull_request" || -n "$PR_NUM" ]]; then
# Fallback for direct PR trigger
if [[ -z "$PR_NUM" || "$PR_NUM" == "null" ]]; then

View File

@@ -21,17 +21,7 @@ name: Docker Build, Publish & Test
# See: docs/plans/current_spec.md (Section 4.1 - docker-build.yml changes)
on:
workflow_run:
workflows: [Docker Lint]
types:
- completed
branches:
- main
- development
- 'feature/**'
- 'hotfix/**'
workflow_dispatch:
workflow_call:
concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_branch || github.head_ref || github.ref_name }}

View File

@@ -1,10 +1,7 @@
name: Docker Lint
on:
push:
branches: [ main, development, 'feature/**', 'hotfix/**' ]
pull_request:
branches: [ main, development, 'feature/**', 'hotfix/**' ]
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.head_ref || github.ref_name }}

View File

@@ -13,9 +13,38 @@
name: 'E2E Tests'
on:
workflow_run:
workflows: ["Docker Build, Publish & Test"]
types: [completed]
workflow_call:
inputs:
browser:
description: 'Browser to test'
required: false
default: 'all'
type: string
test_category:
description: 'Test category'
required: false
default: 'all'
type: string
image_ref:
description: 'Image reference (digest) to test, e.g. docker.io/wikid82/charon@sha256:...'
required: false
type: string
image_tag:
description: 'Local image tag for compose usage (default: charon:e2e-test)'
required: false
type: string
playwright_coverage:
description: 'Enable Playwright coverage (V8)'
required: false
default: false
type: boolean
secrets:
CHARON_EMERGENCY_TOKEN:
required: false
DOCKERHUB_USERNAME:
required: false
DOCKERHUB_TOKEN:
required: false
workflow_dispatch:
inputs:
browser:
@@ -37,37 +66,75 @@ on:
- all
- security
- non-security
image_ref:
description: 'Image reference (digest) to test, e.g. docker.io/wikid82/charon@sha256:...'
required: false
type: string
image_tag:
description: 'Local image tag for compose usage (default: charon:e2e-test)'
required: false
type: string
playwright_coverage:
description: 'Enable Playwright coverage (V8)'
required: false
default: false
type: boolean
env:
NODE_VERSION: '20'
GO_VERSION: '1.25.7'
GOTOOLCHAIN: auto
REGISTRY: ghcr.io
DOCKERHUB_REGISTRY: docker.io
IMAGE_NAME: ${{ github.repository_owner }}/charon
PLAYWRIGHT_COVERAGE: ${{ vars.PLAYWRIGHT_COVERAGE || '0' }}
PLAYWRIGHT_COVERAGE: ${{ (inputs.playwright_coverage && '1') || (vars.PLAYWRIGHT_COVERAGE || '0') }}
DEBUG: 'charon:*,charon-test:*'
PLAYWRIGHT_DEBUG: '1'
CI_LOG_LEVEL: 'verbose'
concurrency:
group: e2e-split-${{ github.workflow }}-${{ github.event.workflow_run.pull_requests[0].number || github.event.pull_request.number || github.event.workflow_run.head_branch || github.head_ref || github.ref_name }}
group: e2e-split-${{ github.workflow }}-${{ github.ref_name }}-${{ github.run_id }}
cancel-in-progress: true
jobs:
# Build application once, share across all browser jobs
# Prepare application image once, share across all browser jobs
build:
name: Build Application
name: Prepare Application Image
runs-on: ubuntu-latest
if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }}
outputs:
image_digest: ${{ steps.build-image.outputs.digest }}
image_source: ${{ steps.resolve-image.outputs.image_source }}
image_ref: ${{ steps.resolve-image.outputs.image_ref }}
image_tag: ${{ steps.resolve-image.outputs.image_tag }}
image_digest: ${{ steps.resolve-image.outputs.image_digest != '' && steps.resolve-image.outputs.image_digest || steps.build-image.outputs.digest }}
steps:
- name: Resolve image inputs
id: resolve-image
run: |
IMAGE_REF="${{ inputs.image_ref }}"
IMAGE_TAG="${{ inputs.image_tag || 'charon:e2e-test' }}"
if [ -n "$IMAGE_REF" ]; then
echo "image_source=registry" >> "$GITHUB_OUTPUT"
echo "image_ref=$IMAGE_REF" >> "$GITHUB_OUTPUT"
echo "image_tag=$IMAGE_TAG" >> "$GITHUB_OUTPUT"
if [[ "$IMAGE_REF" == *@* ]]; then
echo "image_digest=${IMAGE_REF#*@}" >> "$GITHUB_OUTPUT"
else
echo "image_digest=" >> "$GITHUB_OUTPUT"
fi
exit 0
fi
echo "image_source=build" >> "$GITHUB_OUTPUT"
echo "image_ref=" >> "$GITHUB_OUTPUT"
echo "image_tag=$IMAGE_TAG" >> "$GITHUB_OUTPUT"
echo "image_digest=" >> "$GITHUB_OUTPUT"
- name: Checkout repository
if: steps.resolve-image.outputs.image_source == 'build'
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
ref: ${{ github.sha }}
- name: Set up Go
if: steps.resolve-image.outputs.image_source == 'build'
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6
with:
go-version: ${{ env.GO_VERSION }}
@@ -75,12 +142,14 @@ jobs:
cache-dependency-path: backend/go.sum
- name: Set up Node.js
if: steps.resolve-image.outputs.image_source == 'build'
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Cache npm dependencies
if: steps.resolve-image.outputs.image_source == 'build'
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5
with:
path: ~/.npm
@@ -88,27 +157,32 @@ jobs:
restore-keys: npm-
- name: Install dependencies
if: steps.resolve-image.outputs.image_source == 'build'
run: npm ci
- name: Set up Docker Buildx
if: steps.resolve-image.outputs.image_source == 'build'
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
- name: Build Docker image
id: build-image
if: steps.resolve-image.outputs.image_source == 'build'
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
with:
context: .
file: ./Dockerfile
push: false
load: true
tags: charon:e2e-test
tags: ${{ steps.resolve-image.outputs.image_tag }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Save Docker image
run: docker save charon:e2e-test -o charon-e2e-image.tar
if: steps.resolve-image.outputs.image_source == 'build'
run: docker save ${{ steps.resolve-image.outputs.image_tag }} -o charon-e2e-image.tar
- name: Upload Docker image artifact
if: steps.resolve-image.outputs.image_source == 'build'
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: docker-image
@@ -127,21 +201,20 @@ jobs:
runs-on: ubuntu-latest
needs: build
if: |
(github.event_name != 'workflow_dispatch') ||
(github.event.inputs.browser == 'chromium' || github.event.inputs.browser == 'all') &&
(github.event.inputs.test_category == 'security' || github.event.inputs.test_category == 'all')
(inputs.browser == 'chromium' || inputs.browser == 'all') &&
(inputs.test_category == 'security' || inputs.test_category == 'all')
timeout-minutes: 30
env:
CHARON_EMERGENCY_TOKEN: ${{ secrets.CHARON_EMERGENCY_TOKEN }}
CHARON_EMERGENCY_SERVER_ENABLED: "true"
CHARON_SECURITY_TESTS_ENABLED: "true" # Cerberus ON for enforcement tests
CHARON_E2E_IMAGE_TAG: charon:e2e-test
CHARON_E2E_IMAGE_TAG: ${{ needs.build.outputs.image_tag }}
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
ref: ${{ github.sha }}
- name: Set up Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
@@ -149,7 +222,23 @@ jobs:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Download Docker image
- name: Log in to Docker Hub
if: needs.build.outputs.image_source == 'registry' && secrets.DOCKERHUB_TOKEN != ''
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
registry: ${{ env.DOCKERHUB_REGISTRY }}
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Pull shared Docker image
if: needs.build.outputs.image_source == 'registry'
run: |
docker pull "${{ needs.build.outputs.image_ref }}"
docker tag "${{ needs.build.outputs.image_ref }}" "${{ needs.build.outputs.image_tag }}"
docker images | grep charon
- name: Download Docker image artifact
if: needs.build.outputs.image_source == 'build'
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
name: docker-image
@@ -171,7 +260,8 @@ jobs:
env:
CHARON_EMERGENCY_TOKEN: ${{ secrets.CHARON_EMERGENCY_TOKEN }}
- name: Load Docker image
- name: Load Docker image artifact
if: needs.build.outputs.image_source == 'build'
run: |
docker load -i charon-e2e-image.tar
docker images | grep charon
@@ -287,21 +377,20 @@ jobs:
runs-on: ubuntu-latest
needs: build
if: |
(github.event_name != 'workflow_dispatch') ||
(github.event.inputs.browser == 'firefox' || github.event.inputs.browser == 'all') &&
(github.event.inputs.test_category == 'security' || github.event.inputs.test_category == 'all')
(inputs.browser == 'firefox' || inputs.browser == 'all') &&
(inputs.test_category == 'security' || inputs.test_category == 'all')
timeout-minutes: 30
env:
CHARON_EMERGENCY_TOKEN: ${{ secrets.CHARON_EMERGENCY_TOKEN }}
CHARON_EMERGENCY_SERVER_ENABLED: "true"
CHARON_SECURITY_TESTS_ENABLED: "true" # Cerberus ON for enforcement tests
CHARON_E2E_IMAGE_TAG: charon:e2e-test
CHARON_E2E_IMAGE_TAG: ${{ needs.build.outputs.image_tag }}
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
ref: ${{ github.sha }}
- name: Set up Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
@@ -309,7 +398,23 @@ jobs:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Download Docker image
- name: Log in to Docker Hub
if: needs.build.outputs.image_source == 'registry' && secrets.DOCKERHUB_TOKEN != ''
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
registry: ${{ env.DOCKERHUB_REGISTRY }}
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Pull shared Docker image
if: needs.build.outputs.image_source == 'registry'
run: |
docker pull "${{ needs.build.outputs.image_ref }}"
docker tag "${{ needs.build.outputs.image_ref }}" "${{ needs.build.outputs.image_tag }}"
docker images | grep charon
- name: Download Docker image artifact
if: needs.build.outputs.image_source == 'build'
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
name: docker-image
@@ -331,7 +436,8 @@ jobs:
env:
CHARON_EMERGENCY_TOKEN: ${{ secrets.CHARON_EMERGENCY_TOKEN }}
- name: Load Docker image
- name: Load Docker image artifact
if: needs.build.outputs.image_source == 'build'
run: |
docker load -i charon-e2e-image.tar
docker images | grep charon
@@ -455,21 +561,20 @@ jobs:
runs-on: ubuntu-latest
needs: build
if: |
(github.event_name != 'workflow_dispatch') ||
(github.event.inputs.browser == 'webkit' || github.event.inputs.browser == 'all') &&
(github.event.inputs.test_category == 'security' || github.event.inputs.test_category == 'all')
(inputs.browser == 'webkit' || inputs.browser == 'all') &&
(inputs.test_category == 'security' || inputs.test_category == 'all')
timeout-minutes: 30
env:
CHARON_EMERGENCY_TOKEN: ${{ secrets.CHARON_EMERGENCY_TOKEN }}
CHARON_EMERGENCY_SERVER_ENABLED: "true"
CHARON_SECURITY_TESTS_ENABLED: "true" # Cerberus ON for enforcement tests
CHARON_E2E_IMAGE_TAG: charon:e2e-test
CHARON_E2E_IMAGE_TAG: ${{ needs.build.outputs.image_tag }}
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
ref: ${{ github.sha }}
- name: Set up Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
@@ -477,7 +582,23 @@ jobs:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Download Docker image
- name: Log in to Docker Hub
if: needs.build.outputs.image_source == 'registry' && secrets.DOCKERHUB_TOKEN != ''
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
registry: ${{ env.DOCKERHUB_REGISTRY }}
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Pull shared Docker image
if: needs.build.outputs.image_source == 'registry'
run: |
docker pull "${{ needs.build.outputs.image_ref }}"
docker tag "${{ needs.build.outputs.image_ref }}" "${{ needs.build.outputs.image_tag }}"
docker images | grep charon
- name: Download Docker image artifact
if: needs.build.outputs.image_source == 'build'
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
name: docker-image
@@ -499,7 +620,8 @@ jobs:
env:
CHARON_EMERGENCY_TOKEN: ${{ secrets.CHARON_EMERGENCY_TOKEN }}
- name: Load Docker image
- name: Load Docker image artifact
if: needs.build.outputs.image_source == 'build'
run: |
docker load -i charon-e2e-image.tar
docker images | grep charon
@@ -630,15 +752,14 @@ jobs:
runs-on: ubuntu-latest
needs: build
if: |
(github.event_name != 'workflow_dispatch') ||
(github.event.inputs.browser == 'chromium' || github.event.inputs.browser == 'all') &&
(github.event.inputs.test_category == 'non-security' || github.event.inputs.test_category == 'all')
(inputs.browser == 'chromium' || inputs.browser == 'all') &&
(inputs.test_category == 'non-security' || inputs.test_category == 'all')
timeout-minutes: 20
env:
CHARON_EMERGENCY_TOKEN: ${{ secrets.CHARON_EMERGENCY_TOKEN }}
CHARON_EMERGENCY_SERVER_ENABLED: "true"
CHARON_SECURITY_TESTS_ENABLED: "false" # Cerberus OFF for non-security tests
CHARON_E2E_IMAGE_TAG: charon:e2e-test
CHARON_E2E_IMAGE_TAG: ${{ needs.build.outputs.image_tag }}
strategy:
fail-fast: false
matrix:
@@ -649,7 +770,7 @@ jobs:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
ref: ${{ github.sha }}
- name: Set up Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
@@ -657,12 +778,29 @@ jobs:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Download Docker image
- name: Log in to Docker Hub
if: needs.build.outputs.image_source == 'registry' && secrets.DOCKERHUB_TOKEN != ''
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
registry: ${{ env.DOCKERHUB_REGISTRY }}
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Pull shared Docker image
if: needs.build.outputs.image_source == 'registry'
run: |
docker pull "${{ needs.build.outputs.image_ref }}"
docker tag "${{ needs.build.outputs.image_ref }}" "${{ needs.build.outputs.image_tag }}"
docker images | grep charon
- name: Download Docker image artifact
if: needs.build.outputs.image_source == 'build'
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
name: docker-image
- name: Load Docker image
- name: Load Docker image artifact
if: needs.build.outputs.image_source == 'build'
run: |
docker load -i charon-e2e-image.tar
docker images | grep charon
@@ -787,15 +925,14 @@ jobs:
runs-on: ubuntu-latest
needs: build
if: |
(github.event_name != 'workflow_dispatch') ||
(github.event.inputs.browser == 'firefox' || github.event.inputs.browser == 'all') &&
(github.event.inputs.test_category == 'non-security' || github.event.inputs.test_category == 'all')
(inputs.browser == 'firefox' || inputs.browser == 'all') &&
(inputs.test_category == 'non-security' || inputs.test_category == 'all')
timeout-minutes: 20
env:
CHARON_EMERGENCY_TOKEN: ${{ secrets.CHARON_EMERGENCY_TOKEN }}
CHARON_EMERGENCY_SERVER_ENABLED: "true"
CHARON_SECURITY_TESTS_ENABLED: "false" # Cerberus OFF for non-security tests
CHARON_E2E_IMAGE_TAG: charon:e2e-test
CHARON_E2E_IMAGE_TAG: ${{ needs.build.outputs.image_tag }}
strategy:
fail-fast: false
matrix:
@@ -806,7 +943,7 @@ jobs:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
ref: ${{ github.sha }}
- name: Set up Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
@@ -814,12 +951,29 @@ jobs:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Download Docker image
- name: Log in to Docker Hub
if: needs.build.outputs.image_source == 'registry' && secrets.DOCKERHUB_TOKEN != ''
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
registry: ${{ env.DOCKERHUB_REGISTRY }}
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Pull shared Docker image
if: needs.build.outputs.image_source == 'registry'
run: |
docker pull "${{ needs.build.outputs.image_ref }}"
docker tag "${{ needs.build.outputs.image_ref }}" "${{ needs.build.outputs.image_tag }}"
docker images | grep charon
- name: Download Docker image artifact
if: needs.build.outputs.image_source == 'build'
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
name: docker-image
- name: Load Docker image
- name: Load Docker image artifact
if: needs.build.outputs.image_source == 'build'
run: |
docker load -i charon-e2e-image.tar
docker images | grep charon
@@ -952,15 +1106,14 @@ jobs:
runs-on: ubuntu-latest
needs: build
if: |
(github.event_name != 'workflow_dispatch') ||
(github.event.inputs.browser == 'webkit' || github.event.inputs.browser == 'all') &&
(github.event.inputs.test_category == 'non-security' || github.event.inputs.test_category == 'all')
(inputs.browser == 'webkit' || inputs.browser == 'all') &&
(inputs.test_category == 'non-security' || inputs.test_category == 'all')
timeout-minutes: 20
env:
CHARON_EMERGENCY_TOKEN: ${{ secrets.CHARON_EMERGENCY_TOKEN }}
CHARON_EMERGENCY_SERVER_ENABLED: "true"
CHARON_SECURITY_TESTS_ENABLED: "false" # Cerberus OFF for non-security tests
CHARON_E2E_IMAGE_TAG: charon:e2e-test
CHARON_E2E_IMAGE_TAG: ${{ needs.build.outputs.image_tag }}
strategy:
fail-fast: false
matrix:
@@ -971,7 +1124,7 @@ jobs:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
ref: ${{ github.sha }}
- name: Set up Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
@@ -979,12 +1132,29 @@ jobs:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Download Docker image
- name: Log in to Docker Hub
if: needs.build.outputs.image_source == 'registry' && secrets.DOCKERHUB_TOKEN != ''
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
registry: ${{ env.DOCKERHUB_REGISTRY }}
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Pull shared Docker image
if: needs.build.outputs.image_source == 'registry'
run: |
docker pull "${{ needs.build.outputs.image_ref }}"
docker tag "${{ needs.build.outputs.image_ref }}" "${{ needs.build.outputs.image_tag }}"
docker images | grep charon
- name: Download Docker image artifact
if: needs.build.outputs.image_source == 'build'
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
name: docker-image
- name: Load Docker image
- name: Load Docker image artifact
if: needs.build.outputs.image_source == 'build'
run: |
docker load -i charon-e2e-image.tar
docker images | grep charon

View File

@@ -1,12 +1,16 @@
name: Quality Checks
on:
workflow_run:
workflows: ["Docker Build, Publish & Test"]
types: [completed]
workflow_dispatch:
inputs:
run_frontend:
description: 'Run frontend checks'
required: false
default: true
type: boolean
concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.workflow_run.head_branch || github.head_ref || github.ref_name }}
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.run_id }}
cancel-in-progress: true
permissions:
@@ -22,11 +26,10 @@ jobs:
backend-quality:
name: Backend (Go)
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
ref: ${{ github.sha }}
- name: Set up Go
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
@@ -126,12 +129,12 @@ jobs:
frontend-quality:
name: Frontend (React)
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
if: ${{ inputs.run_frontend != false }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
ref: ${{ github.sha }}
- name: Repo health check
run: |
@@ -147,8 +150,8 @@ jobs:
- name: Check if frontend was modified in PR
id: check-frontend
run: |
EVENT_NAME="${{ github.event.workflow_run.event || github.event_name }}"
BASE_REF="${{ github.event.workflow_run.pull_requests[0].base.ref || github.event.pull_request.base.ref }}"
EVENT_NAME="${{ github.event_name }}"
BASE_REF="${{ github.event.pull_request.base.ref }}"
if [ "$EVENT_NAME" = "push" ]; then
echo "frontend_changed=true" >> $GITHUB_OUTPUT
@@ -188,13 +191,13 @@ jobs:
- name: Install dependencies
working-directory: frontend
if: ${{ github.event.workflow_run.event == 'push' || steps.check-frontend.outputs.frontend_changed == 'true' }}
if: ${{ inputs.run_frontend != false && (github.event_name == 'workflow_dispatch' || steps.check-frontend.outputs.frontend_changed == 'true') }}
run: npm ci
- name: Run frontend tests and coverage
id: frontend-tests
working-directory: ${{ github.workspace }}
if: ${{ github.event.workflow_run.event == 'push' || steps.check-frontend.outputs.frontend_changed == 'true' }}
if: ${{ inputs.run_frontend != false && (github.event_name == 'workflow_dispatch' || steps.check-frontend.outputs.frontend_changed == 'true') }}
run: |
bash scripts/frontend-test-coverage.sh 2>&1 | tee frontend/test-output.txt
exit ${PIPESTATUS[0]}

View File

@@ -3,11 +3,6 @@ name: Rate Limit integration
# Phase 2-3: Build Once, Test Many - Use registry image instead of building
# This workflow now waits for docker-build.yml to complete and pulls the built image
on:
workflow_run:
workflows: ["Docker Build, Publish & Test"]
types: [completed]
branches: [main, development, 'feature/**', 'hotfix/**']
# Allow manual trigger for debugging
workflow_dispatch:
inputs:
image_tag:
@@ -27,7 +22,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 15
# Only run if docker-build.yml succeeded, or if manually triggered
if: ${{ github.event_name == 'workflow_dispatch' || (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success') }}
if: ${{ github.event_name == 'workflow_dispatch' || (github.event_name == 'workflow_run' && (github.event.workflow_run.status != 'completed' || github.event.workflow_run.conclusion == 'success')) }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
@@ -57,9 +52,10 @@ jobs:
# Extract 7-character short SHA
SHORT_SHA=$(echo "$SHA" | cut -c1-7)
if [[ "$EVENT" == "pull_request" ]]; then
# Use native pull_requests array (no API calls needed)
PR_NUM=$(echo '${{ toJson(github.event.workflow_run.pull_requests) }}' | jq -r '.[0].number')
# Use native pull_requests array (no API calls needed)
PR_NUM=$(echo '${{ toJson(github.event.workflow_run.pull_requests) }}' | jq -r '.[0].number // empty')
if [[ "$EVENT" == "pull_request" || -n "$PR_NUM" ]]; then
# Fallback for direct PR trigger
if [[ -z "$PR_NUM" || "$PR_NUM" == "null" ]]; then

View File

@@ -3,8 +3,6 @@ name: Repo Health Check
on:
schedule:
- cron: '0 0 * * *'
pull_request:
types: [opened, synchronize, reopened]
workflow_dispatch: {}
concurrency:

View File

@@ -4,12 +4,6 @@
name: Security Scan (PR)
on:
workflow_run:
workflows: ["Docker Build, Publish & Test"]
types:
- completed
branches: [main, development, 'feature/**', 'hotfix/**']
workflow_dispatch:
inputs:
pr_number:
@@ -29,8 +23,8 @@ jobs:
# Run for: manual dispatch, PR builds, or any push builds from docker-build
if: >-
github.event_name == 'workflow_dispatch' ||
((github.event.workflow_run.event == 'pull_request' || github.event.workflow_run.event == 'push') &&
github.event.workflow_run.conclusion == 'success')
((github.event.workflow_run.event == 'push' || github.event.workflow_run.pull_requests[0].number != null) &&
(github.event.workflow_run.status != 'completed' || github.event.workflow_run.conclusion == 'success'))
permissions:
contents: read
@@ -82,7 +76,7 @@ jobs:
fi
# Check if this is a push event (not a PR)
if [[ "${{ github.event.workflow_run.event }}" == "push" || "${{ github.event_name }}" == "push" ]]; then
if [[ "${{ github.event_name }}" == "push" || "${{ github.event.workflow_run.event }}" == "push" || -z "${PR_NUMBER}" ]]; then
HEAD_BRANCH="${{ github.event.workflow_run.head_branch || github.ref_name }}"
echo "is_push=true" >> "$GITHUB_OUTPUT"
echo "✅ Detected push build from branch: ${HEAD_BRANCH}"

View File

@@ -3,12 +3,6 @@
name: Supply Chain Verification (PR)
on:
workflow_run:
workflows: ["Docker Build, Publish & Test"]
types:
- completed
branches: [main, development, 'feature/**', 'hotfix/**']
workflow_dispatch:
inputs:
pr_number:
@@ -35,8 +29,8 @@ jobs:
if: >
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'workflow_run' &&
(github.event.workflow_run.event == 'pull_request' || github.event.workflow_run.event == 'push') &&
github.event.workflow_run.conclusion == 'success')
(github.event.workflow_run.event == 'push' || github.event.workflow_run.pull_requests[0].number != null) &&
(github.event.workflow_run.status != 'completed' || github.event.workflow_run.conclusion == 'success'))
steps:
- name: Checkout repository
@@ -95,7 +89,7 @@ jobs:
fi
# Check if this is a push event (not a PR)
if [[ "${WORKFLOW_RUN_EVENT}" == "push" || "${EVENT_NAME}" == "push" ]]; then
if [[ "${WORKFLOW_RUN_EVENT}" == "push" || "${EVENT_NAME}" == "push" || -z "${PR_NUMBER}" ]]; then
echo "is_push=true" >> "$GITHUB_OUTPUT"
echo "✅ Detected push build from branch: ${HEAD_BRANCH}"
else

View File

@@ -1,26 +1,9 @@
name: Supply Chain Verification
on:
release:
types: [published]
# Triggered after docker-build workflow completes
# Note: workflow_run can only chain 3 levels deep; we're at level 2 (safe)
#
# IMPORTANT: No branches filter here by design
# GitHub Actions limitation: branches filter in workflow_run only matches the default branch.
# Without a filter, this workflow triggers for ALL branches where docker-build completes,
# providing proper supply chain verification coverage for feature branches and PRs.
# Security: The workflow file must exist on the branch to execute, preventing untrusted code.
workflow_run:
workflows: ["Docker Build, Publish & Test"]
types: [completed]
schedule:
# Run weekly on Mondays at 00:00 UTC
- cron: '0 0 * * 1'
workflow_dispatch:
schedule:
- cron: '0 0 * * 1' # Mondays 00:00 UTC
permissions:
contents: read
@@ -39,8 +22,8 @@ jobs:
if: |
(github.event_name != 'schedule' || github.ref == 'refs/heads/main') &&
(github.event_name != 'workflow_run' ||
(github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event != 'pull_request'))
(github.event.workflow_run.event != 'pull_request' &&
(github.event.workflow_run.status != 'completed' || github.event.workflow_run.conclusion == 'success')))
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

View File

@@ -3,11 +3,6 @@ name: WAF integration
# Phase 2-3: Build Once, Test Many - Use registry image instead of building
# This workflow now waits for docker-build.yml to complete and pulls the built image
on:
workflow_run:
workflows: ["Docker Build, Publish & Test"]
types: [completed]
branches: [main, development, 'feature/**', 'hotfix/**']
# Allow manual trigger for debugging
workflow_dispatch:
inputs:
image_tag: