From f817725fbec94afabef9b9de0b7ff78702d323d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikha=C3=ABl=20Regni?= Date: Fri, 24 Jul 2026 12:52:02 +0200 Subject: [PATCH 01/27] chore(ci): add concurrency groups to workflows Co-Authored-By: Claude Fable 5 --- .github/workflows/ci.yml | 4 ++++ .github/workflows/codeql.yml | 4 ++++ .github/workflows/pr.yml | 4 ++++ .github/workflows/security-full-scan.yml | 4 ++++ .github/workflows/security.yml | 4 ++++ 5 files changed, 20 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b83e4324..a267450e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,6 +8,10 @@ on: - "docs/**" - ".github/workflows/docs.yml" +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + env: REGISTRY: ${{ vars.DOCKER_REGISTRY || 'docker.io' }} IMAGE_NAME: ${{ vars.DOCKER_IMAGE_NAME || 'uping/boardgametracker' }} diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index b6ce42dc..c68dcd45 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -8,6 +8,10 @@ on: schedule: - cron: '15 7 * * 4' +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: analyze: name: Analyze (${{ matrix.language }}) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 7c7e0f5d..968a954d 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -5,6 +5,10 @@ on: branches: [master] types: [opened, synchronize, reopened, edited, labeled, unlabeled] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: labeler: name: PR Labeler diff --git a/.github/workflows/security-full-scan.yml b/.github/workflows/security-full-scan.yml index f1ba0665..187aa1f5 100644 --- a/.github/workflows/security-full-scan.yml +++ b/.github/workflows/security-full-scan.yml @@ -6,6 +6,10 @@ on: - cron: "0 1 * * 1-5" workflow_dispatch: +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false + env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index d89a47b8..909e83dc 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -4,6 +4,10 @@ on: pull_request: branches: [master] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: dependency-scan-backend: name: Backend Dependency Scan From dc8cff8c6887ce0fe658c95007b3eecd627af642 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikha=C3=ABl=20Regni?= Date: Fri, 24 Jul 2026 12:52:37 +0200 Subject: [PATCH 02/27] chore(ci): trim redundant pr triggers and fold version job into test job Co-Authored-By: Claude Fable 5 --- .github/workflows/ci.yml | 29 +++++------------------------ .github/workflows/pr.yml | 2 +- 2 files changed, 6 insertions(+), 25 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a267450e..8e1de5d2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,13 +17,13 @@ env: IMAGE_NAME: ${{ vars.DOCKER_IMAGE_NAME || 'uping/boardgametracker' }} jobs: - version: - name: Calculate Version + test-and-analyze: + name: Test and SonarCloud Analysis runs-on: ubuntu-latest permissions: contents: read - outputs: - version: ${{ steps.versioning.outputs.version }} + pull-requests: write + checks: write steps: - name: Harden runner uses: step-security/harden-runner@v2 @@ -45,25 +45,6 @@ jobs: version_format: "${major}.${minor}.${patch}-beta" debug: true - test-and-analyze: - name: Test and SonarCloud Analysis - runs-on: ubuntu-latest - needs: [version] - permissions: - contents: read - pull-requests: write - checks: write - steps: - - name: Harden runner - uses: step-security/harden-runner@v2 - with: - egress-policy: audit - - - name: Checkout - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - name: Set up JDK 17 uses: actions/setup-java@v5 with: @@ -123,7 +104,7 @@ jobs: /o:"${{ env.SONAR_ORGANIZATION }}" \ /d:sonar.host.url="https://sonarcloud.io" \ /d:sonar.token="${{ env.SONAR_TOKEN }}" \ - /v:"${{ needs.version.outputs.version }}" \ + /v:"${{ steps.versioning.outputs.version }}" \ /d:sonar.cs.opencover.reportsPaths="TestResults/**/coverage.opencover.xml" \ /d:sonar.cs.vstest.reportsPaths="TestResults/*.trx" \ /d:sonar.javascript.lcov.reportPaths="coverage/lcov.info" \ diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 968a954d..48cfe5b4 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -3,7 +3,7 @@ name: PR Quality on: pull_request: branches: [master] - types: [opened, synchronize, reopened, edited, labeled, unlabeled] + types: [opened, synchronize, reopened, edited] concurrency: group: ${{ github.workflow }}-${{ github.ref }} From 231ea355c623b43c1c1dca9aaac4520f61f0a235 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikha=C3=ABl=20Regni?= Date: Fri, 24 Jul 2026 12:52:49 +0200 Subject: [PATCH 03/27] chore(ci): skip security scans for docs-only changes Co-Authored-By: Claude Fable 5 --- .github/workflows/security.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 909e83dc..3f230f35 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -3,6 +3,13 @@ name: Security Scanning on: pull_request: branches: [master] + paths-ignore: + - "**/*.md" + - "docs/**" + - ".github/images/**" + - "LICENSE" + - "crowdin.yml" + - ".vscode/**" concurrency: group: ${{ github.workflow }}-${{ github.ref }} From ec41d329c6860087bc0db32e27a1edfa3f2ee181 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikha=C3=ABl=20Regni?= Date: Fri, 24 Jul 2026 12:53:46 +0200 Subject: [PATCH 04/27] fix(ci): remove shell interpolation of secrets and outputs in workflows GitHub expression interpolation inside run: blocks substitutes values before bash parses the line, so credentials or outputs containing quotes, dollars, or backticks break the script or execute as shell. Pass values through env: and reference them as shell variables, build the login JSON with jq, and validate the extracted JWT charset before it is used anywhere downstream. Co-Authored-By: Claude Fable 5 --- .github/workflows/ci.yml | 11 ++++--- .github/workflows/pr.yml | 5 ++- .github/workflows/security-full-scan.yml | 42 ++++++++++++++++-------- 3 files changed, 37 insertions(+), 21 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8e1de5d2..2b05f114 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -98,13 +98,14 @@ jobs: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} SONAR_PROJECT_KEY: ${{ secrets.SONAR_PROJECT_KEY }} SONAR_ORGANIZATION: ${{ secrets.SONAR_ORGANIZATION }} + VERSION: ${{ steps.versioning.outputs.version }} run: | ./.sonar/scanner/dotnet-sonarscanner begin \ - /k:"${{ env.SONAR_PROJECT_KEY }}" \ - /o:"${{ env.SONAR_ORGANIZATION }}" \ + /k:"$SONAR_PROJECT_KEY" \ + /o:"$SONAR_ORGANIZATION" \ /d:sonar.host.url="https://sonarcloud.io" \ - /d:sonar.token="${{ env.SONAR_TOKEN }}" \ - /v:"${{ steps.versioning.outputs.version }}" \ + /d:sonar.token="$SONAR_TOKEN" \ + /v:"$VERSION" \ /d:sonar.cs.opencover.reportsPaths="TestResults/**/coverage.opencover.xml" \ /d:sonar.cs.vstest.reportsPaths="TestResults/*.trx" \ /d:sonar.javascript.lcov.reportPaths="coverage/lcov.info" \ @@ -171,7 +172,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - run: ./.sonar/scanner/dotnet-sonarscanner end /d:sonar.token="${{ env.SONAR_TOKEN }}" + run: ./.sonar/scanner/dotnet-sonarscanner end /d:sonar.token="$SONAR_TOKEN" if: env.SONAR_TOKEN != '' - name: Upload test results diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 48cfe5b4..7730b0a7 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -34,11 +34,10 @@ jobs: - name: Remove old size labels env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} run: | - PR_NUMBER=${{ github.event.pull_request.number }} - REPO=${{ github.repository }} for LABEL in "size/XS" "size/S" "size/M" "size/L" "size/XL"; do - gh api "repos/$REPO/issues/$PR_NUMBER/labels/$LABEL" -X DELETE 2>/dev/null || true + gh api "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/labels/$LABEL" -X DELETE 2>/dev/null || true done - name: Label PR by size diff --git a/.github/workflows/security-full-scan.yml b/.github/workflows/security-full-scan.yml index 187aa1f5..9dc46908 100644 --- a/.github/workflows/security-full-scan.yml +++ b/.github/workflows/security-full-scan.yml @@ -77,20 +77,24 @@ jobs: - name: Create resource group run: | az group create \ - --name "${{ env.RESOURCE_GROUP }}" \ - --location "${{ env.AZURE_LOCATION }}" \ - --tags purpose=security-scan run_id=${{ github.run_id }} + --name "$RESOURCE_GROUP" \ + --location "$AZURE_LOCATION" \ + --tags purpose=security-scan run_id="$GITHUB_RUN_ID" - name: Deploy Bicep template id: deploy + env: + IMAGE_REF: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }} + SEC_DB_PASSWORD: ${{ steps.secrets.outputs.db_password }} + SEC_JWT_SECRET: ${{ steps.secrets.outputs.jwt_secret }} run: | RESULT=$(az deployment group create \ - --resource-group "${{ env.RESOURCE_GROUP }}" \ + --resource-group "$RESOURCE_GROUP" \ --template-file infra/security-scan.bicep \ --parameters \ - imageName="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}" \ - dbPassword="${{ steps.secrets.outputs.db_password }}" \ - jwtSecret="${{ steps.secrets.outputs.jwt_secret }}" \ + imageName="$IMAGE_REF" \ + dbPassword="$SEC_DB_PASSWORD" \ + jwtSecret="$SEC_JWT_SECRET" \ --query "properties.outputs" \ --output json) @@ -100,8 +104,9 @@ jobs: echo "Application URL: $APP_URL" - name: Wait for application to become healthy + env: + APP_URL: ${{ steps.deploy.outputs.app_url }} run: | - APP_URL="${{ steps.deploy.outputs.app_url }}" echo "Polling $APP_URL/api/settings ..." for i in $(seq 1 120); do @@ -122,20 +127,31 @@ jobs: env: ZAP_TEST_USERNAME: ${{ secrets.ZAP_TEST_USERNAME }} ZAP_TEST_PASSWORD: ${{ secrets.ZAP_TEST_PASSWORD }} + APP_URL: ${{ steps.deploy.outputs.app_url }} run: | - APP_URL="${{ steps.deploy.outputs.app_url }}" + set -euo pipefail + + PAYLOAD=$(jq -n \ + --arg u "$ZAP_TEST_USERNAME" \ + --arg p "$ZAP_TEST_PASSWORD" \ + '{username: $u, password: $p}') RESPONSE=$(curl -sf -X POST "$APP_URL/api/auth/login" \ -H "Content-Type: application/json" \ - -d "{\"username\": \"${{ env.ZAP_TEST_USERNAME }}\", \"password\": \"${{ env.ZAP_TEST_PASSWORD }}\"}") + --data-binary "$PAYLOAD") - TOKEN=$(echo "$RESPONSE" | jq -r '.accessToken') + TOKEN=$(printf '%s' "$RESPONSE" | jq -r '.accessToken // empty') - if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then + if [ -z "$TOKEN" ]; then echo "::error::Failed to extract JWT from login response" exit 1 fi + if ! printf '%s' "$TOKEN" | grep -Eq '^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$'; then + echo "::error::Login response did not contain a well-formed JWT" + exit 1 + fi + echo "::add-mask::$TOKEN" echo "token=$TOKEN" >> "$GITHUB_OUTPUT" echo "Authentication successful" @@ -179,7 +195,7 @@ jobs: if: always() run: | az group delete \ - --name "${{ env.RESOURCE_GROUP }}" \ + --name "$RESOURCE_GROUP" \ --yes \ --no-wait echo "Resource group deletion initiated (async)" From d4e1a32b3374fe7ee8d43325c07e9e1acbaaf5ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikha=C3=ABl=20Regni?= Date: Fri, 24 Jul 2026 12:54:07 +0200 Subject: [PATCH 05/27] chore(ci): tighten dependency scan scope and failure behavior pnpm audit now gates on high severity in production dependencies only; dev-tree advisories stay visible through Renovate and the nightly informational audit. The NuGet check fails closed if restore or list errors instead of reporting a false pass. Co-Authored-By: Claude Fable 5 --- .github/workflows/security.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 3f230f35..10adb71a 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -40,6 +40,8 @@ jobs: - name: Check for vulnerable packages run: | + set -euo pipefail + dotnet list ./BoardGameTracker.sln package --vulnerable --include-transitive 2>&1 | tee vulnerable.txt if grep -q "has the following vulnerable packages" vulnerable.txt; then @@ -83,7 +85,7 @@ jobs: - name: Audit for vulnerabilities run: | cd boardgametracker.client - pnpm audit --audit-level=moderate + pnpm audit --audit-level=high --prod secret-detection: name: Secret Detection From 4cacecd91c95b10434df9adef0c07caa18728063 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikha=C3=ABl=20Regni?= Date: Fri, 24 Jul 2026 12:54:34 +0200 Subject: [PATCH 06/27] refactor(ci): move image-heavy scans off the PR path Docker Scout and ZAP both built the full image on every PR push (~7m30s wall-clock). Trivy already gates CVEs on every master publish; deep scanning now lives in the nightly workflow. The PR security workflow keeps the three sub-minute checks: NuGet vulnerabilities, pnpm audit, and Gitleaks. Co-Authored-By: Claude Fable 5 --- .github/workflows/security.yml | 171 --------------------------------- 1 file changed, 171 deletions(-) diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 10adb71a..1cd0bc7d 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -107,174 +107,3 @@ jobs: uses: gitleaks/gitleaks-action@v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - docker-scout: - name: Docker Scout CVE Scan - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: write - security-events: write - steps: - - name: Harden runner - uses: step-security/harden-runner@v2 - with: - egress-policy: audit - - - name: Checkout - uses: actions/checkout@v6 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v4 - - - name: Login to Docker Hub - uses: docker/login-action@v4 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Build image for scanning - uses: docker/build-push-action@v7 - with: - context: . - file: ./Dockerfile - push: false - load: true - tags: bgt:scout-scan - cache-from: type=gha - build-args: | - VERSION=0.0.0-security - - - name: Docker Scout CVE scan - uses: docker/scout-action@v1 - with: - command: cves - image: local://bgt:scout-scan - only-severities: critical,high - sarif-file: scout-results.sarif - exit-code: true - only-fixed: true - - - name: Upload Scout SARIF - uses: github/codeql-action/upload-sarif@v4 - if: always() - with: - sarif_file: scout-results.sarif - category: docker-scout - - zap-baseline: - name: ZAP Baseline Scan - runs-on: ubuntu-latest - permissions: - contents: read - issues: write - pull-requests: write - steps: - - name: Harden runner - uses: step-security/harden-runner@v2 - with: - egress-policy: audit - - - name: Checkout - uses: actions/checkout@v6 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v4 - - - name: Build application image - uses: docker/build-push-action@v7 - with: - context: . - file: ./Dockerfile - push: false - load: true - tags: bgt:security-scan - cache-from: type=gha - build-args: | - VERSION=0.0.0-security - - - name: Generate ephemeral secrets - id: secrets - run: | - DB_PASSWORD=$(openssl rand -base64 24) - JWT_SECRET=$(openssl rand -base64 48) - echo "::add-mask::$DB_PASSWORD" - echo "::add-mask::$JWT_SECRET" - echo "db_password=$DB_PASSWORD" >> "$GITHUB_OUTPUT" - echo "jwt_secret=$JWT_SECRET" >> "$GITHUB_OUTPUT" - - - name: Start application stack - env: - SEC_DB_PASSWORD: ${{ steps.secrets.outputs.db_password }} - SEC_JWT_SECRET: ${{ steps.secrets.outputs.jwt_secret }} - run: | - docker compose -f infra/docker-compose.security.yml up -d - echo "Waiting for application to become healthy..." - for i in $(seq 1 60); do - if curl -sf http://localhost:5444/api/health > /dev/null 2>&1; then - echo "Application is healthy after ${i}s" - break - fi - if [ "$i" -eq 60 ]; then - echo "::error::Application failed to become healthy within 60s" - docker compose -f infra/docker-compose.security.yml logs - exit 1 - fi - sleep 1 - done - - - name: Authenticate and extract JWT - id: auth - env: - ZAP_TEST_USERNAME: ${{ secrets.ZAP_TEST_USERNAME }} - ZAP_TEST_PASSWORD: ${{ secrets.ZAP_TEST_PASSWORD }} - run: | - RESPONSE=$(curl -sf -X POST http://localhost:5444/api/auth/login \ - -H "Content-Type: application/json" \ - -d "{\"username\": \"${{ env.ZAP_TEST_USERNAME }}\", \"password\": \"${{ env.ZAP_TEST_PASSWORD }}\"}") - - TOKEN=$(echo "$RESPONSE" | jq -r '.accessToken') - - if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then - echo "::error::Failed to extract JWT from login response" - echo "Response: $RESPONSE" - exit 1 - fi - - echo "::add-mask::$TOKEN" - echo "token=$TOKEN" >> "$GITHUB_OUTPUT" - - - name: Import OpenAPI spec into ZAP and run baseline - run: | - # Download the OpenAPI spec so ZAP knows all 60+ endpoints - curl -sf http://localhost:5444/swagger/v1/swagger.json -o /tmp/swagger.json - - ENDPOINT_COUNT=$(jq '[.paths | keys[] ] | length' /tmp/swagger.json) - echo "OpenAPI spec contains $ENDPOINT_COUNT endpoint paths" - - if [ "$ENDPOINT_COUNT" -lt 10 ]; then - echo "::error::OpenAPI spec has suspiciously few endpoints ($ENDPOINT_COUNT)" - exit 1 - fi - - - name: Run ZAP API Scan - uses: zaproxy/action-api-scan@v0.10.0 - with: - target: "http://localhost:5444/swagger/v1/swagger.json" - format: openapi - docker_name: "ghcr.io/zaproxy/zaproxy:stable" - allow_issue_writing: true - fail_action: "false" - cmd_options: >- - -z "-config replacer.full_list(0).description=AuthHeader - -config replacer.full_list(0).enabled=true - -config replacer.full_list(0).matchtype=REQ_HEADER - -config replacer.full_list(0).matchstr=Authorization - -config replacer.full_list(0).regex=false - -config replacer.full_list(0).replacement=Bearer\ ${{ steps.auth.outputs.token }} - -config replacer.full_list(0).initiators=" - rules_file_name: ".zap/rules.tsv" - - - name: Tear down application stack - if: always() - run: docker compose -f infra/docker-compose.security.yml down -v From 65c02bd84e2b194295569a4a4a49ff5a3c896f26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikha=C3=ABl=20Regni?= Date: Fri, 24 Jul 2026 12:55:00 +0200 Subject: [PATCH 07/27] refactor(ci): drive nightly ZAP scan from OpenAPI spec The traditional spider discovers almost nothing on a client-rendered SPA, so the full scan exercised only a fraction of the API. The scan now imports swagger.json from the deployed instance and actively scans every documented endpoint, gated on the spec actually being served. The compose stack existed only for the deleted PR-time baseline scan. Co-Authored-By: Claude Fable 5 --- .github/workflows/security-full-scan.yml | 24 +++++++++++++-- infra/docker-compose.security.yml | 38 ------------------------ 2 files changed, 21 insertions(+), 41 deletions(-) delete mode 100644 infra/docker-compose.security.yml diff --git a/.github/workflows/security-full-scan.yml b/.github/workflows/security-full-scan.yml index 9dc46908..ad587cc9 100644 --- a/.github/workflows/security-full-scan.yml +++ b/.github/workflows/security-full-scan.yml @@ -156,12 +156,30 @@ jobs: echo "token=$TOKEN" >> "$GITHUB_OUTPUT" echo "Authentication successful" - - name: Run ZAP Full Scan - uses: zaproxy/action-full-scan@v0.13.0 + - name: Validate OpenAPI spec is served + env: + APP_URL: ${{ steps.deploy.outputs.app_url }} + run: | + set -euo pipefail + + curl -sf "$APP_URL/swagger/v1/swagger.json" -o /tmp/swagger.json + + ENDPOINT_COUNT=$(jq '[.paths | keys[]] | length' /tmp/swagger.json) + echo "OpenAPI spec contains $ENDPOINT_COUNT endpoint paths" + + if [ "$ENDPOINT_COUNT" -lt 10 ]; then + echo "::error::OpenAPI spec has suspiciously few endpoints ($ENDPOINT_COUNT)" + exit 1 + fi + + - name: Run ZAP API Scan + uses: zaproxy/action-api-scan@v0.10.0 with: - target: ${{ steps.deploy.outputs.app_url }} + target: "${{ steps.deploy.outputs.app_url }}/swagger/v1/swagger.json" + format: openapi docker_name: "ghcr.io/zaproxy/zaproxy:stable" allow_issue_writing: false + fail_action: "false" cmd_options: >- -z "-config replacer.full_list(0).description=AuthHeader -config replacer.full_list(0).enabled=true diff --git a/infra/docker-compose.security.yml b/infra/docker-compose.security.yml deleted file mode 100644 index 761e1277..00000000 --- a/infra/docker-compose.security.yml +++ /dev/null @@ -1,38 +0,0 @@ -services: - boardgametracker: - image: bgt:security-scan - depends_on: - db: - condition: service_healthy - ports: - - "5444:5444" - environment: - - ASPNETCORE_ENVIRONMENT=production - - ASPNETCORE_URLS=http://*:5444 - - DB_HOST=db - - DB_USER=secuser - - DB_PASSWORD=${SEC_DB_PASSWORD:-securityscanpassword} - - DB_NAME=boardgametracker - - DB_PORT=5432 - - JWT_SECRET=${SEC_JWT_SECRET:-ci-security-scan-jwt-secret-not-for-production} - - AUTH_ENABLED=true - - TZ=UTC - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:5444/api/health"] - interval: 5s - timeout: 5s - retries: 12 - start_period: 15s - - db: - image: postgres:16 - environment: - - POSTGRES_DB=boardgametracker - - POSTGRES_USER=secuser - - POSTGRES_PASSWORD=${SEC_DB_PASSWORD:-securityscanpassword} - healthcheck: - test: ["CMD-SHELL", "pg_isready -U secuser -d boardgametracker"] - interval: 5s - timeout: 5s - retries: 10 - start_period: 5s From cf77e963dfbaeeceeeb7852660e04650f7b37cc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikha=C3=ABl=20Regni?= Date: Fri, 24 Jul 2026 12:55:24 +0200 Subject: [PATCH 08/27] chore(ci): add nightly Docker Scout and informational dependency audit Scout moves from the PR path to the nightly schedule, keeping the Docker Hub login its CLI requires and the SARIF upload. The moderate full-tree pnpm audit compensates for the PR gate narrowing to high/prod without blocking anything. Co-Authored-By: Claude Fable 5 --- .github/workflows/security-full-scan.yml | 92 ++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/.github/workflows/security-full-scan.yml b/.github/workflows/security-full-scan.yml index ad587cc9..558a83ac 100644 --- a/.github/workflows/security-full-scan.yml +++ b/.github/workflows/security-full-scan.yml @@ -217,3 +217,95 @@ jobs: --yes \ --no-wait echo "Resource group deletion initiated (async)" + + docker-scout: + name: Docker Scout CVE Scan + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: read + security-events: write + steps: + - name: Harden runner + uses: step-security/harden-runner@v2 + with: + egress-policy: audit + + - name: Checkout + uses: actions/checkout@v6 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Login to Docker Hub + uses: docker/login-action@v4 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build image for scanning + uses: docker/build-push-action@v7 + with: + context: . + file: ./Dockerfile + push: false + load: true + tags: bgt:scout-scan + cache-from: type=gha + build-args: | + VERSION=0.0.0-security + + - name: Docker Scout CVE scan + uses: docker/scout-action@v1 + with: + command: cves + image: local://bgt:scout-scan + only-severities: critical,high + sarif-file: scout-results.sarif + exit-code: true + only-fixed: true + + - name: Upload Scout SARIF + uses: github/codeql-action/upload-sarif@v4 + if: always() + with: + sarif_file: scout-results.sarif + category: docker-scout + + full-dependency-audit: + name: Full Frontend Dependency Audit + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + steps: + - name: Harden runner + uses: step-security/harden-runner@v2 + with: + egress-policy: audit + + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + package_json_file: boardgametracker.client/package.json + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: "20.x" + cache: "pnpm" + cache-dependency-path: boardgametracker.client/pnpm-lock.yaml + + - name: Install dependencies + run: | + cd boardgametracker.client + pnpm install --frozen-lockfile --ignore-scripts + + - name: Audit all dependencies (informational) + continue-on-error: true + run: | + cd boardgametracker.client + pnpm audit --audit-level=moderate From 95bc76b6a85d90c9874aca6ffc297e1f28c84da5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikha=C3=ABl=20Regni?= Date: Fri, 24 Jul 2026 12:55:38 +0200 Subject: [PATCH 09/27] chore(ci): enable CodeQL security-extended suite Co-Authored-By: Claude Fable 5 --- .github/codeql/codeql-config.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/codeql/codeql-config.yml b/.github/codeql/codeql-config.yml index 6b4360a0..5890bd4c 100644 --- a/.github/codeql/codeql-config.yml +++ b/.github/codeql/codeql-config.yml @@ -1,5 +1,8 @@ name: "BoardGameTracker CodeQL Config" +queries: + - uses: security-extended + query-filters: - exclude: id: cs/log-forging From 628f2d5ddc8ece926ba8c3ceccefaf83068d7896 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikha=C3=ABl=20Regni?= Date: Fri, 24 Jul 2026 14:59:12 +0200 Subject: [PATCH 10/27] fix(ci): repair nightly image push and Azure image pull The nightly has never passed its build step: github.repository is mixed case and Docker rejects uppercase tags, the job lacked the packages permission to push to GHCR, and the container app pulled the image without registry credentials, which cannot work for the private package the first push creates. Provenance is disabled because the attestation manifest adds nothing to an ephemeral scan image. Co-Authored-By: Claude Fable 5 --- .github/workflows/security-full-scan.yml | 14 +++++++++++--- infra/security-scan.bicep | 15 +++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/.github/workflows/security-full-scan.yml b/.github/workflows/security-full-scan.yml index 558a83ac..84635a39 100644 --- a/.github/workflows/security-full-scan.yml +++ b/.github/workflows/security-full-scan.yml @@ -12,7 +12,6 @@ concurrency: env: REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository }} RESOURCE_GROUP: bgt-security-scan-${{ github.run_id }} AZURE_LOCATION: westeurope @@ -23,6 +22,7 @@ jobs: timeout-minutes: 90 permissions: contents: read + packages: write id-token: write security-events: write actions: read @@ -35,6 +35,10 @@ jobs: - name: Checkout uses: actions/checkout@v6 + - name: Compute image name + id: image + run: echo "name=${GITHUB_REPOSITORY,,}" >> "$GITHUB_OUTPUT" + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 @@ -52,7 +56,8 @@ jobs: context: . file: ./Dockerfile push: true - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:security-scan-${{ github.run_id }} + provenance: false + tags: ${{ env.REGISTRY }}/${{ steps.image.outputs.name }}:security-scan-${{ github.run_id }} cache-from: type=gha build-args: | VERSION=0.0.0-security @@ -84,9 +89,10 @@ jobs: - name: Deploy Bicep template id: deploy env: - IMAGE_REF: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }} + IMAGE_REF: ${{ env.REGISTRY }}/${{ steps.image.outputs.name }}@${{ steps.build.outputs.digest }} SEC_DB_PASSWORD: ${{ steps.secrets.outputs.db_password }} SEC_JWT_SECRET: ${{ steps.secrets.outputs.jwt_secret }} + GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | RESULT=$(az deployment group create \ --resource-group "$RESOURCE_GROUP" \ @@ -95,6 +101,8 @@ jobs: imageName="$IMAGE_REF" \ dbPassword="$SEC_DB_PASSWORD" \ jwtSecret="$SEC_JWT_SECRET" \ + registryUsername="$GITHUB_ACTOR" \ + registryPassword="$GHCR_TOKEN" \ --query "properties.outputs" \ --output json) diff --git a/infra/security-scan.bicep b/infra/security-scan.bicep index 63c38632..ec2df466 100644 --- a/infra/security-scan.bicep +++ b/infra/security-scan.bicep @@ -9,6 +9,13 @@ param dbPassword string @secure() param jwtSecret string +@description('Username for pulling the image from ghcr.io') +param registryUsername string + +@description('Token for pulling the image from ghcr.io') +@secure() +param registryPassword string + @description('Azure region for all resources') param location string = resourceGroup().location @@ -99,6 +106,14 @@ resource bgtApp 'Microsoft.App/containerApps@2024-03-01' = { secrets: [ { name: 'db-password', value: dbPassword } { name: 'jwt-secret', value: jwtSecret } + { name: 'ghcr-token', value: registryPassword } + ] + registries: [ + { + server: 'ghcr.io' + username: registryUsername + passwordSecretRef: 'ghcr-token' + } ] ingress: { external: true From 173d0847bca0296c41199a2c18ab9da093f1f1f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikha=C3=ABl=20Regni?= Date: Fri, 24 Jul 2026 15:21:54 +0200 Subject: [PATCH 11/27] fix(ci): drop invalid log retention on ephemeral scan workspace PerGB2018 requires retentionInDays between 30 and 730; the workspace is deleted with the resource group minutes later, so the default is fine. Co-Authored-By: Claude Fable 5 --- infra/security-scan.bicep | 1 - 1 file changed, 1 deletion(-) diff --git a/infra/security-scan.bicep b/infra/security-scan.bicep index ec2df466..fcdb9dd6 100644 --- a/infra/security-scan.bicep +++ b/infra/security-scan.bicep @@ -29,7 +29,6 @@ resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2023-09-01' = { sku: { name: 'PerGB2018' } - retentionInDays: 7 } } From 5cec9a73c2093223cf829e95a6e3b7faf9b90420 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikha=C3=ABl=20Regni?= Date: Sat, 25 Jul 2026 00:07:11 +0200 Subject: [PATCH 12/27] fix(ci): fail health wait fast and keep Azure evidence on failure Each poll could hang for minutes against an unready ingress, so the loop ran until the job timeout instead of failing in two minutes, and teardown then destroyed all evidence. Polls now carry curl timeouts, a failure-only step dumps container app status and logs before the resource group is deleted, and the SARIF upload is skipped when the scan never produced a report. Co-Authored-By: Claude Fable 5 --- .github/workflows/security-full-scan.yml | 36 +++++++++++++++++++----- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/.github/workflows/security-full-scan.yml b/.github/workflows/security-full-scan.yml index 84635a39..9939c5fe 100644 --- a/.github/workflows/security-full-scan.yml +++ b/.github/workflows/security-full-scan.yml @@ -115,19 +115,22 @@ jobs: env: APP_URL: ${{ steps.deploy.outputs.app_url }} run: | + set -euo pipefail + echo "Polling $APP_URL/api/settings ..." - for i in $(seq 1 120); do - HTTP_CODE=$(curl -sf -o /dev/null -w "%{http_code}" "$APP_URL/api/settings" 2>/dev/null || echo "000") + for i in $(seq 1 60); do + HTTP_CODE=$(curl -sf -o /dev/null -w "%{http_code}" --connect-timeout 5 --max-time 10 "$APP_URL/api/settings" 2>/dev/null || echo "000") + echo "Attempt $i: HTTP $HTTP_CODE" if [ "$HTTP_CODE" = "200" ]; then - echo "Application is healthy after ${i}s" + echo "Application is healthy" break fi - if [ "$i" -eq 120 ]; then - echo "::error::Application did not become healthy within 120s (last HTTP $HTTP_CODE)" + if [ "$i" -eq 60 ]; then + echo "::error::Application did not become healthy (last HTTP $HTTP_CODE)" exit 1 fi - sleep 1 + sleep 5 done - name: Authenticate and extract JWT @@ -200,7 +203,7 @@ jobs: - name: Upload SARIF to GitHub Security tab uses: github/codeql-action/upload-sarif@v4 - if: always() + if: always() && hashFiles('report_sarif.json') != '' with: sarif_file: report_sarif.json category: zap-full-scan @@ -217,6 +220,25 @@ jobs: report_sarif.json retention-days: 90 + - name: Collect Azure diagnostics + if: failure() + run: | + az extension add --name containerapp --upgrade || true + + echo "=== bgt-api-sec status ===" + az containerapp show -g "$RESOURCE_GROUP" -n bgt-api-sec \ + --query "{provisioning: properties.provisioningState, running: properties.runningStatus}" -o json || true + az containerapp revision list -g "$RESOURCE_GROUP" -n bgt-api-sec -o table || true + + echo "=== bgt-api-sec system log ===" + az containerapp logs show -g "$RESOURCE_GROUP" -n bgt-api-sec --type system --tail 100 || true + + echo "=== bgt-api-sec console log ===" + az containerapp logs show -g "$RESOURCE_GROUP" -n bgt-api-sec --type console --tail 100 || true + + echo "=== postgres-bgt-sec console log ===" + az containerapp logs show -g "$RESOURCE_GROUP" -n postgres-bgt-sec --type console --tail 50 || true + - name: Delete Azure resource group if: always() run: | From 4d91915570358829c4c1dcf1bf1bd5342de9656f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikha=C3=ABl=20Regni?= Date: Sat, 1 Aug 2026 12:00:56 +0200 Subject: [PATCH 13/27] chore(ci): drop ZAP SARIF upload the scan never produces The classic ZAP scan actions emit html, json and md reports only; SARIF export is an open upstream feature request, so this upload could never fire. ZAP findings live in the run artifacts, and the Security tab keeps Scout, Trivy and CodeQL. Co-Authored-By: Claude Fable 5 --- .github/workflows/security-full-scan.yml | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/.github/workflows/security-full-scan.yml b/.github/workflows/security-full-scan.yml index 9939c5fe..e4af11e2 100644 --- a/.github/workflows/security-full-scan.yml +++ b/.github/workflows/security-full-scan.yml @@ -201,23 +201,15 @@ jobs: -config replacer.full_list(0).initiators=" rules_file_name: ".zap/rules.tsv" - - name: Upload SARIF to GitHub Security tab - uses: github/codeql-action/upload-sarif@v4 - if: always() && hashFiles('report_sarif.json') != '' - with: - sarif_file: report_sarif.json - category: zap-full-scan - - name: Upload scan reports uses: actions/upload-artifact@v7 if: always() with: - name: zap-full-scan-reports-${{ github.run_id }} + name: zap-api-scan-reports-${{ github.run_id }} path: | report_html.html report_json.json report_md.md - report_sarif.json retention-days: 90 - name: Collect Azure diagnostics From 4c164164754ece10405089fc621a15cdec6f8f94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikha=C3=ABl=20Regni?= Date: Sat, 1 Aug 2026 12:10:30 +0200 Subject: [PATCH 14/27] chore(ci): tag security-scan Azure resources with app name Co-Authored-By: Claude Fable 5 --- .github/workflows/security-full-scan.yml | 2 +- infra/security-scan.bicep | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/security-full-scan.yml b/.github/workflows/security-full-scan.yml index e4af11e2..119a5406 100644 --- a/.github/workflows/security-full-scan.yml +++ b/.github/workflows/security-full-scan.yml @@ -84,7 +84,7 @@ jobs: az group create \ --name "$RESOURCE_GROUP" \ --location "$AZURE_LOCATION" \ - --tags purpose=security-scan run_id="$GITHUB_RUN_ID" + --tags name=boardgametracker purpose=security-scan run_id="$GITHUB_RUN_ID" - name: Deploy Bicep template id: deploy diff --git a/infra/security-scan.bicep b/infra/security-scan.bicep index fcdb9dd6..2daab6e9 100644 --- a/infra/security-scan.bicep +++ b/infra/security-scan.bicep @@ -22,9 +22,14 @@ param location string = resourceGroup().location @description('Unique suffix for resource names') param suffix string = uniqueString(resourceGroup().id) +var tags = { + name: 'boardgametracker' +} + resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2023-09-01' = { name: 'law-bgt-sec-${suffix}' location: location + tags: tags properties: { sku: { name: 'PerGB2018' @@ -35,6 +40,7 @@ resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2023-09-01' = { resource containerAppEnv 'Microsoft.App/managedEnvironments@2024-03-01' = { name: 'cae-bgt-sec-${suffix}' location: location + tags: tags properties: { appLogsConfiguration: { destination: 'log-analytics' @@ -49,6 +55,7 @@ resource containerAppEnv 'Microsoft.App/managedEnvironments@2024-03-01' = { resource postgresApp 'Microsoft.App/containerApps@2024-03-01' = { name: 'postgres-bgt-sec' location: location + tags: tags properties: { managedEnvironmentId: containerAppEnv.id configuration: { @@ -99,6 +106,7 @@ resource postgresApp 'Microsoft.App/containerApps@2024-03-01' = { resource bgtApp 'Microsoft.App/containerApps@2024-03-01' = { name: 'bgt-api-sec' location: location + tags: tags properties: { managedEnvironmentId: containerAppEnv.id configuration: { From 4e510bc324a58ff63eaa48b3291b0c5639d559ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikha=C3=ABl=20Regni?= Date: Sat, 1 Aug 2026 12:22:04 +0200 Subject: [PATCH 15/27] fix(ci): run scan database as sidecar next to the app Internal TCP ingress between container apps resolves DNS but never routes on a consumption environment, so the app could not reach postgres and crashed during startup migrations. Postgres now runs as a sidecar in the same container app and the app connects over localhost, removing the cross-app networking dependency entirely. Co-Authored-By: Claude Fable 5 --- .github/workflows/security-full-scan.yml | 4 +- infra/security-scan.bicep | 66 +++++------------------- 2 files changed, 16 insertions(+), 54 deletions(-) diff --git a/.github/workflows/security-full-scan.yml b/.github/workflows/security-full-scan.yml index 119a5406..20a30f9a 100644 --- a/.github/workflows/security-full-scan.yml +++ b/.github/workflows/security-full-scan.yml @@ -228,8 +228,8 @@ jobs: echo "=== bgt-api-sec console log ===" az containerapp logs show -g "$RESOURCE_GROUP" -n bgt-api-sec --type console --tail 100 || true - echo "=== postgres-bgt-sec console log ===" - az containerapp logs show -g "$RESOURCE_GROUP" -n postgres-bgt-sec --type console --tail 50 || true + echo "=== postgres sidecar console log ===" + az containerapp logs show -g "$RESOURCE_GROUP" -n bgt-api-sec --container postgres --type console --tail 50 || true - name: Delete Azure resource group if: always() diff --git a/infra/security-scan.bicep b/infra/security-scan.bicep index 2daab6e9..0352b97e 100644 --- a/infra/security-scan.bicep +++ b/infra/security-scan.bicep @@ -52,57 +52,6 @@ resource containerAppEnv 'Microsoft.App/managedEnvironments@2024-03-01' = { } } -resource postgresApp 'Microsoft.App/containerApps@2024-03-01' = { - name: 'postgres-bgt-sec' - location: location - tags: tags - properties: { - managedEnvironmentId: containerAppEnv.id - configuration: { - secrets: [ - { name: 'db-password', value: dbPassword } - ] - ingress: { - external: false - targetPort: 5432 - transport: 'tcp' - exposedPort: 5432 - } - } - template: { - containers: [ - { - name: 'postgres' - image: 'docker.io/library/postgres:16-alpine' - resources: { - cpu: json('0.5') - memory: '1Gi' - } - env: [ - { name: 'POSTGRES_DB', value: 'boardgametracker' } - { name: 'POSTGRES_USER', value: 'bgtuser' } - { name: 'POSTGRES_PASSWORD', secretRef: 'db-password' } - ] - probes: [ - { - type: 'Readiness' - tcpSocket: { - port: 5432 - } - initialDelaySeconds: 5 - periodSeconds: 10 - } - ] - } - ] - scale: { - minReplicas: 1 - maxReplicas: 1 - } - } - } -} - resource bgtApp 'Microsoft.App/containerApps@2024-03-01' = { name: 'bgt-api-sec' location: location @@ -131,6 +80,19 @@ resource bgtApp 'Microsoft.App/containerApps@2024-03-01' = { } template: { containers: [ + { + name: 'postgres' + image: 'docker.io/library/postgres:16-alpine' + resources: { + cpu: json('0.5') + memory: '1Gi' + } + env: [ + { name: 'POSTGRES_DB', value: 'boardgametracker' } + { name: 'POSTGRES_USER', value: 'bgtuser' } + { name: 'POSTGRES_PASSWORD', secretRef: 'db-password' } + ] + } { name: 'bgt-api' image: imageName @@ -141,7 +103,7 @@ resource bgtApp 'Microsoft.App/containerApps@2024-03-01' = { env: [ { name: 'ASPNETCORE_ENVIRONMENT', value: 'production' } { name: 'ASPNETCORE_URLS', value: 'http://*:5444' } - { name: 'DB_HOST', value: postgresApp.properties.configuration.ingress.fqdn } + { name: 'DB_HOST', value: 'localhost' } { name: 'DB_PORT', value: '5432' } { name: 'DB_USER', value: 'bgtuser' } { name: 'DB_PASSWORD', secretRef: 'db-password' } From 9ffa5afc62230d7f9589844708d871059d139b80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikha=C3=ABl=20Regni?= Date: Fri, 7 Aug 2026 00:39:00 +0200 Subject: [PATCH 16/27] fix(ci): remove shell interpolation of secrets and outputs in publish workflow Completes the change started in ec41d329 for the one workflow it left untouched. SONAR_TOKEN was interpolated directly into two run: blocks, alongside eight version/repository template expansions. All six steps now read their values from step-level env:. Co-Authored-By: Claude Opus 5 --- .github/workflows/publish-container.yml | 31 ++++++++++++++----------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/.github/workflows/publish-container.yml b/.github/workflows/publish-container.yml index 5529d97a..ae18bbee 100644 --- a/.github/workflows/publish-container.yml +++ b/.github/workflows/publish-container.yml @@ -123,13 +123,14 @@ jobs: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} SONAR_PROJECT_KEY: ${{ secrets.SONAR_PROJECT_KEY }} SONAR_ORGANIZATION: ${{ secrets.SONAR_ORGANIZATION }} + VERSION: ${{ needs.version.outputs.version }} run: | ./.sonar/scanner/dotnet-sonarscanner begin \ - /k:"${{ env.SONAR_PROJECT_KEY }}" \ - /o:"${{ env.SONAR_ORGANIZATION }}" \ + /k:"$SONAR_PROJECT_KEY" \ + /o:"$SONAR_ORGANIZATION" \ /d:sonar.host.url="https://sonarcloud.io" \ - /d:sonar.token="${{ env.SONAR_TOKEN }}" \ - /v:"${{ needs.version.outputs.version }}" \ + /d:sonar.token="$SONAR_TOKEN" \ + /v:"$VERSION" \ /d:sonar.cs.opencover.reportsPaths="TestResults/**/coverage.opencover.xml" \ /d:sonar.cs.vstest.reportsPaths="TestResults/*.trx" \ /d:sonar.javascript.lcov.reportPaths="boardgametracker.client/coverage/lcov.info" \ @@ -180,7 +181,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - run: ./.sonar/scanner/dotnet-sonarscanner end /d:sonar.token="${{ env.SONAR_TOKEN }}" + run: ./.sonar/scanner/dotnet-sonarscanner end /d:sonar.token="$SONAR_TOKEN" - name: Upload test results uses: actions/upload-artifact@v7 @@ -232,9 +233,11 @@ jobs: package_json_file: boardgametracker.client/package.json - name: Update package.json version + env: + VERSION: ${{ needs.version.outputs.version }} run: | cd boardgametracker.client - pnpm version ${{ needs.version.outputs.version }} --no-git-tag-version --allow-same-version + pnpm version "$VERSION" --no-git-tag-version --allow-same-version - name: Set up QEMU uses: docker/setup-qemu-action@v4 @@ -380,9 +383,9 @@ jobs: fetch-depth: 0 - name: Generate Release Notes + env: + VERSION: ${{ needs.version.outputs.version }} run: | - VERSION=${{ needs.version.outputs.version }} - # Get previous tag PREVIOUS_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "") @@ -426,10 +429,11 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Create major and major.minor tags + env: + VERSION: ${{ needs.version.outputs.version }} run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - VERSION=${{ needs.version.outputs.version }} MAJOR=$(echo "$VERSION" | cut -d. -f1) MINOR=$(echo "$VERSION" | cut -d. -f2) git tag -fa "v${MAJOR}" -m "Release v${VERSION}" @@ -449,10 +453,11 @@ jobs: egress-policy: audit - name: Build Summary + env: + VERSION: ${{ needs.version.outputs.version }} + IS_STABLE_RELEASE: ${{ needs.version.outputs.is_stable }} run: | - VERSION=${{ needs.version.outputs.version }} - - if [ "${{ needs.version.outputs.is_stable }}" = "true" ]; then + if [ "$IS_STABLE_RELEASE" = "true" ]; then cat >> $GITHUB_STEP_SUMMARY << EOF # Release v${VERSION} Published @@ -464,7 +469,7 @@ jobs: ## Links - [Docker Hub](https://hub.docker.com/r/uping/boardgametracker) - - [Release Notes](https://github.com/${{ github.repository }}/releases/tag/v${VERSION}) + - [Release Notes](https://github.com/${GITHUB_REPOSITORY}/releases/tag/v${VERSION}) EOF else cat >> $GITHUB_STEP_SUMMARY << EOF From c6fb528d2bd88a6436e32d7693e994193c4f1380 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikha=C3=ABl=20Regni?= Date: Fri, 7 Aug 2026 00:40:37 +0200 Subject: [PATCH 17/27] chore(ci): pin trivy action and gate publish on concurrency and code changes The publish workflow was the last one without a concurrency group; it publishes images and force-pushes tags, so runs queue rather than cancel. paths-ignore now matches security.yml so a markdown-only push no longer builds and releases a beta. trivy-action moves off the mutable @master ref to v0.36.0. Co-Authored-By: Claude Opus 5 --- .github/workflows/publish-container.yml | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish-container.yml b/.github/workflows/publish-container.yml index ae18bbee..9fa09966 100644 --- a/.github/workflows/publish-container.yml +++ b/.github/workflows/publish-container.yml @@ -5,8 +5,13 @@ on: branches: - master paths-ignore: + - "**/*.md" - "docs/**" - ".github/workflows/docs.yml" + - ".github/images/**" + - "LICENSE" + - "crowdin.yml" + - ".vscode/**" workflow_dispatch: inputs: stable_release: @@ -15,6 +20,10 @@ on: default: false type: boolean +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false + env: REGISTRY: ${{ vars.DOCKER_REGISTRY || 'docker.io' }} IMAGE_NAME: ${{ vars.DOCKER_IMAGE_NAME || 'uping/boardgametracker' }} @@ -327,7 +336,7 @@ jobs: VERSION=${{ needs.version.outputs.version }} - name: Run Trivy vulnerability scanner - uses: aquasecurity/trivy-action@master + uses: aquasecurity/trivy-action@v0.36.0 with: image-ref: ${{ env.IMAGE_NAME }}:scan format: 'sarif' @@ -342,7 +351,7 @@ jobs: sarif_file: 'trivy-results.sarif' - name: Run Trivy vulnerability scanner (fail on critical) - uses: aquasecurity/trivy-action@master + uses: aquasecurity/trivy-action@v0.36.0 with: image-ref: ${{ env.IMAGE_NAME }}:scan format: 'table' @@ -351,7 +360,7 @@ jobs: timeout: '10m' - name: Generate SBOM - uses: aquasecurity/trivy-action@master + uses: aquasecurity/trivy-action@v0.36.0 with: image-ref: ${{ env.IMAGE_NAME }}:scan format: 'cyclonedx' From c3d14e3105d0f7d0b0ef8c1b9391abb8644bf0e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikha=C3=ABl=20Regni?= Date: Fri, 7 Aug 2026 00:47:09 +0200 Subject: [PATCH 18/27] ci: run pr checks on every pull request so they can be required A workflow skipped by paths-ignore reports no status at all, so any check it owns sits on "Expected" forever and blocks the merge. With CI, the three security scans and the three CodeQL jobs becoming required status checks on master, these two workflows can no longer be path-filtered. Reverses the PR-side filtering from 231ea355 and predates it in ci.yml. The cost is a full run on docs-only PRs; the alternative is a job-level change filter, whose failure mode is silently skipping real code changes while the required checks still report green. publish-container.yml keeps its paths-ignore: it runs on master pushes and owns no PR check. Co-Authored-By: Claude Opus 5 --- .github/workflows/ci.yml | 3 --- .github/workflows/security.yml | 7 ------- 2 files changed, 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2b05f114..293d75d6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,9 +4,6 @@ on: pull_request: branches: - master - paths-ignore: - - "docs/**" - - ".github/workflows/docs.yml" concurrency: group: ${{ github.workflow }}-${{ github.ref }} diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 1cd0bc7d..02d48c7a 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -3,13 +3,6 @@ name: Security Scanning on: pull_request: branches: [master] - paths-ignore: - - "**/*.md" - - "docs/**" - - ".github/images/**" - - "LICENSE" - - "crowdin.yml" - - ".vscode/**" concurrency: group: ${{ github.workflow }}-${{ github.ref }} From 077e5f827f7523f32cd3bdc99e822f45fc80895a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikha=C3=ABl=20Regni?= Date: Fri, 7 Aug 2026 00:55:51 +0200 Subject: [PATCH 19/27] fix(ci): key publish concurrency on event so manual releases are not cancelled GitHub keeps only one pending run per concurrency group: a newer arrival cancels the older pending run even when cancel-in-progress is false. With a single "Deploy" group, a manually dispatched stable release sitting behind an in-progress build would be silently cancelled by the next push to master. Separating push from workflow_dispatch keeps the newest-master-wins queueing for pushes while a manual release can no longer be discarded. The two can now overlap, which is safe: they publish disjoint tags, and only the stable run creates releases and moves the vN tags. Co-Authored-By: Claude Opus 5 --- .github/workflows/publish-container.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish-container.yml b/.github/workflows/publish-container.yml index 9fa09966..4b1c7a5c 100644 --- a/.github/workflows/publish-container.yml +++ b/.github/workflows/publish-container.yml @@ -21,7 +21,7 @@ on: type: boolean concurrency: - group: ${{ github.workflow }} + group: ${{ github.workflow }}-${{ github.event_name }} cancel-in-progress: false env: From 208344136e41b44924bf88dcf2e70f2b89c4a665 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikha=C3=ABl=20Regni?= Date: Fri, 7 Aug 2026 01:11:20 +0200 Subject: [PATCH 20/27] chore(ci): drop unused registry variables from the ci workflow ci.yml has built no image since the image-heavy scans moved off the PR path, so REGISTRY and IMAGE_NAME were referenced nowhere in the file. Co-Authored-By: Claude Opus 5 --- .github/workflows/ci.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 293d75d6..44e4a4c3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,10 +9,6 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true -env: - REGISTRY: ${{ vars.DOCKER_REGISTRY || 'docker.io' }} - IMAGE_NAME: ${{ vars.DOCKER_IMAGE_NAME || 'uping/boardgametracker' }} - jobs: test-and-analyze: name: Test and SonarCloud Analysis From 898978d546c47fa6ae37b5a34084fe0353d761dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikha=C3=ABl=20Regni?= Date: Fri, 7 Aug 2026 01:20:42 +0200 Subject: [PATCH 21/27] chore: retrigger ci after github actions outage PR #202 was opened during the 2026-08-06 Actions incident, which dropped its pull_request events. Dropped events are not replayed and close/reopen did not re-fire them, so this empty commit exists only to raise a synchronize event. Safe to drop; it disappears on a squash merge. Co-Authored-By: Claude Opus 5 From 28d6a8f228b6dd4abad78d77a167fe747df20d0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikha=C3=ABl=20Regni?= Date: Fri, 7 Aug 2026 01:30:19 +0200 Subject: [PATCH 22/27] fix(ci): stop the backend scan failing on the frontend esproj boardgametracker.client.esproj is in the solution and uses packages.config, so dotnet list package --vulnerable exits non-zero even when every project reports no vulnerable packages. The pipefail added in d4e1a32b propagated that through tee and killed the step before the grep ran, so the scan failed on every PR. Fail-closed behaviour is kept and made explicit: a run that produces no verdict at all is still an error, rather than being inferred from an exit code the command uses for an unrelated condition. Co-Authored-By: Claude Opus 5 --- .github/workflows/security.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 02d48c7a..f87a6246 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -33,15 +33,20 @@ jobs: - name: Check for vulnerable packages run: | - set -euo pipefail + set -uo pipefail - dotnet list ./BoardGameTracker.sln package --vulnerable --include-transitive 2>&1 | tee vulnerable.txt + dotnet list ./BoardGameTracker.sln package --vulnerable --include-transitive 2>&1 | tee vulnerable.txt || true if grep -q "has the following vulnerable packages" vulnerable.txt; then echo "::error::Vulnerable NuGet packages detected" exit 1 fi + if ! grep -q "has no vulnerable packages" vulnerable.txt; then + echo "::error::dotnet list package produced no vulnerability verdict" + exit 1 + fi + echo "No vulnerable packages found." dependency-scan-frontend: From 79a02793933dcee5415bf1f45a7e15651bd19b04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikha=C3=ABl=20Regni?= Date: Fri, 7 Aug 2026 01:36:21 +0200 Subject: [PATCH 23/27] fix(ci): pin third-party actions to commit shas Resolves the CodeQL "Unpinned tag for a non-immutable Action" alerts raised on this PR. A tag is a mutable pointer, so a compromised or retagged release runs with whatever permissions the job holds; a commit sha cannot be moved. All 19 third-party actions are pinned repo-wide rather than only in the two files CodeQL flagged, since it flags changed files and the rest would surface one at a time as they are touched. Actions under actions/ and github/ are left on tags, matching the rule's own trust model. Each pin keeps a version marker so the intended release stays readable, and renovate gains helpers:pinGitHubActionDigests so new actions are pinned on arrival and existing digests keep getting updates. Co-Authored-By: Claude Opus 5 --- .github/workflows/ci.yml | 12 +++---- .github/workflows/codeql.yml | 2 +- .github/workflows/docs.yml | 6 ++-- .github/workflows/pr.yml | 8 ++--- .github/workflows/publish-container.yml | 44 ++++++++++++------------ .github/workflows/security-full-scan.yml | 26 +++++++------- .github/workflows/security.yml | 10 +++--- .github/workflows/stale.yml | 2 +- .github/workflows/welcome.yml | 2 +- renovate.json | 3 +- 10 files changed, 58 insertions(+), 57 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 44e4a4c3..6ba656fd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: checks: write steps: - name: Harden runner - uses: step-security/harden-runner@v2 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2 with: egress-policy: audit @@ -29,7 +29,7 @@ jobs: fetch-depth: 0 - name: Git Semantic Version - uses: PaulHatch/semantic-version@v6.0.3 + uses: PaulHatch/semantic-version@502226b17e3e242e4befd0a45690fcbb27dd1373 # v6.0.3 id: versioning with: enable_prerelease_mode: true @@ -50,7 +50,7 @@ jobs: dotnet-version: "8.x" - name: Setup pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 with: package_json_file: boardgametracker.client/package.json @@ -154,7 +154,7 @@ jobs: sed -i "s|path=\"src/|path=\"$(pwd)/src/|g" coverage/sonar-report.xml - name: Publish .NET test results - uses: EnricoMi/publish-unit-test-result-action@v2 + uses: EnricoMi/publish-unit-test-result-action@d0a4676d0e0b938bc201470d88276b7c74c712b3 # v2 if: always() with: files: TestResults/**/*.trx @@ -179,7 +179,7 @@ jobs: if: success() || failure() - name: Code Coverage Report - uses: danielpalme/ReportGenerator-GitHub-Action@v5.5.10 + uses: danielpalme/ReportGenerator-GitHub-Action@049f7ec958c672fd31d5cc1cb01622dc8d2e23ab # v5.5.10 with: reports: "TestResults/**/coverage.opencover.xml;boardgametracker.client/coverage/lcov.info" targetdir: "coveragereport" @@ -192,7 +192,7 @@ jobs: run: cat coveragereport/SummaryGithub.md >> $GITHUB_STEP_SUMMARY - name: Add Coverage PR Comment - uses: marocchino/sticky-pull-request-comment@v3 + uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3 with: recreate: true path: coveragereport/SummaryGithub.md diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index c68dcd45..f3438efe 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -35,7 +35,7 @@ jobs: steps: - name: Harden runner - uses: step-security/harden-runner@v2 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2 with: egress-policy: audit diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 51ac069b..c3179727 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -24,7 +24,7 @@ jobs: working-directory: docs steps: - name: Harden runner - uses: step-security/harden-runner@v2 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2 with: egress-policy: audit @@ -32,7 +32,7 @@ jobs: uses: actions/checkout@v6 - name: Setup pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 with: package_json_file: docs/package.json @@ -71,7 +71,7 @@ jobs: needs: build steps: - name: Harden runner - uses: step-security/harden-runner@v2 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2 with: egress-policy: audit diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 7730b0a7..e886f65e 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -18,7 +18,7 @@ jobs: pull-requests: write steps: - name: Harden runner - uses: step-security/harden-runner@v2 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2 with: egress-policy: audit @@ -41,7 +41,7 @@ jobs: done - name: Label PR by size - uses: codelytv/pr-size-labeler@v1 + uses: codelytv/pr-size-labeler@095a41fca88b8764fd9e008ad269bcdb82bb38b9 # v1 with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} xs_label: "size/XS" @@ -64,12 +64,12 @@ jobs: permissions: {} steps: - name: Harden runner - uses: step-security/harden-runner@v2 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2 with: egress-policy: audit - name: Validate PR title - uses: amannn/action-semantic-pull-request@v6 + uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/.github/workflows/publish-container.yml b/.github/workflows/publish-container.yml index 4b1c7a5c..22218b82 100644 --- a/.github/workflows/publish-container.yml +++ b/.github/workflows/publish-container.yml @@ -40,7 +40,7 @@ jobs: is_stable: ${{ env.IS_STABLE }} steps: - name: Harden runner - uses: step-security/harden-runner@v2 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2 with: egress-policy: audit @@ -50,7 +50,7 @@ jobs: fetch-depth: 0 - name: Git Semantic Version - uses: PaulHatch/semantic-version@v6.0.3 + uses: PaulHatch/semantic-version@502226b17e3e242e4befd0a45690fcbb27dd1373 # v6.0.3 id: versioning with: major_pattern: "(MAJOR)" @@ -69,7 +69,7 @@ jobs: contents: read steps: - name: Harden runner - uses: step-security/harden-runner@v2 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2 with: egress-policy: audit @@ -90,7 +90,7 @@ jobs: dotnet-version: "8.x" - name: Setup pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 with: package_json_file: boardgametracker.client/package.json @@ -203,7 +203,7 @@ jobs: if: success() || failure() - name: Code Coverage Report - uses: danielpalme/ReportGenerator-GitHub-Action@v5.5.10 + uses: danielpalme/ReportGenerator-GitHub-Action@049f7ec958c672fd31d5cc1cb01622dc8d2e23ab # v5.5.10 with: reports: "TestResults/**/coverage.opencover.xml;boardgametracker.client/coverage/lcov.info" targetdir: "coveragereport" @@ -227,7 +227,7 @@ jobs: contents: read steps: - name: Harden runner - uses: step-security/harden-runner@v2 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2 with: egress-policy: audit @@ -237,7 +237,7 @@ jobs: fetch-depth: 0 - name: Setup pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 with: package_json_file: boardgametracker.client/package.json @@ -249,15 +249,15 @@ jobs: pnpm version "$VERSION" --no-git-tag-version --allow-same-version - name: Set up QEMU - uses: docker/setup-qemu-action@v4 + uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4 with: platforms: all - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v4 + uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4 - name: Login to Docker Hub - uses: docker/login-action@v4 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -265,7 +265,7 @@ jobs: - name: Extract metadata for Docker (beta) id: meta-beta if: env.IS_STABLE != 'true' - uses: docker/metadata-action@v6 + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6 with: images: ${{ env.IMAGE_NAME }} tags: | @@ -275,7 +275,7 @@ jobs: - name: Extract metadata for Docker (stable) id: meta-stable if: env.IS_STABLE == 'true' - uses: docker/metadata-action@v6 + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6 with: images: ${{ env.IMAGE_NAME }} tags: | @@ -285,7 +285,7 @@ jobs: type=raw,value=latest - name: Build and Push Multi-Architecture Images - uses: docker/build-push-action@v7 + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7 with: context: . file: ./Dockerfile @@ -313,7 +313,7 @@ jobs: contents: read steps: - name: Harden runner - uses: step-security/harden-runner@v2 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2 with: egress-policy: audit @@ -321,10 +321,10 @@ jobs: uses: actions/checkout@v6 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v4 + uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4 - name: Build local image for scanning - uses: docker/build-push-action@v7 + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7 with: context: . file: ./Dockerfile @@ -336,7 +336,7 @@ jobs: VERSION=${{ needs.version.outputs.version }} - name: Run Trivy vulnerability scanner - uses: aquasecurity/trivy-action@v0.36.0 + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 with: image-ref: ${{ env.IMAGE_NAME }}:scan format: 'sarif' @@ -351,7 +351,7 @@ jobs: sarif_file: 'trivy-results.sarif' - name: Run Trivy vulnerability scanner (fail on critical) - uses: aquasecurity/trivy-action@v0.36.0 + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 with: image-ref: ${{ env.IMAGE_NAME }}:scan format: 'table' @@ -360,7 +360,7 @@ jobs: timeout: '10m' - name: Generate SBOM - uses: aquasecurity/trivy-action@v0.36.0 + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 with: image-ref: ${{ env.IMAGE_NAME }}:scan format: 'cyclonedx' @@ -382,7 +382,7 @@ jobs: contents: write steps: - name: Harden runner - uses: step-security/harden-runner@v2 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2 with: egress-policy: audit @@ -426,7 +426,7 @@ jobs: EOF - name: Create Release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2 with: tag_name: v${{ needs.version.outputs.version }} name: v${{ needs.version.outputs.version }} @@ -457,7 +457,7 @@ jobs: permissions: {} steps: - name: Harden runner - uses: step-security/harden-runner@v2 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2 with: egress-policy: audit diff --git a/.github/workflows/security-full-scan.yml b/.github/workflows/security-full-scan.yml index 20a30f9a..b9def673 100644 --- a/.github/workflows/security-full-scan.yml +++ b/.github/workflows/security-full-scan.yml @@ -28,7 +28,7 @@ jobs: actions: read steps: - name: Harden runner - uses: step-security/harden-runner@v2 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2 with: egress-policy: audit @@ -40,10 +40,10 @@ jobs: run: echo "name=${GITHUB_REPOSITORY,,}" >> "$GITHUB_OUTPUT" - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v4 + uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4 - name: Login to GHCR - uses: docker/login-action@v4 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4 with: registry: ghcr.io username: ${{ github.actor }} @@ -51,7 +51,7 @@ jobs: - name: Build and push image id: build - uses: docker/build-push-action@v7 + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7 with: context: . file: ./Dockerfile @@ -73,7 +73,7 @@ jobs: echo "jwt_secret=$JWT_SECRET" >> "$GITHUB_OUTPUT" - name: Azure OIDC Login - uses: azure/login@v3 + uses: azure/login@f5d393ae46f8fde4be8b75f32e3fc50e654ad0ca # v3 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} @@ -184,7 +184,7 @@ jobs: fi - name: Run ZAP API Scan - uses: zaproxy/action-api-scan@v0.10.0 + uses: zaproxy/action-api-scan@5158fe4d9d8fcc75ea204db81317cce7f9e5453d # v0.10.0 with: target: "${{ steps.deploy.outputs.app_url }}/swagger/v1/swagger.json" format: openapi @@ -249,7 +249,7 @@ jobs: security-events: write steps: - name: Harden runner - uses: step-security/harden-runner@v2 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2 with: egress-policy: audit @@ -257,16 +257,16 @@ jobs: uses: actions/checkout@v6 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v4 + uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4 - name: Login to Docker Hub - uses: docker/login-action@v4 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build image for scanning - uses: docker/build-push-action@v7 + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7 with: context: . file: ./Dockerfile @@ -278,7 +278,7 @@ jobs: VERSION=0.0.0-security - name: Docker Scout CVE scan - uses: docker/scout-action@v1 + uses: docker/scout-action@bacf462e8d090c09660de30a6ccc718035f961e3 # v1 with: command: cves image: local://bgt:scout-scan @@ -302,7 +302,7 @@ jobs: contents: read steps: - name: Harden runner - uses: step-security/harden-runner@v2 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2 with: egress-policy: audit @@ -310,7 +310,7 @@ jobs: uses: actions/checkout@v6 - name: Setup pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 with: package_json_file: boardgametracker.client/package.json diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index f87a6246..3419d517 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -16,7 +16,7 @@ jobs: contents: read steps: - name: Harden runner - uses: step-security/harden-runner@v2 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2 with: egress-policy: audit @@ -56,7 +56,7 @@ jobs: contents: read steps: - name: Harden runner - uses: step-security/harden-runner@v2 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2 with: egress-policy: audit @@ -64,7 +64,7 @@ jobs: uses: actions/checkout@v6 - name: Setup pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 with: package_json_file: boardgametracker.client/package.json @@ -92,7 +92,7 @@ jobs: contents: read steps: - name: Harden runner - uses: step-security/harden-runner@v2 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2 with: egress-policy: audit @@ -102,6 +102,6 @@ jobs: fetch-depth: 0 - name: Run Gitleaks - uses: gitleaks/gitleaks-action@v2 + uses: gitleaks/gitleaks-action@ff98106e4c7b2bc287b24eaf42907196329070c7 # v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 98b8f73d..e8d62201 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -18,7 +18,7 @@ jobs: steps: - name: Harden runner - uses: step-security/harden-runner@v2 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2 with: egress-policy: audit diff --git a/.github/workflows/welcome.yml b/.github/workflows/welcome.yml index 39cd2036..12c5da63 100644 --- a/.github/workflows/welcome.yml +++ b/.github/workflows/welcome.yml @@ -16,7 +16,7 @@ jobs: steps: - name: Harden runner - uses: step-security/harden-runner@v2 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2 with: egress-policy: audit diff --git a/renovate.json b/renovate.json index 0e9d86d6..010457e7 100644 --- a/renovate.json +++ b/renovate.json @@ -5,7 +5,8 @@ "config:recommended", ":dependencyDashboard", ":semanticCommits", - "group:allNonMajor" + "group:allNonMajor", + "helpers:pinGitHubActionDigests" ], "timezone": "Europe/Brussels", "schedule": [ From 7ebcf1f767c6a8fd4de2f516361e6bd89e41ab49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikha=C3=ABl=20Regni?= Date: Fri, 7 Aug 2026 01:45:59 +0200 Subject: [PATCH 24/27] fix(ci): pin first-party actions to commit shas as well Completes the pinning started in 79a02793. Actions under actions/ and github/ were left on tags because CodeQL's trust model exempts them, but a mutable ref is a mutable ref: the exemption reflects who owns the repo, not whether the pointer can move. Every uses: in the repo now resolves to an immutable commit, 99 in total, each keeping its version marker. The three codeql-action subpaths share one sha because they ship from a single repository. Co-Authored-By: Claude Opus 5 --- .github/workflows/ci.yml | 14 ++++++------- .github/workflows/codeql.yml | 6 +++--- .github/workflows/docs.yml | 10 ++++----- .github/workflows/pr.yml | 4 ++-- .github/workflows/publish-container.yml | 26 ++++++++++++------------ .github/workflows/security-full-scan.yml | 12 +++++------ .github/workflows/security.yml | 10 ++++----- .github/workflows/stale.yml | 2 +- .github/workflows/welcome.yml | 2 +- 9 files changed, 43 insertions(+), 43 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6ba656fd..f656d846 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,7 @@ jobs: egress-policy: audit - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 with: fetch-depth: 0 @@ -39,13 +39,13 @@ jobs: debug: true - name: Set up JDK 17 - uses: actions/setup-java@v5 + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: java-version: 17 distribution: 'zulu' - name: Setup dotnet v8 - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5 with: dotnet-version: "8.x" @@ -55,14 +55,14 @@ jobs: package_json_file: boardgametracker.client/package.json - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 with: node-version: "20.x" cache: 'pnpm' cache-dependency-path: boardgametracker.client/pnpm-lock.yaml - name: Cache SonarCloud packages - uses: actions/cache@v5 + uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5 with: path: ~/.sonar/cache key: ${{ runner.os }}-sonar @@ -70,7 +70,7 @@ jobs: - name: Cache SonarCloud scanner id: cache-sonar-scanner - uses: actions/cache@v5 + uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5 with: path: ./.sonar/scanner key: ${{ runner.os }}-sonar-scanner @@ -169,7 +169,7 @@ jobs: if: env.SONAR_TOKEN != '' - name: Upload test results - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: test-results path: | diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index f3438efe..cf95ee79 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -40,16 +40,16 @@ jobs: egress-policy: audit - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - name: Initialize CodeQL - uses: github/codeql-action/init@v4 + uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 # v4 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} config-file: ./.github/codeql/codeql-config.yml - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v4 + uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # v4 with: category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index c3179727..281ba3e9 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -29,7 +29,7 @@ jobs: egress-policy: audit - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - name: Setup pnpm uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 @@ -37,7 +37,7 @@ jobs: package_json_file: docs/package.json - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 with: node-version: 22 cache: pnpm @@ -45,7 +45,7 @@ jobs: - name: Setup Pages id: pages - uses: actions/configure-pages@v6 + uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6 - name: Install dependencies run: pnpm install --frozen-lockfile --ignore-scripts @@ -56,7 +56,7 @@ jobs: VITE_BASE_PATH: ${{ steps.pages.outputs.base_path }}/ - name: Upload artifact - uses: actions/upload-pages-artifact@v4 + uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4 with: path: docs/dist @@ -77,4 +77,4 @@ jobs: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v5 + uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5 diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index e886f65e..5912b780 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -23,10 +23,10 @@ jobs: egress-policy: audit - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - name: Apply file-based labels - uses: actions/labeler@v6 + uses: actions/labeler@b8dd2d9be0f68b860e7dae5dae7d772984eacd6d # v6 with: repo-token: ${{ secrets.GITHUB_TOKEN }} configuration-path: .github/labeler.yml diff --git a/.github/workflows/publish-container.yml b/.github/workflows/publish-container.yml index 22218b82..e7cff55a 100644 --- a/.github/workflows/publish-container.yml +++ b/.github/workflows/publish-container.yml @@ -45,7 +45,7 @@ jobs: egress-policy: audit - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 with: fetch-depth: 0 @@ -74,18 +74,18 @@ jobs: egress-policy: audit - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 with: fetch-depth: 0 - name: Set up JDK 17 - uses: actions/setup-java@v5 + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: java-version: 17 distribution: 'zulu' - name: Setup dotnet v8 - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5 with: dotnet-version: "8.x" @@ -95,14 +95,14 @@ jobs: package_json_file: boardgametracker.client/package.json - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 with: node-version: "20.x" cache: 'pnpm' cache-dependency-path: boardgametracker.client/pnpm-lock.yaml - name: Cache SonarCloud packages - uses: actions/cache@v5 + uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5 with: path: ~/.sonar/cache key: ${{ runner.os }}-sonar @@ -110,7 +110,7 @@ jobs: - name: Cache SonarCloud scanner id: cache-sonar-scanner - uses: actions/cache@v5 + uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5 with: path: ./.sonar/scanner key: ${{ runner.os }}-sonar-scanner @@ -193,7 +193,7 @@ jobs: run: ./.sonar/scanner/dotnet-sonarscanner end /d:sonar.token="$SONAR_TOKEN" - name: Upload test results - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: test-results path: | @@ -232,7 +232,7 @@ jobs: egress-policy: audit - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 with: fetch-depth: 0 @@ -318,7 +318,7 @@ jobs: egress-policy: audit - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - name: Set up Docker Buildx uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4 @@ -345,7 +345,7 @@ jobs: timeout: '10m' - name: Upload Trivy results to GitHub Security tab - uses: github/codeql-action/upload-sarif@v4 + uses: github/codeql-action/upload-sarif@5595ccaf912efad79be6eef63a5619ff05969be3 # v4 if: always() with: sarif_file: 'trivy-results.sarif' @@ -367,7 +367,7 @@ jobs: output: 'sbom.json' - name: Upload SBOM artifact - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: sbom-${{ needs.version.outputs.version }} path: sbom.json @@ -387,7 +387,7 @@ jobs: egress-policy: audit - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 with: fetch-depth: 0 diff --git a/.github/workflows/security-full-scan.yml b/.github/workflows/security-full-scan.yml index b9def673..557facd4 100644 --- a/.github/workflows/security-full-scan.yml +++ b/.github/workflows/security-full-scan.yml @@ -33,7 +33,7 @@ jobs: egress-policy: audit - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - name: Compute image name id: image @@ -202,7 +202,7 @@ jobs: rules_file_name: ".zap/rules.tsv" - name: Upload scan reports - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 if: always() with: name: zap-api-scan-reports-${{ github.run_id }} @@ -254,7 +254,7 @@ jobs: egress-policy: audit - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - name: Set up Docker Buildx uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4 @@ -288,7 +288,7 @@ jobs: only-fixed: true - name: Upload Scout SARIF - uses: github/codeql-action/upload-sarif@v4 + uses: github/codeql-action/upload-sarif@5595ccaf912efad79be6eef63a5619ff05969be3 # v4 if: always() with: sarif_file: scout-results.sarif @@ -307,7 +307,7 @@ jobs: egress-policy: audit - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - name: Setup pnpm uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 @@ -315,7 +315,7 @@ jobs: package_json_file: boardgametracker.client/package.json - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 with: node-version: "20.x" cache: "pnpm" diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 3419d517..dab7aa08 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -21,10 +21,10 @@ jobs: egress-policy: audit - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - name: Setup dotnet v8 - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5 with: dotnet-version: "8.x" @@ -61,7 +61,7 @@ jobs: egress-policy: audit - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - name: Setup pnpm uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 @@ -69,7 +69,7 @@ jobs: package_json_file: boardgametracker.client/package.json - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 with: node-version: "20.x" cache: "pnpm" @@ -97,7 +97,7 @@ jobs: egress-policy: audit - name: Checkout (full history) - uses: actions/checkout@v6 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 with: fetch-depth: 0 diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index e8d62201..c38e6213 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -84,7 +84,7 @@ jobs: --force - name: Process stale issues and PRs - uses: actions/stale@v10 + uses: actions/stale@1e223db275d687790206a7acac4d1a11bd6fe629 # v10 with: stale-issue-label: stale stale-issue-message: > diff --git a/.github/workflows/welcome.yml b/.github/workflows/welcome.yml index 12c5da63..e5be72a5 100644 --- a/.github/workflows/welcome.yml +++ b/.github/workflows/welcome.yml @@ -20,7 +20,7 @@ jobs: with: egress-policy: audit - - uses: actions/first-interaction@v3 + - uses: actions/first-interaction@1c4688942c71f71d4f5502a26ea67c331730fa4d # v3 with: repo_token: ${{ secrets.GITHUB_TOKEN }} From 2b6000e0ccb9ef550e0e4315ff55d5217b039d24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikha=C3=ABl=20Regni?= Date: Fri, 7 Aug 2026 02:03:53 +0200 Subject: [PATCH 25/27] fix(ci): pin the sonarscanner version dotnet tool update with no --version always installed the latest scanner, so an upstream release could change behaviour with no commit here. That is how SonarCloud analysis broke on 2026-05-21 without a repo change: a newer scanner moved the quality gate poll to api.sonarcloud.io, where the current token is not authorised. The version is now declared once per job and reused in the cache key, because a key that ignores the version would restore an older scanner on a cache hit and silently defeat the pin. restore-keys is dropped for the same reason: a prefix match would return the previous version's cache. Pinning does not fix the broken token; it makes the next change deliberate. Co-Authored-By: Claude Opus 5 --- .github/workflows/ci.yml | 7 ++++--- .github/workflows/publish-container.yml | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f656d846..8efc8961 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,6 +13,8 @@ jobs: test-and-analyze: name: Test and SonarCloud Analysis runs-on: ubuntu-latest + env: + SONAR_SCANNER_VERSION: "11.2.1" permissions: contents: read pull-requests: write @@ -73,14 +75,13 @@ jobs: uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5 with: path: ./.sonar/scanner - key: ${{ runner.os }}-sonar-scanner - restore-keys: ${{ runner.os }}-sonar-scanner + key: ${{ runner.os }}-sonar-scanner-${{ env.SONAR_SCANNER_VERSION }} - name: Install SonarCloud scanner if: steps.cache-sonar-scanner.outputs.cache-hit != 'true' run: | mkdir -p ./.sonar/scanner - dotnet tool update dotnet-sonarscanner --tool-path ./.sonar/scanner + dotnet tool update dotnet-sonarscanner --tool-path ./.sonar/scanner --version "$SONAR_SCANNER_VERSION" - name: Install .NET dependencies run: dotnet restore ./BoardGameTracker.sln diff --git a/.github/workflows/publish-container.yml b/.github/workflows/publish-container.yml index e7cff55a..11b454f4 100644 --- a/.github/workflows/publish-container.yml +++ b/.github/workflows/publish-container.yml @@ -65,6 +65,8 @@ jobs: name: Test and SonarCloud Analysis runs-on: ubuntu-latest needs: [version] + env: + SONAR_SCANNER_VERSION: "11.2.1" permissions: contents: read steps: @@ -113,14 +115,13 @@ jobs: uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5 with: path: ./.sonar/scanner - key: ${{ runner.os }}-sonar-scanner - restore-keys: ${{ runner.os }}-sonar-scanner + key: ${{ runner.os }}-sonar-scanner-${{ env.SONAR_SCANNER_VERSION }} - name: Install SonarCloud scanner if: steps.cache-sonar-scanner.outputs.cache-hit != 'true' run: | mkdir -p ./.sonar/scanner - dotnet tool update dotnet-sonarscanner --tool-path ./.sonar/scanner + dotnet tool update dotnet-sonarscanner --tool-path ./.sonar/scanner --version "$SONAR_SCANNER_VERSION" - name: Install .NET dependencies run: dotnet restore ./BoardGameTracker.sln From 78f13f3f9941b3c5af5e4c8a955d0f7d429a1e41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikha=C3=ABl=20Regni?= Date: Fri, 7 Aug 2026 02:38:03 +0200 Subject: [PATCH 26/27] chore(ci): treat the sonar project key and organization as configuration Both values are already public: the README badges link to sonarcloud.io/summary/new_code?id=mregni_BoardGameTracker, and the uping organization is visible on its Docker Hub badge. Holding them as secrets adds no protection and masks them in logs, which is why a failing analysis reports "Downloading cache. Project key: ***" and cannot be diagnosed from the run. Only SONAR_TOKEN stays a secret. This also removes the key and organization as variables when investigating the current authorisation failure at the quality gate poll. Co-Authored-By: Claude Opus 5 --- .github/workflows/ci.yml | 4 ++-- .github/workflows/publish-container.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8efc8961..9dac98ee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,6 +15,8 @@ jobs: runs-on: ubuntu-latest env: SONAR_SCANNER_VERSION: "11.2.1" + SONAR_PROJECT_KEY: mregni_BoardGameTracker + SONAR_ORGANIZATION: uping permissions: contents: read pull-requests: write @@ -90,8 +92,6 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - SONAR_PROJECT_KEY: ${{ secrets.SONAR_PROJECT_KEY }} - SONAR_ORGANIZATION: ${{ secrets.SONAR_ORGANIZATION }} VERSION: ${{ steps.versioning.outputs.version }} run: | ./.sonar/scanner/dotnet-sonarscanner begin \ diff --git a/.github/workflows/publish-container.yml b/.github/workflows/publish-container.yml index 11b454f4..1190d903 100644 --- a/.github/workflows/publish-container.yml +++ b/.github/workflows/publish-container.yml @@ -67,6 +67,8 @@ jobs: needs: [version] env: SONAR_SCANNER_VERSION: "11.2.1" + SONAR_PROJECT_KEY: mregni_BoardGameTracker + SONAR_ORGANIZATION: uping permissions: contents: read steps: @@ -131,8 +133,6 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - SONAR_PROJECT_KEY: ${{ secrets.SONAR_PROJECT_KEY }} - SONAR_ORGANIZATION: ${{ secrets.SONAR_ORGANIZATION }} VERSION: ${{ needs.version.outputs.version }} run: | ./.sonar/scanner/dotnet-sonarscanner begin \ From a1b9fcd762bd3baddb0de4fb5fa0aee9285e2ebc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikha=C3=ABl=20Regni?= Date: Fri, 7 Aug 2026 08:39:35 +0200 Subject: [PATCH 27/27] fix(ci): let sonarcloud report its own quality gate The analysis was never broken. SonarCloud ingests the report, evaluates the gate and posts a "Quality Gate passed" check from its own app within seconds of upload; on the run that prompted this, upload and that check both landed at 00:42:20. The only failing call was the scanner polling api.sonarcloud.io for a verdict already being delivered, which returned "Not authorized or project not found" under two different tokens and with the key and organization set to verified literals. Dropping sonar.qualitygate.wait removes the duplicate. The gate is still enforced, by the SonarCloud Code Analysis check, which links straight to the per-PR dashboard. publish-container.yml loses its continue-on-error on the same step. It existed to tolerate this failure, and it is why a broken step went unnoticed on master for months. Co-Authored-By: Claude Opus 5 --- .github/workflows/ci.yml | 2 -- .github/workflows/publish-container.yml | 3 --- 2 files changed, 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9dac98ee..ca6e71c7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -106,8 +106,6 @@ jobs: /d:sonar.testExecutionReportPaths="boardgametracker.client/coverage/sonar-report.xml" \ /d:sonar.exclusions="**/node_modules/**,**/dist/**,**/build/**,**/coverage/**,**/TestResults/**,**/obj/**,**/bin/**,**/Migrations/**,**/routeTree.gen.ts" \ /d:sonar.coverage.exclusions="**/BoardGameTracker.Host/**/*.cs,**/BoardGameTracker.Core/Datastore/**/*.cs,**/ViewModels/**/*.cs,**/Entities/**/*.cs,**/routeTree.gen.ts,**/tailwind.config.js,**/node_modules/**" \ - /d:sonar.qualitygate.wait=true \ - /d:sonar.qualitygate.timeout=300 \ /d:sonar.issue.ignore.multicriteria=e1 \ /d:sonar.issue.ignore.multicriteria.e1.ruleKey=githubactions:S7637 \ /d:sonar.issue.ignore.multicriteria.e1.resourceKey="**/.github/**" diff --git a/.github/workflows/publish-container.yml b/.github/workflows/publish-container.yml index 1190d903..dfe7ac3c 100644 --- a/.github/workflows/publish-container.yml +++ b/.github/workflows/publish-container.yml @@ -146,8 +146,6 @@ jobs: /d:sonar.javascript.lcov.reportPaths="boardgametracker.client/coverage/lcov.info" \ /d:sonar.exclusions="**/node_modules/**,**/dist/**,**/build/**,**/coverage/**,**/TestResults/**,**/obj/**,**/bin/**,**/Migrations/**,**/routeTree.gen.ts" \ /d:sonar.coverage.exclusions="**/BoardGameTracker.Host/**/*.cs,**/BoardGameTracker.Core/Datastore/**/*.cs,**/ViewModels/**/*.cs,**/Entities/**/*.cs,**/routeTree.gen.ts,**/tailwind.config.js,**/node_modules/**" \ - /d:sonar.qualitygate.wait=true \ - /d:sonar.qualitygate.timeout=300 \ /d:sonar.issue.ignore.multicriteria=e1 \ /d:sonar.issue.ignore.multicriteria.e1.ruleKey=githubactions:S7637 \ /d:sonar.issue.ignore.multicriteria.e1.resourceKey="**/.github/**" @@ -187,7 +185,6 @@ jobs: - name: End SonarCloud analysis if: env.SONAR_TOKEN != '' - continue-on-error: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}