Skip to content

Commit 50f5c4f

Browse files
committed
feat: add AIDA validation with auto-fix support
Add validate_python.sh wrapper script with --auto-fix and --scope support, following the gdc-nas pattern. Update AIDA validation registry and policy to use the new script with external_json processor. Pre-commit scope runs: format-fix, lint-fix, types (no tests). Pre-push scope runs: format-fix, lint-fix, types, test. Auto-fix mode uses make format-fix and make lint-fix targets to automatically resolve ruff formatting and import ordering issues. JIRA: DX-326 risk: nonprod
1 parent 00f8651 commit 50f5c4f

3 files changed

Lines changed: 256 additions & 49 deletions

File tree

.aida/validation_policy.yaml

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
# AIDA validation policy
33
#
44
# This file wires domains -> pipelines -> steps (command_id + processor_id).
5-
# Initially empty (validate may no-op until you configure routes).
65
version: 1
76
validation_policy:
87
codegen:
@@ -26,22 +25,16 @@ validation_policy:
2625
pipelines:
2726
package-fast:
2827
steps:
29-
- command_id: package-lint
30-
processor_id: passthrough
31-
- command_id: package-type-check
32-
processor_id: passthrough
33-
- command_id: package-test-py314
34-
processor_id: pytest
28+
- command_id: package-validate
29+
processor_id: external_json
30+
repo-fast:
31+
steps:
32+
- command_id: package-validate-no-tests
33+
processor_id: external_json
3534
api-client-fast:
3635
steps:
3736
- command_id: api-client-tests
3837
processor_id: pytest
39-
repo-fast:
40-
steps:
41-
- command_id: workspace-lint
42-
processor_id: passthrough
43-
- command_id: workspace-type-check
44-
processor_id: passthrough
4538
aida-config:
4639
steps:
4740
- command_id: aida-doctor

.aida/validation_registry.yaml

Lines changed: 27 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -6,39 +6,36 @@ version: 1
66
registry:
77
includes: []
88
commands:
9-
package-lint:
9+
package-validate:
1010
argv:
11-
- make
12-
- -C
13-
- '{root}'
14-
- lint
11+
- bash
12+
- '{workspace_root}/scripts/validate_python.sh'
13+
- --project-path
14+
- '{workspace_root}/{root}'
15+
- --workspace-root
16+
- '{workspace_root}'
17+
- --scope
18+
- '{scope}'
19+
- --auto-fix
20+
- "true"
1521
cwd: '{workspace_root}'
16-
package-type-check:
22+
timeout_sec: 900
23+
package-validate-no-tests:
1724
argv:
18-
- make
19-
- -C
20-
- '{root}'
21-
- type-check
22-
cwd: '{workspace_root}'
23-
package-test-py314:
24-
argv:
25-
- make
26-
- -C
27-
- '{root}'
28-
- test
29-
cwd: '{workspace_root}'
30-
env:
31-
TEST_ENVS: py314
32-
workspace-lint:
33-
argv:
34-
- make
35-
- lint
36-
cwd: '{workspace_root}'
37-
workspace-type-check:
38-
argv:
39-
- make
40-
- type-check
25+
- bash
26+
- '{workspace_root}/scripts/validate_python.sh'
27+
- --project-path
28+
- '{workspace_root}/{root}'
29+
- --workspace-root
30+
- '{workspace_root}'
31+
- --scope
32+
- '{scope}'
33+
- --auto-fix
34+
- "true"
35+
- --steps-csv
36+
- format,lint,types
4137
cwd: '{workspace_root}'
38+
timeout_sec: 600
4239
api-client-tests:
4340
argv:
4441
- uv
@@ -52,10 +49,4 @@ registry:
5249
- aida-mcp
5350
- doctor
5451
cwd: '{workspace_root}'
55-
processors:
56-
passthrough:
57-
kind: builtin
58-
builtin_id: passthrough
59-
pytest:
60-
kind: builtin
61-
builtin_id: pytest
52+
processors: {}

scripts/validate_python.sh

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
#!/usr/bin/env bash
2+
# (C) 2026 GoodData Corporation
3+
#
4+
# Python validation wrapper for AIDA validate_command.
5+
#
6+
# Contract (streaming-first):
7+
# - Emits human-readable progress lines to stdout.
8+
# - Writes raw command output to stderr only when a step fails.
9+
# - Prints the final JSON result as the last non-empty line on stdout.
10+
11+
set -uo pipefail
12+
13+
TOOL="validate_python"
14+
15+
json_escape() {
16+
local s="${1-}"
17+
s="${s//\\/\\\\}"
18+
s="${s//\"/\\\"}"
19+
s="${s//$'\t'/\\t}"
20+
s="${s//$'\r'/\\r}"
21+
s="${s//$'\n'/\\n}"
22+
printf '%s' "$s"
23+
}
24+
25+
emit_line() {
26+
local msg="${2-${1-}}"
27+
printf '%s\n' "$msg"
28+
}
29+
30+
usage() {
31+
cat <<'EOF'
32+
Usage:
33+
validate_python.sh --project-path <path> --workspace-root <path> [options]
34+
35+
Options:
36+
--scope <scope> Validation scope: pre_commit or pre_push (default: pre_push)
37+
pre_commit: format,lint,types
38+
pre_push: format,lint,types,test
39+
--steps-csv "<csv>" Comma-separated steps (overrides --scope defaults)
40+
--auto-fix <true|false> Default: true (uses *-fix Make targets)
41+
--test-filter <pattern> Optional pytest selector (runs uv run pytest -v <pattern>)
42+
43+
-h, --help Show help
44+
EOF
45+
}
46+
47+
PROJECT_PATH=""
48+
WORKSPACE_ROOT=""
49+
SCOPE=""
50+
STEPS_CSV=""
51+
AUTO_FIX="true"
52+
TEST_FILTER=""
53+
54+
while [[ $# -gt 0 ]]; do
55+
case "$1" in
56+
--project-path) PROJECT_PATH="${2-}"; shift 2 ;;
57+
--workspace-root) WORKSPACE_ROOT="${2-}"; shift 2 ;;
58+
--scope) SCOPE="${2-}"; shift 2 ;;
59+
--steps-csv) STEPS_CSV="${2-}"; shift 2 ;;
60+
--auto-fix) AUTO_FIX="${2-}"; shift 2 ;;
61+
--test-filter) TEST_FILTER="${2-}"; shift 2 ;;
62+
-h|--help) usage; exit 0 ;;
63+
*) echo "Unknown arg: $1" >&2; usage >&2; exit 2 ;;
64+
esac
65+
done
66+
67+
if [[ -z "$PROJECT_PATH" || -z "$WORKSPACE_ROOT" ]]; then
68+
echo "Missing required args: --project-path and --workspace-root" >&2
69+
usage >&2
70+
exit 2
71+
fi
72+
73+
PROJECT_NAME="$(basename "$PROJECT_PATH")"
74+
75+
to_bool() {
76+
local v="${1-}"
77+
v="$(printf '%s' "$v" | tr '[:upper:]' '[:lower:]')"
78+
case "$v" in
79+
1|true|yes|on) printf 'true' ;;
80+
*) printf 'false' ;;
81+
esac
82+
}
83+
84+
AUTO_FIX_BOOL="$(to_bool "$AUTO_FIX")"
85+
86+
split_csv() {
87+
local csv="${1-}"
88+
local out=()
89+
local item
90+
IFS=',' read -r -a out <<<"$csv"
91+
for item in "${out[@]}"; do
92+
item="$(printf '%s' "$item" | xargs)"
93+
if [[ -n "$item" ]]; then
94+
printf '%s\n' "$item"
95+
fi
96+
done
97+
}
98+
99+
if [[ -n "${STEPS_CSV// }" ]]; then
100+
mapfile -t STEPS < <(split_csv "$STEPS_CSV")
101+
else
102+
case "${SCOPE}" in
103+
pre_commit)
104+
STEPS=("format" "lint" "types")
105+
;;
106+
pre_push|"")
107+
STEPS=("format" "lint" "types" "test")
108+
;;
109+
*)
110+
STEPS=("format" "lint" "types" "test")
111+
;;
112+
esac
113+
fi
114+
115+
if [[ ! -d "$PROJECT_PATH" ]]; then
116+
emit_line info "Python ${PROJECT_NAME}: project path not found: ${PROJECT_PATH}"
117+
printf '{"tool":"%s","success":false,"text":"%s"}\n' \
118+
"$(json_escape "$TOOL")" \
119+
"$(json_escape "project path not found: ${PROJECT_PATH}")"
120+
exit 1
121+
fi
122+
123+
if [[ ! -f "${PROJECT_PATH%/}/Makefile" ]]; then
124+
emit_line info "Python ${PROJECT_NAME}: Makefile missing (required)"
125+
printf '{"tool":"%s","success":false,"text":"%s"}\n' \
126+
"$(json_escape "$TOOL")" \
127+
"$(json_escape "No Makefile found in ${PROJECT_PATH}")"
128+
exit 1
129+
fi
130+
131+
run_in_project() {
132+
local label="$1"
133+
shift
134+
local -a cmd=("$@")
135+
local tmp_out
136+
tmp_out="$(mktemp)"
137+
138+
emit_line progress "Python ${PROJECT_NAME}: ${label}"
139+
140+
(cd "$PROJECT_PATH" && env -u VIRTUAL_ENV "${cmd[@]}") >"$tmp_out" 2>&1
141+
local rc=$?
142+
if [[ "$rc" -ne 0 ]]; then
143+
cat "$tmp_out" >&2
144+
fi
145+
rm -f "$tmp_out"
146+
return "$rc"
147+
}
148+
149+
run_in_project_capture() {
150+
local label="$1"
151+
local out_file="$2"
152+
shift 2
153+
local -a cmd=("$@")
154+
155+
emit_line progress "Python ${PROJECT_NAME}: ${label}"
156+
157+
(cd "$PROJECT_PATH" && env -u VIRTUAL_ENV "${cmd[@]}") >"$out_file" 2>&1
158+
return $?
159+
}
160+
161+
has_make_target() {
162+
local target="$1"
163+
local code
164+
(cd "$PROJECT_PATH" && make -q "$target" >/dev/null 2>&1)
165+
code=$?
166+
[[ $code -eq 0 || $code -eq 1 ]]
167+
}
168+
169+
FAIL_STEP=""
170+
171+
for step in "${STEPS[@]}"; do
172+
case "$step" in
173+
format)
174+
if [[ "$AUTO_FIX_BOOL" == "true" ]] && has_make_target "format-fix"; then
175+
if ! run_in_project "Format (make format-fix)" make format-fix; then FAIL_STEP="Format"; break; fi
176+
else
177+
if ! run_in_project "Format (make format)" make format; then FAIL_STEP="Format"; break; fi
178+
fi
179+
;;
180+
lint)
181+
if [[ "$AUTO_FIX_BOOL" == "true" ]] && has_make_target "lint-fix"; then
182+
if ! run_in_project "Lint (make lint-fix)" make lint-fix; then FAIL_STEP="Lint"; break; fi
183+
else
184+
if ! run_in_project "Lint (make lint)" make lint; then FAIL_STEP="Lint"; break; fi
185+
fi
186+
;;
187+
types)
188+
tmp_types="$(mktemp)"
189+
if run_in_project_capture "Types (make type-check)" "$tmp_types" make type-check; then
190+
rm -f "$tmp_types"
191+
else
192+
cat "$tmp_types" >&2
193+
rm -f "$tmp_types"
194+
FAIL_STEP="Types"
195+
break
196+
fi
197+
;;
198+
test)
199+
if [[ -n "${TEST_FILTER}" ]]; then
200+
if ! run_in_project "Test (pytest ${TEST_FILTER})" uv run pytest -v "$TEST_FILTER"; then
201+
FAIL_STEP="Test"; break
202+
fi
203+
else
204+
if ! run_in_project "Test (make test)" make test; then FAIL_STEP="Test"; break; fi
205+
fi
206+
;;
207+
*)
208+
emit_line info "Python ${PROJECT_NAME}: skipping unknown step '${step}'"
209+
;;
210+
esac
211+
done
212+
213+
if [[ -n "$FAIL_STEP" ]]; then
214+
printf '{"tool":"%s","success":false,"text":"%s"}\n' \
215+
"$(json_escape "$TOOL")" \
216+
"$(json_escape "Python ${PROJECT_NAME}: FAILED at ${FAIL_STEP}")"
217+
exit 1
218+
fi
219+
220+
printf '{"tool":"%s","success":true,"text":"%s"}\n' \
221+
"$(json_escape "$TOOL")" \
222+
"$(json_escape "Python ${PROJECT_NAME}: PASSED")"
223+
exit 0

0 commit comments

Comments
 (0)