name: Build and Test Workflow # Top-level PR / merge-queue gate. Detects which paths changed and dispatches # to the dedicated reusable workflows under .github/workflows/. Each child # workflow keeps its own setup/teardown so this file stays a routing layer. # # The final `all-checks-passed` job is the single status check that branch # protection should require — it succeeds only if every required upstream # job either succeeded or was legitimately skipped by its path filter. on: pull_request: branches: ["main"] merge_group: branches: ["main"] workflow_dispatch: # cancel in-progress jobs if a new job is triggered # This is useful to avoid running multiple builds for the same branch if a new commit is pushed # or a pull request is updated. # It helps to save resources and time by ensuring that only the latest commit is built and tested # This is particularly useful for long-running jobs that may take a while to complete. # The `group` is set to a combination of the workflow name, event name, and branch name. # This ensures that jobs are grouped by the workflow and branch, allowing for cancellation of # in-progress jobs when a new commit is pushed to the same branch or a new pull request is opened. concurrency: group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref_name || github.ref }} cancel-in-progress: true permissions: contents: read jobs: files-changed: name: detect what files changed runs-on: ubuntu-latest timeout-minutes: 3 outputs: build: ${{ steps.changes.outputs.build }} backend: ${{ steps.changes.outputs.backend }} project: ${{ steps.changes.outputs.project }} openapi: ${{ steps.changes.outputs.openapi }} frontend: ${{ steps.changes.outputs.frontend }} docker-base: ${{ steps.changes.outputs.docker-base }} dockerfiles: ${{ steps.changes.outputs.dockerfiles }} tauri: ${{ steps.changes.outputs.tauri }} engine: ${{ steps.changes.outputs.engine }} generated-models: ${{ steps.changes.outputs.generated-models }} proprietary: ${{ steps.changes.outputs.proprietary }} steps: - name: Harden the runner (Audit all outbound calls) uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0 with: egress-policy: audit - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Check for file changes uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3 id: changes with: filters: .github/config/.files.yaml gradle-cache-prime: needs: [files-changed] uses: ./.github/workflows/gradle-cache-prime.yml secrets: inherit build: if: needs.files-changed.outputs.backend == 'true' needs: [files-changed, gradle-cache-prime] permissions: actions: read contents: read security-events: write pull-requests: write uses: ./.github/workflows/backend-build.yml secrets: inherit db-migration-test: # Boots the current bootJar against H2 fixtures captured from past # releases (v2.0.0 / v2.5.0 / v2.10.0) and verifies admin login still # works after Hibernate's ddl-auto=update migrates the schema. Gated on # the `project` filter so doc-only PRs skip this ~5-minute job. if: needs.files-changed.outputs.project == 'true' needs: [files-changed, gradle-cache-prime] permissions: contents: read uses: ./.github/workflows/db-migration-test.yml secrets: inherit check-generateOpenApiDocs: if: needs.files-changed.outputs.openapi == 'true' needs: [files-changed, gradle-cache-prime] permissions: contents: read uses: ./.github/workflows/check-openapi.yml secrets: inherit frontend-validation: if: needs.files-changed.outputs.frontend == 'true' needs: [files-changed] permissions: contents: read pull-requests: write uses: ./.github/workflows/frontend-validation.yml secrets: inherit # Required (in all-checks-passed). Scans the stories a branch touches in both # light and dark; an axe violation in either theme blocks the merge. The # whole-suite sweep (nightly.yml) still covers stories a change affects without # touching them directly. frontend-a11y: if: needs.files-changed.outputs.frontend == 'true' needs: [files-changed] permissions: contents: read uses: ./.github/workflows/frontend-a11y.yml secrets: inherit playwright-e2e: if: needs.files-changed.outputs.frontend == 'true' needs: [files-changed] permissions: contents: read uses: ./.github/workflows/e2e-stubbed.yml secrets: inherit playwright-e2e-live: if: needs.files-changed.outputs.frontend == 'true' needs: [files-changed, gradle-cache-prime] permissions: contents: read uses: ./.github/workflows/e2e-live.yml secrets: inherit playwright-e2e-enterprise: if: needs.files-changed.outputs.proprietary == 'true' needs: [files-changed, gradle-cache-prime] permissions: contents: read uses: ./.github/workflows/build-enterprise.yml secrets: inherit with: use_shared_cache: true check-licence: if: needs.files-changed.outputs.build == 'true' needs: [files-changed, gradle-cache-prime] permissions: contents: read uses: ./.github/workflows/check-licence.yml secrets: inherit docker-compose-tests: if: needs.files-changed.outputs.project == 'true' needs: [files-changed, gradle-cache-prime] permissions: actions: write contents: read checks: write uses: ./.github/workflows/docker-compose-tests.yml secrets: inherit with: docker-base-changed: ${{ needs.files-changed.outputs.docker-base }} test-build-docker-images: if: | always() && github.event_name == 'pull_request' && needs.files-changed.outputs.project == 'true' && contains(fromJSON('["success", "skipped"]'), needs.gradle-cache-prime.result) && contains(fromJSON('["success", "skipped"]'), needs.build.result) && contains(fromJSON('["success", "skipped"]'), needs.check-generateOpenApiDocs.result) && contains(fromJSON('["success", "skipped"]'), needs.check-licence.result) needs: [ files-changed, build, check-generateOpenApiDocs, check-licence, gradle-cache-prime, ] permissions: contents: read packages: read uses: ./.github/workflows/test-build-docker.yml secrets: inherit with: docker-base-changed: ${{ needs.files-changed.outputs.docker-base }} dockerfiles-changed: ${{ needs.files-changed.outputs.dockerfiles }} tauri-build: if: needs.files-changed.outputs.tauri == 'true' needs: [files-changed, gradle-cache-prime] permissions: contents: read pull-requests: write uses: ./.github/workflows/tauri-build.yml secrets: inherit # PR smoke build: macOS + Windows (the platforms our developers use). # sign: true only reaches macOS - tauri-build's per-platform gate keeps # Windows/Linux signing on main, and an unsigned .dmg cannot be opened. # The full signed multi-OS matrix runs on release; # nightly still warms the Rust cache with all-OS defaults. with: platform: windows-macos sign: true use_shared_cache: true ai-engine: if: needs.files-changed.outputs.engine == 'true' needs: [files-changed] permissions: contents: read pull-requests: write uses: ./.github/workflows/ai-engine.yml secrets: inherit # The generated frontend types and engine tool models are both derived from # the Java OpenAPI spec. This job regenerates and diffs them; it boots the # backend, so it is gated on the narrow generated-models filter (spec source, # generators, generated files, generation tasks) rather than the broad # frontend filter, so a CSS-only PR does not pay for a backend build. generated-models: if: needs.files-changed.outputs.generated-models == 'true' needs: [files-changed, gradle-cache-prime] permissions: contents: read pull-requests: write uses: ./.github/workflows/check-generated-models.yml secrets: inherit with: use_shared_cache: true pre-commit: needs: [files-changed] permissions: contents: read uses: ./.github/workflows/pre_commit.yml secrets: inherit dependency-review: needs: [files-changed] permissions: contents: read uses: ./.github/workflows/dependency-review.yml secrets: inherit # Coverage aggregate: merges the JUnit + e2e:live + cucumber .exec # artifacts produced by the jobs above into one report, plus pulls # in vitest + Playwright frontend coverage for the per-area matrix. # `if: always()` so a producer failing partway still gets credit # for whatever did record. Advisory only - intentionally NOT in # all-checks-passed, so a flaky aggregate run never blocks merging. coverage-aggregate: if: always() needs: - build - playwright-e2e-live - docker-compose-tests - frontend-validation permissions: contents: read uses: ./.github/workflows/coverage-aggregate.yml secrets: inherit with: frontend-validation-result: ${{ needs.frontend-validation.result }} playwright-e2e-live-result: ${{ needs.playwright-e2e-live.result }} # Single status check that branch protection should mark as required. # Succeeds when every upstream job is either `success` or `skipped` (path- # gated jobs that didn't apply this run). Any `failure` or `cancelled` # result fails the gate. `if: always()` ensures the gate evaluates even # when an upstream job fails. all-checks-passed: name: All checks passed if: always() needs: - files-changed - gradle-cache-prime - build - db-migration-test - check-generateOpenApiDocs - frontend-validation - frontend-a11y - playwright-e2e - playwright-e2e-live - playwright-e2e-enterprise - check-licence - docker-compose-tests - test-build-docker-images - tauri-build - ai-engine - generated-models - pre-commit - dependency-review runs-on: ubuntu-latest steps: - name: Harden the runner (Audit all outbound calls) uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0 with: egress-policy: audit - name: Verify every required job passed (or was legitimately skipped) env: RESULTS: | files-changed=${{ needs.files-changed.result }} gradle-cache-prime=${{ needs.gradle-cache-prime.result }} build=${{ needs.build.result }} db-migration-test=${{ needs.db-migration-test.result }} check-generateOpenApiDocs=${{ needs.check-generateOpenApiDocs.result }} frontend-validation=${{ needs.frontend-validation.result }} frontend-a11y=${{ needs.frontend-a11y.result }} playwright-e2e=${{ needs.playwright-e2e.result }} playwright-e2e-live=${{ needs.playwright-e2e-live.result }} playwright-e2e-enterprise=${{ needs.playwright-e2e-enterprise.result }} check-licence=${{ needs.check-licence.result }} docker-compose-tests=${{ needs.docker-compose-tests.result }} test-build-docker-images=${{ needs.test-build-docker-images.result }} tauri-build=${{ needs.tauri-build.result }} ai-engine=${{ needs.ai-engine.result }} generated-models=${{ needs.generated-models.result }} pre-commit=${{ needs.pre-commit.result }} dependency-review=${{ needs.dependency-review.result }} run: | ok=true while IFS='=' read -r name result; do [ -z "$name" ] && continue case "$result" in success|skipped) printf ' %-30s %s\n' "$name" "$result" ;; *) printf '✗ %-30s %s\n' "$name" "$result"; ok=false ;; esac done <<< "$RESULTS" if [ "$ok" != "true" ]; then echo "" echo "One or more required checks failed or were cancelled." exit 1 fi echo "" echo "All required checks passed."