From d6de73ba45b813c7f33317ee022d957d5c31ac2b Mon Sep 17 00:00:00 2001 From: Saqib Date: Fri, 11 Sep 2026 23:30:22 +0530 Subject: [PATCH 1/7] feat: two-stage dev smoke gate, readonly-only prod tests dev: smoke first; functional runs only if smoke passes; either failing rolls back, both passing finalizes. main: only the readonly allowlist runs against prod (provider never), replacing skip_provider. Finalize uses !failure() because functional-tests is skipped on main by design. --- .github/workflows/deploy-backend.yml | 32 ++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/.github/workflows/deploy-backend.yml b/.github/workflows/deploy-backend.yml index 0a67128..fe5d01e 100644 --- a/.github/workflows/deploy-backend.yml +++ b/.github/workflows/deploy-backend.yml @@ -259,9 +259,9 @@ jobs: # force_smoke_failure test path. deployed_sha: ${{ (inputs.force_smoke_failure == true && 'forced-failure-sentinel') || github.sha }} min_passed: ${{ inputs.force_smoke_failure && 999 || 1 }} - # provider-smoke creates datasets/use cases with the test account, so - # prod deploys run only the read-only api and consumer suites. - skip_provider: ${{ github.ref_name == 'main' }} + # main: only the readonly allowlist ever runs against prod. + # dev: stage 1 of 2 -- functional-tests runs only if this passes. + suite: ${{ github.ref_name == 'main' && 'readonly' || 'smoke' }} secrets: # This job can't see environment-scoped secrets, and the repo-level # HOME_URL_DEV is the dev site -- passing it on main smoke-tested dev @@ -277,10 +277,28 @@ jobs: # and the job fails its preflight. KEYCLOAK_CLIENT_SECRET: ${{ secrets.KEYCLOAK_CLIENT_SECRET }} + functional-tests: + name: Functional Tests + needs: smoke-tests + # dev only, stage 2: runs only once smoke has passed. Functional tests + # write (datasets, use cases), so they never run against prod. + if: github.ref_name != 'main' + uses: CivicDataLab/CivicDataSpace-test/.github/workflows/run-smoke.yml@CI + with: + suite: functional + api_base_url: ${{ vars.DEV_API_BASE_URL }} + secrets: + HOME_URL_DEV: ${{ secrets.HOME_URL_DEV }} + TEST_EMAIL_1: ${{ secrets.TEST_EMAIL_1 }} + TEST_PASSWORD_1: ${{ secrets.TEST_PASSWORD_1 }} + TEST_EMAIL_2: ${{ secrets.TEST_EMAIL_2 }} + TEST_PASSWORD_2: ${{ secrets.TEST_PASSWORD_2 }} + KEYCLOAK_CLIENT_SECRET: ${{ secrets.KEYCLOAK_CLIENT_SECRET }} + rollback-on-smoke-failure: name: Rollback (smoke tests failed) # `deploy` must be in needs: for needs.deploy.result to resolve here. - needs: [deploy, smoke-tests] + needs: [deploy, smoke-tests, functional-tests] # failure()/success() builtins rather than needs.smoke-tests.result -- # both are false on cancellation, which is the behaviour we want; # if: always() would ignore cancellation entirely. @@ -331,8 +349,10 @@ jobs: finalize-deploy: name: Finalize Deploy - needs: [deploy, smoke-tests] - if: success() + needs: [deploy, smoke-tests, functional-tests] + # Not success(): on main functional-tests is skipped by design, and a + # skipped need would skip this job too. Nothing failed + smoke passed. + if: ${{ !failure() && !cancelled() && needs.smoke-tests.result == 'success' }} runs-on: ubuntu-latest environment: ${{ github.ref_name == 'main' && 'production' || 'development' }} timeout-minutes: 10 From d4ae23b6724dc1b5e391b65a8ae1638564cf087c Mon Sep 17 00:00:00 2001 From: Saqib Date: Fri, 11 Sep 2026 23:30:22 +0530 Subject: [PATCH 2/7] feat: full-suite PR gate against dev for PRs into main Runs the full suite (smoke, functional, regression) against dev on every PR into main. deployed_sha is the PR head, so the gate fails unless dev is running exactly the code being merged. --- .github/workflows/pr-gate.yml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 .github/workflows/pr-gate.yml diff --git a/.github/workflows/pr-gate.yml b/.github/workflows/pr-gate.yml new file mode 100644 index 0000000..0b5b065 --- /dev/null +++ b/.github/workflows/pr-gate.yml @@ -0,0 +1,29 @@ +# Full test suite against dev for every PR into main. Required by branch +# protection on main, so a PR merges (and prod deploys) only once it's green. +# dev already runs the PR's code (it was merged to dev first); deployed_sha +# makes the suite fail if dev is running anything else. +name: PR Gate + +on: + pull_request: + branches: [main] + +concurrency: + group: pr-gate-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + full-suite: + name: Full Suite (dev) + uses: CivicDataLab/CivicDataSpace-test/.github/workflows/run-smoke.yml@CI + with: + suite: full + api_base_url: ${{ vars.DEV_API_BASE_URL }} + deployed_sha: ${{ github.event.pull_request.head.sha }} + secrets: + HOME_URL_DEV: ${{ secrets.HOME_URL_DEV }} + TEST_EMAIL_1: ${{ secrets.TEST_EMAIL_1 }} + TEST_PASSWORD_1: ${{ secrets.TEST_PASSWORD_1 }} + TEST_EMAIL_2: ${{ secrets.TEST_EMAIL_2 }} + TEST_PASSWORD_2: ${{ secrets.TEST_PASSWORD_2 }} + KEYCLOAK_CLIENT_SECRET: ${{ secrets.KEYCLOAK_CLIENT_SECRET }} From ec0f301413d2e0345d55e23a7ad4c3fcefc40e9b Mon Sep 17 00:00:00 2001 From: Saqib Date: Tue, 15 Sep 2026 11:35:12 +0530 Subject: [PATCH 3/7] feat: run provider functional report-only while it is flaky against dev Provider functional tests fail at shifting points against dev (autosave races, slow create flows), so gating on them would roll back dev deploys spuriously. The gating functional stage and the PR gate now skip provider; separate provider jobs run with only_provider and report, without being a dependency of rollback/promote or a required check. --- .github/workflows/deploy-backend.yml | 21 +++++++++++++++++++++ .github/workflows/pr-gate.yml | 16 ++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/.github/workflows/deploy-backend.yml b/.github/workflows/deploy-backend.yml index fe5d01e..74a10ae 100644 --- a/.github/workflows/deploy-backend.yml +++ b/.github/workflows/deploy-backend.yml @@ -287,6 +287,9 @@ jobs: with: suite: functional api_base_url: ${{ vars.DEV_API_BASE_URL }} + # Provider functional is flaky against dev, so it runs report-only in + # provider-functional-report instead of gating the deploy. + skip_provider: true secrets: HOME_URL_DEV: ${{ secrets.HOME_URL_DEV }} TEST_EMAIL_1: ${{ secrets.TEST_EMAIL_1 }} @@ -295,6 +298,24 @@ jobs: TEST_PASSWORD_2: ${{ secrets.TEST_PASSWORD_2 }} KEYCLOAK_CLIENT_SECRET: ${{ secrets.KEYCLOAK_CLIENT_SECRET }} + provider-functional-report: + name: Provider Functional (report-only) + needs: smoke-tests + # Report-only: rollback and finalize do not list this job in needs, so + # its result never rolls back or blocks a dev deploy. Promote it to a + # gate once the provider suite is stable against dev. + if: github.ref_name != 'main' + uses: CivicDataLab/CivicDataSpace-test/.github/workflows/run-smoke.yml@CI + with: + suite: functional + only_provider: true + secrets: + HOME_URL_DEV: ${{ secrets.HOME_URL_DEV }} + TEST_EMAIL_1: ${{ secrets.TEST_EMAIL_1 }} + TEST_PASSWORD_1: ${{ secrets.TEST_PASSWORD_1 }} + TEST_EMAIL_2: ${{ secrets.TEST_EMAIL_2 }} + TEST_PASSWORD_2: ${{ secrets.TEST_PASSWORD_2 }} + rollback-on-smoke-failure: name: Rollback (smoke tests failed) # `deploy` must be in needs: for needs.deploy.result to resolve here. diff --git a/.github/workflows/pr-gate.yml b/.github/workflows/pr-gate.yml index 0b5b065..dd8e6e9 100644 --- a/.github/workflows/pr-gate.yml +++ b/.github/workflows/pr-gate.yml @@ -18,6 +18,9 @@ jobs: uses: CivicDataLab/CivicDataSpace-test/.github/workflows/run-smoke.yml@CI with: suite: full + # Provider is flaky against dev: it reports in provider-report below, which + # branch protection does not require. + skip_provider: true api_base_url: ${{ vars.DEV_API_BASE_URL }} deployed_sha: ${{ github.event.pull_request.head.sha }} secrets: @@ -27,3 +30,16 @@ jobs: TEST_EMAIL_2: ${{ secrets.TEST_EMAIL_2 }} TEST_PASSWORD_2: ${{ secrets.TEST_PASSWORD_2 }} KEYCLOAK_CLIENT_SECRET: ${{ secrets.KEYCLOAK_CLIENT_SECRET }} + + provider-report: + name: Provider Full Suite (report-only) + uses: CivicDataLab/CivicDataSpace-test/.github/workflows/run-smoke.yml@CI + with: + suite: full + only_provider: true + secrets: + HOME_URL_DEV: ${{ secrets.HOME_URL_DEV }} + TEST_EMAIL_1: ${{ secrets.TEST_EMAIL_1 }} + TEST_PASSWORD_1: ${{ secrets.TEST_PASSWORD_1 }} + TEST_EMAIL_2: ${{ secrets.TEST_EMAIL_2 }} + TEST_PASSWORD_2: ${{ secrets.TEST_PASSWORD_2 }} From d100bcbc1361b707ed921a3f69bf5a09ee6e2496 Mon Sep 17 00:00:00 2001 From: Saqib Date: Tue, 15 Sep 2026 12:25:54 +0530 Subject: [PATCH 4/7] fix: wait for every test before finalizing or rolling back Finalize and rollback did not list the report-only provider job in needs, so a deploy asked for finalize approval while provider tests were still running. Both now wait for it, and condition on the gating jobs' results explicitly instead of failure()/!failure(), which would count the report-only job's result. --- .github/workflows/deploy-backend.yml | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/.github/workflows/deploy-backend.yml b/.github/workflows/deploy-backend.yml index 74a10ae..72006f2 100644 --- a/.github/workflows/deploy-backend.yml +++ b/.github/workflows/deploy-backend.yml @@ -319,11 +319,14 @@ jobs: rollback-on-smoke-failure: name: Rollback (smoke tests failed) # `deploy` must be in needs: for needs.deploy.result to resolve here. - needs: [deploy, smoke-tests, functional-tests] - # failure()/success() builtins rather than needs.smoke-tests.result -- - # both are false on cancellation, which is the behaviour we want; - # if: always() would ignore cancellation entirely. - if: failure() && needs.deploy.result == 'success' + # provider-functional-report is listed so this waits for every test to + # finish, but its result is deliberately not checked (report-only). + needs: [deploy, smoke-tests, functional-tests, provider-functional-report] + # Explicit gating results, not failure(): failure() would also count the + # report-only provider job. !cancelled() keeps cancellation a no-op. + if: >- + ${{ !cancelled() && needs.deploy.result == 'success' && + (needs.smoke-tests.result == 'failure' || needs.functional-tests.result == 'failure') }} runs-on: ubuntu-latest environment: ${{ github.ref_name == 'main' && 'production' || 'development' }} # 50m: must comfortably exceed the Restore previous image step's own @@ -370,10 +373,15 @@ jobs: finalize-deploy: name: Finalize Deploy - needs: [deploy, smoke-tests, functional-tests] - # Not success(): on main functional-tests is skipped by design, and a - # skipped need would skip this job too. Nothing failed + smoke passed. - if: ${{ !failure() && !cancelled() && needs.smoke-tests.result == 'success' }} + # provider-functional-report is listed so this waits for every test to + # finish, but its result is deliberately not checked (report-only). + needs: [deploy, smoke-tests, functional-tests, provider-functional-report] + # Explicit gating results, not success()/!failure(): on main the functional + # jobs are skipped by design, and the provider job must not block. + if: >- + ${{ !cancelled() && needs.deploy.result == 'success' && + needs.smoke-tests.result == 'success' && + (needs.functional-tests.result == 'success' || needs.functional-tests.result == 'skipped') }} runs-on: ubuntu-latest environment: ${{ github.ref_name == 'main' && 'production' || 'development' }} timeout-minutes: 10 From f66f6da677c5433830c5bb6545fae6b22579e103 Mon Sep 17 00:00:00 2001 From: Saqib Date: Tue, 15 Sep 2026 12:28:47 +0530 Subject: [PATCH 5/7] Potential fix for pull request finding 'CodeQL / Workflow does not contain permissions' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .github/workflows/deploy-backend.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/deploy-backend.yml b/.github/workflows/deploy-backend.yml index 72006f2..31c3d61 100644 --- a/.github/workflows/deploy-backend.yml +++ b/.github/workflows/deploy-backend.yml @@ -51,6 +51,9 @@ name: Deploy Backend to EC2 +permissions: + contents: read + on: push: branches: ['dev', 'main'] From 085f4704a8fd0560c6017e16ef53c7999a975b98 Mon Sep 17 00:00:00 2001 From: Saqib Date: Tue, 15 Sep 2026 12:28:57 +0530 Subject: [PATCH 6/7] Potential fix for pull request finding 'CodeQL / Workflow does not contain permissions' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .github/workflows/pr-gate.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/pr-gate.yml b/.github/workflows/pr-gate.yml index dd8e6e9..a9d3048 100644 --- a/.github/workflows/pr-gate.yml +++ b/.github/workflows/pr-gate.yml @@ -8,6 +8,10 @@ on: pull_request: branches: [main] +permissions: + contents: read + pull-requests: read + concurrency: group: pr-gate-${{ github.event.pull_request.number }} cancel-in-progress: true From 1ef2ffdc6943fd0f13b6906ff3c9f9d95040352f Mon Sep 17 00:00:00 2001 From: Saqib Date: Tue, 15 Sep 2026 12:29:05 +0530 Subject: [PATCH 7/7] Potential fix for pull request finding 'CodeQL / Workflow does not contain permissions' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .github/workflows/pr-gate.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/pr-gate.yml b/.github/workflows/pr-gate.yml index a9d3048..778681e 100644 --- a/.github/workflows/pr-gate.yml +++ b/.github/workflows/pr-gate.yml @@ -4,6 +4,8 @@ # makes the suite fail if dev is running anything else. name: PR Gate +permissions: {} + on: pull_request: branches: [main]