name: integration-main # Stage 2: is the merged state healthy. # # The question this stage answers is different from stage 1's. Stage 1 asks whether a diff is safe # and blocks a merge; stage 2 asks whether main is healthy and does not — the merge has already # happened. That difference is the point, and it is what lets a control exist without being an # obstacle: a gate here still fails loudly, it just fails after the thing it is reporting on. # # Two kinds of work live here. # # 1. The lanes that need a machine that is not simultaneously compiling something else — load, # abuse, graceful shutdown, TCP fault injection, resource bounds. They were web-nightly.yml and # httpclient-nightly.yml, two module-shaped files whose only real difference was the cadence they # shared. They now run on every push to main as well as nightly, which is strictly more often # than before. # # 2. Lanes that were registered in Gradle and invoked by nothing. Ten Gradle tasks — six MongoDB # container lanes, app-bootstrap's Testcontainers `integrationTest`, and the three messaging # evidence tasks that `verifyMessagingContracts` reaches — existed, failed closed, and executed # in no workflow. A lane nobody runs is not coverage; it is a file that looks like coverage. They # are here rather than in stage 1 because every one of them either starts containers or re-runs # suites the PR gate already covers, and the pull-request budget is minutes for the whole gate. # # What is deliberately NOT here: the web and WebSocket "Advanced capability" nightly lanes that used # to exist as web-advanced-nightly.yml and websocket-advanced-nightly.yml. Both leaves' build files # say it outright — "They also run inside `test`, deliberately ... excluding them from the PR gate to # make this lane look meaningful would mean the PR gate stopped covering a fifth of the leaf" — so # `webAdvancedTest` and `websocketAdvancedTest` select tagged tests that `::test` already runs, # and `::test` runs inside the root `check` on every pull request and every push to main. The # strict lanes themselves survive in release.yml, where their fail-on-nothing-discovered guard is # worth a job. on: push: branches: ["main"] schedule: # 03:00 UTC. Late enough that the day's merges are in, early enough that a failure is triaged # before the next working day starts. - cron: '0 3 * * *' workflow_dispatch: permissions: contents: read concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: false # The documentation-drift gates that used to run here are gone rather than demoted. # # They were four hand-written parsers: README shell blocks compared against the Gradle task graph, # runbook identifiers compared against every declared Java type, a leaf count written in prose # compared against the registry, and a Markdown table compared against the declared source sets. # Each was a custom parser for a file format nobody controls, and each made a documentation edit a # precondition for a build. A stale sentence is a defect, but it is not one a build can be failed # for, and link-check.yml already answers the one documentation question with a stable machine # answer: does this link resolve. jobs: # Load, abuse and graceful shutdown measure behaviour that degrades gradually rather than breaking # outright — which is exactly the kind of regression a per-PR gate never catches. web-load-abuse-and-shutdown: runs-on: ubuntu-latest timeout-minutes: 60 steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2 - uses: ./.github/actions/setup-gradle-java - name: Run the load, abuse and shutdown lanes on every container working-directory: src run: >- ./gradlew :adapter:inbound:web:test :adapter:inbound:web:webJettyCompatTest :adapter:inbound:web:webFluxContractTest --no-daemon --stacktrace - name: Publish the test reports if: always() uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # actions/upload-artifact@v4.6.2 with: name: web-integration-reports path: src/adapter/inbound/web/build/reports/tests/ if-no-files-found: warn # Needs a container runtime and real time (design §29). Separated from the per-PR gate rather than # made optional inside it: a lane that cannot run here fails, it does not skip. httpclient-fault-injection: runs-on: ubuntu-latest timeout-minutes: 45 steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2 - uses: ./.github/actions/setup-gradle-java - name: Inject TCP faults against a real upstream working-directory: src run: >- ./gradlew :adapter:outbound:httpclient:httpClientFailureInjectionTest --no-daemon --stacktrace httpclient-performance: runs-on: ubuntu-latest timeout-minutes: 45 env: # A project property rather than a command-line flag, so the run command stays a plain, # verifiable task invocation while the machine-dependent bounds are still asserted. GRADLE_OPTS: -Dorg.gradle.project.performance.assertions.enabled=true steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2 - uses: ./.github/actions/setup-gradle-java - name: Certify pool, streaming, retry, and rotation bounds working-directory: src run: >- ./gradlew :adapter:outbound:httpclient:httpClientPerformanceTest --no-daemon --stacktrace httpclient-http3-experimental: runs-on: ubuntu-latest timeout-minutes: 30 # Experimental by design (D-08): the result is reported, never used to block a merge. Registered # advisory so that "this job cannot fail the # build" is written down rather than inferred from a field two hundred lines into a workflow. continue-on-error: true steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2 - uses: ./.github/actions/setup-gradle-java - name: Exercise the experimental HTTP/3 opt-in working-directory: src run: >- ./gradlew :adapter:outbound:httpclient:test -Phttp3.tests.enabled=true --no-daemon --stacktrace # The six Docker-backed MongoDB lanes. Until now they ran in no workflow at all: the leaf excludes # every one of their tags from `test` (build.gradle "Docker-backed lanes are excluded from the # default unit run"), `check` gains only the hermetic `mongoStableContractTest`, and the only thing # that named them was scripts/verify-mongodb-platform.sh, which nothing in .github invokes. Six # lanes that fail closed without Docker, and no machine with Docker was ever asked to run them. # # Stage 2 rather than stage 1 because each lane starts real MongoDB containers — mongo:8.0.16, # mongo:7.0.28 and a Toxiproxy in front of a three-node replica set. That is minutes per lane, and # the pull-request budget is minutes for the whole gate. # # One single-line `./gradlew ` step per lane, not one folded command running six, because # These command lines name each lane explicitly so that a lane which stops being invoked is # actually executed — a folded command would leave six matrix rows unverifiable. It also means a # red replica-set lane does not hide the compatibility lane behind it. mongo-container-lanes: runs-on: ubuntu-latest timeout-minutes: 90 env: # Reuse would hand the failover lane a replica set another lane had already faulted. TESTCONTAINERS_REUSE_ENABLE: "false" steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2 - uses: ./.github/actions/setup-gradle-java - name: Single-node replica set contract lane working-directory: src run: ./gradlew :adapter:outbound:persistence-mongo:mongoReplicaSetTest --no-daemon --stacktrace - name: Three-node failover lane working-directory: src run: ./gradlew :adapter:outbound:persistence-mongo:mongoFailoverTest --no-daemon --stacktrace - name: Migration and backfill restart lane working-directory: src run: ./gradlew :adapter:outbound:persistence-mongo:mongoMigrationTest --no-daemon --stacktrace - name: MongoDB 7.0 compatibility lane working-directory: src run: ./gradlew :adapter:outbound:persistence-mongo:mongoCompatibilityTest --no-daemon --stacktrace - name: Credential, TLS and redaction lane working-directory: src run: ./gradlew :adapter:outbound:persistence-mongo:mongoSecurityIntegrationTest --no-daemon --stacktrace - name: Resource-bound certification lane working-directory: src run: ./gradlew :adapter:outbound:persistence-mongo:mongoPerformanceTest --no-daemon --stacktrace - name: Publish the MongoDB lane reports if: always() uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # actions/upload-artifact@v4.6.2 with: name: mongo-lane-reports path: src/adapter/outbound/persistence-mongo/build/reports/tests/ if-no-files-found: warn # The messaging contract evidence DAG. `verifyMessagingContracts` is the root of a chain that ran # nowhere: it depends on five qualification tasks (application-core, shared-contract, # sample-portfolio and two in adapter:outbound:messaging), each of which depends on # `prepareMessagingContractEvidence`; it is finalizedBy # `validateMessagingContractsEvidenceManifestSchema`; and it depends on # `validateMessagingJsonSchemaV1EvidenceManifestSchema`, which depends on # `verifyMessagingJsonSchemaV1`. Strict qualification tasks are registered outside `check` by # design (ca.strict-qualification.gradle), so none of the seven was reachable from any workflow. # # The schema validators are the part that matters. They re-read the manifest bytes the run just # wrote and validate them against config/messaging/evidence/build-evidence-manifest-v1.schema.json # — a manifest that claims a qualification nobody executed is exactly the failure they exist to # catch, and until now nothing executed them either. # # Stage 2 rather than stage 1: no containers, but it runs five qualification suites across four # leaves plus two JavaExec validators, and the tests it re-runs are already inside the PR gate's # `check`. What this job adds is the evidence manifest, which is a main-branch artifact. messaging-contract-evidence: runs-on: ubuntu-latest timeout-minutes: 30 steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2 - uses: ./.github/actions/setup-gradle-java - name: Qualify the messaging contract, catalog, binding and schema evidence working-directory: src run: ./gradlew verifyMessagingContracts --no-daemon --stacktrace - name: Publish the messaging evidence manifest if: always() uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # actions/upload-artifact@v4.6.2 with: name: messaging-contract-evidence path: src/build/messaging-evidence/ if-no-files-found: warn # app-bootstrap's Testcontainers lane. The leaf gave it a source set of its own precisely so that # `./gradlew :app-bootstrap:test` would not require a Docker daemon — and the consequence nobody # closed is that a source set outside `test` is also outside `check`, so the real-PostgreSQL # outbox and idempotency contracts compiled on every build and executed on none. bootstrap-integration: runs-on: ubuntu-latest timeout-minutes: 45 env: TESTCONTAINERS_REUSE_ENABLE: "false" steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2 - uses: ./.github/actions/setup-gradle-java - name: Run the real-PostgreSQL integration contracts working-directory: src run: ./gradlew :app-bootstrap:integrationTest --no-daemon --stacktrace