외부 리뷰("현재 상태를 유지하기 위한 검증이 너무 많고, 그 검증 자체를
다시 검증하는 구조까지 생겼다")를 설계 문서로 정리하고 코드로 반영한다.
설계·판단 근거는 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>
194 lines
9.1 KiB
YAML
194 lines
9.1 KiB
YAML
# Redis SDK topology evidence.
|
|
#
|
|
# The lanes in infra/redis-sdk answer what the deterministic in-memory gateway cannot — Sentinel
|
|
# promotion behaviour, Cluster redirects, ACL coverage. docs/redis/support-matrix.md records which
|
|
# lane produced which evidence, and RedisSupportMatrixTest refuses an evidence claim that does not
|
|
# name the test class behind it.
|
|
#
|
|
# Three cadences, because the cost and the question differ:
|
|
#
|
|
# pull_request standalone only, current supported version. The cheapest lane that can still
|
|
# catch "this change cannot talk to a real Redis at all". A PR gate that starts
|
|
# three topologies is a PR gate people learn to ignore.
|
|
# schedule the full supported-version x topology matrix, nightly. This is where Sentinel
|
|
# promotion and Cluster redirect evidence comes from.
|
|
# workflow_dispatch one lane on demand, for reproducing a specific failure.
|
|
#
|
|
# A release candidate uses the nightly matrix run for its tag: `release-candidate` selects the full
|
|
# matrix on demand so an RC does not have to wait for the next scheduled run.
|
|
#
|
|
# Each lane has its own endpoint. A sentinel is not a data node and a cluster node is not the whole
|
|
# cluster, so the address, port, and (for Sentinel) the monitored primary's name are per-lane rather
|
|
# than one hardcoded 6379 that happens to be right for standalone only.
|
|
#
|
|
# The Gradle task is fail-closed on its own account: an unknown mode, a missing endpoint, a lane
|
|
# with no tagged test class, and a run that executed zero tests are all errors. This workflow does
|
|
# not need to re-check those, but it does have to keep the evidence, which is why every run uploads
|
|
# the JUnit XML together with the commit SHA, the server version and the resolved image digest. An
|
|
# evidence artifact that cannot say which image produced it is not evidence.
|
|
name: redis-sdk-topology
|
|
|
|
on:
|
|
pull_request:
|
|
paths:
|
|
- "src/adapter/outbound/cache-redis/**"
|
|
- "infra/redis-sdk/**"
|
|
- ".github/workflows/redis-sdk-topology.yml"
|
|
# Every Gradle job here installs its toolchain through this composite action, so a change to
|
|
# it changes what this gate runs.
|
|
- ".github/actions/setup-gradle-java/action.yml"
|
|
schedule:
|
|
# 02:30 UTC daily. Nightly, not hourly: the matrix starts real servers.
|
|
- cron: "30 2 * * *"
|
|
workflow_dispatch:
|
|
inputs:
|
|
topology:
|
|
description: standalone, sentinel, cluster, tls, or release-candidate for the full matrix
|
|
required: true
|
|
default: standalone
|
|
type: choice
|
|
options: [standalone, sentinel, cluster, tls, release-candidate]
|
|
redis_version:
|
|
description: server version tag
|
|
required: true
|
|
default: "7.4"
|
|
type: string
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
jobs:
|
|
# The matrix is computed rather than duplicated per trigger, so adding a supported version is one
|
|
# edit and no trigger can silently keep testing an old set.
|
|
lanes:
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
matrix: ${{ steps.select.outputs.matrix }}
|
|
steps:
|
|
- id: select
|
|
run: |
|
|
set -euo pipefail
|
|
case "${{ github.event_name }}" in
|
|
pull_request)
|
|
matrix='{"include":[{"topology":"standalone","redis_version":"7.4"}]}'
|
|
;;
|
|
schedule)
|
|
matrix='{"include":[
|
|
{"topology":"standalone","redis_version":"7.2"},
|
|
{"topology":"standalone","redis_version":"7.4"},
|
|
{"topology":"standalone","redis_version":"8.2"},
|
|
{"topology":"sentinel","redis_version":"7.2"},
|
|
{"topology":"sentinel","redis_version":"7.4"},
|
|
{"topology":"sentinel","redis_version":"8.2"},
|
|
{"topology":"cluster","redis_version":"7.2"},
|
|
{"topology":"cluster","redis_version":"7.4"},
|
|
{"topology":"cluster","redis_version":"8.2"},
|
|
{"topology":"tls","redis_version":"7.4"},
|
|
{"topology":"tls","redis_version":"8.2"}]}'
|
|
;;
|
|
*)
|
|
if [ "${{ inputs.topology }}" = "release-candidate" ]; then
|
|
matrix='{"include":[
|
|
{"topology":"standalone","redis_version":"7.2"},
|
|
{"topology":"standalone","redis_version":"7.4"},
|
|
{"topology":"standalone","redis_version":"8.2"},
|
|
{"topology":"sentinel","redis_version":"7.2"},
|
|
{"topology":"sentinel","redis_version":"7.4"},
|
|
{"topology":"sentinel","redis_version":"8.2"},
|
|
{"topology":"cluster","redis_version":"7.2"},
|
|
{"topology":"cluster","redis_version":"7.4"},
|
|
{"topology":"cluster","redis_version":"8.2"},
|
|
{"topology":"tls","redis_version":"7.4"},
|
|
{"topology":"tls","redis_version":"8.2"}]}'
|
|
else
|
|
matrix='{"include":[{"topology":"${{ inputs.topology }}","redis_version":"${{ inputs.redis_version }}"}]}'
|
|
fi
|
|
;;
|
|
esac
|
|
printf 'matrix=%s\n' "$(printf '%s' "$matrix" | tr -d '\n ')" >> "$GITHUB_OUTPUT"
|
|
|
|
topology-evidence:
|
|
needs: lanes
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 30
|
|
strategy:
|
|
fail-fast: false
|
|
matrix: ${{ fromJson(needs.lanes.outputs.matrix) }}
|
|
steps:
|
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
|
- uses: ./.github/actions/setup-gradle-java
|
|
- name: Start the topology
|
|
env:
|
|
REDIS_VERSION: ${{ matrix.redis_version }}
|
|
run: docker compose -f "infra/redis-sdk/${{ matrix.topology }}/compose.yml" up -d --wait
|
|
- name: Record the image digest
|
|
id: image
|
|
run: |
|
|
set -euo pipefail
|
|
# The tag says 7.4; the digest says which 7.4. Evidence that names only the tag cannot be
|
|
# reproduced once the tag moves.
|
|
#
|
|
# This used to end in `|| echo 'unresolved'`, which absorbed the failure that `set -e` was
|
|
# there to catch: the manifest below recorded `image_digest=unresolved`, the upload
|
|
# satisfied `if-no-files-found: error`, and the lane went green holding exactly the
|
|
# artifact this workflow's header calls "not evidence". Compose pulls the image in the
|
|
# step before this one, so RepoDigests is populated; if it is not, the run has nothing to
|
|
# certify and says so.
|
|
digest="$(docker image inspect --format '{{index .RepoDigests 0}}' \
|
|
"redis:${{ matrix.redis_version }}")"
|
|
if [[ -z "$digest" ]]; then
|
|
echo "::error::no repository digest for redis:${{ matrix.redis_version }}; this run cannot say which image produced its evidence"
|
|
exit 1
|
|
fi
|
|
printf 'digest=%s\n' "$digest" >> "$GITHUB_OUTPUT"
|
|
- name: Run the topology contracts
|
|
working-directory: src
|
|
run: |
|
|
set -euo pipefail
|
|
case '${{ matrix.topology }}' in
|
|
standalone) port=6379; extra='' ;;
|
|
sentinel) port=27010; extra='-Predis.topology.master=skeleton' ;;
|
|
cluster) port=7100; extra='' ;;
|
|
# The TLS lane's CA is generated at start-up, so the trust material is extracted from
|
|
# the lane rather than checked in. A checked-in key is a secret in the repository
|
|
# however loudly the file is named "test".
|
|
tls)
|
|
port=6390
|
|
docker compose -f ../infra/redis-sdk/tls/compose.yml cp redis:/tls/ca.crt "$RUNNER_TEMP/redis-lane-ca.pem"
|
|
extra="-Predis.topology.trust-material=$RUNNER_TEMP/redis-lane-ca.pem"
|
|
;;
|
|
*) echo "unknown topology"; exit 1 ;;
|
|
esac
|
|
./gradlew :adapter:outbound:cache-redis:redisTopologyTest --console=plain \
|
|
-Predis.topology.host=localhost \
|
|
-Predis.topology.port="$port" \
|
|
-Predis.topology.mode='${{ matrix.topology }}' \
|
|
$extra
|
|
- name: Write the evidence manifest
|
|
if: always()
|
|
run: |
|
|
set -euo pipefail
|
|
out=src/adapter/outbound/cache-redis/build/test-results/redisTopologyTest
|
|
mkdir -p "$out"
|
|
cat > "$out/evidence-manifest.txt" <<MANIFEST
|
|
commit=${{ github.sha }}
|
|
workflow_run=${{ github.run_id }}
|
|
trigger=${{ github.event_name }}
|
|
topology=${{ matrix.topology }}
|
|
redis_version=${{ matrix.redis_version }}
|
|
image_digest=${{ steps.image.outputs.digest }}
|
|
MANIFEST
|
|
- name: Preserve the evidence
|
|
if: always()
|
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # actions/upload-artifact@v4.6.2
|
|
with:
|
|
name: redis-topology-${{ matrix.topology }}-${{ matrix.redis_version }}
|
|
path: |
|
|
src/adapter/outbound/cache-redis/build/test-results/redisTopologyTest/**
|
|
src/adapter/outbound/cache-redis/build/reports/tests/redisTopologyTest/**
|
|
if-no-files-found: error
|
|
retention-days: 90
|
|
- name: Stop the topology
|
|
if: always()
|
|
run: docker compose -f "infra/redis-sdk/${{ matrix.topology }}/compose.yml" down -v
|