The review found one defect shape repeated across the platform: surfaces that were declared, bound, and documented, but that nothing read. An operator configuring fullUrlRecording, bodyLogging, retry.policy, validatedDnsPinning, timeout.dns, or any of ten declared metric names got a guarantee the code never delivered. Every such surface is now in exactly one of three states -- wired for real, rejected at startup, or registered in a test-enforced gap list with its reason. No silent no-ops remain. P0: - Activate the platform from bootstrap behind app.httpclient.enabled, with a single auto-configuration importing the nine child configurations. - Give the platform a strict, repository-level ENV contract: 74 leaf fields derived from the settings record tree, unknown APP_HTTPCLIENT_* rejected. - Route typed HTTP service clients through the call kernel via KernelHttpExchangeAdapter, so they stop bypassing platform policy. - Pin dynamic-target DNS resolution to the socket for the life of a call, closing the resolve-then-connect TOCTOU / rebinding window. - Actually transmit the idempotency key, and make retry eligibility depend on transmission rather than on merely holding one. - Reject reactive authentication and reactive redirect at startup instead of declaring support that does not function. - Fix the Reactor-only Stable contract row so the lane stops failing. - Stop advertising HTTP/3 on a transport that negotiates HTTP/1. P1 covers execution and retry accounting, redirect security (per-hop target guarding, sensitive-header stripping, 303 body handling), runtime rotation and transport resource ownership keyed by generation, dynamic-target hardening (subdomain matching, global-unicast classification, strict CIDR parsing), protocol intent, pool and timeout wiring, streaming and body limits, observability parity, and OAuth single-flight refresh on a bounded pool with a bounded wait. P2 covers configuration and documentation drift, the Gradle check wiring for the four hermetic lanes, and the CI gate matrix. Two test-quality defects surfaced while closing these: the HTTP/2 stream saturation test ran against cleartext HTTP/1.1 while asserting nothing about the protocol, and an OAuth contention test slept on a latch that could fire before the callers it meant to observe. Both now assert what their names claim. Verification run: :adapter:outbound:httpclient:check and :app-bootstrap:check (checkstyle, spotless, spotbugs, and the four hermetic lanes), verifyCleanArchitectureDependencies, verifyEnvKeys, verifyOneTypePerFile, verifyDependencyLocks, the documentation and gate-matrix verifiers, and the performance lane against a real TLS+ALPN HTTP/2 server. Not executed, and tracked rather than claimed: Docker/Toxiproxy fault injection, JMH, a real QUIC/HTTP3 server, a real Spring Framework 6.2 distribution (now a delegated-pending gate), live OAuth/TLS/proxy/DNS integration, and a whole-repository check. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
366 lines
11 KiB
Bash
366 lines
11 KiB
Bash
#!/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"
|
|
# Deliberately a literal: a gate silently appearing or disappearing is the drift this lint exists to
|
|
# catch, so growing the matrix is an explicit edit here. 38 as of the HTTP Client platform hardening,
|
|
# which registered httpclient-spring62-runtime as a delegated-pending control — the 6.2 *runtime*
|
|
# claim, distinct from the API-surface scan that was standing in for it.
|
|
readonly EXPECTED_GATE_COUNT=38
|
|
|
|
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
|
|
|
|
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}"
|
|
}
|
|
|
|
gradle_custom_task_is_registered() {
|
|
local task_name="$1"
|
|
local build_file
|
|
while IFS= read -r -d '' build_file; do
|
|
if gradle_custom_task_is_registered_in_build_file "${task_name}" "${build_file}"; then
|
|
return 0
|
|
fi
|
|
done < <(find "${REPO_ROOT}/src" -type f -name '*.gradle' -print0)
|
|
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
|
|
}
|
|
|
|
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
|
|
|
|
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
|
|
if [[ "${mechanism}" == "gradle-custom-task" ]] \
|
|
&& ! grep -RqsE -- "dependsOn.*named\\(['\"]${ref}['\"]\\)" "${REPO_ROOT}/src" \
|
|
--include='build.gradle'; 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 != EXPECTED_GATE_COUNT )); then
|
|
failures+=("matrix has ${total} gates; expected ${EXPECTED_GATE_COUNT}")
|
|
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'
|