diff --git a/.github/workflows/cerberus-integration.yml b/.github/workflows/cerberus-integration.yml index 899e839f..d52af6e2 100644 --- a/.github/workflows/cerberus-integration.yml +++ b/.github/workflows/cerberus-integration.yml @@ -1,27 +1,11 @@ name: Cerberus Integration Tests on: - push: - branches: [ main, development, 'feature/**' ] - paths: - - 'backend/internal/caddy/**' - - 'backend/internal/security/**' - - 'backend/internal/handlers/security*.go' - - 'backend/internal/models/security*.go' - - 'scripts/cerberus_integration.sh' - - 'Dockerfile' - - '.github/workflows/cerberus-integration.yml' - pull_request: - branches: [ main, development ] - paths: - - 'backend/internal/caddy/**' - - 'backend/internal/security/**' - - 'backend/internal/handlers/security*.go' - - 'backend/internal/models/security*.go' - - 'scripts/cerberus_integration.sh' - - 'Dockerfile' - - '.github/workflows/cerberus-integration.yml' - # Allow manual trigger + workflow_run: + workflows: ["Docker Build, Publish & Test"] + types: [completed] + branches: [main, development, 'feature/**'] # Explicit branch filter prevents unexpected triggers + # Allow manual trigger for debugging workflow_dispatch: concurrency: @@ -33,19 +17,134 @@ jobs: name: Cerberus Security Stack Integration runs-on: ubuntu-latest timeout-minutes: 20 + # Only run if docker-build.yml succeeded, or if manually triggered + if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }} steps: - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 - - - name: Build Docker image + # Determine the correct image tag based on trigger context + # For PRs: pr-{number}-{sha}, For branches: {sanitized-branch}-{sha} + - name: Determine image tag + id: image + env: + EVENT: ${{ github.event.workflow_run.event }} + REF: ${{ github.event.workflow_run.head_branch }} + SHA: ${{ github.event.workflow_run.head_sha }} + MANUAL_TAG: ${{ inputs.image_tag }} run: | - docker build \ - --no-cache \ - --build-arg VCS_REF=${{ github.sha }} \ - -t charon:local . + # Manual trigger uses provided tag + if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + if [[ -n "$MANUAL_TAG" ]]; then + echo "tag=${MANUAL_TAG}" >> $GITHUB_OUTPUT + else + # Default to latest if no tag provided + echo "tag=latest" >> $GITHUB_OUTPUT + fi + echo "source_type=manual" >> $GITHUB_OUTPUT + exit 0 + fi + + # 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') + + if [[ -z "$PR_NUM" || "$PR_NUM" == "null" ]]; then + echo "❌ ERROR: Could not determine PR number" + echo "Event: $EVENT" + echo "Ref: $REF" + echo "SHA: $SHA" + echo "Pull Requests JSON: ${{ toJson(github.event.workflow_run.pull_requests) }}" + exit 1 + fi + + # Immutable tag with SHA suffix prevents race conditions + echo "tag=pr-${PR_NUM}-${SHORT_SHA}" >> $GITHUB_OUTPUT + echo "source_type=pr" >> $GITHUB_OUTPUT + else + # Branch push: sanitize branch name and append SHA + # Sanitization: lowercase, replace / with -, remove special chars + SANITIZED=$(echo "$REF" | \ + tr '[:upper:]' '[:lower:]' | \ + tr '/' '-' | \ + sed 's/[^a-z0-9-._]/-/g' | \ + sed 's/^-//; s/-$//' | \ + sed 's/--*/-/g' | \ + cut -c1-121) # Leave room for -SHORT_SHA (7 chars) + + echo "tag=${SANITIZED}-${SHORT_SHA}" >> $GITHUB_OUTPUT + echo "source_type=branch" >> $GITHUB_OUTPUT + fi + + echo "sha=${SHORT_SHA}" >> $GITHUB_OUTPUT + echo "Determined image tag: $(cat $GITHUB_OUTPUT | grep tag=)" + + # Pull image from registry with retry logic (dual-source strategy) + # Try registry first (fast), fallback to artifact if registry fails + - name: Pull Docker image from registry + id: pull_image + uses: nick-fields/retry@v3 + with: + timeout_minutes: 5 + max_attempts: 3 + retry_wait_seconds: 10 + command: | + IMAGE_NAME="ghcr.io/${{ github.repository_owner }}/charon:${{ steps.image.outputs.tag }}" + echo "Pulling image: $IMAGE_NAME" + docker pull "$IMAGE_NAME" + docker tag "$IMAGE_NAME" charon:local + echo "✅ Successfully pulled from registry" + continue-on-error: true + + # Fallback: Download artifact if registry pull failed + - name: Fallback to artifact download + if: steps.pull_image.outcome == 'failure' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SHA: ${{ steps.image.outputs.sha }} + run: | + echo "⚠️ Registry pull failed, falling back to artifact..." + + # Determine artifact name based on source type + if [[ "${{ steps.image.outputs.source_type }}" == "pr" ]]; then + PR_NUM=$(echo '${{ toJson(github.event.workflow_run.pull_requests) }}' | jq -r '.[0].number') + ARTIFACT_NAME="pr-image-${PR_NUM}" + else + ARTIFACT_NAME="push-image" + fi + + echo "Downloading artifact: $ARTIFACT_NAME" + gh run download ${{ github.event.workflow_run.id }} \ + --name "$ARTIFACT_NAME" \ + --dir /tmp/docker-image || { + echo "❌ ERROR: Artifact download failed!" + echo "Available artifacts:" + gh run view ${{ github.event.workflow_run.id }} --json artifacts --jq '.artifacts[].name' + exit 1 + } + + docker load < /tmp/docker-image/charon-image.tar + docker tag $(docker images --format "{{.Repository}}:{{.Tag}}" | head -1) charon:local + echo "✅ Successfully loaded from artifact" + + # Validate image freshness by checking SHA label + - name: Validate image SHA + env: + SHA: ${{ steps.image.outputs.sha }} + run: | + LABEL_SHA=$(docker inspect charon:local --format '{{index .Config.Labels "org.opencontainers.image.revision"}}' | cut -c1-7) + echo "Expected SHA: $SHA" + echo "Image SHA: $LABEL_SHA" + + if [[ "$LABEL_SHA" != "$SHA" ]]; then + echo "⚠️ WARNING: Image SHA mismatch!" + echo "Image may be stale. Proceeding with caution..." + else + echo "✅ Image SHA matches expected commit" + fi - name: Run Cerberus integration tests id: cerberus-test diff --git a/.github/workflows/crowdsec-integration.yml b/.github/workflows/crowdsec-integration.yml index dbed06fc..fa530736 100644 --- a/.github/workflows/crowdsec-integration.yml +++ b/.github/workflows/crowdsec-integration.yml @@ -1,31 +1,11 @@ name: CrowdSec Integration Tests on: - push: - branches: [ main, development, 'feature/**' ] - paths: - - 'backend/internal/crowdsec/**' - - 'backend/internal/models/crowdsec*.go' - - 'configs/crowdsec/**' - - 'scripts/crowdsec_integration.sh' - - 'scripts/crowdsec_decision_integration.sh' - - 'scripts/crowdsec_startup_test.sh' - - '.github/skills/integration-test-crowdsec*/**' - - 'Dockerfile' - - '.github/workflows/crowdsec-integration.yml' - pull_request: - branches: [ main, development ] - paths: - - 'backend/internal/crowdsec/**' - - 'backend/internal/models/crowdsec*.go' - - 'configs/crowdsec/**' - - 'scripts/crowdsec_integration.sh' - - 'scripts/crowdsec_decision_integration.sh' - - 'scripts/crowdsec_startup_test.sh' - - '.github/skills/integration-test-crowdsec*/**' - - 'Dockerfile' - - '.github/workflows/crowdsec-integration.yml' - # Allow manual trigger + workflow_run: + workflows: ["Docker Build, Publish & Test"] + types: [completed] + branches: [main, development, 'feature/**'] # Explicit branch filter prevents unexpected triggers + # Allow manual trigger for debugging workflow_dispatch: concurrency: @@ -37,19 +17,134 @@ jobs: name: CrowdSec Bouncer Integration runs-on: ubuntu-latest timeout-minutes: 15 + # Only run if docker-build.yml succeeded, or if manually triggered + if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }} steps: - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 - - - name: Build Docker image + # Determine the correct image tag based on trigger context + # For PRs: pr-{number}-{sha}, For branches: {sanitized-branch}-{sha} + - name: Determine image tag + id: image + env: + EVENT: ${{ github.event.workflow_run.event }} + REF: ${{ github.event.workflow_run.head_branch }} + SHA: ${{ github.event.workflow_run.head_sha }} + MANUAL_TAG: ${{ inputs.image_tag }} run: | - docker build \ - --no-cache \ - --build-arg VCS_REF=${{ github.sha }} \ - -t charon:local . + # Manual trigger uses provided tag + if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + if [[ -n "$MANUAL_TAG" ]]; then + echo "tag=${MANUAL_TAG}" >> $GITHUB_OUTPUT + else + # Default to latest if no tag provided + echo "tag=latest" >> $GITHUB_OUTPUT + fi + echo "source_type=manual" >> $GITHUB_OUTPUT + exit 0 + fi + + # 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') + + if [[ -z "$PR_NUM" || "$PR_NUM" == "null" ]]; then + echo "❌ ERROR: Could not determine PR number" + echo "Event: $EVENT" + echo "Ref: $REF" + echo "SHA: $SHA" + echo "Pull Requests JSON: ${{ toJson(github.event.workflow_run.pull_requests) }}" + exit 1 + fi + + # Immutable tag with SHA suffix prevents race conditions + echo "tag=pr-${PR_NUM}-${SHORT_SHA}" >> $GITHUB_OUTPUT + echo "source_type=pr" >> $GITHUB_OUTPUT + else + # Branch push: sanitize branch name and append SHA + # Sanitization: lowercase, replace / with -, remove special chars + SANITIZED=$(echo "$REF" | \ + tr '[:upper:]' '[:lower:]' | \ + tr '/' '-' | \ + sed 's/[^a-z0-9-._]/-/g' | \ + sed 's/^-//; s/-$//' | \ + sed 's/--*/-/g' | \ + cut -c1-121) # Leave room for -SHORT_SHA (7 chars) + + echo "tag=${SANITIZED}-${SHORT_SHA}" >> $GITHUB_OUTPUT + echo "source_type=branch" >> $GITHUB_OUTPUT + fi + + echo "sha=${SHORT_SHA}" >> $GITHUB_OUTPUT + echo "Determined image tag: $(cat $GITHUB_OUTPUT | grep tag=)" + + # Pull image from registry with retry logic (dual-source strategy) + # Try registry first (fast), fallback to artifact if registry fails + - name: Pull Docker image from registry + id: pull_image + uses: nick-fields/retry@v3 + with: + timeout_minutes: 5 + max_attempts: 3 + retry_wait_seconds: 10 + command: | + IMAGE_NAME="ghcr.io/${{ github.repository_owner }}/charon:${{ steps.image.outputs.tag }}" + echo "Pulling image: $IMAGE_NAME" + docker pull "$IMAGE_NAME" + docker tag "$IMAGE_NAME" charon:local + echo "✅ Successfully pulled from registry" + continue-on-error: true + + # Fallback: Download artifact if registry pull failed + - name: Fallback to artifact download + if: steps.pull_image.outcome == 'failure' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SHA: ${{ steps.image.outputs.sha }} + run: | + echo "⚠️ Registry pull failed, falling back to artifact..." + + # Determine artifact name based on source type + if [[ "${{ steps.image.outputs.source_type }}" == "pr" ]]; then + PR_NUM=$(echo '${{ toJson(github.event.workflow_run.pull_requests) }}' | jq -r '.[0].number') + ARTIFACT_NAME="pr-image-${PR_NUM}" + else + ARTIFACT_NAME="push-image" + fi + + echo "Downloading artifact: $ARTIFACT_NAME" + gh run download ${{ github.event.workflow_run.id }} \ + --name "$ARTIFACT_NAME" \ + --dir /tmp/docker-image || { + echo "❌ ERROR: Artifact download failed!" + echo "Available artifacts:" + gh run view ${{ github.event.workflow_run.id }} --json artifacts --jq '.artifacts[].name' + exit 1 + } + + docker load < /tmp/docker-image/charon-image.tar + docker tag $(docker images --format "{{.Repository}}:{{.Tag}}" | head -1) charon:local + echo "✅ Successfully loaded from artifact" + + # Validate image freshness by checking SHA label + - name: Validate image SHA + env: + SHA: ${{ steps.image.outputs.sha }} + run: | + LABEL_SHA=$(docker inspect charon:local --format '{{index .Config.Labels "org.opencontainers.image.revision"}}' | cut -c1-7) + echo "Expected SHA: $SHA" + echo "Image SHA: $LABEL_SHA" + + if [[ "$LABEL_SHA" != "$SHA" ]]; then + echo "⚠️ WARNING: Image SHA mismatch!" + echo "Image may be stale. Proceeding with caution..." + else + echo "✅ Image SHA matches expected commit" + fi - name: Run CrowdSec integration tests id: crowdsec-test diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 89b70024..d9126276 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -31,17 +31,10 @@ name: E2E Tests on: - pull_request: - branches: - - main - - development - - 'feature/**' - paths: - - 'frontend/**' - - 'backend/**' - - 'tests/**' - - 'playwright.config.js' - - '.github/workflows/e2e-tests.yml' + workflow_run: + workflows: ["Docker Build, Publish & Test"] + types: [completed] + branches: [main, development, 'feature/**'] # Explicit branch filter prevents unexpected triggers workflow_dispatch: inputs: @@ -137,6 +130,8 @@ jobs: runs-on: ubuntu-latest needs: build timeout-minutes: 30 + # Only run if docker-build.yml succeeded, or if manually triggered + if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }} env: # Required for security teardown (emergency reset fallback when ACL blocks API) CHARON_EMERGENCY_TOKEN: ${{ secrets.CHARON_EMERGENCY_TOKEN }} @@ -161,8 +156,70 @@ jobs: node-version: ${{ env.NODE_VERSION }} cache: 'npm' - - name: Download Docker image - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 + # Determine the correct image tag based on trigger context + # For PRs: pr-{number}-{sha}, For branches: {sanitized-branch}-{sha} + - name: Determine image tag + id: image + env: + EVENT: ${{ github.event.workflow_run.event }} + REF: ${{ github.event.workflow_run.head_branch }} + SHA: ${{ github.event.workflow_run.head_sha }} + MANUAL_TAG: ${{ inputs.image_tag }} + run: | + # Manual trigger uses provided tag + if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + if [[ -n "$MANUAL_TAG" ]]; then + echo "tag=${MANUAL_TAG}" >> $GITHUB_OUTPUT + else + # Default to latest if no tag provided + echo "tag=latest" >> $GITHUB_OUTPUT + fi + echo "source_type=manual" >> $GITHUB_OUTPUT + exit 0 + fi + + # 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') + + if [[ -z "$PR_NUM" || "$PR_NUM" == "null" ]]; then + echo "❌ ERROR: Could not determine PR number" + echo "Event: $EVENT" + echo "Ref: $REF" + echo "SHA: $SHA" + echo "Pull Requests JSON: ${{ toJson(github.event.workflow_run.pull_requests) }}" + exit 1 + fi + + # Immutable tag with SHA suffix prevents race conditions + echo "tag=pr-${PR_NUM}-${SHORT_SHA}" >> $GITHUB_OUTPUT + echo "source_type=pr" >> $GITHUB_OUTPUT + else + # Branch push: sanitize branch name and append SHA + # Sanitization: lowercase, replace / with -, remove special chars + SANITIZED=$(echo "$REF" | \ + tr '[:upper:]' '[:lower:]' | \ + tr '/' '-' | \ + sed 's/[^a-z0-9-._]/-/g' | \ + sed 's/^-//; s/-$//' | \ + sed 's/--*/-/g' | \ + cut -c1-121) # Leave room for -SHORT_SHA (7 chars) + + echo "tag=${SANITIZED}-${SHORT_SHA}" >> $GITHUB_OUTPUT + echo "source_type=branch" >> $GITHUB_OUTPUT + fi + + echo "sha=${SHORT_SHA}" >> $GITHUB_OUTPUT + echo "Determined image tag: $(cat $GITHUB_OUTPUT | grep tag=)" + + # Pull image from registry with retry logic (dual-source strategy) + # Try registry first (fast), fallback to artifact if registry fails + - name: Pull Docker image from registry + id: pull_image + uses: nick-fields/retry@v3 with: name: docker-image diff --git a/.github/workflows/rate-limit-integration.yml b/.github/workflows/rate-limit-integration.yml index 8625c1ad..f1dc032f 100644 --- a/.github/workflows/rate-limit-integration.yml +++ b/.github/workflows/rate-limit-integration.yml @@ -1,27 +1,11 @@ name: Rate Limit Integration Tests on: - push: - branches: [ main, development, 'feature/**' ] - paths: - - 'backend/internal/caddy/**' - - 'backend/internal/security/**' - - 'backend/internal/handlers/security*.go' - - 'backend/internal/models/security*.go' - - 'scripts/rate_limit_integration.sh' - - 'Dockerfile' - - '.github/workflows/rate-limit-integration.yml' - pull_request: - branches: [ main, development ] - paths: - - 'backend/internal/caddy/**' - - 'backend/internal/security/**' - - 'backend/internal/handlers/security*.go' - - 'backend/internal/models/security*.go' - - 'scripts/rate_limit_integration.sh' - - 'Dockerfile' - - '.github/workflows/rate-limit-integration.yml' - # Allow manual trigger + workflow_run: + workflows: ["Docker Build, Publish & Test"] + types: [completed] + branches: [main, development, 'feature/**'] # Explicit branch filter prevents unexpected triggers + # Allow manual trigger for debugging workflow_dispatch: concurrency: @@ -33,19 +17,134 @@ jobs: name: Rate Limiting Integration runs-on: ubuntu-latest timeout-minutes: 15 + # Only run if docker-build.yml succeeded, or if manually triggered + if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }} steps: - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 - - - name: Build Docker image + # Determine the correct image tag based on trigger context + # For PRs: pr-{number}-{sha}, For branches: {sanitized-branch}-{sha} + - name: Determine image tag + id: image + env: + EVENT: ${{ github.event.workflow_run.event }} + REF: ${{ github.event.workflow_run.head_branch }} + SHA: ${{ github.event.workflow_run.head_sha }} + MANUAL_TAG: ${{ inputs.image_tag }} run: | - docker build \ - --no-cache \ - --build-arg VCS_REF=${{ github.sha }} \ - -t charon:local . + # Manual trigger uses provided tag + if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + if [[ -n "$MANUAL_TAG" ]]; then + echo "tag=${MANUAL_TAG}" >> $GITHUB_OUTPUT + else + # Default to latest if no tag provided + echo "tag=latest" >> $GITHUB_OUTPUT + fi + echo "source_type=manual" >> $GITHUB_OUTPUT + exit 0 + fi + + # 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') + + if [[ -z "$PR_NUM" || "$PR_NUM" == "null" ]]; then + echo "❌ ERROR: Could not determine PR number" + echo "Event: $EVENT" + echo "Ref: $REF" + echo "SHA: $SHA" + echo "Pull Requests JSON: ${{ toJson(github.event.workflow_run.pull_requests) }}" + exit 1 + fi + + # Immutable tag with SHA suffix prevents race conditions + echo "tag=pr-${PR_NUM}-${SHORT_SHA}" >> $GITHUB_OUTPUT + echo "source_type=pr" >> $GITHUB_OUTPUT + else + # Branch push: sanitize branch name and append SHA + # Sanitization: lowercase, replace / with -, remove special chars + SANITIZED=$(echo "$REF" | \ + tr '[:upper:]' '[:lower:]' | \ + tr '/' '-' | \ + sed 's/[^a-z0-9-._]/-/g' | \ + sed 's/^-//; s/-$//' | \ + sed 's/--*/-/g' | \ + cut -c1-121) # Leave room for -SHORT_SHA (7 chars) + + echo "tag=${SANITIZED}-${SHORT_SHA}" >> $GITHUB_OUTPUT + echo "source_type=branch" >> $GITHUB_OUTPUT + fi + + echo "sha=${SHORT_SHA}" >> $GITHUB_OUTPUT + echo "Determined image tag: $(cat $GITHUB_OUTPUT | grep tag=)" + + # Pull image from registry with retry logic (dual-source strategy) + # Try registry first (fast), fallback to artifact if registry fails + - name: Pull Docker image from registry + id: pull_image + uses: nick-fields/retry@v3 + with: + timeout_minutes: 5 + max_attempts: 3 + retry_wait_seconds: 10 + command: | + IMAGE_NAME="ghcr.io/${{ github.repository_owner }}/charon:${{ steps.image.outputs.tag }}" + echo "Pulling image: $IMAGE_NAME" + docker pull "$IMAGE_NAME" + docker tag "$IMAGE_NAME" charon:local + echo "✅ Successfully pulled from registry" + continue-on-error: true + + # Fallback: Download artifact if registry pull failed + - name: Fallback to artifact download + if: steps.pull_image.outcome == 'failure' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SHA: ${{ steps.image.outputs.sha }} + run: | + echo "⚠️ Registry pull failed, falling back to artifact..." + + # Determine artifact name based on source type + if [[ "${{ steps.image.outputs.source_type }}" == "pr" ]]; then + PR_NUM=$(echo '${{ toJson(github.event.workflow_run.pull_requests) }}' | jq -r '.[0].number') + ARTIFACT_NAME="pr-image-${PR_NUM}" + else + ARTIFACT_NAME="push-image" + fi + + echo "Downloading artifact: $ARTIFACT_NAME" + gh run download ${{ github.event.workflow_run.id }} \ + --name "$ARTIFACT_NAME" \ + --dir /tmp/docker-image || { + echo "❌ ERROR: Artifact download failed!" + echo "Available artifacts:" + gh run view ${{ github.event.workflow_run.id }} --json artifacts --jq '.artifacts[].name' + exit 1 + } + + docker load < /tmp/docker-image/charon-image.tar + docker tag $(docker images --format "{{.Repository}}:{{.Tag}}" | head -1) charon:local + echo "✅ Successfully loaded from artifact" + + # Validate image freshness by checking SHA label + - name: Validate image SHA + env: + SHA: ${{ steps.image.outputs.sha }} + run: | + LABEL_SHA=$(docker inspect charon:local --format '{{index .Config.Labels "org.opencontainers.image.revision"}}' | cut -c1-7) + echo "Expected SHA: $SHA" + echo "Image SHA: $LABEL_SHA" + + if [[ "$LABEL_SHA" != "$SHA" ]]; then + echo "⚠️ WARNING: Image SHA mismatch!" + echo "Image may be stale. Proceeding with caution..." + else + echo "✅ Image SHA matches expected commit" + fi - name: Run rate limit integration tests id: ratelimit-test diff --git a/.github/workflows/waf-integration.yml b/.github/workflows/waf-integration.yml index 91527a62..28c09fc1 100644 --- a/.github/workflows/waf-integration.yml +++ b/.github/workflows/waf-integration.yml @@ -1,23 +1,11 @@ name: WAF Integration Tests on: - push: - branches: [ main, development, 'feature/**' ] - paths: - - 'backend/internal/caddy/**' - - 'backend/internal/models/security*.go' - - 'scripts/coraza_integration.sh' - - 'Dockerfile' - - '.github/workflows/waf-integration.yml' - pull_request: - branches: [ main, development ] - paths: - - 'backend/internal/caddy/**' - - 'backend/internal/models/security*.go' - - 'scripts/coraza_integration.sh' - - 'Dockerfile' - - '.github/workflows/waf-integration.yml' - # Allow manual trigger + workflow_run: + workflows: ["Docker Build, Publish & Test"] + types: [completed] + branches: [main, development, 'feature/**'] # Explicit branch filter prevents unexpected triggers + # Allow manual trigger for debugging workflow_dispatch: concurrency: @@ -29,19 +17,134 @@ jobs: name: Coraza WAF Integration runs-on: ubuntu-latest timeout-minutes: 15 + # Only run if docker-build.yml succeeded, or if manually triggered + if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }} steps: - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 - - - name: Build Docker image + # Determine the correct image tag based on trigger context + # For PRs: pr-{number}-{sha}, For branches: {sanitized-branch}-{sha} + - name: Determine image tag + id: image + env: + EVENT: ${{ github.event.workflow_run.event }} + REF: ${{ github.event.workflow_run.head_branch }} + SHA: ${{ github.event.workflow_run.head_sha }} + MANUAL_TAG: ${{ inputs.image_tag }} run: | - docker build \ - --no-cache \ - --build-arg VCS_REF=${{ github.sha }} \ - -t charon:local . + # Manual trigger uses provided tag + if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + if [[ -n "$MANUAL_TAG" ]]; then + echo "tag=${MANUAL_TAG}" >> $GITHUB_OUTPUT + else + # Default to latest if no tag provided + echo "tag=latest" >> $GITHUB_OUTPUT + fi + echo "source_type=manual" >> $GITHUB_OUTPUT + exit 0 + fi + + # 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') + + if [[ -z "$PR_NUM" || "$PR_NUM" == "null" ]]; then + echo "❌ ERROR: Could not determine PR number" + echo "Event: $EVENT" + echo "Ref: $REF" + echo "SHA: $SHA" + echo "Pull Requests JSON: ${{ toJson(github.event.workflow_run.pull_requests) }}" + exit 1 + fi + + # Immutable tag with SHA suffix prevents race conditions + echo "tag=pr-${PR_NUM}-${SHORT_SHA}" >> $GITHUB_OUTPUT + echo "source_type=pr" >> $GITHUB_OUTPUT + else + # Branch push: sanitize branch name and append SHA + # Sanitization: lowercase, replace / with -, remove special chars + SANITIZED=$(echo "$REF" | \ + tr '[:upper:]' '[:lower:]' | \ + tr '/' '-' | \ + sed 's/[^a-z0-9-._]/-/g' | \ + sed 's/^-//; s/-$//' | \ + sed 's/--*/-/g' | \ + cut -c1-121) # Leave room for -SHORT_SHA (7 chars) + + echo "tag=${SANITIZED}-${SHORT_SHA}" >> $GITHUB_OUTPUT + echo "source_type=branch" >> $GITHUB_OUTPUT + fi + + echo "sha=${SHORT_SHA}" >> $GITHUB_OUTPUT + echo "Determined image tag: $(cat $GITHUB_OUTPUT | grep tag=)" + + # Pull image from registry with retry logic (dual-source strategy) + # Try registry first (fast), fallback to artifact if registry fails + - name: Pull Docker image from registry + id: pull_image + uses: nick-fields/retry@v3 + with: + timeout_minutes: 5 + max_attempts: 3 + retry_wait_seconds: 10 + command: | + IMAGE_NAME="ghcr.io/${{ github.repository_owner }}/charon:${{ steps.image.outputs.tag }}" + echo "Pulling image: $IMAGE_NAME" + docker pull "$IMAGE_NAME" + docker tag "$IMAGE_NAME" charon:local + echo "✅ Successfully pulled from registry" + continue-on-error: true + + # Fallback: Download artifact if registry pull failed + - name: Fallback to artifact download + if: steps.pull_image.outcome == 'failure' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SHA: ${{ steps.image.outputs.sha }} + run: | + echo "⚠️ Registry pull failed, falling back to artifact..." + + # Determine artifact name based on source type + if [[ "${{ steps.image.outputs.source_type }}" == "pr" ]]; then + PR_NUM=$(echo '${{ toJson(github.event.workflow_run.pull_requests) }}' | jq -r '.[0].number') + ARTIFACT_NAME="pr-image-${PR_NUM}" + else + ARTIFACT_NAME="push-image" + fi + + echo "Downloading artifact: $ARTIFACT_NAME" + gh run download ${{ github.event.workflow_run.id }} \ + --name "$ARTIFACT_NAME" \ + --dir /tmp/docker-image || { + echo "❌ ERROR: Artifact download failed!" + echo "Available artifacts:" + gh run view ${{ github.event.workflow_run.id }} --json artifacts --jq '.artifacts[].name' + exit 1 + } + + docker load < /tmp/docker-image/charon-image.tar + docker tag $(docker images --format "{{.Repository}}:{{.Tag}}" | head -1) charon:local + echo "✅ Successfully loaded from artifact" + + # Validate image freshness by checking SHA label + - name: Validate image SHA + env: + SHA: ${{ steps.image.outputs.sha }} + run: | + LABEL_SHA=$(docker inspect charon:local --format '{{index .Config.Labels "org.opencontainers.image.revision"}}' | cut -c1-7) + echo "Expected SHA: $SHA" + echo "Image SHA: $LABEL_SHA" + + if [[ "$LABEL_SHA" != "$SHA" ]]; then + echo "⚠️ WARNING: Image SHA mismatch!" + echo "Image may be stale. Proceeding with caution..." + else + echo "✅ Image SHA matches expected commit" + fi - name: Run WAF integration tests id: waf-test diff --git a/.version b/.version index 1b9d9f00..6b60281a 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -v0.16.8 +v0.17.0