748 lines
24 KiB
Bash
Executable File
748 lines
24 KiB
Bash
Executable File
#!/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' }}"
|
|
# Workflow-lock update procedure (only after intentional review of the complete workflow 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
|
|
# Replace this entire sorted array in the same reviewed change. Never refresh a single digest
|
|
# merely to make this verifier pass.
|
|
readonly EXPECTED_WORKFLOW_LOCK=(
|
|
'a5986c6d865e28d6160dc09c513c430c9d9c38d154c67423cb34448cb1e9863c .github/workflows/ci-quality-gates.yml'
|
|
'59de260a70c2c0a0d686d97035a189dc0567395977dfa18758f1a2d89d15a00d .github/workflows/dependency-vulnerability.yml'
|
|
'1b3220c922f954500f727c6a799b24e4962915845b9248e8e496e5050e829f28 .github/workflows/fileserver-nightly.yml'
|
|
'26812e16b8d6e4472543ddd49c7b16ee6b7697834ddbb653fa0424befd71c544 .github/workflows/fileserver-pr.yml'
|
|
'86a240c4ce7d0d293616e30de30ed77bcfdc700fedb8916f083eda9567099096 .github/workflows/fileserver-release.yml'
|
|
'58e28f3358d794ca08f4aa8df4516e03f50a9ee58488b3f0d2619998e069ef14 .github/workflows/httpclient-contract.yml'
|
|
'823bc346e58a58b2c0814cd1e3e55ec90d360c138419ec3d8f05deb59c62c7eb .github/workflows/httpclient-nightly.yml'
|
|
'ad84000efc438ee7439517b8f85819e62b13dab0aa4f94066c2905060f3bb581 .github/workflows/httpclient-release.yml'
|
|
'3be84c9f15fa3b2ac5a085f8d725ec6d05e7007ae0b433da9e79b3bf340d57ea .github/workflows/jpa-next-hibernate8.yml'
|
|
'a2b74bfb3af12d6d03cd2ea8a5e48490dd131afb89b79694d498c5798387ac53 .github/workflows/jpa-next-jpa4.yml'
|
|
'cd955ef4af895df477896dad9577810f010b2beea8570b09b008f9e94e928bd0 .github/workflows/jpa-next-postgresql19.yml'
|
|
'b56b548a867b74eaeccb42e7df4f4e52cf7ce657ab27f91e2c8d7ea9944d64af .github/workflows/jpa-nightly.yml'
|
|
'04851f44ba94533bfbc8fabe2b3a2b408726a9996e86ed3864986d1499d16b50 .github/workflows/jpa-pr.yml'
|
|
'59cb3a0ffc687a15eefe96bc5e3a70d42be78e1cc85d2e7f7880dac6124ca4c7 .github/workflows/jpa-r2-evidence.yml'
|
|
'4748f2ba0a0b77dc1a858ebcfa7db6e41627d97843df5f0aa978bc2facccaad2 .github/workflows/jpa-release.yml'
|
|
'5be7e931db749029d89787da042d6d7cf8e683d60698bd8a2993c29db26355fb .github/workflows/link-check.yml'
|
|
'4e4ccfa267ecd63b9369803d49f2dbdb2fa899517ad4cf23ab11d29104557a91 .github/workflows/notification-platform.yml'
|
|
'64245586cd5936f1a5647b57f2cd9acd316f96fd75f713b1890decb812e7d5fe .github/workflows/object-storage-qualification.yml'
|
|
'cbc104ea486c746229895e804e3be7716e056a02cce0588c537bce9f442f8b38 .github/workflows/redis-sdk-topology.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"
|
|
|
|
[[ -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
|
|
actual_workflow_lock=()
|
|
while IFS= read -r -d '' locked_workflow; do
|
|
locked_workflow_relative=${locked_workflow#"${REPOSITORY_ROOT}"/}
|
|
if [[ -L "${locked_workflow}" || ! -f "${locked_workflow}" ]]; then
|
|
locked_workflow_sha256='<invalid-file-type>'
|
|
else
|
|
locked_workflow_sha256=$(sha256sum -- "${locked_workflow}" | awk '{print $1}')
|
|
fi
|
|
actual_workflow_lock+=("${locked_workflow_sha256} ${locked_workflow_relative}")
|
|
done < <(
|
|
find "${WORKFLOWS_DIRECTORY}" -mindepth 1 -maxdepth 1 \
|
|
\( -name '*.yml' -o -name '*.yaml' \) -print0 \
|
|
| LC_ALL=C sort -z
|
|
)
|
|
|
|
workflow_lock_entry_count=${#EXPECTED_WORKFLOW_LOCK[@]}
|
|
if ((${#actual_workflow_lock[@]} > workflow_lock_entry_count)); then
|
|
workflow_lock_entry_count=${#actual_workflow_lock[@]}
|
|
fi
|
|
for ((workflow_lock_index = 0; workflow_lock_index < workflow_lock_entry_count; workflow_lock_index++)); do
|
|
expected_workflow_lock_entry=${EXPECTED_WORKFLOW_LOCK[workflow_lock_index]-<missing>}
|
|
actual_workflow_lock_entry=${actual_workflow_lock[workflow_lock_index]-<missing>}
|
|
if [[ "${actual_workflow_lock_entry}" != "${expected_workflow_lock_entry}" ]]; then
|
|
printf 'gradle-wrapper-contract: workflow lock mismatch: expected %q; actual %q\n' \
|
|
"${expected_workflow_lock_entry}" "${actual_workflow_lock_entry}" >&2
|
|
workflow_lock_valid=0
|
|
fi
|
|
done
|
|
|
|
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: workflow set or bytes differ from the reviewed embedded manifest'
|
|
|
|
printf 'gradle-wrapper-contract: PASS\n'
|