From 5350474e91484b015ad9678490a2f0020470e2dd Mon Sep 17 00:00:00 2001 From: Emmanuel Knafo Date: Tue, 2 Jun 2026 16:41:26 -0400 Subject: [PATCH 1/2] ci: bring GitHub workflows to parity with ADO pipelines + ACR build retry Adds .github/workflows equivalents for the ADO pipelines that had no GitHub counterpart: - load-test.yml -> mirrors .azuredevops/pipelines/load-test.yml (Locust, workshop-dev environment, EnricoMi JUnit publish, HTML/CSV artifact). - ui-tests.yml -> mirrors .azuredevops/pipelines/ui-tests.yml (Playwright Chromium, EnricoMi JUnit publish, full report artifact). Wiki-publish step intentionally dropped; artifact + Checks summary are the GH-native equivalent. - codeql.yml -> mirrors .azuredevops/pipelines/adv-sec.yml using github/codeql-action (matrix csharp/javascript/python with explicit --no-incremental /p:UseSharedCompilation=false build for the C# leg) plus actions/dependency-review-action on PRs to cover the ADO AdvancedSecurity dependency scanning task. Also fixes the deploy.yml ACR build failure (ParentResourceNotFound on listBuildSourceUploadUrl right after azd provision) by: - pinning az CLI to AZURE_SUBSCRIPTION_ID with az account set, - waiting for ACR ARM propagation via az acr show poll (5 min cap), - wrapping az acr build in a 5-attempt retry with 20s backoff. The same retry/propagation guard is applied to .azuredevops/pipelines/deploy.yml to keep both pipelines in lockstep. Deploy concurrency: adds concurrency group scoped to the target azd env so back-to-back runs against the same rg- are serialized (mirrors the ADO lockBehavior: runLatest on the workshop-dev environment). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .azuredevops/pipelines/deploy.yml | 51 +++++++- .github/workflows/codeql.yml | 91 ++++++++++++++ .github/workflows/deploy.yml | 61 ++++++++- .github/workflows/load-test.yml | 202 ++++++++++++++++++++++++++++++ .github/workflows/ui-tests.yml | 144 +++++++++++++++++++++ 5 files changed, 541 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/codeql.yml create mode 100644 .github/workflows/load-test.yml create mode 100644 .github/workflows/ui-tests.yml diff --git a/.azuredevops/pipelines/deploy.yml b/.azuredevops/pipelines/deploy.yml index 0a9909d..9c96ab4 100644 --- a/.azuredevops/pipelines/deploy.yml +++ b/.azuredevops/pipelines/deploy.yml @@ -157,14 +157,57 @@ stages: ACR_NAME="$(azd env get-value ACR_NAME --environment "$AZD_ENV_NAME")" RG="$(azd env get-value AZURE_RESOURCE_GROUP --environment "$AZD_ENV_NAME")" + # Pin az CLI to the subscription azd just provisioned into. Without + # this, az may default to a different active subscription for the SP, + # causing az acr build to return ParentResourceNotFound even though + # the registry exists. + az account set --subscription "$AZURE_SUBSCRIPTION_ID" + + # Wait for ACR ARM propagation. Right after `azd provision` completes, + # the registry resource exists but the ContainerRegistry RP can return + # ParentResourceNotFound from data-plane actions like + # listBuildSourceUploadUrl for up to ~60s while the resource graph + # propagates. Poll `az acr show` until it succeeds (or 5 min cap). + echo "Waiting for ACR '$ACR_NAME' in '$RG' to become queryable..." + for attempt in $(seq 1 30); do + if az acr show --name "$ACR_NAME" --resource-group "$RG" --query id -o tsv >/dev/null 2>&1; then + echo " ACR queryable after ${attempt} attempt(s)." + break + fi + if [[ "$attempt" == "30" ]]; then + echo "##vso[task.logissue type=error]ACR '$ACR_NAME' still not queryable after 5 minutes." + exit 1 + fi + sleep 10 + done + # Server-side build+push - no Docker daemon, no docker login (WIF identity active). # The SemVer is baked into the binary (/p:Version) and image (OCI label + # APP_VERSION env) via the VERSION build arg, and pushed as an extra # human-readable :$SEMVER tag alongside the immutable :$TAG (git SHA). - az acr build --registry "$ACR_NAME" --build-arg VERSION="$SEMVER" \ - --image "mapaq-api:$TAG" --image "mapaq-api:$SEMVER" --file src/Mapaq.Api/Dockerfile . - az acr build --registry "$ACR_NAME" --build-arg VERSION="$SEMVER" \ - --image "mapaq-web:$TAG" --image "mapaq-web:$SEMVER" --file src/Mapaq.Web/Dockerfile . + # Wrap acr build in a small retry to absorb the residual RP propagation + # window (ParentResourceNotFound) immediately after provision. + acr_build() { + local image_base="$1" dockerfile="$2" + for attempt in 1 2 3 4 5; do + if az acr build \ + --registry "$ACR_NAME" \ + --resource-group "$RG" \ + --build-arg VERSION="$SEMVER" \ + --image "${image_base}:$TAG" \ + --image "${image_base}:$SEMVER" \ + --file "$dockerfile" .; then + return 0 + fi + echo "##vso[task.logissue type=warning]az acr build (${image_base}) attempt ${attempt} failed; retrying in 20s..." + sleep 20 + done + echo "##vso[task.logissue type=error]az acr build (${image_base}) failed after 5 attempts." + return 1 + } + + acr_build mapaq-api src/Mapaq.Api/Dockerfile + acr_build mapaq-web src/Mapaq.Web/Dockerfile # Force both sites to pull the now-present image. API_SITE="$(az webapp list -g "$RG" --query "[?starts_with(name,'mapaq-api-')].name | [0]" -o tsv)" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..5386e2d --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,91 @@ +# GitHub Advanced Security — CodeQL code scanning + dependency review. +# Mirrors .azuredevops/pipelines/adv-sec.yml (which uses ADO's AdvancedSecurity +# CodeQL + Dependency Scanning tasks). On GitHub, dependency scanning is split +# between Dependabot alerts (always-on, repo Security tab) and the +# dependency-review-action which gates PRs on new vulnerable advisories. +# +# CodeQL needs to observe the compiler for compiled languages (C#). We do an +# explicit dotnet build with --no-incremental + /p:UseSharedCompilation=false so +# the CodeQL extractor sees every csc invocation; this matches the ADO pipeline +# rationale (the extractor would otherwise miss source on warm Roslyn build +# servers). JavaScript and Python are extracted from source and need no build. +name: codeql + +on: + push: + branches: [main] + pull_request: + branches: [main] + schedule: + # Weekly Monday 06:00 UTC — keeps the latest query suite running even when + # no code changes land in a quiet week. + - cron: "0 6 * * 1" + +permissions: + security-events: write + actions: read + contents: read + +jobs: + analyze: + name: CodeQL analyze (${{ matrix.language }}) + runs-on: ubuntu-latest + timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + language: [csharp, javascript, python] + env: + DOTNET_VERSION: "10.0.x" + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Setup .NET ${{ env.DOTNET_VERSION }} + if: matrix.language == 'csharp' + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: CodeQL init (${{ matrix.language }}) + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + queries: security-and-quality + + - name: dotnet restore + if: matrix.language == 'csharp' + run: dotnet restore Mapaq.sln + + # --no-incremental + /p:UseSharedCompilation=false defeat Roslyn's build- + # server caching, which otherwise causes CodeQL to miss source files. + - name: dotnet build (no-incremental, no-shared-compilation) + if: matrix.language == 'csharp' + run: | + dotnet build Mapaq.sln \ + --configuration Release \ + --no-restore \ + --no-incremental \ + /p:UseSharedCompilation=false + + - name: CodeQL analyze + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{ matrix.language }}" + + dependency_review: + name: Dependency review (PR diff) + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Dependency review + uses: actions/dependency-review-action@v4 + with: + fail-on-severity: high + comment-summary-in-pr: on-failure diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 257b617..0e7bcac 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -22,6 +22,16 @@ permissions: # contents: write lets the deploy job push the release git tag (v). contents: write +# Serialize deploys per target environment, mirroring the ADO pipeline's +# `lockBehavior: runLatest` on the workshop-dev environment. With +# cancel-in-progress: false, an in-flight deploy finishes and any newer queued +# run replaces older pending runs in the queue — preventing the DeploymentActive +# race when back-to-back CI completions both start `azd provision` and clash on +# the same ARM deployment names (pe-, etc.) inside rg-. +concurrency: + group: deploy-${{ inputs.azureEnvName || vars.AZURE_ENV_NAME || 'dev-001' }} + cancel-in-progress: false + jobs: deploy: name: azd up @@ -98,14 +108,57 @@ jobs: ACR_NAME="$(azd env get-value ACR_NAME)" RG="$(azd env get-value AZURE_RESOURCE_GROUP)" + # Pin az CLI to the same subscription azd just provisioned into. Without + # this, az may default to a different active subscription (or none) for + # the SP, and az acr build returns ParentResourceNotFound even though + # the registry exists. + az account set --subscription "$AZURE_SUBSCRIPTION_ID" + + # Wait for ACR ARM propagation. Right after `azd provision` completes, + # the registry resource exists but the ContainerRegistry RP can return + # ParentResourceNotFound from data-plane actions like + # listBuildSourceUploadUrl for up to ~60s while the resource graph + # propagates. Poll `az acr show` until it succeeds (or 5 min cap). + echo "Waiting for ACR '$ACR_NAME' in '$RG' to become queryable..." + for attempt in $(seq 1 30); do + if az acr show --name "$ACR_NAME" --resource-group "$RG" --query id -o tsv >/dev/null 2>&1; then + echo " ACR queryable after ${attempt} attempt(s)." + break + fi + if [[ "$attempt" == "30" ]]; then + echo "::error::ACR '$ACR_NAME' still not queryable after 5 minutes." + exit 1 + fi + sleep 10 + done + # Server-side build+push - no Docker daemon, no docker login (OIDC identity active). # The SemVer is baked into the binary (/p:Version) and image (OCI label + APP_VERSION # env) via the VERSION build arg, and pushed as an extra human-readable :$SEMVER tag # alongside the immutable :$TAG (git SHA) that the sites actually run. - az acr build --registry "$ACR_NAME" --build-arg VERSION="$SEMVER" \ - --image "mapaq-api:$TAG" --image "mapaq-api:$SEMVER" --file src/Mapaq.Api/Dockerfile . - az acr build --registry "$ACR_NAME" --build-arg VERSION="$SEMVER" \ - --image "mapaq-web:$TAG" --image "mapaq-web:$SEMVER" --file src/Mapaq.Web/Dockerfile . + # Wrap acr build in a small retry to absorb the residual RP propagation window + # (ParentResourceNotFound on listBuildSourceUploadUrl) immediately after provision. + acr_build() { + local image_base="$1" dockerfile="$2" + for attempt in 1 2 3 4 5; do + if az acr build \ + --registry "$ACR_NAME" \ + --resource-group "$RG" \ + --build-arg VERSION="$SEMVER" \ + --image "${image_base}:$TAG" \ + --image "${image_base}:$SEMVER" \ + --file "$dockerfile" .; then + return 0 + fi + echo "::warning::az acr build (${image_base}) attempt ${attempt} failed; retrying in 20s..." + sleep 20 + done + echo "::error::az acr build (${image_base}) failed after 5 attempts." + return 1 + } + + acr_build mapaq-api src/Mapaq.Api/Dockerfile + acr_build mapaq-web src/Mapaq.Web/Dockerfile # Force both sites to pull the now-present image. api_site="$(az webapp list -g "$RG" --query "[?starts_with(name,'mapaq-api-')].name | [0]" -o tsv)" diff --git a/.github/workflows/load-test.yml b/.github/workflows/load-test.yml new file mode 100644 index 0000000..9940868 --- /dev/null +++ b/.github/workflows/load-test.yml @@ -0,0 +1,202 @@ +# Locust load test against the deployed Mapaq.Api / Mapaq.Web App Services. +# Mirrors .azuredevops/pipelines/load-test.yml. +# +# Defaults to the workshop-dev environment (rg-dev-001) and discovers the public +# URLs from the resource group at run time, so attendees do not have to memorize +# the resourceToken-suffixed hostnames. Override apiUrl / webUrl (or the +# azureEnvName input) at dispatch time to point at a different environment. +# +# Outputs: +# * GitHub Checks tab — one testcase per endpoint via JUnit XML (EnricoMi publish). +# * Workflow artifact `load-test-report` — full Locust HTML + CSV bundle. +# A run is marked failed when any endpoint had HTTP failures or its p95 latency +# exceeded p95ThresholdMs (when failOnLoadErrors=true). +name: load-test + +on: + workflow_dispatch: + inputs: + azureEnvName: + description: "azd environment name (resource group rg-)" + required: false + type: string + default: "dev-001" + apiUrl: + description: 'API base URL (use "auto" to discover from rg-)' + required: false + type: string + default: "auto" + webUrl: + description: 'Web base URL (use "auto" to discover from rg-)' + required: false + type: string + default: "auto" + users: + description: "Peak concurrent users" + required: false + type: number + default: 25 + spawnRate: + description: "Users to spawn per second" + required: false + type: number + default: 5 + duration: + description: "Run duration (Locust syntax, e.g. 30s, 2m, 5m)" + required: false + type: string + default: "2m" + p95ThresholdMs: + description: "p95 latency budget per endpoint (ms). 0 disables the latency gate." + required: false + type: number + default: 1500 + failOnLoadErrors: + description: "Fail the pipeline when any endpoint exceeds the budget or has HTTP errors" + required: false + type: boolean + default: true + +permissions: + id-token: write + contents: read + checks: write + pull-requests: write + +jobs: + load_test: + name: Run Locust against deployed App Services + runs-on: ubuntu-latest + timeout-minutes: 30 + environment: + name: workshop-dev + env: + PYTHON_VERSION: "3.12" + REPORT_DIR: ${{ github.workspace }}/load-test-report + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: pip install -r tests/load/requirements.txt + run: | + set -euo pipefail + python -m pip install --upgrade pip + python -m pip install -r tests/load/requirements.txt + + - name: Azure login (OIDC) + uses: azure/login@v2 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + + - name: Resolve API / Web URLs from rg-${{ inputs.azureEnvName }} + id: resolve + env: + AZ_ENV: ${{ inputs.azureEnvName }} + API_URL_PARAM: ${{ inputs.apiUrl }} + WEB_URL_PARAM: ${{ inputs.webUrl }} + run: | + set -euo pipefail + RG="rg-${AZ_ENV}" + + resolve_site() { + local prefix="$1" + local hostname + hostname="$(az webapp list \ + --resource-group "$RG" \ + --query "[?starts_with(name, '${prefix}-')].defaultHostName | [0]" \ + -o tsv)" + if [[ -z "$hostname" ]]; then + echo "" + else + echo "https://${hostname}" + fi + } + + if [[ -n "$API_URL_PARAM" && "$API_URL_PARAM" != "auto" ]]; then + RESOLVED_API="$API_URL_PARAM" + else + RESOLVED_API="$(resolve_site mapaq-api)" + fi + if [[ -n "$WEB_URL_PARAM" && "$WEB_URL_PARAM" != "auto" ]]; then + RESOLVED_WEB="$WEB_URL_PARAM" + else + RESOLVED_WEB="$(resolve_site mapaq-web)" + fi + + if [[ -z "$RESOLVED_API" || -z "$RESOLVED_WEB" ]]; then + echo "::error::Could not resolve API/Web URLs from $RG. Pass apiUrl/webUrl explicitly. api='$RESOLVED_API' web='$RESOLVED_WEB'" + exit 1 + fi + + echo "Resolved API -> $RESOLVED_API" + echo "Resolved Web -> $RESOLVED_WEB" + echo "MAPAQ_API_HOST=$RESOLVED_API" >> "$GITHUB_ENV" + echo "MAPAQ_WEB_HOST=$RESOLVED_WEB" >> "$GITHUB_ENV" + + - name: Run Locust headless + env: + LOCUST_VERIFY_SSL: "1" + run: | + set -uo pipefail + mkdir -p "$REPORT_DIR" + + echo "Targets:" + echo " api -> $MAPAQ_API_HOST" + echo " web -> $MAPAQ_WEB_HOST" + + # Pre-flight against the API; do not fail here — Locust surfaces persistent outages. + if curl -fsS --max-time 10 "$MAPAQ_API_HOST/healthz" -o /dev/null; then + echo "API /healthz reachable." + else + echo "::warning::API /healthz did not respond before the run started." + fi + + # Locust's runner overwrites every user_class.host with --host when + # passed, so we omit --host and let MapaqApiUser / MapaqWebUser pin + # themselves via MAPAQ_API_HOST / MAPAQ_WEB_HOST (see locustfile.py). + # Always exit 0 — the JUnit publish step decides build outcome per-endpoint. + python -m locust \ + -f tests/load/locustfile.py \ + --headless \ + --users ${{ inputs.users }} \ + --spawn-rate ${{ inputs.spawnRate }} \ + --run-time ${{ inputs.duration }} \ + --html "$REPORT_DIR/report.html" \ + --csv "$REPORT_DIR/stats" \ + --only-summary + LOCUST_EXIT=$? + echo "Locust exit code: $LOCUST_EXIT (per-endpoint pass/fail decided by the JUnit publish step)" + exit 0 + + - name: Convert Locust stats to JUnit + if: always() + run: | + set -euo pipefail + python tests/load/locust_stats_to_junit.py \ + --stats-prefix "$REPORT_DIR/stats" \ + --output "$REPORT_DIR/junit.xml" \ + --suite-name "Mapaq Load Test (${{ inputs.azureEnvName }})" \ + --p95-threshold-ms ${{ inputs.p95ThresholdMs }} + + - name: Publish JUnit results to Checks tab + if: always() + uses: EnricoMi/publish-unit-test-result-action@v2 + with: + files: ${{ env.REPORT_DIR }}/junit.xml + check_name: "Mapaq Load Test (${{ inputs.azureEnvName }})" + comment_mode: off + fail_on: ${{ inputs.failOnLoadErrors && 'test failures' || 'nothing' }} + + - name: Upload HTML + CSV report bundle + if: always() + uses: actions/upload-artifact@v4 + with: + name: load-test-report + path: ${{ env.REPORT_DIR }} diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml new file mode 100644 index 0000000..c554ce7 --- /dev/null +++ b/.github/workflows/ui-tests.yml @@ -0,0 +1,144 @@ +# Playwright UI tests against the deployed Mapaq.Web App Service. +# Mirrors .azuredevops/pipelines/ui-tests.yml. +# +# Defaults to the workshop-dev environment (rg-dev-001) and discovers the public +# Web URL at run time, so attendees do not have to memorize the resourceToken- +# suffixed hostname. Override webUrl (or azureEnvName) at dispatch time to retarget. +# +# Outputs: +# * GitHub Checks tab — Playwright JUnit XML, one row per test (EnricoMi publish). +# * Workflow artifact `ui-test-report` — playwright-report/ + screenshots + raw test-results. +# +# Note: the ADO pipeline also publishes screenshots to the project wiki via PAT. +# That step is intentionally omitted here — artifact + Checks summary are the +# GitHub-native equivalents, and a wiki commit step adds repo churn and PAT scope +# that has no clear consumer on GitHub today. +name: ui-tests + +on: + workflow_dispatch: + inputs: + azureEnvName: + description: "azd environment name (resource group rg-)" + required: false + type: string + default: "dev-001" + webUrl: + description: 'Web base URL (use "auto" to discover from rg-)' + required: false + type: string + default: "auto" + failOnTestErrors: + description: "Fail the workflow when any UI test fails" + required: false + type: boolean + default: true + +permissions: + id-token: write + contents: read + checks: write + pull-requests: write + +jobs: + ui_tests: + name: Run Playwright against deployed Web + runs-on: ubuntu-latest + timeout-minutes: 30 + environment: + name: workshop-dev + env: + NODE_VERSION: "20.x" + REPORT_DIR: ${{ github.workspace }}/ui-test-report + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Setup Node ${{ env.NODE_VERSION }} + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Azure login (OIDC) + uses: azure/login@v2 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + + - name: Resolve Web URL from rg-${{ inputs.azureEnvName }} + env: + AZ_ENV: ${{ inputs.azureEnvName }} + WEB_URL_PARAM: ${{ inputs.webUrl }} + run: | + set -euo pipefail + RG="rg-${AZ_ENV}" + + if [[ -n "$WEB_URL_PARAM" && "$WEB_URL_PARAM" != "auto" ]]; then + RESOLVED_WEB="$WEB_URL_PARAM" + else + HOSTNAME="$(az webapp list \ + --resource-group "$RG" \ + --query "[?starts_with(name, 'mapaq-web-')].defaultHostName | [0]" \ + -o tsv)" + if [[ -z "$HOSTNAME" ]]; then + echo "::error::Could not resolve Web URL from $RG. Pass webUrl explicitly." + exit 1 + fi + RESOLVED_WEB="https://${HOSTNAME}" + fi + + echo "Resolved Web -> $RESOLVED_WEB" + echo "MAPAQ_WEB_URL=$RESOLVED_WEB" >> "$GITHUB_ENV" + echo "TARGET_ENV_LABEL=${AZ_ENV}" >> "$GITHUB_ENV" + + - name: npm ci (Playwright project) + working-directory: tests/ui + run: npm ci --no-audit --no-fund + + - name: Install Playwright browsers (Chromium) + working-directory: tests/ui + # --with-deps installs the OS libs Playwright needs on Ubuntu runners. + run: npx --no-install playwright install --with-deps chromium + + - name: Run Playwright + working-directory: tests/ui + env: + CI: "true" + run: | + set -uo pipefail + + # Pre-flight: warn if the home page does not respond, but let Playwright drive pass/fail. + if curl -fsS --max-time 10 "$MAPAQ_WEB_URL/" -o /dev/null; then + echo "Web tier reachable at $MAPAQ_WEB_URL" + else + echo "::warning::Pre-flight curl to $MAPAQ_WEB_URL/ failed." + fi + + npx --no-install playwright test + PLAYWRIGHT_EXIT=$? + + # Always publish the report bundle, regardless of pass/fail. + mkdir -p "$REPORT_DIR" + cp -r playwright-report "$REPORT_DIR/" 2>/dev/null || true + cp -r test-results "$REPORT_DIR/" 2>/dev/null || true + cp -r screenshots "$REPORT_DIR/" 2>/dev/null || true + + echo "Playwright exit code: $PLAYWRIGHT_EXIT (per-test pass/fail decided by the JUnit publish step)" + exit 0 + + - name: Publish Playwright JUnit results to Checks tab + if: always() + uses: EnricoMi/publish-unit-test-result-action@v2 + with: + files: tests/ui/test-results/junit.xml + check_name: "Mapaq UI Tests (${{ inputs.azureEnvName }})" + comment_mode: off + fail_on: ${{ inputs.failOnTestErrors && 'test failures' || 'nothing' }} + + - name: Upload HTML + screenshots bundle + if: always() + uses: actions/upload-artifact@v4 + with: + name: ui-test-report + path: ${{ env.REPORT_DIR }} From e204e91d5850da9b5b5d15cfe8def4fb38adde8d Mon Sep 17 00:00:00 2001 From: Emmanuel Knafo Date: Tue, 2 Jun 2026 16:56:59 -0400 Subject: [PATCH 2/2] ci: replace codeql.yml with dependency-review only Repo has GitHub default code scanning enabled, which rejects SARIF uploads from advanced configurations with: 'CodeQL analyses from advanced configurations cannot be processed when the default setup is enabled'. Default setup already runs CodeQL across all detected languages, providing full parity with the ADO AdvancedSecurity-Codeql tasks. Keep only the actions/dependency-review-action piece since GitHub default scanning does not include a dependency-review-on-PR equivalent. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/codeql.yml | 91 ------------------------- .github/workflows/dependency-review.yml | 34 +++++++++ 2 files changed, 34 insertions(+), 91 deletions(-) delete mode 100644 .github/workflows/codeql.yml create mode 100644 .github/workflows/dependency-review.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml deleted file mode 100644 index 5386e2d..0000000 --- a/.github/workflows/codeql.yml +++ /dev/null @@ -1,91 +0,0 @@ -# GitHub Advanced Security — CodeQL code scanning + dependency review. -# Mirrors .azuredevops/pipelines/adv-sec.yml (which uses ADO's AdvancedSecurity -# CodeQL + Dependency Scanning tasks). On GitHub, dependency scanning is split -# between Dependabot alerts (always-on, repo Security tab) and the -# dependency-review-action which gates PRs on new vulnerable advisories. -# -# CodeQL needs to observe the compiler for compiled languages (C#). We do an -# explicit dotnet build with --no-incremental + /p:UseSharedCompilation=false so -# the CodeQL extractor sees every csc invocation; this matches the ADO pipeline -# rationale (the extractor would otherwise miss source on warm Roslyn build -# servers). JavaScript and Python are extracted from source and need no build. -name: codeql - -on: - push: - branches: [main] - pull_request: - branches: [main] - schedule: - # Weekly Monday 06:00 UTC — keeps the latest query suite running even when - # no code changes land in a quiet week. - - cron: "0 6 * * 1" - -permissions: - security-events: write - actions: read - contents: read - -jobs: - analyze: - name: CodeQL analyze (${{ matrix.language }}) - runs-on: ubuntu-latest - timeout-minutes: 60 - strategy: - fail-fast: false - matrix: - language: [csharp, javascript, python] - env: - DOTNET_VERSION: "10.0.x" - steps: - - name: Checkout - uses: actions/checkout@v5 - - - name: Setup .NET ${{ env.DOTNET_VERSION }} - if: matrix.language == 'csharp' - uses: actions/setup-dotnet@v4 - with: - dotnet-version: ${{ env.DOTNET_VERSION }} - - - name: CodeQL init (${{ matrix.language }}) - uses: github/codeql-action/init@v3 - with: - languages: ${{ matrix.language }} - queries: security-and-quality - - - name: dotnet restore - if: matrix.language == 'csharp' - run: dotnet restore Mapaq.sln - - # --no-incremental + /p:UseSharedCompilation=false defeat Roslyn's build- - # server caching, which otherwise causes CodeQL to miss source files. - - name: dotnet build (no-incremental, no-shared-compilation) - if: matrix.language == 'csharp' - run: | - dotnet build Mapaq.sln \ - --configuration Release \ - --no-restore \ - --no-incremental \ - /p:UseSharedCompilation=false - - - name: CodeQL analyze - uses: github/codeql-action/analyze@v3 - with: - category: "/language:${{ matrix.language }}" - - dependency_review: - name: Dependency review (PR diff) - if: github.event_name == 'pull_request' - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: write - steps: - - name: Checkout - uses: actions/checkout@v5 - - - name: Dependency review - uses: actions/dependency-review-action@v4 - with: - fail-on-severity: high - comment-summary-in-pr: on-failure diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 0000000..0f78ef9 --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,34 @@ +# Dependency review on PRs — equivalent of the ADO AdvancedSecurity-Dependency- +# Scanning task in .azuredevops/pipelines/adv-sec.yml. +# +# CodeQL parity with ADO's AdvancedSecurity-Codeql tasks is provided by GitHub's +# repository-level **default code scanning** (see Settings → Code security and +# analysis → Code scanning). The default setup runs CodeQL across every detected +# language and is mutually exclusive with an "advanced" workflow file — adding a +# custom codeql.yml here causes uploads to be rejected with: +# "CodeQL analyses from advanced configurations cannot be processed when the +# default setup is enabled" +# Keep CodeQL under the default setup and only layer dependency review on top. +name: dependency-review + +on: + pull_request: + branches: [main] + +permissions: + contents: read + pull-requests: write + +jobs: + review: + name: Dependency review (PR diff) + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Dependency review + uses: actions/dependency-review-action@v4 + with: + fail-on-severity: high + comment-summary-in-pr: on-failure