alt/agent-ops/bin/roadmap-dependency-checker.sh

370 lines
7.8 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]
roadmap-dependency-checker.sh --find-milestone <identity> [locked|rely-on|both] [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.
With --find-milestone, prints matching lock ids as locked:<id> or rely-on:<id>,
or none when the milestone identity is not referenced.
Exit codes:
0 all dependencies are enabled
1 at least one dependency is still disabled
2 configuration, input, or parse error
For --find-milestone, exit code 0 means the lookup completed; stdout is either
matching entries or none.
EOF
}
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
usage
exit 0
fi
MODE="check"
LOCK_ID=""
FIND_IDENTITY=""
FIND_DIRECTION="both"
if [[ "${1:-}" == "--find-milestone" ]]; then
MODE="find"
if [[ $# -lt 2 || $# -gt 4 ]]; then
usage >&2
exit 2
fi
FIND_IDENTITY="$2"
if [[ -z "$FIND_IDENTITY" ]]; then
echo "error: milestone identity is required" >&2
exit 2
fi
if [[ $# -ge 3 ]]; then
case "$3" in
locked|rely-on|both)
FIND_DIRECTION="$3"
if [[ $# -eq 4 ]]; then
LOCKS_FILE="$4"
fi
;;
*)
if [[ $# -eq 3 ]]; then
LOCKS_FILE="$3"
else
usage >&2
exit 2
fi
;;
esac
fi
elif [[ $# -lt 1 || $# -gt 2 ]]; then
usage >&2
exit 2
else
LOCK_ID="$1"
if [[ -n "${2:-}" ]]; then
LOCKS_FILE="$2"
fi
fi
if [[ "$MODE" == "check" && -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
if [[ "$MODE" == "find" ]]; then
awk -v identity="$FIND_IDENTITY" -v direction="$FIND_DIRECTION" '
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 reset_entry() {
entry_id = ""
locked = ""
in_rely_on = 0
rely_count = 0
delete rely_targets
}
function finish_entry( i) {
if (entry_id == "") {
return
}
if ((direction == "both" || direction == "locked") && locked == identity) {
print "locked:" entry_id
found = 1
}
if (direction == "both" || direction == "rely-on") {
for (i = 1; i <= rely_count; i++) {
if (rely_targets[i] == identity) {
print "rely-on:" entry_id
found = 1
break
}
}
}
}
BEGIN {
found = 0
reset_entry()
}
/^[[:space:]]*($|#)/ {
next
}
/^[[:space:]]*-[[:space:]]*id:[[:space:]]*/ {
finish_entry()
reset_entry()
value = $0
sub(/^[[:space:]]*-[[:space:]]*id:[[:space:]]*/, "", value)
entry_id = unquote(value)
next
}
entry_id != "" && /^[[:space:]]*locked:[[:space:]]*/ {
value = $0
sub(/^[[:space:]]*locked:[[:space:]]*/, "", value)
locked = unquote(value)
next
}
entry_id != "" && /^[[:space:]]*rely-on:[[:space:]]*/ {
in_rely_on = 1
next
}
entry_id != "" && in_rely_on && /^[[:space:]]*-[[:space:]]*target:[[:space:]]*/ {
value = $0
sub(/^[[:space:]]*-[[:space:]]*target:[[:space:]]*/, "", value)
rely_targets[++rely_count] = unquote(value)
next
}
entry_id != "" && in_rely_on && /^[[:space:]]*target:[[:space:]]*/ {
value = $0
sub(/^[[:space:]]*target:[[:space:]]*/, "", value)
rely_targets[++rely_count] = unquote(value)
next
}
END {
finish_entry()
if (!found) {
print "none"
}
}
' "$LOCKS_FILE"
exit 0
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"