203 lines
6.2 KiB
Bash
203 lines
6.2 KiB
Bash
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
|
|
readonly REPO_ROOT="$(git -C "${SCRIPT_DIR}" rev-parse --show-toplevel)"
|
|
readonly EXPECTED_SCRIPT_DIR="$(cd -- "${REPO_ROOT}/.github/scripts" && pwd -P)"
|
|
readonly MATRIX="${REPO_ROOT}/.github/ci-gate-matrix.yml"
|
|
readonly EXPECTED_GATE_COUNT=26
|
|
|
|
if [[ "${SCRIPT_DIR}" != "${EXPECTED_SCRIPT_DIR}" ]]; then
|
|
printf '::error::gate-matrix-lint: script resolved outside the repository .github/scripts directory\n' >&2
|
|
exit 1
|
|
fi
|
|
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}"
|
|
}
|
|
|
|
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 ! grep -RqsE -- "tasks\\.register\\(['\"]${ref}['\"]" "${REPO_ROOT}/src" \
|
|
--include='build.gradle'; then
|
|
failures+=("gate '${id}' references unregistered Gradle task '${ref}'")
|
|
continue
|
|
fi
|
|
;;
|
|
gradle-plugin-task)
|
|
plugin="${ref%@*}"
|
|
task="${ref#*@}"
|
|
if [[ "${plugin}" == "${ref}" || -z "${task}" ]]; then
|
|
failures+=("gate '${id}' must use plugin@task for gradle-plugin-task")
|
|
continue
|
|
fi
|
|
if ! grep -RqsE -- "(id|apply plugin:)[[:space:]]+['\"]${plugin}['\"]" "${REPO_ROOT}/src" \
|
|
--include='build.gradle'; 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_body "${workflow_file}" "${job}" | grep -Eqs -- '\./gradlew[[:space:]]+check([[:space:]]|$)'; 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_body "${workflow_file}" "${job}" | grep -Fqs -- "${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'
|