231 lines
4.9 KiB
Bash
Executable file
231 lines
4.9 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
AGENT_OPS_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
PROJECT_ROOT="$(cd "$AGENT_OPS_DIR/.." && pwd)"
|
|
WORKSPACE_ROOT="$(cd "$PROJECT_ROOT/.." && pwd)"
|
|
LOCKS_FILE="${AGENT_ROADMAP_LOCKS_FILE:-$WORKSPACE_ROOT/.agent-roadmap-sync/locks.yaml}"
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Usage: roadmap-dependency-checker.sh <lock-id> [locks.yaml]
|
|
|
|
Validates locked, rely-on.target, and rely-on.status for <lock-id>.
|
|
Prints true when every rely-on.status is enable.
|
|
Prints false when at least one rely-on.status is disable.
|
|
|
|
Exit codes:
|
|
0 all dependencies are enabled
|
|
1 at least one dependency is still disabled
|
|
2 configuration, input, or parse error
|
|
EOF
|
|
}
|
|
|
|
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
|
usage
|
|
exit 0
|
|
fi
|
|
|
|
if [[ $# -lt 1 || $# -gt 2 ]]; then
|
|
usage >&2
|
|
exit 2
|
|
fi
|
|
|
|
LOCK_ID="$1"
|
|
if [[ -n "${2:-}" ]]; then
|
|
LOCKS_FILE="$2"
|
|
fi
|
|
|
|
if [[ -z "$LOCK_ID" ]]; then
|
|
echo "error: lock id is required" >&2
|
|
exit 2
|
|
fi
|
|
|
|
if [[ ! -r "$LOCKS_FILE" ]]; then
|
|
echo "error: locks file not readable: $LOCKS_FILE" >&2
|
|
exit 2
|
|
fi
|
|
|
|
awk -v wanted="$LOCK_ID" '
|
|
function trim(value) {
|
|
gsub(/\r/, "", value)
|
|
sub(/^[[:space:]]+/, "", value)
|
|
sub(/[[:space:]]+$/, "", value)
|
|
return value
|
|
}
|
|
|
|
function unquote(value) {
|
|
value = trim(value)
|
|
sub(/[[:space:]]+#.*$/, "", value)
|
|
value = trim(value)
|
|
if (value ~ /^".*"$/ || value ~ /^'\''.*'\''$/) {
|
|
return substr(value, 2, length(value) - 2)
|
|
}
|
|
return trim(value)
|
|
}
|
|
|
|
function fail(message) {
|
|
error = message
|
|
printf "error: %s\n", error > "/dev/stderr"
|
|
exit 2
|
|
}
|
|
|
|
function finish_dependency() {
|
|
if (!selected || !in_dependency) {
|
|
return
|
|
}
|
|
if (!dependency_has_target) {
|
|
fail("missing rely-on.target for lock id: " wanted)
|
|
}
|
|
if (!dependency_has_status) {
|
|
fail("missing rely-on.status for lock id: " wanted)
|
|
}
|
|
in_dependency = 0
|
|
}
|
|
|
|
function finish_entry() {
|
|
if (!selected) {
|
|
return
|
|
}
|
|
finish_dependency()
|
|
if (!entry_has_locked) {
|
|
fail("missing locked for lock id: " wanted)
|
|
}
|
|
if (total == 0) {
|
|
fail("no rely-on.status entries for lock id: " wanted)
|
|
}
|
|
selected = 0
|
|
}
|
|
|
|
BEGIN {
|
|
selected = 0
|
|
matches = 0
|
|
total = 0
|
|
enabled = 0
|
|
error = ""
|
|
entry_has_locked = 0
|
|
in_rely_on = 0
|
|
in_dependency = 0
|
|
dependency_has_target = 0
|
|
dependency_has_status = 0
|
|
}
|
|
|
|
/^[[:space:]]*($|#)/ {
|
|
next
|
|
}
|
|
|
|
/^[[:space:]]*-[[:space:]]*id:[[:space:]]*/ {
|
|
finish_entry()
|
|
value = $0
|
|
sub(/^[[:space:]]*-[[:space:]]*id:[[:space:]]*/, "", value)
|
|
id = unquote(value)
|
|
if (id == wanted) {
|
|
if (matches > 0) {
|
|
fail("duplicate lock id: " wanted)
|
|
}
|
|
selected = 1
|
|
matches++
|
|
total = 0
|
|
enabled = 0
|
|
entry_has_locked = 0
|
|
in_rely_on = 0
|
|
in_dependency = 0
|
|
dependency_has_target = 0
|
|
dependency_has_status = 0
|
|
}
|
|
next
|
|
}
|
|
|
|
selected && /^[[:space:]]*locked:[[:space:]]*/ {
|
|
value = $0
|
|
sub(/^[[:space:]]*locked:[[:space:]]*/, "", value)
|
|
locked = unquote(value)
|
|
if (locked == "") {
|
|
fail("empty locked for lock id: " wanted)
|
|
}
|
|
entry_has_locked = 1
|
|
next
|
|
}
|
|
|
|
selected && /^[[:space:]]*rely-on:[[:space:]]*/ {
|
|
in_rely_on = 1
|
|
next
|
|
}
|
|
|
|
selected && in_rely_on && /^[[:space:]]*-[[:space:]]*target:[[:space:]]*/ {
|
|
finish_dependency()
|
|
in_dependency = 1
|
|
dependency_has_target = 0
|
|
dependency_has_status = 0
|
|
value = $0
|
|
sub(/^[[:space:]]*-[[:space:]]*target:[[:space:]]*/, "", value)
|
|
target = unquote(value)
|
|
if (target == "") {
|
|
fail("empty rely-on.target for lock id: " wanted)
|
|
}
|
|
dependency_has_target = 1
|
|
next
|
|
}
|
|
|
|
selected && in_rely_on && /^[[:space:]]*target:[[:space:]]*/ {
|
|
if (!in_dependency) {
|
|
fail("rely-on.target outside dependency item for lock id: " wanted)
|
|
}
|
|
value = $0
|
|
sub(/^[[:space:]]*target:[[:space:]]*/, "", value)
|
|
target = unquote(value)
|
|
if (target == "") {
|
|
fail("empty rely-on.target for lock id: " wanted)
|
|
}
|
|
dependency_has_target = 1
|
|
next
|
|
}
|
|
|
|
selected && in_rely_on && /^[[:space:]]*status:[[:space:]]*/ {
|
|
if (!in_dependency) {
|
|
fail("rely-on.status outside dependency item for lock id: " wanted)
|
|
}
|
|
value = $0
|
|
sub(/^[[:space:]]*status:[[:space:]]*/, "", value)
|
|
status = unquote(value)
|
|
if (status != "enable" && status != "disable") {
|
|
fail("invalid rely-on.status for " wanted ": " status)
|
|
}
|
|
if (dependency_has_status) {
|
|
fail("duplicate rely-on.status for lock id: " wanted)
|
|
}
|
|
dependency_has_status = 1
|
|
total++
|
|
if (status == "enable") {
|
|
enabled++
|
|
}
|
|
next
|
|
}
|
|
|
|
selected && in_rely_on && /^[[:space:]]*-[[:space:]]*/ {
|
|
finish_dependency()
|
|
fail("missing rely-on.target for lock id: " wanted)
|
|
}
|
|
|
|
selected && /^[[:space:]]*status:[[:space:]]*/ {
|
|
fail("status outside rely-on for lock id: " wanted)
|
|
}
|
|
|
|
END {
|
|
if (error != "") {
|
|
exit 2
|
|
}
|
|
if (matches == 0) {
|
|
printf "error: lock id not found: %s\n", wanted > "/dev/stderr"
|
|
exit 2
|
|
}
|
|
finish_entry()
|
|
if (total == enabled) {
|
|
print "true"
|
|
exit 0
|
|
}
|
|
print "false"
|
|
exit 1
|
|
}
|
|
' "$LOCKS_FILE"
|