diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 9ff165a0..00000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: CI - Lint, Test & Coverage - -on: - push: - branches: [ main, development, 'feature/**' ] - pull_request: - branches: [ main, development ] - -jobs: - test-backend: - name: Backend Tests (Go) - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version: '1.22' - - name: Run Go tests - working-directory: backend - run: | - go test -v ./... - - test-frontend: - name: Frontend Tests (React) - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - cache: 'npm' - cache-dependency-path: frontend/package-lock.json - - name: Install dependencies - working-directory: frontend - run: npm ci - - name: Run frontend tests - working-directory: frontend - run: npm test diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml deleted file mode 100644 index 5dccaf88..00000000 --- a/.github/workflows/docker-build.yml +++ /dev/null @@ -1,274 +0,0 @@ -name: Build and Push Docker Images - -on: - push: - branches: - - main # Pushes to main โ†’ tags as "latest" - - development # Pushes to development โ†’ tags as "dev" - tags: - - 'v*.*.*' # Version tags (v1.0.0, v1.2.3, etc.) โ†’ tags as version number - workflow_dispatch: # Allows manual trigger from GitHub UI - -env: - REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository_owner }}/${{ github.event.repository.name }} - -jobs: - build-and-push: - name: Build and Push Docker Image - runs-on: ubuntu-latest - concurrency: - group: docker-build-${{ github.ref }} - cancel-in-progress: true - outputs: - skip_build: ${{ steps.skip.outputs.skip_build }} - permissions: - contents: read - packages: write - security-events: write - - steps: - # Step 1: Download the code - - name: ๐Ÿ“ฅ Checkout code - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 - - # Normalize IMAGE_NAME to lowercase to satisfy container registry format - - name: ๐Ÿ”ค Normalize image name - run: | - raw="${{ github.repository_owner }}/${{ github.event.repository.name }}" - echo "IMAGE_NAME=$(echo "$raw" | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV - - - name: ๐Ÿงช Determine skip condition - id: skip - run: | - should_skip=false - actor='${{ github.actor }}' - event='${{ github.event_name }}' - head_msg='${{ github.event.head_commit.message }}' - pr_title="" - if [ "$event" = "pull_request" ]; then - pr_title=$(jq -r '.pull_request.title' "$GITHUB_EVENT_PATH" 2>/dev/null || echo '') - fi - if [ "$actor" = "renovate[bot]" ]; then should_skip=true; fi - if echo "$head_msg" | grep -Ei '^chore\(deps' >/dev/null 2>&1; then should_skip=true; fi - if echo "$head_msg" | grep -Ei '^chore:' >/dev/null 2>&1; then should_skip=true; fi - if echo "$pr_title" | grep -Ei '^chore\(deps' >/dev/null 2>&1; then should_skip=true; fi - if echo "$pr_title" | grep -Ei '^chore:' >/dev/null 2>&1; then should_skip=true; fi - echo "skip_build=$should_skip" >> $GITHUB_OUTPUT - if [ "$should_skip" = true ]; then - echo "Skipping heavy docker build for actor=$actor event=$event (message/title matched)" - else - echo "Proceeding with full docker build" - fi - - # Step 2: Set up QEMU for multi-platform builds (ARM, AMD64, etc.) - - name: ๐Ÿ”ง Set up QEMU - uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3 - - # Step 3: Set up Docker Buildx (advanced Docker builder) - - name: ๐Ÿ”ง Set up Docker Buildx - uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3 - - # Resolve immutable digest for Caddy base - - name: ๐Ÿ“ฆ Resolve Caddy base digest - id: caddy - run: | - docker pull caddy:2-alpine - DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' caddy:2-alpine) - echo "image=$DIGEST" >> $GITHUB_OUTPUT - - # Step 4: Log in to GitHub Container Registry - - name: ๐Ÿ” Log in to GitHub Container Registry - uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.PROJECT_TOKEN }} - - # Step 5: Figure out what tags to use - - name: ๐Ÿท๏ธ Extract metadata (tags, labels) - id: meta - uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - tags: | - # Tag "latest" for main branch - type=raw,value=latest,enable={{is_default_branch}} - # Tag "dev" for development branch - type=raw,value=dev,enable=${{ github.ref == 'refs/heads/development' }} - # Tag version numbers from git tags (v1.0.0 โ†’ 1.0.0) - type=semver,pattern={{version}} - # Tag major.minor from git tags (v1.2.3 โ†’ 1.2) - type=semver,pattern={{major}}.{{minor}} - # Tag major from git tags (v1.2.3 โ†’ 1) - type=semver,pattern={{major}} - # Ephemeral tag for pull requests (derive number from GITHUB_REF if available) - type=raw,value=pr-${{ github.ref_name }},enable=${{ github.event_name == 'pull_request' }} - # Short SHA tag as fallback (for non-default non-dev push events) - type=sha,format=short,enable=${{ github.event_name != 'pull_request' }} - - # Step 6: Build the frontend first - - name: ๐ŸŽจ Build frontend - if: steps.skip.outputs.skip_build != 'true' - run: | - cd frontend - npm ci - npm run build - - # Step 7: Build and push Docker image - - name: ๐Ÿณ Build and push Docker image - if: steps.skip.outputs.skip_build != 'true' - uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 # v5 - id: build - with: - context: . - file: ./Dockerfile - platforms: ${{ github.event_name == 'pull_request' && 'linux/amd64' || 'linux/amd64,linux/arm64' }} - push: ${{ github.event_name != 'pull_request' }} - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max - build-args: | - CADDY_IMAGE=${{ steps.caddy.outputs.image }} - - # Step 8: Run Trivy scan (table output first for visibility) - - name: ๐Ÿ“‹ Run Trivy scan (table output) - if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true' - id: trivy_table - continue-on-error: true - uses: aquasecurity/trivy-action@master - with: - image-ref: ${{ fromJSON(steps.meta.outputs.json).tags[0] }} - format: 'table' - severity: 'CRITICAL,HIGH' - exit-code: '0' - - # Step 9: Run Trivy security scan (SARIF) - - name: ๐Ÿ” Run Trivy vulnerability scanner - if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true' - id: trivy - continue-on-error: true - uses: aquasecurity/trivy-action@master - with: - image-ref: ${{ fromJSON(steps.meta.outputs.json).tags[0] }} - format: 'sarif' - output: 'trivy-results.sarif' - exit-code: '0' - severity: 'CRITICAL,HIGH' - - # Step 10: Upload Trivy results to GitHub Security tab - - name: ๐Ÿ“ค Upload Trivy results to GitHub Security - uses: github/codeql-action/upload-sarif@v3 - if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true' && (steps.trivy.outcome == 'success' || steps.trivy.outcome == 'failure') - with: - sarif_file: 'trivy-results.sarif' - - # Step 11: Fail if vulnerabilities found - - name: โŒ Check for vulnerabilities - if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true' && steps.trivy_table.outcome == 'failure' - run: | - echo "::error::CRITICAL or HIGH vulnerabilities found in image" - exit 1 - - # Step 11: Create a summary - - name: ๐Ÿ“‹ Create summary - if: steps.skip.outputs.skip_build != 'true' - run: | - echo "## ๐ŸŽ‰ Docker Image Built Successfully!" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### ๐Ÿ“ฆ Image Details" >> $GITHUB_STEP_SUMMARY - echo "- **Registry**: GitHub Container Registry (ghcr.io)" >> $GITHUB_STEP_SUMMARY - echo "- **Repository**: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}" >> $GITHUB_STEP_SUMMARY - echo "- **Tags**: " >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo "${{ steps.meta.outputs.tags }}" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### ๐Ÿš€ How to Use" >> $GITHUB_STEP_SUMMARY - echo '```bash' >> $GITHUB_STEP_SUMMARY - echo "docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" >> $GITHUB_STEP_SUMMARY - echo "docker run -d -p 8080:8080 -v caddy_data:/app/data ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - - - name: ๐Ÿ“‹ Create skip summary - if: steps.skip.outputs.skip_build == 'true' - run: | - echo "## ๐Ÿšซ Docker Build Skipped" >> $GITHUB_STEP_SUMMARY - echo "Reason: renovate bot or chore(deps)/chore commit/PR title." >> $GITHUB_STEP_SUMMARY - echo "Actor: ${{ github.actor }}" >> $GITHUB_STEP_SUMMARY - echo "Event: ${{ github.event_name }}" >> $GITHUB_STEP_SUMMARY - - test-image: - name: Test Docker Image - needs: build-and-push - runs-on: ubuntu-latest - if: needs.build-and-push.outputs.skip_build != 'true' - - steps: - # Ensure normalized lowercase IMAGE_NAME for this job as well - - name: ๐Ÿ”ค Normalize image name - run: | - raw="${{ github.repository_owner }}/${{ github.event.repository.name }}" - echo "IMAGE_NAME=$(echo "$raw" | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV - # Step 1: Figure out which tag to test - - name: ๐Ÿท๏ธ Determine image tag - id: tag - run: | - if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then - echo "tag=latest" >> $GITHUB_OUTPUT - elif [[ "${{ github.ref }}" == "refs/heads/development" ]]; then - echo "tag=dev" >> $GITHUB_OUTPUT - elif [[ "${{ github.ref }}" == refs/tags/v* ]]; then - echo "tag=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT - else - echo "tag=sha-$(echo ${{ github.sha }} | cut -c1-7)" >> $GITHUB_OUTPUT - fi - - # Step 2: Pull the image we just built - - name: ๐Ÿ“ฅ Pull Docker image - run: docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }} - - # Step 3: Start the container - - name: ๐Ÿš€ Run container - run: | - docker run -d \ - --name test-container \ - -p 8080:8080 \ - ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }} - - # Step 4/5: Wait and check health with retries - - name: ๐Ÿฅ Test health endpoint (retries) - run: | - set +e - for i in $(seq 1 30); do - code=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/api/v1/health || echo "000") - if [ "$code" = "200" ]; then - echo "โœ… Health check passed on attempt $i" - exit 0 - fi - echo "Attempt $i/30: health not ready (code=$code); waiting..." - sleep 2 - done - echo "โŒ Health check failed after retries" - docker logs test-container || true - exit 1 - - # Step 6: Check the logs for errors - - name: ๐Ÿ“‹ Check container logs - if: always() - run: docker logs test-container - - # Step 7: Clean up - - name: ๐Ÿงน Stop container - if: always() - run: docker stop test-container && docker rm test-container - - # Step 8: Summary - - name: ๐Ÿ“‹ Create test summary - if: always() - run: | - echo "## ๐Ÿงช Docker Image Test Results" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "- **Image**: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }}" >> $GITHUB_STEP_SUMMARY - echo "- **Health Check**: ${{ job.status == 'success' && 'โœ… Passed' || 'โŒ Failed' }}" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index a25a11dc..7a91053f 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -11,7 +11,7 @@ on: branches: - main - development - workflow_call: # Allow this workflow to be called by other workflows + workflow_call: env: REGISTRY: ghcr.io @@ -20,6 +20,7 @@ env: jobs: build-and-push: runs-on: ubuntu-latest + timeout-minutes: 30 permissions: contents: read packages: write @@ -27,7 +28,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Normalize image name run: | @@ -51,16 +52,17 @@ jobs: if echo "$pr_title" | grep -Ei '^chore\(deps' >/dev/null 2>&1; then should_skip=true; fi if echo "$pr_title" | grep -Ei '^chore:' >/dev/null 2>&1; then should_skip=true; fi echo "skip_build=$should_skip" >> $GITHUB_OUTPUT - if [ "$should_skip" = true ]; then - echo "Skipping heavy docker publish for actor=$actor event=$event (message/title matched)" - else - echo "Proceeding with docker publish" - fi + + - name: Set up QEMU + if: steps.skip.outputs.skip_build != 'true' + uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3.0.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3 + if: steps.skip.outputs.skip_build != 'true' + uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0 - name: Resolve Caddy base digest + if: steps.skip.outputs.skip_build != 'true' id: caddy run: | docker pull caddy:2-alpine @@ -68,8 +70,8 @@ jobs: echo "image=$DIGEST" >> $GITHUB_OUTPUT - name: Log in to Container Registry - if: github.event_name != 'pull_request' - uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3 + if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true' + uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -78,15 +80,12 @@ jobs: - name: Extract metadata (tags, labels) if: steps.skip.outputs.skip_build != 'true' id: meta - uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5 + uses: docker/metadata-action@dbef88086f6cef02e264edb7dbf63250c17cef6c # v5.0.1 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | - # Tag 'latest' for main branch type=raw,value=latest,enable={{is_default_branch}} - # Tag 'dev' for development branch type=raw,value=dev,enable=${{ github.ref == 'refs/heads/development' }} - # Semver tags for version releases (v1.0.0 -> 1.0.0, 1.0, 1) type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}} @@ -94,10 +93,11 @@ jobs: - name: Build and push Docker image if: steps.skip.outputs.skip_build != 'true' id: build-and-push - uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 # v5 + uses: docker/build-push-action@4a13e500e55cf31b7a5d59a38ab2040ab0f42f56 # v5.1.0 with: context: . - platforms: linux/amd64,linux/arm64 + # PRs: amd64 only, no push. Pushes: amd64+arm64, push. + platforms: ${{ github.event_name == 'pull_request' && 'linux/amd64' || 'linux/amd64,linux/arm64' }} push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} @@ -109,51 +109,30 @@ jobs: VCS_REF=${{ github.sha }} CADDY_IMAGE=${{ steps.caddy.outputs.image }} - - name: Image digest - if: steps.skip.outputs.skip_build != 'true' - run: echo ${{ steps.build-and-push.outputs.digest }} - - - name: Run Trivy scan (table output first for visibility) - if: github.event_name != 'pull_request' - && steps.skip.outputs.skip_build != 'true' - id: trivy_table - continue-on-error: true - uses: aquasecurity/trivy-action@master + # Trivy steps only on push + - name: Run Trivy scan (table output) + if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true' + uses: aquasecurity/trivy-action@d43c1f16c00cfd3978dde6c07f4bbcf9eb6993ca # v0.16.1 with: image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build-and-push.outputs.digest }} format: 'table' severity: 'CRITICAL,HIGH' exit-code: '0' - - - name: Run Trivy vulnerability scanner - if: github.event_name != 'pull_request' - && steps.skip.outputs.skip_build != 'true' - id: trivy continue-on-error: true - uses: aquasecurity/trivy-action@master + + - name: Run Trivy vulnerability scanner (SARIF) + if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true' + id: trivy + uses: aquasecurity/trivy-action@d43c1f16c00cfd3978dde6c07f4bbcf9eb6993ca # v0.16.1 with: image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build-and-push.outputs.digest }} format: 'sarif' output: 'trivy-results.sarif' - exit-code: '0' severity: 'CRITICAL,HIGH' + continue-on-error: true - - name: Upload Trivy results to GitHub Security - if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true' && (steps.trivy.outcome == 'success' || steps.trivy.outcome == 'failure') + - name: Upload Trivy results + if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true' uses: github/codeql-action/upload-sarif@v3 with: sarif_file: 'trivy-results.sarif' - - - name: Check for vulnerabilities - if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true' && steps.trivy_table.outcome == 'failure' - run: | - echo "::error::CRITICAL or HIGH vulnerabilities found in image" - exit 1 - - - name: Skip summary - if: steps.skip.outputs.skip_build == 'true' - run: | - echo "## ๐Ÿšซ Docker Publish Skipped" >> $GITHUB_STEP_SUMMARY - echo "Reason: renovate bot or chore(deps)/chore commit/PR title." >> $GITHUB_STEP_SUMMARY - echo "Actor: ${{ github.actor }}" >> $GITHUB_STEP_SUMMARY - echo "Event: ${{ github.event_name }}" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/quality-checks.yml b/.github/workflows/quality-checks.yml new file mode 100644 index 00000000..5cddf2a2 --- /dev/null +++ b/.github/workflows/quality-checks.yml @@ -0,0 +1,58 @@ +name: Quality Checks + +on: + push: + branches: [ main, development, 'feature/**' ] + pull_request: + branches: [ main, development ] + +jobs: + backend-quality: + name: Backend (Go) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - name: Set up Go + uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 + with: + go-version: '1.22' + cache-dependency-path: backend/go.sum + + - name: Run Go tests + working-directory: backend + run: go test -v ./... + + - name: Run golangci-lint + uses: golangci/golangci-lint-action@3cfe3a4abbb849e10058ce4af15d205b6da42804 # v4.0.0 + with: + version: latest + working-directory: backend + args: --timeout=5m + continue-on-error: true + + frontend-quality: + name: Frontend (React) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - name: Set up Node.js + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: frontend/package-lock.json + + - name: Install dependencies + working-directory: frontend + run: npm ci + + - name: Run frontend tests + working-directory: frontend + run: npm test + + - name: Run frontend lint + working-directory: frontend + run: npm run lint + continue-on-error: true