외부 리뷰("현재 상태를 유지하기 위한 검증이 너무 많고, 그 검증 자체를
다시 검증하는 구조까지 생겼다")를 설계 문서로 정리하고 코드로 반영한다.
설계·판단 근거는 docs/superpowers/specs/2026-09-16-verification-surface-reduction-design.md.
삭제
- .github/ci-gate-matrix.yml(1,025줄) + verify-gate-matrix.sh(568줄):
Gradle task graph와 workflow graph에 이미 있는 정보의 3중 복제
- verify-gradle-wrapper.sh(799줄): workflow 바이트 해시 잠금.
wrapper 검증은 gradle/actions/wrapper-validation(full SHA 핀)에 위임
- DeveloperExperienceContractTest 등의 CI YAML mutation 테스트:
애플리케이션 test suite가 GitHub Actions YAML 파서를 검증하던 계층 역전
- 문서 drift 파서: verifyReadmeCommands, verifyRunbookReferences,
verifyDocumentedLeafCount, verifyTestSourceSetRegistry
- 빈 레지스트리를 지키던 커스텀 YAML 파서: verifyTrivyignore,
verifyQuarantineSunset, flaky-quarantine.yaml
- verifyConfigurationPropertiesProcessor, verifyOneTypePerFile:
각각 ca.spring-config convention과 Checkstyle OneTopLevelClass가 대체
- 정상 입력으로도 성공할 수 없던 messaging always-fail task
- ModuleRegistry의 JSON 필드 집합 정확 일치, sample-portfolio negative guard
이동
- java/quality/spring 공통 설정을 configure(subprojects) 블록에서
ca.java-conventions / ca.quality-conventions / ca.java-library /
ca.spring-library convention plugin으로
- 아키텍처 검증을 ca.architecture로, JPA·messaging qualification을
gradle/qualification/ 아래로, verifyEnvKeys를 :app-bootstrap 소유로
완화
- Git revision은 releaseCheck·아카이브 생성에서만 요구. 일반 빌드는 SNAPSHOT
- SpotBugs/FindSecBugs는 로컬 check에서 빼고 qualityCheck 레인으로
task 계층
- leaf check는 그 leaf만. architectureCheck / qualityCheck /
configContractCheck / integrationCheck / ci / releaseCheck로 이름 분리
CI
- _reusable-gradle.yml 신규. checkout + wrapper validation + JDK/캐시 공통화
- fileserver-release.yml -> fileserver-certification.yml (CD가 아니라 certification)
- GitHub Actions = CI + artifact, Argo CD = CD 경계를 docs/ci-cd/boundary.md로 고정
순증감 +3,274 / -7,483.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
237 lines
12 KiB
YAML
237 lines
12 KiB
YAML
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 `:<leaf>:test` already runs,
|
|
# and `:<leaf>: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 <task>` 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
|