refactor(build,ci): 현재 상태 검증을 걷어내고 불변조건만 남기는 검증 표면 축소
외부 리뷰("현재 상태를 유지하기 위한 검증이 너무 많고, 그 검증 자체를
다시 검증하는 구조까지 생겼다")를 설계 문서로 정리하고 코드로 반영한다.
설계·판단 근거는 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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
d00c76241c
commit
ef947e5bb0
@@ -1,27 +1,30 @@
|
||||
name: Set up Java and the Gradle cache
|
||||
name: Set up Java and Gradle
|
||||
description: >-
|
||||
Installs the repository's pinned Temurin JDK and restores the Gradle cache keyed on this
|
||||
repository's build files. Every Gradle job used to carry this block verbatim, so the JDK patch
|
||||
level and the cache key lived in fifty-nine places and could drift in any one of them.
|
||||
Installs the repository's pinned Temurin JDK, then configures Gradle through the official
|
||||
setup-gradle action — which validates every checked-in wrapper jar and manages the Gradle cache.
|
||||
Every Gradle job used to carry the JDK block verbatim, so the JDK patch level lived in fifty-nine
|
||||
places; every job also carried a separate three-line wrapper-validation step, so the pinned action
|
||||
SHA lived in forty.
|
||||
|
||||
# Deliberately NOT in this action: `actions/checkout` and the Gradle wrapper validation step.
|
||||
# Wrapper validation is INSIDE this action now.
|
||||
#
|
||||
# Neither can move here, and the reasons are different:
|
||||
# It could not be before, and the reason was not a GitHub limitation: .github/scripts/
|
||||
# verify-gradle-wrapper.sh read every workflow job and required it to contain, literally and in this
|
||||
# order, an `actions/checkout@` step, the exact three-field pinned wrapper-validation step, and then
|
||||
# the Gradle invocation. That literalness was the whole guard — "this job validated the wrapper" had
|
||||
# to be answerable from the workflow file alone — and it is what made the step uninlineable.
|
||||
#
|
||||
# * checkout — a `./.github/actions/...` reference is resolved from the checked-out working
|
||||
# copy, so the action file does not exist until checkout has already run. A composite action
|
||||
# cannot contain the step that makes itself readable.
|
||||
# * wrapper validation — .github/scripts/verify-gradle-wrapper.sh reads each workflow job and
|
||||
# requires it to contain, literally and in this order, an `actions/checkout@` step, the exact
|
||||
# three-field pinned wrapper-validation step, and then the Gradle invocation. That literalness
|
||||
# is the guard: it is what makes "this job validated the wrapper before running it" checkable
|
||||
# from the workflow file alone. Hiding the step behind an action would also break the guarded
|
||||
# `if: ${{ always() && steps.gradle-wrapper-validation.outcome == 'success' }}` form the same
|
||||
# script enforces, because a composite action's step ids are not visible to its caller — the
|
||||
# condition would silently evaluate to false and skip the step it was protecting.
|
||||
# That script is gone (it also byte-hashed all twelve workflow files, so a comment change needed a
|
||||
# hash update, while an attacker with write access would simply have updated both). The guarantee it
|
||||
# was protecting is now the official action's own: `gradle/actions/setup-gradle` validates all
|
||||
# wrapper jars by default (`validate-wrappers`, default true), and the action is pinned to a full
|
||||
# commit SHA here — which GitHub's own hardening guide calls the only immutable action reference.
|
||||
#
|
||||
# So a Gradle job is four lines of preamble (checkout, the three-line validation step) plus one
|
||||
# line for this action, instead of thirteen.
|
||||
# `actions/checkout` still cannot move here: a `./.github/actions/...` reference is resolved from the
|
||||
# checked-out working copy, so this file does not exist until checkout has already run. A composite
|
||||
# action cannot contain the step that makes itself readable.
|
||||
#
|
||||
# So a Gradle job is two lines — checkout, then this action.
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
@@ -30,8 +33,9 @@ runs:
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "21.0.11+10"
|
||||
cache: gradle
|
||||
cache-dependency-path: |
|
||||
src/**/*.gradle
|
||||
src/**/gradle-wrapper.properties
|
||||
src/**/gradle.lockfile
|
||||
# Gradle's own caching, not setup-java's `cache: gradle`. The two cache the same directory with
|
||||
# different keys, and running both is how a job restores one cache and saves the other.
|
||||
- uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
with:
|
||||
build-scan-publish: false
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@
|
||||
This policy is enforced by
|
||||
[`dependency-vulnerability.yml`](workflows/dependency-vulnerability.yml),
|
||||
[`dependency-review-config.yml`](dependency-review-config.yml),
|
||||
[`../.trivyignore.yaml`](../.trivyignore.yaml), `verifyTrivyignore`, CODEOWNERS, and
|
||||
[`../.trivyignore.yaml`](../.trivyignore.yaml), CODEOWNERS, and
|
||||
[`../renovate.json`](../renovate.json).
|
||||
|
||||
## Execution and platform boundary
|
||||
@@ -73,7 +73,7 @@ dependencies; stale mirrors can delay detection.
|
||||
|
||||
The only suppression source is repository-root `.trivyignore.yaml`. Every Trivy scan passes it
|
||||
explicitly with `--ignorefile .trivyignore.yaml`. Each future entry must contain an identifier, a
|
||||
non-empty rationale, and a future expiry no more than 90 days away. `verifyTrivyignore` validates
|
||||
non-empty rationale, and a future expiry no more than 90 days away. A CODEOWNERS reviewer validates
|
||||
the shape and expiry; CODEOWNERS plus branch protection controls who may approve the change.
|
||||
Neither control substitutes for the other.
|
||||
|
||||
|
||||
@@ -1,568 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
|
||||
if (( $# > 1 )); then
|
||||
printf '::error::gate-matrix-lint: expected zero arguments or one repository root\n' >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if (( $# == 1 )); then
|
||||
if [[ ! -d "$1" ]]; then
|
||||
printf '::error::gate-matrix-lint: repository root is not a directory: %s\n' "$1" >&2
|
||||
exit 2
|
||||
fi
|
||||
REPO_ROOT="$(cd -- "$1" && pwd -P)"
|
||||
else
|
||||
REPO_ROOT="$(git -C "${SCRIPT_DIR}" rev-parse --show-toplevel)"
|
||||
EXPECTED_SCRIPT_DIR="$(cd -- "${REPO_ROOT}/.github/scripts" && pwd -P)"
|
||||
if [[ "${SCRIPT_DIR}" != "${EXPECTED_SCRIPT_DIR}" ]]; then
|
||||
printf '::error::gate-matrix-lint: script location must be repository .github/scripts directory\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
readonly REPO_ROOT
|
||||
readonly MATRIX="${REPO_ROOT}/.github/ci-gate-matrix.yml"
|
||||
|
||||
# There is deliberately no expected gate count here. A hand-edited integer made the matrix
|
||||
# un-editable: no control could be registered without editing the guard whose purpose was to stop
|
||||
# the matrix changing, and the guard caught nothing a per-row rule does not already catch — a row
|
||||
# whose task, workflow or job does not exist fails below regardless of how many rows there are.
|
||||
# What replaces it is the per-row invariant set: required fields, valid enums, a workflow and job
|
||||
# that exist, a registered and actually-executed mechanism, unique ids, and the release-blocking
|
||||
# rule below. Those hold at any count.
|
||||
#
|
||||
# The one property the count did carry is kept explicitly: a matrix with no gates at all is drift,
|
||||
# not a clean run.
|
||||
|
||||
# The release gate every pull request and push to main passes through. Named rather than inferred:
|
||||
# `release_blocking: true` is checked against what this job waits on, so the field means something a
|
||||
# machine can verify instead of being an enum nobody reads.
|
||||
readonly RELEASE_GATE_WORKFLOW='ci-quality-gates.yml'
|
||||
readonly RELEASE_GATE_JOB='release-gate'
|
||||
|
||||
if [[ ! -f "${MATRIX}" ]]; then
|
||||
printf '::error::gate-matrix-lint: missing %s\n' "${MATRIX}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
records="$(
|
||||
awk '
|
||||
function flush() {
|
||||
if (id != "") {
|
||||
printf "%s\t%s\t%s\t%s\t%s\t%s\t%s\n", id, blocking, mechanism, ref, workflow, job, execution
|
||||
}
|
||||
}
|
||||
/^[[:space:]]*-[[:space:]]+id:[[:space:]]*/ {
|
||||
flush()
|
||||
id=$0
|
||||
sub(/^[[:space:]]*-[[:space:]]+id:[[:space:]]*/, "", id)
|
||||
blocking=mechanism=ref=workflow=job=execution=""
|
||||
next
|
||||
}
|
||||
/^[[:space:]]+release_blocking:[[:space:]]*/ {
|
||||
blocking=$0
|
||||
sub(/^[[:space:]]+release_blocking:[[:space:]]*/, "", blocking)
|
||||
next
|
||||
}
|
||||
/^[[:space:]]+mechanism:[[:space:]]*/ {
|
||||
mechanism=$0
|
||||
sub(/^[[:space:]]+mechanism:[[:space:]]*/, "", mechanism)
|
||||
next
|
||||
}
|
||||
/^[[:space:]]+ref:[[:space:]]*/ {
|
||||
ref=$0
|
||||
sub(/^[[:space:]]+ref:[[:space:]]*/, "", ref)
|
||||
next
|
||||
}
|
||||
/^[[:space:]]+workflow:[[:space:]]*/ {
|
||||
workflow=$0
|
||||
sub(/^[[:space:]]+workflow:[[:space:]]*/, "", workflow)
|
||||
next
|
||||
}
|
||||
/^[[:space:]]+job:[[:space:]]*/ {
|
||||
job=$0
|
||||
sub(/^[[:space:]]+job:[[:space:]]*/, "", job)
|
||||
next
|
||||
}
|
||||
/^[[:space:]]+execution:[[:space:]]*/ {
|
||||
execution=$0
|
||||
sub(/^[[:space:]]+execution:[[:space:]]*/, "", execution)
|
||||
next
|
||||
}
|
||||
END { flush() }
|
||||
' "${MATRIX}"
|
||||
)"
|
||||
|
||||
declare -A seen_ids=()
|
||||
declare -a failures=()
|
||||
total=0
|
||||
verified=0
|
||||
delegated=0
|
||||
|
||||
job_body() {
|
||||
local workflow_file="$1"
|
||||
local job_id="$2"
|
||||
awk -v target="${job_id}" '
|
||||
$0 ~ "^ " target ":[[:space:]]*$" { inside=1; print; next }
|
||||
inside && $0 ~ "^ [A-Za-z0-9_-]+:[[:space:]]*$" { exit }
|
||||
inside { print }
|
||||
' "${workflow_file}"
|
||||
}
|
||||
|
||||
gradle_command_has_safe_literal_grammar() {
|
||||
local command="$1"
|
||||
[[ "${command}" =~ ^\./gradlew([[:space:]]+[A-Za-z0-9_.:/@=,+-]+)+[[:space:]]*$ ]]
|
||||
}
|
||||
|
||||
gradle_token_suppresses_execution() {
|
||||
local token="$1"
|
||||
case "${token}" in
|
||||
'--dry-run'|'--dry-run='*|'-m'|'-x'|'-x'*|'--exclude-task'|'--exclude-task='*) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
gradle_token_is_allowed_gate_argument() {
|
||||
local token="$1"
|
||||
case "${token}" in
|
||||
'--no-daemon'|'--stacktrace'|'--warning-mode=fail') return 0 ;;
|
||||
esac
|
||||
[[ "${token}" =~ ^:?[A-Za-z0-9_][A-Za-z0-9_.-]*(:[A-Za-z0-9_][A-Za-z0-9_.-]*)*$ ]]
|
||||
}
|
||||
|
||||
gradle_plugin_is_applied() {
|
||||
local plugin_id="$1"
|
||||
grep -RqsF --include='build.gradle' -- "id '${plugin_id}'" "${REPO_ROOT}/src" \
|
||||
|| grep -RqsF --include='build.gradle' -- "id \"${plugin_id}\"" "${REPO_ROOT}/src" \
|
||||
|| grep -RqsF --include='build.gradle' -- "apply plugin: '${plugin_id}'" "${REPO_ROOT}/src" \
|
||||
|| grep -RqsF --include='build.gradle' -- "apply plugin: \"${plugin_id}\"" "${REPO_ROOT}/src"
|
||||
}
|
||||
|
||||
gradle_custom_task_is_registered_in_build_file() {
|
||||
local task_name="$1"
|
||||
local build_file="$2"
|
||||
if grep -qsE -- "tasks\\.register\\(['\"]${task_name}['\"]" "${build_file}"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# A lane declared through the `ca.strict-test-lane` convention. The convention exists because the
|
||||
# five lines every lane used to repeat were copied per lane and per leaf, and two copies had
|
||||
# already lost `failOnNoDiscoveredTests`; registering through it is still registering, so this lint
|
||||
# has to recognise the declaration or it reports every converted lane as missing.
|
||||
if grep -qsE -- "lane\\(['\"]${task_name}['\"]\\)" "${build_file}"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# An API surface gate declared through the `ca.api-surface` convention, which derives every task
|
||||
# name from one label so a leaf cannot verify one surface while telling the reader about another.
|
||||
# The name is computed, so there is no literal `tasks.register('verifyMongoApiSurface')` anywhere;
|
||||
# what the build file says is `apiSurface { label = 'Mongo' }`.
|
||||
if [[ "${task_name}" =~ ^verify(.+)ApiSurface$ ]]; then
|
||||
local surface_label="${BASH_REMATCH[1]}"
|
||||
if grep -qsE -- "label[[:space:]]*=[[:space:]]*['\"]${surface_label}['\"]" "${build_file}"; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
awk -v required_task="${task_name}" '
|
||||
index($0, "registerStrictQualificationTest(") > 0 { inside_registration=1 }
|
||||
inside_registration && /^[[:space:]]*name:[[:space:]]*/ {
|
||||
candidate=$0
|
||||
sub(/^[[:space:]]*name:[[:space:]]*/, "", candidate)
|
||||
quote=substr(candidate, 1, 1)
|
||||
if (quote != "\"" && quote != sprintf("%c", 39)) {
|
||||
next
|
||||
}
|
||||
candidate=substr(candidate, 2)
|
||||
closing_quote=index(candidate, quote)
|
||||
if (closing_quote == 0) {
|
||||
next
|
||||
}
|
||||
candidate=substr(candidate, 1, closing_quote - 1)
|
||||
if (candidate == required_task) {
|
||||
found=1
|
||||
}
|
||||
}
|
||||
inside_registration && /\)[[:space:]]*$/ { inside_registration=0 }
|
||||
END { exit found ? 0 : 1 }
|
||||
' "${build_file}"
|
||||
}
|
||||
|
||||
# Every `dependsOn ... named('x')` in the build, collected once.
|
||||
#
|
||||
# This used to be one recursive grep per gate. That was affordable at 38 gates and stopped being so
|
||||
# at 48: the whole lint crossed the ten-second budget its own contract test asserts, and the first
|
||||
# symptom was that test failing rather than anything about gate coverage. One pass, then membership
|
||||
# tests against the result.
|
||||
CHECK_WIRING_CACHE=""
|
||||
load_check_wiring() {
|
||||
[[ -n "${CHECK_WIRING_CACHE}" ]] && return 0
|
||||
CHECK_WIRING_CACHE="$(grep -RhoE -- "dependsOn[^\n]*named\((['\"])[A-Za-z0-9_.-]+\1\)" \
|
||||
"${REPO_ROOT}/src" --include='build.gradle' --include='ca.*.gradle' 2>/dev/null \
|
||||
| grep -oE "(['\"])[A-Za-z0-9_.-]+\1" | tr -d "\"'" | sort -u)"
|
||||
# A build with no such wiring at all would leave this empty and make every membership test pass by
|
||||
# vacuity, so an empty result is a marker rather than an answer.
|
||||
[[ -z "${CHECK_WIRING_CACHE}" ]] && CHECK_WIRING_CACHE="<none>"
|
||||
return 0
|
||||
}
|
||||
|
||||
gradle_custom_task_wired_into_check() {
|
||||
local task_name="$1"
|
||||
load_check_wiring
|
||||
if printf '%s\n' "${CHECK_WIRING_CACHE}" | grep -qxF -- "${task_name}"; then
|
||||
return 0
|
||||
fi
|
||||
# `ca.api-surface` wires check as `dependsOn tasks.named(verifyName())`, where verifyName() is
|
||||
# derived from the leaf's label. The declaration that makes the gate real is the label, so that is
|
||||
# what proves the wiring — the convention has exactly one check wiring and it is unconditional.
|
||||
if [[ "${task_name}" =~ ^verify(.+)ApiSurface$ ]]; then
|
||||
local surface_label="${BASH_REMATCH[1]}"
|
||||
if grep -RqsE -- "label[[:space:]]*=[[:space:]]*['\"]${surface_label}['\"]" "${REPO_ROOT}/src" \
|
||||
--include='build.gradle' \
|
||||
&& grep -qsE -- "dependsOn tasks\.named\(verifyName\(\)\)" \
|
||||
"${REPO_ROOT}/src/build-logic/src/main/groovy/ca.api-surface.gradle"; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
# The build files, found once rather than once per gate. Same reason as the wiring cache above: the
|
||||
# per-gate `find` was a fixed cost multiplied by a number that grew.
|
||||
GRADLE_FILE_CACHE=""
|
||||
load_gradle_files() {
|
||||
[[ -n "${GRADLE_FILE_CACHE}" ]] && return 0
|
||||
GRADLE_FILE_CACHE="$(find "${REPO_ROOT}/src" -type f -name '*.gradle' | sort)"
|
||||
return 0
|
||||
}
|
||||
|
||||
gradle_custom_task_is_registered() {
|
||||
local task_name="$1"
|
||||
local build_file
|
||||
load_gradle_files
|
||||
while IFS= read -r build_file; do
|
||||
[[ -z "${build_file}" ]] && continue
|
||||
if gradle_custom_task_is_registered_in_build_file "${task_name}" "${build_file}"; then
|
||||
return 0
|
||||
fi
|
||||
done <<< "${GRADLE_FILE_CACHE}"
|
||||
return 1
|
||||
}
|
||||
|
||||
gradle_token_matches_registered_task() {
|
||||
local token="$1"
|
||||
local required_task="$2"
|
||||
local project_path build_file
|
||||
if [[ "${token}" == "${required_task}" || "${token}" == ":${required_task}" ]]; then
|
||||
return 0
|
||||
fi
|
||||
if [[ "${token}" != :* || "${token}" != *:"${required_task}" ]]; then
|
||||
return 1
|
||||
fi
|
||||
project_path="${token%:"${required_task}"}"
|
||||
project_path="${project_path#:}"
|
||||
project_path="${project_path%:}"
|
||||
build_file="${REPO_ROOT}/src/${project_path//:/\/}/build.gradle"
|
||||
[[ -f "${build_file}" ]] \
|
||||
&& gradle_custom_task_is_registered_in_build_file "${required_task}" "${build_file}"
|
||||
}
|
||||
|
||||
job_runs_gradle_task() {
|
||||
local workflow_file="$1"
|
||||
local job_id="$2"
|
||||
local required_task="$3"
|
||||
local command token
|
||||
local found_task suppressed
|
||||
local -a tokens=()
|
||||
|
||||
while IFS= read -r command; do
|
||||
if ! gradle_command_has_safe_literal_grammar "${command}"; then
|
||||
continue
|
||||
fi
|
||||
read -r -a tokens <<< "${command}"
|
||||
if (( ${#tokens[@]} < 2 )) || [[ "${tokens[0]}" != './gradlew' ]]; then
|
||||
continue
|
||||
fi
|
||||
found_task=0
|
||||
suppressed=0
|
||||
for token in "${tokens[@]:1}"; do
|
||||
case "${token}" in
|
||||
'&&'|'||'|';'|'|'|'#'*) break ;;
|
||||
esac
|
||||
if gradle_token_suppresses_execution "${token}"; then
|
||||
suppressed=1
|
||||
break
|
||||
fi
|
||||
if ! gradle_token_is_allowed_gate_argument "${token}"; then
|
||||
suppressed=1
|
||||
break
|
||||
fi
|
||||
if gradle_token_matches_registered_task "${token}" "${required_task}"; then
|
||||
found_task=1
|
||||
fi
|
||||
done
|
||||
if (( found_task == 1 && suppressed == 0 )); then
|
||||
return 0
|
||||
fi
|
||||
done < <(
|
||||
job_body "${workflow_file}" "${job_id}" | awk '
|
||||
/^[[:space:]]+(-[[:space:]]+)?run:[[:space:]]+/ {
|
||||
command=$0
|
||||
sub(/^[[:space:]]+(-[[:space:]]+)?run:[[:space:]]+/, "", command)
|
||||
if (command !~ /^(\||>)/) {
|
||||
print command
|
||||
}
|
||||
}
|
||||
'
|
||||
)
|
||||
return 1
|
||||
}
|
||||
|
||||
# A workflow that only runs for a release tag. Its jobs need no separate release gate: the workflow
|
||||
# run *is* the release, so a failing job fails it. Detected from the `on:` block rather than from a
|
||||
# filename, because "release" in a filename is a naming convention and `on: push: tags:` is not.
|
||||
workflow_is_release_tag_triggered() {
|
||||
local workflow_file="$1"
|
||||
[[ -f "${workflow_file}" ]] || return 1
|
||||
awk '
|
||||
/^on:[[:space:]]*$/ { in_on=1; next }
|
||||
/^[^[:space:]#]/ { in_on=0 }
|
||||
in_on && /^[[:space:]]+tags:/ { found=1 }
|
||||
END { exit found ? 0 : 1 }
|
||||
' "${workflow_file}"
|
||||
}
|
||||
|
||||
# Jobs the release gate actually waits on: its `needs:` inside its own workflow, plus the job names
|
||||
# in REQUIRED_CHECKS, which is how it requires a check run produced by a different workflow.
|
||||
RELEASE_GATE_NEEDS=""
|
||||
RELEASE_GATE_REQUIRED_CHECKS=""
|
||||
load_release_gate_requirements() {
|
||||
[[ -n "${RELEASE_GATE_NEEDS}" ]] && return 0
|
||||
RELEASE_GATE_NEEDS="<none>"
|
||||
RELEASE_GATE_REQUIRED_CHECKS="<none>"
|
||||
local workflow_file="${REPO_ROOT}/.github/workflows/${RELEASE_GATE_WORKFLOW}"
|
||||
[[ -f "${workflow_file}" ]] || return 0
|
||||
grep -Eqs -- "^[[:space:]]{2}${RELEASE_GATE_JOB}:[[:space:]]*$" "${workflow_file}" || return 0
|
||||
|
||||
local entry kind value
|
||||
local -a needs=()
|
||||
local -a checks=()
|
||||
while IFS= read -r entry; do
|
||||
[[ "${entry}" =~ ^(need|check)\ [A-Za-z0-9_-]+$ ]] || continue
|
||||
kind="${entry%% *}"
|
||||
value="${entry#* }"
|
||||
if [[ "${kind}" == "need" ]]; then
|
||||
needs+=("${value}")
|
||||
else
|
||||
checks+=("${value}")
|
||||
fi
|
||||
done < <(
|
||||
job_body "${workflow_file}" "${RELEASE_GATE_JOB}" | awk '
|
||||
/^[[:space:]]+needs:[[:space:]]*\[/ {
|
||||
value=$0
|
||||
sub(/^[[:space:]]+needs:[[:space:]]*\[/, "", value)
|
||||
sub(/\].*$/, "", value)
|
||||
count=split(value, parts, /[[:space:]]*,[[:space:]]*/)
|
||||
for (index_value = 1; index_value <= count; index_value++) {
|
||||
gsub(/[[:space:]]/, "", parts[index_value])
|
||||
if (parts[index_value] != "") { print "need " parts[index_value] }
|
||||
}
|
||||
next
|
||||
}
|
||||
/^[[:space:]]+needs:[[:space:]]*[A-Za-z0-9_-]+[[:space:]]*$/ {
|
||||
value=$0
|
||||
sub(/^[[:space:]]+needs:[[:space:]]*/, "", value)
|
||||
sub(/[[:space:]]+$/, "", value)
|
||||
print "need " value
|
||||
next
|
||||
}
|
||||
/^[[:space:]]+needs:[[:space:]]*$/ { in_needs=1; next }
|
||||
in_needs && /^[[:space:]]+-[[:space:]]+/ {
|
||||
value=$0
|
||||
sub(/^[[:space:]]+-[[:space:]]+/, "", value)
|
||||
sub(/[[:space:]]+$/, "", value)
|
||||
print "need " value
|
||||
next
|
||||
}
|
||||
in_needs { in_needs=0 }
|
||||
/^[[:space:]]+REQUIRED_CHECKS:[[:space:]]*/ {
|
||||
value=$0
|
||||
sub(/^[[:space:]]+REQUIRED_CHECKS:[[:space:]]*/, "", value)
|
||||
count=split(value, entries, /[[:space:]]+/)
|
||||
for (index_value = 1; index_value <= count; index_value++) {
|
||||
if (entries[index_value] != "") { print "check " entries[index_value] }
|
||||
}
|
||||
}
|
||||
'
|
||||
)
|
||||
(( ${#needs[@]} > 0 )) && RELEASE_GATE_NEEDS="$(printf '%s\n' "${needs[@]}" | sort -u)"
|
||||
(( ${#checks[@]} > 0 )) && RELEASE_GATE_REQUIRED_CHECKS="$(printf '%s\n' "${checks[@]}" | sort -u)"
|
||||
return 0
|
||||
}
|
||||
|
||||
# `release_blocking: true` used to be read by nothing but an enum test, so a gate could claim to
|
||||
# block a release that no job anywhere waited on — filesystem-vulnerability-scan was red while
|
||||
# release-gate was green and nothing in the repository joined the two. A gate earns `true` by being
|
||||
# required on a path a release actually takes:
|
||||
# - it is the release gate job itself, or one of that job's `needs:` in the same workflow;
|
||||
# - its job name is listed in the release gate's REQUIRED_CHECKS (the cross-workflow hook);
|
||||
# - its workflow only runs for a release tag, so the job failing fails that release run.
|
||||
# A control that is real but reachable by none of those is `conditional`, which is the honest value
|
||||
# and is what the enum is for.
|
||||
gate_is_enforced_by_a_release_gate() {
|
||||
local gate_workflow="$1"
|
||||
local gate_job="$2"
|
||||
load_release_gate_requirements
|
||||
if [[ "${gate_workflow}" == "${RELEASE_GATE_WORKFLOW}" ]]; then
|
||||
if [[ "${gate_job}" == "${RELEASE_GATE_JOB}" ]]; then
|
||||
return 0
|
||||
fi
|
||||
if printf '%s\n' "${RELEASE_GATE_NEEDS}" | grep -qxF -- "${gate_job}"; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
if printf '%s\n' "${RELEASE_GATE_REQUIRED_CHECKS}" | grep -qxF -- "${gate_job}"; then
|
||||
return 0
|
||||
fi
|
||||
workflow_is_release_tag_triggered "${REPO_ROOT}/.github/workflows/${gate_workflow}"
|
||||
}
|
||||
|
||||
while IFS=$'\t' read -r id blocking mechanism ref workflow job execution; do
|
||||
[[ -z "${id}" ]] && continue
|
||||
total=$((total + 1))
|
||||
|
||||
if [[ -n "${seen_ids[${id}]:-}" ]]; then
|
||||
failures+=("duplicate gate id '${id}'")
|
||||
fi
|
||||
seen_ids["${id}"]=1
|
||||
|
||||
if [[ -z "${blocking}" || -z "${mechanism}" || -z "${ref}" || -z "${workflow}" \
|
||||
|| -z "${job}" || -z "${execution}" ]]; then
|
||||
failures+=("gate '${id}' has an empty required field")
|
||||
continue
|
||||
fi
|
||||
if [[ ! "${blocking}" =~ ^(true|false|conditional)$ ]]; then
|
||||
failures+=("gate '${id}' has invalid release_blocking '${blocking}'")
|
||||
fi
|
||||
if [[ ! "${workflow}" =~ ^[A-Za-z0-9._-]+\.ya?ml$ || ! "${job}" =~ ^[A-Za-z0-9_-]+$ ]]; then
|
||||
failures+=("gate '${id}' has an unsafe workflow or job identifier")
|
||||
continue
|
||||
fi
|
||||
|
||||
workflow_file="${REPO_ROOT}/.github/workflows/${workflow}"
|
||||
if [[ ! -f "${workflow_file}" ]]; then
|
||||
failures+=("gate '${id}' references missing workflow '.github/workflows/${workflow}'")
|
||||
continue
|
||||
fi
|
||||
if ! grep -Eqs -- "^[[:space:]]{2}${job}:[[:space:]]*$" "${workflow_file}"; then
|
||||
failures+=("gate '${id}' references missing job '${job}' in '${workflow}'")
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ "${blocking}" == "true" ]] \
|
||||
&& ! gate_is_enforced_by_a_release_gate "${workflow}" "${job}"; then
|
||||
failures+=("gate '${id}' is release_blocking: true but no release gate requires job '${job}' in '${workflow}'")
|
||||
continue
|
||||
fi
|
||||
|
||||
case "${mechanism}" in
|
||||
gradle-custom-task)
|
||||
if [[ ! "${ref}" =~ ^[A-Za-z_][A-Za-z0-9_-]*$ ]]; then
|
||||
failures+=("gate '${id}' has unsafe Gradle custom task ref '${ref}'")
|
||||
continue
|
||||
fi
|
||||
if ! gradle_custom_task_is_registered "${ref}"; then
|
||||
failures+=("gate '${id}' references unregistered Gradle task '${ref}'")
|
||||
continue
|
||||
fi
|
||||
;;
|
||||
gradle-plugin-task)
|
||||
plugin="${ref%@*}"
|
||||
task="${ref#*@}"
|
||||
if [[ "${plugin}" == "${ref}" \
|
||||
|| ! "${plugin}" =~ ^[A-Za-z][A-Za-z0-9.-]*$ \
|
||||
|| ! "${task}" =~ ^[A-Za-z_][A-Za-z0-9_-]*$ ]]; then
|
||||
failures+=("gate '${id}' has unsafe Gradle plugin task ref '${ref}'")
|
||||
continue
|
||||
fi
|
||||
if ! gradle_plugin_is_applied "${plugin}"; then
|
||||
failures+=("gate '${id}' references unapplied Gradle plugin '${plugin}'")
|
||||
continue
|
||||
fi
|
||||
;;
|
||||
contract-test)
|
||||
if [[ "${ref}" == /* || "${ref}" == *".."* || ! -f "${REPO_ROOT}/src/${ref}" ]]; then
|
||||
failures+=("gate '${id}' references missing or unsafe contract test 'src/${ref}'")
|
||||
continue
|
||||
fi
|
||||
;;
|
||||
workflow-job)
|
||||
if [[ "${ref}" != "${job}" ]]; then
|
||||
failures+=("gate '${id}' workflow-job ref '${ref}' must equal job '${job}'")
|
||||
continue
|
||||
fi
|
||||
;;
|
||||
delegated-pending)
|
||||
delegated=$((delegated + 1))
|
||||
printf "gate '%s': explicitly delegated-pending\n" "${id}"
|
||||
continue
|
||||
;;
|
||||
*)
|
||||
failures+=("gate '${id}' has unknown mechanism '${mechanism}'")
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
|
||||
case "${execution}" in
|
||||
check)
|
||||
if ! job_runs_gradle_task "${workflow_file}" "${job}" 'check'; then
|
||||
failures+=("gate '${id}' expects Gradle check in job '${job}'")
|
||||
continue
|
||||
fi
|
||||
# Build files *and* convention plugins. A gate can now be wired into check from an included
|
||||
# build's convention rather than from a leaf's build.gradle, and a lint that only reads
|
||||
# build.gradle would call such a gate unwired while it runs on every leaf — a false failure
|
||||
# that teaches the next author to delete the matrix row instead of trusting it.
|
||||
#
|
||||
# A convention that derives the task name from a label wires check by that derived name, so
|
||||
# there is no literal to grep for either; `gradle_custom_task_wired_into_check` handles both
|
||||
# the literal and the derived form.
|
||||
if [[ "${mechanism}" == "gradle-custom-task" ]] \
|
||||
&& ! gradle_custom_task_wired_into_check "${ref}"; then
|
||||
failures+=("gate '${id}' task '${ref}' exists but is not wired into Gradle check")
|
||||
continue
|
||||
fi
|
||||
;;
|
||||
explicit)
|
||||
if ! job_runs_gradle_task "${workflow_file}" "${job}" "${ref}"; then
|
||||
failures+=("gate '${id}' task '${ref}' is not explicit in job '${job}'")
|
||||
continue
|
||||
fi
|
||||
;;
|
||||
job)
|
||||
;;
|
||||
*)
|
||||
failures+=("gate '${id}' has unknown execution '${execution}'")
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
|
||||
verified=$((verified + 1))
|
||||
done <<< "${records}"
|
||||
|
||||
if (( total == 0 )); then
|
||||
failures+=("matrix declares no gates")
|
||||
fi
|
||||
|
||||
printf 'gate-matrix-lint: %d gates, %d verified, %d delegated-pending\n' \
|
||||
"${total}" "${verified}" "${delegated}"
|
||||
if (( ${#failures[@]} > 0 )); then
|
||||
printf '::error::gate-matrix-lint: %d drift(s) found\n' "${#failures[@]}" >&2
|
||||
for failure in "${failures[@]}"; do
|
||||
printf ' - %s\n' "${failure}" >&2
|
||||
done
|
||||
exit 1
|
||||
fi
|
||||
printf 'gate-matrix-lint: OK\n'
|
||||
@@ -1,799 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
readonly EXPECTED_DISTRIBUTION_SUFFIX='/gradle-9.0.0-bin.zip'
|
||||
readonly EXPECTED_DISTRIBUTION_SHA256='8fad3d78296ca518113f3d29016617c7f9367dc005f932bd9d93bf45ba46072b'
|
||||
readonly EXPECTED_WRAPPER_JAR_SHA256='76805e32c009c0cf0dd5d206bddc9fb22ea42e84db904b764f3047de095493f3'
|
||||
readonly EXPECTED_VALIDATION_ACTION='gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6'
|
||||
readonly EXPECTED_DEPENDENCY_SUBMISSION_ACTION='gradle/actions/dependency-submission@748248ddd2a24f49513d8f472f81c3a07d4d50e1'
|
||||
readonly EXPECTED_GUARDED_GRADLE_IF="\${{ always() && steps.gradle-wrapper-validation.outcome == 'success' }}"
|
||||
# Lock update procedure (only after intentional review of the complete .github diff):
|
||||
# find .github/workflows -mindepth 1 -maxdepth 1 \
|
||||
# \( -name '*.yml' -o -name '*.yaml' \) ! -type f -print # must print nothing
|
||||
# find .github/workflows -mindepth 1 -maxdepth 1 -type f \
|
||||
# \( -name '*.yml' -o -name '*.yaml' \) -print0 \
|
||||
# | LC_ALL=C sort -z | xargs -0 sha256sum # EXPECTED_WORKFLOW_LOCK
|
||||
# find .github/actions -mindepth 2 -maxdepth 2 \
|
||||
# \( -name 'action.yml' -o -name 'action.yaml' \) ! -type f -print # must print nothing
|
||||
# find .github/actions -mindepth 2 -maxdepth 2 -type f \
|
||||
# \( -name 'action.yml' -o -name 'action.yaml' \) -print0 \
|
||||
# | LC_ALL=C sort -z | xargs -0 sha256sum # EXPECTED_COMPOSITE_ACTION_LOCK
|
||||
# Replace an entire sorted array in the same reviewed change. Never refresh a single digest merely
|
||||
# to make this verifier pass.
|
||||
#
|
||||
# Composite actions are locked alongside the workflows, and for the same reason. A job's Java
|
||||
# toolchain and Gradle cache configuration used to be written out in every workflow that needed it,
|
||||
# so the pinned actions/setup-java commit sat inside the locked bytes fifty-nine times over.
|
||||
# .github/actions/setup-gradle-java/action.yml now holds the single copy: leaving it out of this
|
||||
# lock would mean one unreviewed edit could change what every Gradle job in the repository installs
|
||||
# and runs, while this verifier still said PASS. The two arrays are compared separately so that a
|
||||
# drifting action does not shift every workflow's expected position and bury the real message.
|
||||
readonly EXPECTED_WORKFLOW_LOCK=(
|
||||
'444bb0da12f631fa20f492d3dc37e93b762d144640e4f86b81b7bdd3d4c81312 .github/workflows/ci-quality-gates.yml'
|
||||
'e7f355c7eb81a72e0f1d2892843621bf11384ca2a4bf36f1daf3900b82ae46e7 .github/workflows/dependency-vulnerability.yml'
|
||||
'2fa9c8081df1679c1feb9aa101aff47d7d2c24995c155aff6d1e4799eaad8f21 .github/workflows/fileserver-nightly.yml'
|
||||
'1686b7b637611c8cd5eb87b2cc759f5cd2c6b878154363fc336c16b93c635ada .github/workflows/fileserver-pr.yml'
|
||||
'b47932200c9ac9db57070b43bc70c40c89c152e9235d7a1325baab407df215e9 .github/workflows/fileserver-release.yml'
|
||||
'a18a0f08982b393177a843c1bdd03a881d9d12491819cebb44b6891a87ff2a6d .github/workflows/integration-main.yml'
|
||||
'4345d5cfb5a139a11cf3647c58fff61ab08397ace186919cdc7a769cdfc4d4b7 .github/workflows/jpa-next-hibernate8.yml'
|
||||
'726b3d91603a2529205d1d5568253b57d85fcbb9d10d3efe182491c9da744d78 .github/workflows/jpa-next-jpa4.yml'
|
||||
'3c073a928dfb266051a1a52f4d66bf6d6903b9dbd2cdb6459fab661228f27e88 .github/workflows/jpa-next-postgresql19.yml'
|
||||
'c098946cfa7ba9c2959a6f8217f20af1ced28a45f22d088bc7ee4df661d45e84 .github/workflows/jpa-nightly.yml'
|
||||
'b73314359be3391f8b569bb2ea0a5757927c4bbbd42d84c242e0e15e494320cd .github/workflows/jpa-r2-evidence.yml'
|
||||
'43c565aa2709bc4d72cfcedf56816c6442bb63a23cc1db011e425ae0181d0bcd .github/workflows/jpa-release.yml'
|
||||
'5be7e931db749029d89787da042d6d7cf8e683d60698bd8a2993c29db26355fb .github/workflows/link-check.yml'
|
||||
'62a852157481e89c778c0498067a7443bde22bf421995ade8714a89e4eca347c .github/workflows/messaging-certification.yml'
|
||||
'ee9f247297559077c7766f7f0f8b5e39538b496922f6b2cc2621aa04593f320a .github/workflows/notification-platform.yml'
|
||||
'e685bc846108503ee2cf1e06b6cec040174d49348bd205400f891828f24dda68 .github/workflows/object-storage-qualification.yml'
|
||||
'67ef53adb80551629a482e2610a0753dd0fadf85f523e985c4693354df543748 .github/workflows/pr-adapters.yml'
|
||||
'376a71f7a2b9990e1e96937ad3dd46a33f266cc742ca499b208bc909897b67f3 .github/workflows/redis-sdk-topology.yml'
|
||||
'42b57385c1f87170ba6d882345c709c11dff019f1860e72ad989b0c5c1a67ece .github/workflows/release.yml'
|
||||
)
|
||||
readonly EXPECTED_COMPOSITE_ACTION_LOCK=(
|
||||
'7ec6591f26a1bd76658c55472e16b195b80db2c4792b429efda5a0dcbde61a45 .github/actions/setup-gradle-java/action.yml'
|
||||
)
|
||||
readonly EXPECTED_WRAPPER_PROPERTIES=(
|
||||
'distributionBase=GRADLE_USER_HOME'
|
||||
'distributionPath=wrapper/dists'
|
||||
"distributionUrl=https\://services.gradle.org/distributions${EXPECTED_DISTRIBUTION_SUFFIX}"
|
||||
"distributionSha256Sum=${EXPECTED_DISTRIBUTION_SHA256}"
|
||||
'networkTimeout=10000'
|
||||
'validateDistributionUrl=true'
|
||||
'zipStoreBase=GRADLE_USER_HOME'
|
||||
'zipStorePath=wrapper/dists'
|
||||
)
|
||||
|
||||
fail() {
|
||||
printf 'gradle-wrapper-contract: FAIL: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
if [[ $# -ne 1 ]]; then
|
||||
fail 'expected exactly one repository-root argument'
|
||||
fi
|
||||
|
||||
readonly REPOSITORY_ROOT=$1
|
||||
[[ -d "${REPOSITORY_ROOT}" ]] || fail "repository root is not a directory: ${REPOSITORY_ROOT}"
|
||||
|
||||
readonly WRAPPER_PROPERTIES="${REPOSITORY_ROOT}/src/gradle/wrapper/gradle-wrapper.properties"
|
||||
readonly WRAPPER_JAR="${REPOSITORY_ROOT}/src/gradle/wrapper/gradle-wrapper.jar"
|
||||
readonly WORKFLOWS_DIRECTORY="${REPOSITORY_ROOT}/.github/workflows"
|
||||
# Not asserted to exist here, deliberately. The structural and wrapper-validation diagnostics below
|
||||
# are what a reader needs first; a missing composite action surfaces as a lock mismatch at the end,
|
||||
# which is still fail-closed.
|
||||
readonly ACTIONS_DIRECTORY="${REPOSITORY_ROOT}/.github/actions"
|
||||
|
||||
[[ -f "${WRAPPER_PROPERTIES}" ]] || fail "missing wrapper properties: ${WRAPPER_PROPERTIES}"
|
||||
[[ -f "${WRAPPER_JAR}" ]] || fail "missing wrapper JAR: ${WRAPPER_JAR}"
|
||||
[[ -d "${WORKFLOWS_DIRECTORY}" ]] || fail "missing workflows directory: ${WORKFLOWS_DIRECTORY}"
|
||||
|
||||
if ! printf '%s\n' "${EXPECTED_WRAPPER_PROPERTIES[@]}" | cmp -s - "${WRAPPER_PROPERTIES}"; then
|
||||
fail 'wrapper properties must match the exact canonical Gradle 9.0.0 eight-line contract'
|
||||
fi
|
||||
|
||||
readonly actual_wrapper_jar_sha256=$(sha256sum "${WRAPPER_JAR}" | awk '{print $1}')
|
||||
[[ "${actual_wrapper_jar_sha256}" == "${EXPECTED_WRAPPER_JAR_SHA256}" ]] \
|
||||
|| fail "wrapper JAR SHA-256 mismatch: ${actual_wrapper_jar_sha256}"
|
||||
|
||||
workflow_lock_valid=1
|
||||
|
||||
# One digest line per locked file, in the same LC_ALL=C order the update procedure prints. A symlink
|
||||
# or a non-regular file is reported as such rather than followed: a workflow replaced by a link to
|
||||
# another workflow is exactly the substitution this lock exists to catch.
|
||||
collect_actual_lock() {
|
||||
local locked_file locked_file_relative locked_file_sha256
|
||||
while IFS= read -r -d '' locked_file; do
|
||||
locked_file_relative=${locked_file#"${REPOSITORY_ROOT}"/}
|
||||
if [[ -L "${locked_file}" || ! -f "${locked_file}" ]]; then
|
||||
locked_file_sha256='<invalid-file-type>'
|
||||
else
|
||||
locked_file_sha256=$(sha256sum -- "${locked_file}" | awk '{print $1}')
|
||||
fi
|
||||
printf '%s %s\n' "${locked_file_sha256}" "${locked_file_relative}"
|
||||
done
|
||||
}
|
||||
|
||||
# Compared position by position rather than as a set, so an added, removed, renamed or reordered
|
||||
# entry is a mismatch and the message names both sides.
|
||||
compare_lock() {
|
||||
local label=$1
|
||||
shift
|
||||
local -a expected=("$@")
|
||||
local entry_count=${#expected[@]}
|
||||
if ((${#actual_lock[@]} > entry_count)); then
|
||||
entry_count=${#actual_lock[@]}
|
||||
fi
|
||||
local index expected_entry actual_entry
|
||||
for ((index = 0; index < entry_count; index++)); do
|
||||
expected_entry=${expected[index]-<missing>}
|
||||
actual_entry=${actual_lock[index]-<missing>}
|
||||
if [[ "${actual_entry}" != "${expected_entry}" ]]; then
|
||||
printf 'gradle-wrapper-contract: %s lock mismatch: expected %q; actual %q\n' \
|
||||
"${label}" "${expected_entry}" "${actual_entry}" >&2
|
||||
workflow_lock_valid=0
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
mapfile -t actual_lock < <(
|
||||
find "${WORKFLOWS_DIRECTORY}" -mindepth 1 -maxdepth 1 \
|
||||
\( -name '*.yml' -o -name '*.yaml' \) -print0 \
|
||||
| LC_ALL=C sort -z \
|
||||
| collect_actual_lock
|
||||
)
|
||||
compare_lock 'workflow' ${EXPECTED_WORKFLOW_LOCK[@]+"${EXPECTED_WORKFLOW_LOCK[@]}"}
|
||||
|
||||
# A missing .github/actions directory yields an empty list, which mismatches every expected entry.
|
||||
# That is the fail-closed answer: a composite action every Gradle job uses cannot be absent.
|
||||
actual_lock=()
|
||||
if [[ -d "${ACTIONS_DIRECTORY}" ]]; then
|
||||
mapfile -t actual_lock < <(
|
||||
find "${ACTIONS_DIRECTORY}" -mindepth 2 -maxdepth 2 \
|
||||
\( -name 'action.yml' -o -name 'action.yaml' \) -print0 \
|
||||
| LC_ALL=C sort -z \
|
||||
| collect_actual_lock
|
||||
)
|
||||
fi
|
||||
compare_lock 'composite action' \
|
||||
${EXPECTED_COMPOSITE_ACTION_LOCK[@]+"${EXPECTED_COMPOSITE_ACTION_LOCK[@]}"}
|
||||
|
||||
workflow_count=0
|
||||
gradle_job_count=0
|
||||
while IFS= read -r -d '' workflow; do
|
||||
if ! awk -v workflow="${workflow#"${REPOSITORY_ROOT}"/}" '
|
||||
function reset_step(known_field) {
|
||||
step_active = 0
|
||||
run_block = 0
|
||||
for (known_field in step_fields) {
|
||||
delete step_fields[known_field]
|
||||
}
|
||||
}
|
||||
|
||||
function reset_job() {
|
||||
job = ""
|
||||
in_steps = 0
|
||||
steps_count = 0
|
||||
reset_step()
|
||||
}
|
||||
|
||||
function indentation(line, first_non_space) {
|
||||
if (line ~ /^ *$/) {
|
||||
return length(line)
|
||||
}
|
||||
first_non_space = match(line, /[^ ]/)
|
||||
return first_non_space - 1
|
||||
}
|
||||
|
||||
function trim(value) {
|
||||
sub(/^[[:space:]]+/, "", value)
|
||||
sub(/[[:space:]]+$/, "", value)
|
||||
return value
|
||||
}
|
||||
|
||||
function grammar_error(message) {
|
||||
printf "%s: job %s %s\n", workflow, job == "" ? "<unknown>" : job, message > "/dev/stderr"
|
||||
invalid = 1
|
||||
}
|
||||
|
||||
function workflow_grammar_error(message) {
|
||||
printf "%s: %s\n", workflow, message > "/dev/stderr"
|
||||
invalid = 1
|
||||
}
|
||||
|
||||
function validate_job_shape() {
|
||||
if (job != "" && steps_count != 1) {
|
||||
grammar_error("must contain exactly one canonical steps block")
|
||||
}
|
||||
}
|
||||
|
||||
function is_allowed_step_field(field) {
|
||||
return field == "name" \
|
||||
|| field == "id" \
|
||||
|| field == "uses" \
|
||||
|| field == "run" \
|
||||
|| field == "if" \
|
||||
|| field == "shell" \
|
||||
|| field == "with" \
|
||||
|| field == "env" \
|
||||
|| field == "working-directory" \
|
||||
|| field == "continue-on-error" \
|
||||
|| field == "timeout-minutes"
|
||||
}
|
||||
|
||||
function validate_uses_scalar(value, first, quote, closing, index_value, suffix, action, single_quote) {
|
||||
value = trim(value)
|
||||
if (value == "" || index(value, "\\") != 0) {
|
||||
grammar_error("has unsupported uses scalar")
|
||||
return
|
||||
}
|
||||
|
||||
first = substr(value, 1, 1)
|
||||
single_quote = sprintf("%c", 39)
|
||||
if (first == "\"" || first == single_quote) {
|
||||
quote = first
|
||||
closing = 0
|
||||
for (index_value = 2; index_value <= length(value); index_value++) {
|
||||
if (substr(value, index_value, 1) == quote) {
|
||||
closing = index_value
|
||||
break
|
||||
}
|
||||
}
|
||||
if (closing == 0) {
|
||||
grammar_error("has unsupported uses scalar")
|
||||
return
|
||||
}
|
||||
suffix = substr(value, closing + 1)
|
||||
if (suffix !~ /^[[:space:]]*(#.*)?$/) {
|
||||
grammar_error("has unsupported uses scalar")
|
||||
return
|
||||
}
|
||||
action = substr(value, 2, closing - 2)
|
||||
if (index(action, quote) != 0) {
|
||||
grammar_error("has unsupported uses scalar")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
action = value
|
||||
sub(/[[:space:]]+#.*$/, "", action)
|
||||
action = trim(action)
|
||||
if (action ~ /["'"'"'\\]/ || action ~ /^[*!&|>]/) {
|
||||
grammar_error("has unsupported uses scalar")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (action !~ /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+(\/[A-Za-z0-9_.-]+)*@[A-Za-z0-9_.\/-]+$/ \
|
||||
&& action !~ /^\.\/[A-Za-z0-9_.\/-]+$/ \
|
||||
&& action !~ /^docker:\/\/[^[:space:]]+$/) {
|
||||
grammar_error("has unsupported uses scalar")
|
||||
}
|
||||
}
|
||||
|
||||
function validate_run_scalar(value, first) {
|
||||
value = trim(value)
|
||||
if (value ~ /^(\||>)[+-]?([[:space:]]+#.*)?$/) {
|
||||
run_block = 1
|
||||
return
|
||||
}
|
||||
first = substr(value, 1, 1)
|
||||
if (value == "" || first == "\"" || first == sprintf("%c", 39) \
|
||||
|| first ~ /[*&!|>]/ || index(value, "\\") != 0) {
|
||||
grammar_error("has unsupported run scalar")
|
||||
}
|
||||
}
|
||||
|
||||
function validate_step_field(content, field, value, separator) {
|
||||
content = trim(content)
|
||||
if (content ~ /^[{[]/) {
|
||||
grammar_error("contains unsupported flow-style step syntax")
|
||||
return
|
||||
}
|
||||
if (content ~ /^<</) {
|
||||
grammar_error("contains a forbidden step merge key")
|
||||
return
|
||||
}
|
||||
if (content ~ /^[*&!]/) {
|
||||
grammar_error("contains unsupported step anchor, alias, or tag syntax")
|
||||
return
|
||||
}
|
||||
if (content !~ /^[A-Za-z][A-Za-z0-9-]*:/) {
|
||||
grammar_error("contains unsupported step field syntax")
|
||||
return
|
||||
}
|
||||
|
||||
separator = index(content, ":")
|
||||
field = substr(content, 1, separator - 1)
|
||||
value = substr(content, separator + 1)
|
||||
sub(/^[[:space:]]*/, "", value)
|
||||
if (!is_allowed_step_field(field)) {
|
||||
grammar_error("contains unsupported step field: " field)
|
||||
return
|
||||
}
|
||||
if (field in step_fields) {
|
||||
grammar_error("contains duplicate step field: " field)
|
||||
return
|
||||
}
|
||||
step_fields[field] = 1
|
||||
|
||||
if (field == "uses") {
|
||||
validate_uses_scalar(value)
|
||||
} else if (field == "run") {
|
||||
validate_run_scalar(value)
|
||||
}
|
||||
}
|
||||
|
||||
BEGIN {
|
||||
in_jobs = 0
|
||||
invalid = 0
|
||||
jobs_count = 0
|
||||
single_quote = sprintf("%c", 39)
|
||||
reset_job()
|
||||
}
|
||||
|
||||
/^jobs:/ {
|
||||
if ($0 !~ /^jobs:[[:space:]]*(#.*)?$/) {
|
||||
workflow_grammar_error("jobs container must use a canonical block mapping")
|
||||
next
|
||||
}
|
||||
jobs_count++
|
||||
if (jobs_count != 1) {
|
||||
workflow_grammar_error("workflow must contain exactly one canonical jobs block")
|
||||
}
|
||||
in_jobs = 1
|
||||
next
|
||||
}
|
||||
|
||||
/^"jobs":/ {
|
||||
workflow_grammar_error("jobs container must use a canonical block mapping")
|
||||
next
|
||||
}
|
||||
|
||||
substr($0, 1, 7) == single_quote "jobs" single_quote ":" {
|
||||
workflow_grammar_error("jobs container must use a canonical block mapping")
|
||||
next
|
||||
}
|
||||
|
||||
run_block == 0 && /^<<:/ {
|
||||
workflow_grammar_error("workflow contains a forbidden merge key")
|
||||
next
|
||||
}
|
||||
|
||||
in_jobs && /^[^[:space:]#]/ {
|
||||
validate_job_shape()
|
||||
reset_job()
|
||||
in_jobs = 0
|
||||
}
|
||||
|
||||
in_jobs && /^ [^[:space:]#]/ {
|
||||
if ($0 !~ /^ [A-Za-z0-9_.-]+:[[:space:]]*(#.*)?$/) {
|
||||
grammar_error("job declaration must use a canonical block mapping")
|
||||
next
|
||||
}
|
||||
validate_job_shape()
|
||||
reset_job()
|
||||
job = $0
|
||||
sub(/^ /, "", job)
|
||||
sub(/:.*/, "", job)
|
||||
next
|
||||
}
|
||||
|
||||
in_jobs && job != "" {
|
||||
raw = $0
|
||||
line_indent = indentation(raw)
|
||||
|
||||
if (run_block != 0) {
|
||||
if (raw ~ /^ *$/ || line_indent > 8) {
|
||||
next
|
||||
}
|
||||
run_block = 0
|
||||
}
|
||||
|
||||
if (raw ~ /^ *#/) {
|
||||
next
|
||||
}
|
||||
if (raw ~ /^ steps:/ || raw ~ /^ "steps":/ \
|
||||
|| substr(raw, 1, 11) == " " single_quote "steps" single_quote ":") {
|
||||
if (raw != " steps:") {
|
||||
grammar_error("steps container must use a canonical block sequence")
|
||||
next
|
||||
}
|
||||
steps_count++
|
||||
if (steps_count != 1) {
|
||||
grammar_error("must contain exactly one canonical steps block")
|
||||
}
|
||||
in_steps = 1
|
||||
reset_step()
|
||||
next
|
||||
}
|
||||
if (in_steps != 0 && line_indent == 4) {
|
||||
in_steps = 0
|
||||
reset_step()
|
||||
}
|
||||
|
||||
if (raw ~ /^ *<<:/) {
|
||||
grammar_error("contains a forbidden merge key")
|
||||
next
|
||||
}
|
||||
|
||||
if (in_steps != 0 && raw ~ /^ - /) {
|
||||
reset_step()
|
||||
step_active = 1
|
||||
content = substr(raw, 9)
|
||||
validate_step_field(content)
|
||||
next
|
||||
}
|
||||
|
||||
if (in_steps != 0 && raw ~ /^ -[[:space:]]*$/) {
|
||||
grammar_error("contains unsupported empty step syntax")
|
||||
next
|
||||
}
|
||||
|
||||
if (in_steps != 0 && step_active != 0 && line_indent == 8) {
|
||||
content = substr(raw, 9)
|
||||
validate_step_field(content)
|
||||
next
|
||||
}
|
||||
|
||||
if (in_steps != 0 && line_indent == 6 && raw !~ /^ *$/) {
|
||||
grammar_error("contains unsupported step-list syntax")
|
||||
}
|
||||
}
|
||||
|
||||
END {
|
||||
validate_job_shape()
|
||||
if (jobs_count != 1) {
|
||||
workflow_grammar_error("workflow must contain exactly one canonical jobs block")
|
||||
}
|
||||
if (invalid) {
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
' "${workflow}"; then
|
||||
fail "workflow structural validation failed: ${workflow#"${REPOSITORY_ROOT}"/}"
|
||||
fi
|
||||
|
||||
if ! grep -Fq -- './gradlew' "${workflow}" \
|
||||
&& ! grep -Fq -- 'gradle/actions/dependency-submission@' "${workflow}"; then
|
||||
continue
|
||||
fi
|
||||
((workflow_count += 1))
|
||||
|
||||
if ! jobs_in_workflow=$(
|
||||
awk -v workflow="${workflow#"${REPOSITORY_ROOT}"/}" \
|
||||
-v validation_action="${EXPECTED_VALIDATION_ACTION}" \
|
||||
-v dependency_action="${EXPECTED_DEPENDENCY_SUBMISSION_ACTION}" \
|
||||
-v guarded_gradle_if="${EXPECTED_GUARDED_GRADLE_IF}" '
|
||||
function reset_step(known_field) {
|
||||
step_active = 0
|
||||
run_block = 0
|
||||
step_kind = ""
|
||||
step_name = ""
|
||||
step_id = ""
|
||||
step_uses = ""
|
||||
step_uses_action = ""
|
||||
step_if = ""
|
||||
step_if_present = 0
|
||||
step_continue_on_error = 0
|
||||
step_gradle = 0
|
||||
step_gradle_line = 0
|
||||
step_unsupported_gradle = 0
|
||||
step_field_count = 0
|
||||
step_name_line = 0
|
||||
step_id_line = 0
|
||||
step_uses_line = 0
|
||||
step_extra_field = ""
|
||||
for (known_field in step_fields) {
|
||||
delete step_fields[known_field]
|
||||
delete step_field_raw[known_field]
|
||||
}
|
||||
}
|
||||
|
||||
function reset_job() {
|
||||
job = ""
|
||||
checkout_line = 0
|
||||
validation_line = 0
|
||||
gradle_line = 0
|
||||
in_steps = 0
|
||||
unsupported_gradle = 0
|
||||
reset_step()
|
||||
}
|
||||
|
||||
function indentation(line, first_non_space) {
|
||||
if (line ~ /^ *$/) {
|
||||
return length(line)
|
||||
}
|
||||
first_non_space = match(line, /[^ ]/)
|
||||
return first_non_space - 1
|
||||
}
|
||||
|
||||
function has_gradle_reference(line) {
|
||||
return index(line, "./gradlew") != 0 \
|
||||
|| index(line, "gradle/actions/dependency-submission@") != 0
|
||||
}
|
||||
|
||||
function trim(value) {
|
||||
sub(/^[[:space:]]+/, "", value)
|
||||
sub(/[[:space:]]+$/, "", value)
|
||||
return value
|
||||
}
|
||||
|
||||
function normalize_action(value, scalar, first, quote, closing, index_value) {
|
||||
scalar = trim(value)
|
||||
first = substr(scalar, 1, 1)
|
||||
if (first == "\"" || first == single_quote) {
|
||||
quote = first
|
||||
closing = index(substr(scalar, 2), quote)
|
||||
if (closing == 0) {
|
||||
return ""
|
||||
}
|
||||
return substr(scalar, 2, closing - 1)
|
||||
}
|
||||
sub(/[[:space:]]+#.*$/, "", scalar)
|
||||
return trim(scalar)
|
||||
}
|
||||
|
||||
function record_gradle(line_number) {
|
||||
step_gradle = 1
|
||||
if (step_gradle_line == 0) {
|
||||
step_gradle_line = line_number
|
||||
}
|
||||
if (gradle_line == 0) {
|
||||
gradle_line = line_number
|
||||
}
|
||||
}
|
||||
|
||||
function record_uses(value, line_number, action) {
|
||||
if (step_kind == "run") {
|
||||
if (index(value, "gradle/actions/dependency-submission@") != 0) {
|
||||
step_unsupported_gradle = 1
|
||||
}
|
||||
return
|
||||
}
|
||||
step_kind = "uses"
|
||||
action = normalize_action(value)
|
||||
step_uses = trim(value)
|
||||
step_uses_action = action
|
||||
step_uses_line = line_number
|
||||
if (checkout_line == 0 && action ~ /^actions\/checkout@/) {
|
||||
checkout_line = line_number
|
||||
}
|
||||
if (action == dependency_action) {
|
||||
record_gradle(line_number)
|
||||
} else if (index(action, "gradle/actions/dependency-submission@") != 0) {
|
||||
record_gradle(line_number)
|
||||
step_unsupported_gradle = 1
|
||||
}
|
||||
}
|
||||
|
||||
function record_run(value, line_number) {
|
||||
if (step_kind == "uses") {
|
||||
if (index(value, "./gradlew") != 0) {
|
||||
step_unsupported_gradle = 1
|
||||
}
|
||||
return
|
||||
}
|
||||
step_kind = "run"
|
||||
if (value ~ /^(\||>)[+-]?([[:space:]]+#.*)?$/) {
|
||||
run_block = 1
|
||||
} else if (index(value, "./gradlew") != 0) {
|
||||
record_gradle(line_number)
|
||||
}
|
||||
}
|
||||
|
||||
function record_step_field(content, line_number, separator, field, value) {
|
||||
separator = index(content, ":")
|
||||
field = substr(content, 1, separator - 1)
|
||||
value = substr(content, separator + 1)
|
||||
sub(/^[[:space:]]*/, "", value)
|
||||
step_fields[field] = 1
|
||||
step_field_raw[field] = trim(content)
|
||||
step_field_count++
|
||||
|
||||
if (field == "name") {
|
||||
step_name = trim(value)
|
||||
step_name_line = line_number
|
||||
} else if (field == "id") {
|
||||
step_id = trim(value)
|
||||
step_id_line = line_number
|
||||
} else if (field == "uses") {
|
||||
record_uses(value, line_number)
|
||||
} else if (field == "run") {
|
||||
record_run(trim(value), line_number)
|
||||
} else if (field == "if") {
|
||||
step_if_present = 1
|
||||
step_if = trim(value)
|
||||
} else if (field == "continue-on-error") {
|
||||
step_continue_on_error = 1
|
||||
}
|
||||
|
||||
if (field != "name" && field != "id" && field != "uses" && step_extra_field == "") {
|
||||
step_extra_field = step_field_raw[field]
|
||||
}
|
||||
}
|
||||
|
||||
function validate_wrapper_step() {
|
||||
if (step_uses_action != validation_reference) {
|
||||
return
|
||||
}
|
||||
if (step_extra_field != "") {
|
||||
printf "%s: job %s wrapper validation step contains unsupported field: %s\n", workflow, job, step_extra_field > "/dev/stderr"
|
||||
invalid = 1
|
||||
return
|
||||
}
|
||||
if (step_field_count != 3 \
|
||||
|| step_name != "Validate Gradle wrapper" \
|
||||
|| step_id != "gradle-wrapper-validation" \
|
||||
|| step_uses != validation_action \
|
||||
|| !(step_name_line < step_id_line && step_id_line < step_uses_line)) {
|
||||
printf "%s: job %s wrapper validation step must contain exact name, id, and uses fields only\n", workflow, job > "/dev/stderr"
|
||||
invalid = 1
|
||||
return
|
||||
}
|
||||
if (validation_line == 0) {
|
||||
validation_line = step_uses_line
|
||||
}
|
||||
}
|
||||
|
||||
function validate_gradle_step() {
|
||||
if (step_gradle == 0 && step_unsupported_gradle == 0) {
|
||||
return
|
||||
}
|
||||
if (step_unsupported_gradle != 0 || ("uses" in step_fields && "run" in step_fields)) {
|
||||
unsupported_gradle = 1
|
||||
}
|
||||
if (step_if_present != 0 && step_if != guarded_gradle_if) {
|
||||
printf "%s: job %s has Gradle step with unsupported if condition: %s\n", workflow, job, step_if > "/dev/stderr"
|
||||
invalid = 1
|
||||
}
|
||||
if (step_continue_on_error != 0) {
|
||||
printf "%s: job %s has Gradle step with unsupported field: %s\n", workflow, job, step_field_raw["continue-on-error"] > "/dev/stderr"
|
||||
invalid = 1
|
||||
}
|
||||
}
|
||||
|
||||
function finalize_step() {
|
||||
if (step_active == 0) {
|
||||
return
|
||||
}
|
||||
validate_wrapper_step()
|
||||
validate_gradle_step()
|
||||
}
|
||||
|
||||
function start_step() {
|
||||
finalize_step()
|
||||
reset_step()
|
||||
step_active = 1
|
||||
}
|
||||
|
||||
function validate_job() {
|
||||
finalize_step()
|
||||
if (job == "" || (gradle_line == 0 && unsupported_gradle == 0)) {
|
||||
return
|
||||
}
|
||||
gradle_jobs++
|
||||
if (unsupported_gradle != 0) {
|
||||
printf "%s: job %s uses a Gradle invocation outside the canonical workflow structure\n", workflow, job > "/dev/stderr"
|
||||
invalid = 1
|
||||
}
|
||||
if (gradle_line == 0) {
|
||||
return
|
||||
} else if (checkout_line == 0) {
|
||||
printf "%s: job %s invokes Gradle without checkout\n", workflow, job > "/dev/stderr"
|
||||
invalid = 1
|
||||
} else if (validation_line == 0) {
|
||||
printf "%s: job %s invokes Gradle without the exact pinned wrapper validation action\n", workflow, job > "/dev/stderr"
|
||||
invalid = 1
|
||||
} else if (!(checkout_line < validation_line && validation_line < gradle_line)) {
|
||||
printf "%s: job %s must order checkout, exact wrapper validation, then Gradle\n", workflow, job > "/dev/stderr"
|
||||
invalid = 1
|
||||
}
|
||||
}
|
||||
|
||||
BEGIN {
|
||||
in_jobs = 0
|
||||
invalid = 0
|
||||
gradle_jobs = 0
|
||||
single_quote = sprintf("%c", 39)
|
||||
validation_reference = validation_action
|
||||
sub(/[[:space:]]+#.*$/, "", validation_reference)
|
||||
reset_job()
|
||||
}
|
||||
|
||||
/^jobs:[[:space:]]*(#.*)?$/ {
|
||||
in_jobs = 1
|
||||
next
|
||||
}
|
||||
|
||||
in_jobs && /^[^[:space:]#]/ {
|
||||
validate_job()
|
||||
reset_job()
|
||||
in_jobs = 0
|
||||
}
|
||||
|
||||
in_jobs && /^ [A-Za-z0-9_.-]+:[[:space:]]*(#.*)?$/ {
|
||||
validate_job()
|
||||
reset_job()
|
||||
job = $0
|
||||
sub(/^ /, "", job)
|
||||
sub(/:.*/, "", job)
|
||||
next
|
||||
}
|
||||
|
||||
in_jobs && job != "" {
|
||||
raw = $0
|
||||
line_indent = indentation(raw)
|
||||
|
||||
if (run_block != 0) {
|
||||
if (raw ~ /^ *$/) {
|
||||
next
|
||||
}
|
||||
if (line_indent > 8) {
|
||||
if (index(raw, "./gradlew") != 0) {
|
||||
record_gradle(NR)
|
||||
}
|
||||
if (index(raw, "gradle/actions/dependency-submission@") != 0) {
|
||||
step_unsupported_gradle = 1
|
||||
}
|
||||
next
|
||||
}
|
||||
run_block = 0
|
||||
}
|
||||
|
||||
if (raw ~ /^ *#/) {
|
||||
next
|
||||
}
|
||||
|
||||
if (raw == " steps:") {
|
||||
in_steps = 1
|
||||
reset_step()
|
||||
next
|
||||
}
|
||||
|
||||
if (in_steps != 0 && line_indent == 4) {
|
||||
finalize_step()
|
||||
in_steps = 0
|
||||
reset_step()
|
||||
}
|
||||
|
||||
if (in_steps != 0 && raw ~ /^ - /) {
|
||||
start_step()
|
||||
content = substr(raw, 9)
|
||||
record_step_field(content, NR)
|
||||
next
|
||||
}
|
||||
|
||||
if (in_steps != 0 && step_active != 0 && line_indent == 8) {
|
||||
content = substr(raw, 9)
|
||||
record_step_field(content, NR)
|
||||
next
|
||||
}
|
||||
|
||||
if (has_gradle_reference(raw)) {
|
||||
unsupported_gradle = 1
|
||||
}
|
||||
}
|
||||
|
||||
END {
|
||||
validate_job()
|
||||
print gradle_jobs
|
||||
if (invalid) {
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
' "${workflow}"
|
||||
); then
|
||||
fail "workflow validation failed: ${workflow#"${REPOSITORY_ROOT}"/}"
|
||||
fi
|
||||
[[ "${jobs_in_workflow}" =~ ^[0-9]+$ ]] \
|
||||
|| fail "workflow parser returned an invalid Gradle job count: ${workflow#"${REPOSITORY_ROOT}"/}"
|
||||
((jobs_in_workflow > 0)) \
|
||||
|| fail "Gradle-running workflow contains no detected Gradle job: ${workflow#"${REPOSITORY_ROOT}"/}"
|
||||
((gradle_job_count += jobs_in_workflow))
|
||||
done < <(find "${WORKFLOWS_DIRECTORY}" -type f \( -name '*.yml' -o -name '*.yaml' \) -print0)
|
||||
|
||||
((workflow_count > 0)) || fail 'no Gradle-running workflow was found'
|
||||
((gradle_job_count > 0)) || fail 'no individual Gradle-running job was found'
|
||||
((workflow_lock_valid != 0)) \
|
||||
|| fail 'workflow lock mismatch: the workflow or composite-action set or bytes differ from the reviewed embedded manifest'
|
||||
|
||||
printf 'gradle-wrapper-contract: PASS\n'
|
||||
@@ -0,0 +1,63 @@
|
||||
name: reusable-gradle
|
||||
|
||||
# One place that knows how a Gradle job starts.
|
||||
#
|
||||
# Every job in this repository opened with the same preamble: checkout, a three-line pinned
|
||||
# wrapper-validation step, then the JDK/cache action. The wrapper step is gone (setup-gradle
|
||||
# validates wrappers itself), and this workflow removes the rest of the repetition for the jobs whose
|
||||
# only variation is the Gradle command they run.
|
||||
#
|
||||
# Jobs that need service containers, a matrix, artifact uploads or per-job env stay written out with
|
||||
# `./.github/actions/setup-gradle-java`, because expressing those through `workflow_call` inputs
|
||||
# means encoding YAML inside strings — which is how a "shared" workflow becomes less readable than
|
||||
# the duplication it replaced.
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
tasks:
|
||||
description: The Gradle task list, whitespace-separated.
|
||||
required: true
|
||||
type: string
|
||||
gradle-args:
|
||||
description: Flags appended after the task list.
|
||||
required: false
|
||||
type: string
|
||||
default: "--no-daemon --stacktrace"
|
||||
working-directory:
|
||||
description: Directory the wrapper is invoked from.
|
||||
required: false
|
||||
type: string
|
||||
default: src
|
||||
timeout-minutes:
|
||||
required: false
|
||||
type: number
|
||||
default: 30
|
||||
continue-on-error:
|
||||
description: Run the job as an advisory signal rather than a gate.
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
gradle:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: ${{ inputs.timeout-minutes }}
|
||||
continue-on-error: ${{ inputs.continue-on-error }}
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- uses: ./.github/actions/setup-gradle-java
|
||||
- name: Run ${{ inputs.tasks }}
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
env:
|
||||
GRADLE_TASKS: ${{ inputs.tasks }}
|
||||
GRADLE_ARGS: ${{ inputs.gradle-args }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Word-split on purpose: both inputs are task/flag lists. They come from this repository's
|
||||
# own workflow files, never from a pull request.
|
||||
# shellcheck disable=SC2086
|
||||
./gradlew ${GRADLE_TASKS} ${GRADLE_ARGS}
|
||||
@@ -1,5 +1,14 @@
|
||||
name: ci-quality-gates
|
||||
|
||||
# The pull-request gate. Everything here blocks a merge.
|
||||
#
|
||||
# The job list used to include `gate-matrix-lint`, which ran .github/scripts/verify-gate-matrix.sh
|
||||
# against .github/ci-gate-matrix.yml: a 1,025-line register of all 107 CI controls, checked for
|
||||
# consistency against the Gradle task graph and this workflow by a 568-line shell script, which was
|
||||
# itself checked by contract tests in :app-bootstrap. Adding one check meant editing Gradle, a
|
||||
# workflow, the matrix, the verifier's expectations and a Java test. The information was already in
|
||||
# the task graph and the job graph; the matrix was a third copy that had to be kept equal to both.
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
@@ -21,9 +30,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- name: Require the committed public-path security baseline
|
||||
run: |
|
||||
set -euo pipefail
|
||||
@@ -37,21 +43,22 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
- uses: ./.github/actions/setup-gradle-java
|
||||
- name: Check quality, public paths, and dependency locks
|
||||
# `ci`, not `check`. A leaf's `check` is that leaf's — compile, its tests, Spotless, Checkstyle
|
||||
# and Error Prone — and the repository-wide gates are named tasks of their own:
|
||||
# ci = every leaf check + architectureCheck + qualityCheck + configContractCheck
|
||||
# so CI runs strictly more than it used to while `./gradlew :domain-core:check` runs strictly
|
||||
# less.
|
||||
- name: Run the pull-request gate
|
||||
working-directory: src
|
||||
run: ./gradlew check verifyPublicPathSnapshot verifyDependencyLocks --warning-mode=fail --no-daemon --stacktrace
|
||||
run: ./gradlew ci verifyPublicPathSnapshot verifyDependencyLocks --warning-mode=fail --no-daemon --stacktrace
|
||||
# build-logic is an included build: its own suite is not reachable from the root project's
|
||||
# `check`, so the convention plugins every leaf applies shipped untested in CI. Kept as its
|
||||
# own step rather than folded into the aggregate invocation above, which
|
||||
# ConditionalTransportQualificationContractTest asserts on byte-for-byte.
|
||||
# `check`, so the convention plugins every leaf applies would otherwise ship untested.
|
||||
- name: Test the build-logic convention plugins
|
||||
working-directory: src
|
||||
run: ./gradlew -p build-logic test --no-daemon --stacktrace
|
||||
# Named as its own step because nothing else runs it: `check` does not depend on
|
||||
# graphqlStableTest, so the lane's required-class guard — the check that its module-boundary
|
||||
# suite has not silently stopped being discovered — protected nothing in CI. A separate step
|
||||
# keeps the aggregate invocation below byte-identical, which ConditionalTransportQualification
|
||||
# ContractTest asserts on, and the two tasks do not overlap.
|
||||
# suite has not silently stopped being discovered — would protect nothing in CI.
|
||||
- name: Qualify the GraphQL Stable lane
|
||||
working-directory: src
|
||||
run: ./gradlew :adapter:inbound:graphql:graphqlStableTest --no-daemon --stacktrace
|
||||
@@ -60,55 +67,33 @@ jobs:
|
||||
run: ./gradlew conditionalTransportQualification --no-daemon --stacktrace
|
||||
|
||||
sample-off:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: ./.github/actions/setup-gradle-java
|
||||
- name: Verify the application without the sample fixture
|
||||
working-directory: src
|
||||
run: ./gradlew :app-bootstrap:sampleOffTest verifyCleanArchitectureDependencies --no-daemon --stacktrace
|
||||
|
||||
gate-matrix-lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Verify the gate matrix against the repository
|
||||
run: bash .github/scripts/verify-gate-matrix.sh
|
||||
uses: ./.github/workflows/_reusable-gradle.yml
|
||||
with:
|
||||
tasks: ":app-bootstrap:sampleOffTest verifyCleanArchitectureDependencies"
|
||||
|
||||
redis-sdk:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: ./.github/actions/setup-gradle-java
|
||||
# Milestone A of the Redis wrapper/typed API plan: policy catalog, typed API parity,
|
||||
# permit provenance, connection isolation, and the executor guard. There is no real-server
|
||||
# lane yet — Tasks 10-17 add the contract suites that need one.
|
||||
- name: Verify the Redis SDK policy, API parity, and guardrail contracts
|
||||
working-directory: src
|
||||
run: >-
|
||||
./gradlew
|
||||
:shared-contract:edgeRateLimitContractTest
|
||||
:adapter:outbound:cache-redis:check
|
||||
verifyCleanArchitectureDependencies
|
||||
verifyEnvKeys
|
||||
verifyPublicPathSnapshot
|
||||
verifyConfigurationPropertiesProcessor
|
||||
--no-daemon --stacktrace
|
||||
# Milestone A of the Redis wrapper/typed API plan: policy catalog, typed API parity, permit
|
||||
# provenance, connection isolation, and the executor guard. There is no real-server lane yet.
|
||||
#
|
||||
# `verifyConfigurationPropertiesProcessor` used to be in this list. It is deleted: the parity it
|
||||
# enforced — a leaf declares Spring's configuration processor exactly when it owns
|
||||
# @ConfigurationProperties — is now what applying `ca.spring-config` means.
|
||||
# `verifyEnvKeys` is no longer named here either; it belongs to :app-bootstrap and runs through
|
||||
# `configContractCheck`, which the quality-gates job covers.
|
||||
uses: ./.github/workflows/_reusable-gradle.yml
|
||||
with:
|
||||
tasks: >-
|
||||
:shared-contract:edgeRateLimitContractTest
|
||||
:adapter:outbound:cache-redis:check
|
||||
:app-bootstrap:verifyEnvKeys
|
||||
verifyCleanArchitectureDependencies
|
||||
verifyPublicPathSnapshot
|
||||
|
||||
jpa-candidate-evidence:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: ./.github/actions/setup-gradle-java
|
||||
- name: Produce zero-skip JPA candidate manifests
|
||||
working-directory: src
|
||||
@@ -125,25 +110,20 @@ jobs:
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
# Advisory only. Quarantine expiry/drift remains blocking through verifyQuarantineSunset in check.
|
||||
# Advisory. The quarantine bucket runs so a flaky test is still executed and reported; it never
|
||||
# blocks. The 14-day sunset registry that used to make an expired quarantine entry a build failure
|
||||
# is gone — it was a 250-line YAML-and-Java parser guarding a registry with zero entries.
|
||||
quarantine:
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: ./.github/actions/setup-gradle-java
|
||||
- name: Run quarantined tests as an advisory signal
|
||||
working-directory: src
|
||||
run: ./gradlew quarantineTest --no-daemon
|
||||
uses: ./.github/workflows/_reusable-gradle.yml
|
||||
with:
|
||||
tasks: quarantineTest
|
||||
gradle-args: "--no-daemon"
|
||||
continue-on-error: true
|
||||
|
||||
release-gate:
|
||||
needs:
|
||||
- quality-gates
|
||||
- sample-off
|
||||
- gate-matrix-lint
|
||||
- redis-sdk
|
||||
- jpa-candidate-evidence
|
||||
if: always()
|
||||
@@ -156,7 +136,6 @@ jobs:
|
||||
env:
|
||||
QUALITY_RESULT: ${{ needs.quality-gates.result }}
|
||||
SAMPLE_OFF_RESULT: ${{ needs.sample-off.result }}
|
||||
MATRIX_RESULT: ${{ needs.gate-matrix-lint.result }}
|
||||
REDIS_RESULT: ${{ needs.redis-sdk.result }}
|
||||
JPA_CANDIDATE_RESULT: ${{ needs.jpa-candidate-evidence.result }}
|
||||
run: |
|
||||
@@ -164,7 +143,6 @@ jobs:
|
||||
for result in \
|
||||
"${QUALITY_RESULT}" \
|
||||
"${SAMPLE_OFF_RESULT}" \
|
||||
"${MATRIX_RESULT}" \
|
||||
"${REDIS_RESULT}" \
|
||||
"${JPA_CANDIDATE_RESULT}"; do
|
||||
if [[ "${result}" != "success" ]]; then
|
||||
@@ -174,23 +152,16 @@ jobs:
|
||||
done
|
||||
echo "release-gate: all current blocking quality jobs succeeded."
|
||||
|
||||
# `needs` cannot reach another workflow, so every gate .github/ci-gate-matrix.yml marks
|
||||
# release_blocking outside this file was invisible here: the field was read by nothing but an
|
||||
# enum check in verify-gate-matrix.sh. filesystem-vulnerability-scan
|
||||
# (dependency-vulnerability.yml::trivy-fs) is release_blocking: true and blocks on
|
||||
# CRITICAL/HIGH and on the CISA KEV catalogue — it could be red while this job reported green
|
||||
# and nothing in the repository joined the two.
|
||||
# `needs` cannot reach another workflow, so a blocking check in another file has to be required
|
||||
# by result. dependency-vulnerability.yml answers the same pull_request and push-to-main
|
||||
# triggers as this workflow and trivy-fs carries no `if:` guard, so its check run always exists
|
||||
# for this SHA — which is what makes it requirable rather than a matter of scheduling luck.
|
||||
# Only `success` passes: a skipped or cancelled security scan is not a scan.
|
||||
#
|
||||
# dependency-vulnerability.yml answers the same pull_request and push-to-main triggers as this
|
||||
# workflow and trivy-fs carries no `if:` guard, so its check run always exists for this SHA.
|
||||
# That is what makes it requirable by result rather than by scheduling luck. Only `success`
|
||||
# passes: a skipped or cancelled security scan is not a scan.
|
||||
#
|
||||
# The other release_blocking gates outside this file run on triggers this job does not share
|
||||
# and so cannot be required here without changing when they run: release.yml, jpa-release.yml
|
||||
# and fileserver-release.yml answer a release tag, and object-storage-qualification.yml and
|
||||
# messaging-certification.yml answer a path filter or a schedule. That is left as a stated gap
|
||||
# rather than a silently different one.
|
||||
# The release-tag and path-filtered workflows (release.yml, jpa-release.yml,
|
||||
# fileserver-certification.yml, object-storage-qualification.yml, messaging-certification.yml)
|
||||
# run on triggers this job does not share, so they cannot be required here without changing
|
||||
# when they run. That is a stated gap, not a hidden one.
|
||||
- name: Require the cross-workflow release-blocking checks to have succeeded
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
|
||||
@@ -35,9 +35,6 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: ./.github/actions/setup-gradle-java
|
||||
- name: Submit the resolved Gradle dependency graph
|
||||
uses: gradle/actions/dependency-submission@748248ddd2a24f49513d8f472f81c3a07d4d50e1 # gradle/actions@v4.4.4
|
||||
|
||||
+11
-17
@@ -1,7 +1,13 @@
|
||||
name: fileserver-release
|
||||
name: fileserver-certification
|
||||
|
||||
# The gate a release must clear. Its job list is deliberately the same shape as the support matrix:
|
||||
# nothing may be advertised at a support level whose evidence job is absent here.
|
||||
# The certification a release must clear. Its job list is deliberately the same shape as the support
|
||||
# matrix: nothing may be advertised at a support level whose evidence job is absent here.
|
||||
#
|
||||
# Named "certification", not "release", and the name is the point. This workflow proves a storage
|
||||
# topology, a support matrix and a telemetry redaction claim. It deploys nothing and holds no cluster
|
||||
# credential. Calling it `fileserver-release.yml` read as if GitHub Actions released the fileserver,
|
||||
# which is the CI/CD boundary this repository has now fixed in docs/ci-cd/boundary.md: GitHub Actions
|
||||
# tests, scans and publishes artifacts; Argo CD deploys.
|
||||
#
|
||||
# It used to be workflow_dispatch only, which made that sentence false: the four jobs below are the
|
||||
# only place the fileserver support matrix, the PVC manifest and the telemetry redaction proof are
|
||||
@@ -36,9 +42,6 @@ jobs:
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: ./.github/actions/setup-gradle-java
|
||||
- name: Run the architecture-wide dependency and module verification
|
||||
working-directory: src
|
||||
@@ -62,9 +65,6 @@ jobs:
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: ./.github/actions/setup-gradle-java
|
||||
- name: Prove every support claim maps to a job and every endpoint is documented
|
||||
working-directory: src
|
||||
@@ -79,9 +79,6 @@ jobs:
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
# This job checks the manifest, and only the manifest. It deliberately does not apply anything
|
||||
# to a cluster.
|
||||
#
|
||||
@@ -96,8 +93,8 @@ jobs:
|
||||
# The cluster result comes from an operator running infra/fileserver/kubernetes/
|
||||
# pvc-certification-job.yaml against a real cluster and recording it in
|
||||
# docs/fileserver/storage-certification.md. That is registered as
|
||||
# fileserver-pvc-cluster-certification (delegated-pending) in .github/ci-gate-matrix.yml, so
|
||||
# the absence is a tracked control rather than a green check.
|
||||
# docs/fileserver/storage-certification.md, and the absence of a cluster result is stated
|
||||
# there rather than hidden behind a green check.
|
||||
- name: Check the certification manifest still says what the claim depends on
|
||||
run: |
|
||||
set -euo pipefail
|
||||
@@ -115,9 +112,6 @@ jobs:
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: ./.github/actions/setup-gradle-java
|
||||
- name: Prove telemetry carries no filename, path, or raw identifier
|
||||
working-directory: src
|
||||
@@ -24,9 +24,6 @@ jobs:
|
||||
FILESERVER_NFS_TESTS: "true"
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: ./.github/actions/setup-gradle-java
|
||||
- name: Start the NFSv4 certification environment
|
||||
run: docker compose -f infra/fileserver/nfs/compose.yml up -d --wait
|
||||
@@ -46,9 +43,6 @@ jobs:
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: ./.github/actions/setup-gradle-java
|
||||
- name: Run the crash matrix and reconciliation suites
|
||||
working-directory: src
|
||||
@@ -65,9 +59,6 @@ jobs:
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: ./.github/actions/setup-gradle-java
|
||||
- name: Run the large-file and slow-client suites under a constrained heap
|
||||
working-directory: src
|
||||
@@ -86,9 +77,6 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: ./.github/actions/setup-gradle-java
|
||||
- name: Prove no run commits bytes from a stale lease
|
||||
working-directory: src
|
||||
|
||||
@@ -44,9 +44,6 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: ./.github/actions/setup-gradle-java
|
||||
- name: Run the fileserver application and architecture suites
|
||||
working-directory: src
|
||||
@@ -62,9 +59,6 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: ./.github/actions/setup-gradle-java
|
||||
- name: Certify the local content store against the shared contract
|
||||
working-directory: src
|
||||
@@ -79,9 +73,6 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: ./.github/actions/setup-gradle-java
|
||||
- name: Run the servlet and reactive transport contracts
|
||||
working-directory: src
|
||||
@@ -96,9 +87,6 @@ jobs:
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: ./.github/actions/setup-gradle-java
|
||||
- name: Run the path, filename, range, and problem-detail hardening suite
|
||||
working-directory: src
|
||||
@@ -114,9 +102,6 @@ jobs:
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: ./.github/actions/setup-gradle-java
|
||||
- name: Prove transfer cost does not scale with file size
|
||||
working-directory: src
|
||||
|
||||
@@ -9,20 +9,13 @@ name: integration-main
|
||||
#
|
||||
# Two kinds of work live here.
|
||||
#
|
||||
# 1. The documentation-drift gates. They used to be `dependsOn` of the root `check`, so a README
|
||||
# sentence about a renamed task failed a compile-and-test run and the fix was to edit a document
|
||||
# before unrelated code could build. src/build.gradle now aggregates them as
|
||||
# `verifyDocumentationContracts` and leaves them out of `check`. That demotion is only half a
|
||||
# change: a gate nothing invokes has not been demoted, it has been deleted. This job is the other
|
||||
# half, and it is the reason the four gates still run at all.
|
||||
#
|
||||
# 2. The lanes that need a machine that is not simultaneously compiling something else — load,
|
||||
# 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.
|
||||
#
|
||||
# 3. Lanes that were registered in Gradle and invoked by nothing. Ten Gradle tasks — six MongoDB
|
||||
# 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
|
||||
@@ -54,24 +47,17 @@ concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
# verifyReadmeCommands, verifyDocumentedLeafCount, verifyRunbookReferences and
|
||||
# verifyTestSourceSetRegistry, as one task. Named as the aggregate rather than as four steps so
|
||||
# that adding a fifth documentation gate is a build-file edit and not a workflow edit — and so
|
||||
# that the demotion out of `check` has exactly one consumer to point at.
|
||||
documentation-contracts:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: ./.github/actions/setup-gradle-java
|
||||
- name: Verify the documentation contracts
|
||||
working-directory: src
|
||||
run: ./gradlew verifyDocumentationContracts --no-daemon --stacktrace
|
||||
# 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:
|
||||
@@ -79,9 +65,6 @@ jobs:
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: ./.github/actions/setup-gradle-java
|
||||
- name: Run the load, abuse and shutdown lanes on every container
|
||||
working-directory: src
|
||||
@@ -107,9 +90,6 @@ jobs:
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: ./.github/actions/setup-gradle-java
|
||||
- name: Inject TCP faults against a real upstream
|
||||
working-directory: src
|
||||
@@ -128,9 +108,6 @@ jobs:
|
||||
GRADLE_OPTS: -Dorg.gradle.project.performance.assertions.enabled=true
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: ./.github/actions/setup-gradle-java
|
||||
- name: Certify pool, streaming, retry, and rotation bounds
|
||||
working-directory: src
|
||||
@@ -144,14 +121,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
# Experimental by design (D-08): the result is reported, never used to block a merge. Registered
|
||||
# in .github/ci-gate-matrix.yml as release_blocking: false so that "this job cannot fail the
|
||||
# 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
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: ./.github/actions/setup-gradle-java
|
||||
- name: Exercise the experimental HTTP/3 opt-in
|
||||
working-directory: src
|
||||
@@ -173,7 +147,7 @@ jobs:
|
||||
# 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
|
||||
# .github/scripts/verify-gate-matrix.sh reads these command lines to prove each registered lane is
|
||||
# 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:
|
||||
@@ -184,9 +158,6 @@ jobs:
|
||||
TESTCONTAINERS_REUSE_ENABLE: "false"
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: ./.github/actions/setup-gradle-java
|
||||
- name: Single-node replica set contract lane
|
||||
working-directory: src
|
||||
@@ -236,9 +207,6 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: ./.github/actions/setup-gradle-java
|
||||
- name: Qualify the messaging contract, catalog, binding and schema evidence
|
||||
working-directory: src
|
||||
@@ -262,9 +230,6 @@ jobs:
|
||||
TESTCONTAINERS_REUSE_ENABLE: "false"
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: ./.github/actions/setup-gradle-java
|
||||
- name: Run the real-PostgreSQL integration contracts
|
||||
working-directory: src
|
||||
|
||||
@@ -21,9 +21,6 @@ jobs:
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: ./.github/actions/setup-gradle-java
|
||||
- name: Report Hibernate ORM 8 compatibility
|
||||
id: compatibility-probe
|
||||
|
||||
@@ -21,9 +21,6 @@ jobs:
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: ./.github/actions/setup-gradle-java
|
||||
- name: Report Jakarta Persistence 4.0 compatibility
|
||||
id: compatibility-probe
|
||||
|
||||
@@ -27,9 +27,6 @@ jobs:
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: ./.github/actions/setup-gradle-java
|
||||
- name: Report PostgreSQL 19 compatibility
|
||||
id: compatibility-probe
|
||||
|
||||
@@ -30,9 +30,6 @@ jobs:
|
||||
postgresql: ["16", "17", "18"]
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: ./.github/actions/setup-gradle-java
|
||||
- name: Certify the platform against PostgreSQL ${{ matrix.postgresql }}
|
||||
working-directory: src
|
||||
@@ -48,9 +45,6 @@ jobs:
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: ./.github/actions/setup-gradle-java
|
||||
- name: Reproduce deadlock, serialization, and commit-ambiguity scenarios
|
||||
working-directory: src
|
||||
@@ -65,9 +59,6 @@ jobs:
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: ./.github/actions/setup-gradle-java
|
||||
- name: Run the query plan and database security suites
|
||||
working-directory: src
|
||||
@@ -83,9 +74,6 @@ jobs:
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: ./.github/actions/setup-gradle-java
|
||||
- name: Verify pool saturation and REQUIRES_NEW connection behaviour
|
||||
working-directory: src
|
||||
|
||||
@@ -26,9 +26,6 @@ jobs:
|
||||
JPA_EVIDENCE_TOPOLOGY: postgresql-16-testcontainers-tls-and-fault-matrix
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: ./.github/actions/setup-gradle-java
|
||||
- name: Verify the production-profile JPA R2 manifest DAG
|
||||
working-directory: src
|
||||
|
||||
@@ -43,9 +43,6 @@ jobs:
|
||||
postgresql: ["16", "17", "18"]
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: ./.github/actions/setup-gradle-java
|
||||
- name: Run the full JPA release gate on PostgreSQL ${{ matrix.postgresql }}
|
||||
working-directory: src
|
||||
@@ -118,9 +115,6 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: ./.github/actions/setup-gradle-java
|
||||
- name: Verify architecture boundaries and the support matrix
|
||||
working-directory: src
|
||||
|
||||
@@ -36,9 +36,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: ./.github/actions/setup-gradle-java
|
||||
- name: Certify the Kafka adapter against a real broker
|
||||
working-directory: src
|
||||
|
||||
@@ -55,9 +55,6 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: ./.github/actions/setup-gradle-java
|
||||
- name: Compile and format check
|
||||
working-directory: src
|
||||
@@ -89,7 +86,7 @@ jobs:
|
||||
- name: Configuration surface
|
||||
working-directory: src
|
||||
run: |
|
||||
./gradlew verifyEnvKeys verifyPublicPathSnapshot --console=plain
|
||||
./gradlew :app-bootstrap:verifyEnvKeys verifyPublicPathSnapshot --console=plain
|
||||
./gradlew verifyNotificationApiSurface verifyNotificationConfiguration --console=plain
|
||||
# A support grade is a promise about production behaviour. This refuses one the pipeline
|
||||
# cannot back — the check that would have caught five channels reading "Stable" while no
|
||||
@@ -108,9 +105,6 @@ jobs:
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: ./.github/actions/setup-gradle-java
|
||||
# This job is named for ambiguity, restart recovery and callback burst. It used to run a
|
||||
# unit-test filter and then `test` — neither of which restarts anything or bursts anything —
|
||||
|
||||
@@ -34,9 +34,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: ./.github/actions/setup-gradle-java
|
||||
- name: Run non-skipping Poster image migration qualification
|
||||
working-directory: src
|
||||
@@ -46,9 +43,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: ./.github/actions/setup-gradle-java
|
||||
- name: Run exact-release MinIO managed contract
|
||||
working-directory: src
|
||||
@@ -59,9 +53,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: ./.github/actions/setup-gradle-java
|
||||
- name: Run digest-pinned MinIO and Toxiproxy fault contract
|
||||
working-directory: src
|
||||
@@ -82,9 +73,6 @@ jobs:
|
||||
OBJECT_STORAGE_AWS_EXPECTED_OWNER: ${{ secrets.OBJECT_STORAGE_AWS_EXPECTED_OWNER }}
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: ./.github/actions/setup-gradle-java
|
||||
- name: Run protected AWS common-subset qualification
|
||||
working-directory: src
|
||||
|
||||
@@ -136,9 +136,6 @@ jobs:
|
||||
timeout-minutes: 40
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: ./.github/actions/setup-gradle-java
|
||||
- name: Compare the wire contract across Tomcat, Jetty and Reactor Netty
|
||||
working-directory: src
|
||||
@@ -165,9 +162,6 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: ./.github/actions/setup-gradle-java
|
||||
- name: Run the proxy, prefix and spoofing contract behind a real Nginx
|
||||
working-directory: src
|
||||
@@ -184,9 +178,6 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: ./.github/actions/setup-gradle-java
|
||||
- name: Run the runtime contract on the second servlet container
|
||||
working-directory: src
|
||||
@@ -205,9 +196,6 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: ./.github/actions/setup-gradle-java
|
||||
- name: Run the upgrade and forwarded-header contract behind a real Nginx
|
||||
working-directory: src
|
||||
@@ -230,9 +218,6 @@ jobs:
|
||||
transport: [apache, jdk, reactor]
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: ./.github/actions/setup-gradle-java
|
||||
- name: Certify one transport against the shared contract
|
||||
working-directory: src
|
||||
@@ -254,9 +239,6 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: ./.github/actions/setup-gradle-java
|
||||
- name: Run the next-major Spring compatibility lane
|
||||
working-directory: src
|
||||
@@ -280,9 +262,6 @@ jobs:
|
||||
postgresql: ["16", "18"]
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: ./.github/actions/setup-gradle-java
|
||||
- name: Certify the platform against PostgreSQL ${{ matrix.postgresql }}
|
||||
working-directory: src
|
||||
@@ -300,9 +279,6 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: ./.github/actions/setup-gradle-java
|
||||
- name: Run the migration upgrade smoke scenarios
|
||||
working-directory: src
|
||||
|
||||
@@ -116,9 +116,6 @@ jobs:
|
||||
matrix: ${{ fromJson(needs.lanes.outputs.matrix) }}
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: ./.github/actions/setup-gradle-java
|
||||
- name: Start the topology
|
||||
env:
|
||||
|
||||
@@ -19,11 +19,13 @@ name: release
|
||||
# design one:
|
||||
# * jpa-release.yml — JpaReleaseRenderingTest reads that exact path and holds its PostgreSQL
|
||||
# matrix and promotion list to src/config/jpa/release-registry.json.
|
||||
# * fileserver-release.yml — FileserverDocumentationCoverageTest requires every job id named in
|
||||
# docs/fileserver/support-matrix.md to be defined in a `.github/workflows/fileserver-*.yml`.
|
||||
# * fileserver-certification.yml — FileserverDocumentationCoverageTest requires every job id named
|
||||
# in docs/fileserver/support-matrix.md to be defined in a `.github/workflows/fileserver-*.yml`.
|
||||
# It is named "certification" rather than "release" on purpose: it certifies a storage topology
|
||||
# and a support matrix, it deploys nothing, and the CI/CD boundary in docs/ci-cd/boundary.md
|
||||
# says GitHub Actions does not deploy.
|
||||
# Folding either one in needs its src-side test (and, for fileserver, the support document) changed
|
||||
# in the same commit. Until then the image job below cannot wait on them, which is what the
|
||||
# `container-release-evidence-join` row in .github/ci-gate-matrix.yml records.
|
||||
# in the same commit. Until then the image job below cannot wait on them — a stated gap.
|
||||
#
|
||||
# The image job DOES now wait on the evidence jobs in this file, which is new: while the image build
|
||||
# lived in its own workflow it could publish while a sibling suite was still running or already red,
|
||||
@@ -52,9 +54,6 @@ jobs:
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: ./.github/actions/setup-gradle-java
|
||||
- name: Verify architecture boundaries and the published surfaces
|
||||
working-directory: src
|
||||
@@ -62,7 +61,7 @@ jobs:
|
||||
./gradlew
|
||||
verifyCleanArchitectureDependencies
|
||||
verifyPublicPathSnapshot
|
||||
verifyEnvKeys
|
||||
:app-bootstrap:verifyEnvKeys
|
||||
:app-bootstrap:test --tests 'dev.caskeleton.bootstrap.architecture.*'
|
||||
--no-daemon
|
||||
--stacktrace
|
||||
@@ -80,9 +79,6 @@ jobs:
|
||||
timeout-minutes: 90
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: ./.github/actions/setup-gradle-java
|
||||
- name: Run every web lane, Stable and Advanced
|
||||
working-directory: src
|
||||
@@ -108,9 +104,6 @@ jobs:
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: ./.github/actions/setup-gradle-java
|
||||
- name: Run every websocket lane, Stable and Advanced
|
||||
working-directory: src
|
||||
@@ -151,9 +144,6 @@ jobs:
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: ./.github/actions/setup-gradle-java
|
||||
- name: In-process contract lane
|
||||
working-directory: src
|
||||
@@ -172,11 +162,9 @@ jobs:
|
||||
path: src/grpc/grpc-testkit/build/reports/tests/
|
||||
if-no-files-found: warn
|
||||
|
||||
# Each declared gate runs as its own single-line `./gradlew <task>` step, because
|
||||
# .github/scripts/verify-gate-matrix.sh reads these commands to prove the gate is actually
|
||||
# executed — a folded or flag-laden command would make the declaration in
|
||||
# .github/ci-gate-matrix.yml unverifiable. The architecture dependency gate that used to end this
|
||||
# list is now architecture-and-surface above; it was the fourth copy of the same invocation.
|
||||
# Each gate runs as its own single-line `./gradlew <task>` step so that a failure names the gate
|
||||
# rather than a folded command. The architecture dependency gate that used to end this list is now
|
||||
# architecture-and-surface above; it was the fourth copy of the same invocation.
|
||||
httpclient-release-gate:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
@@ -189,9 +177,6 @@ jobs:
|
||||
GRADLE_OPTS: -Dorg.gradle.project.performance.assertions.enabled=true
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: ./.github/actions/setup-gradle-java
|
||||
- name: Focused module tests
|
||||
run: ./gradlew :adapter:outbound:httpclient:test --no-daemon --stacktrace
|
||||
@@ -241,9 +226,6 @@ jobs:
|
||||
# The builder stage inside src/Dockerfile runs this repository's Gradle wrapper to produce the
|
||||
# JAR that becomes the image. Validating the wrapper here checks the thing that is about to
|
||||
# execute, before it executes, rather than after an image already exists.
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
# The tag is the release identity; everything below derives from it. A tag that does not parse
|
||||
# stops the release here, rather than producing an image named after whatever ref happened to
|
||||
# be checked out.
|
||||
@@ -393,8 +375,9 @@ jobs:
|
||||
# a green trivy-fs has never been evidence about the artifact.
|
||||
#
|
||||
# --ignorefile is mandatory here as everywhere: .trivyignore.yaml is the single suppression
|
||||
# source and verifyTrivyignore enforces that each entry carries a rationale and an expiry.
|
||||
# An inline --skip or a second ignore file would be a suppression nobody reviews.
|
||||
# source. Each entry carries a rationale and an expiry by policy, reviewed through CODEOWNERS
|
||||
# (.github/dependency-vulnerability-policy.md); an inline --skip or a second ignore file would
|
||||
# be a suppression nobody reviews.
|
||||
#
|
||||
# This step is the reason `docker push` is further down. A vulnerable image that was pushed and
|
||||
# then reported is already pullable by everything that watches the tag.
|
||||
|
||||
Reference in New Issue
Block a user