Add pre-commit hooks for flow validation - #4
Conversation
- husky pre-commit no longer exits before running the e2e check when the pre-commit binary is missing, so a commit touching both flows/ and e2e/ still runs both checks. - narrow the check-flows hook's files pattern to *_flow.py so check_flows.py no longer needs to guess whether staged paths are an explicit selection, and drop the now-redundant --require-selection flag and the untested flows-relative path fallback.
Not part of this change; leaving it as an untracked working-tree file as it was before.
README claimed check-flows validates all discovered flows, but pre-commit only passes staged filenames. Also fixed test_rejects_invalid_selected_paths, which resolved relative paths against tmp_path.parent and accidentally tripped the outside-root check instead of the missing-file/wrong-suffix checks it meant to test. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
Two moderate pre-commit integration issues remain unresolved.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Adds flow validation tooling and integrates Ruff, Metaflow checks, pytest, and Husky pre-commit routing.
Changes:
- Adds AST-based flow discovery, convention checks, native Metaflow validation, and tests.
- Configures Ruff, pytest, dependency pins, documentation, and tool-version checks.
- Consolidates Husky routing and removes the nested E2E hook.
- Reformats flow model code without logic changes.
File summaries
| File | Summary | Review notes |
|---|---|---|
README.md |
Documents development checks. | Nit (2 votes): Narrow the routing claim to staged *_flow.py files. |
pyproject.toml |
Configures Ruff and pytest. | — |
flows/tests/test_tool_versions.py |
Tests tool-version consistency. | — |
flows/tests/test_check_flows.py |
Tests flow checker behavior. | — |
flows/scripts/check_flows.py |
Implements flow discovery and validation. | — |
flows/requirements.txt |
Pins Outerbounds dependencies. | — |
flows/requirements-dev.txt |
Adds development dependencies. | — |
flows/README.md |
Documents flow tooling and validation. | Nits (2, 1, and 2 votes): Clarify hook selector scope, all-files exceptions, and the distinction between staged native checks and repository-wide discovery checks. |
flows/models/utils/model_validators.py |
Applies Ruff formatting. | — |
flows/models/local_cache_model_flow.py |
Applies formatting and documentation updates. | — |
flows/models/download_safetensors_model_flow.py |
Applies formatting and documentation updates. | — |
flows/models/download_gguf_model_flow.py |
Applies formatting and documentation updates. | — |
flows/models/browse_models_flow.py |
Applies formatting and documentation updates. | — |
e2e/.husky/pre-commit |
Removes the nested E2E hook. | — |
.pre-commit-config.yaml |
Defines flow validation hooks. | — |
.husky/pre-commit |
Routes flow and E2E checks. | Moderate (2 votes): Make Husky setup durable across later e2e installs. Moderate (1 vote): Run the E2E package hook for every staged path under e2e/. |
.gitignore |
Preserves root pre-commit configuration. | — |
Review details
Suppressed comments (2)
.husky/pre-commit:53
playwright-utils-precommitis gated here only fore2e/testsande2e/test-setup, so stagede2e/playwright.config.ts,package.json, JSON, or Markdown changes bypass the E2E hook even though the package's lint-staged and quality configuration covers them. Invoke the package hook for every staged path undere2e/; its own QA-scoped gate can skip non-QA commits, and run it frome2eso it resolves the correct consumer root.
if git diff --cached --name-only --diff-filter=ACMR | grep -Eq '^e2e/(tests|test-setup)/'; then
npm --prefix e2e run precommit || status=1
flows/README.md:83
- This is not true for all normal commits: the root hook explicitly switches to
pre-commit run --all-fileswhen hook/configuration files change or anything underflows/is deleted. Please document these exceptions so the staged-only behavior is not relied on for those commits.
tool-version consistency checks. `check-flows` validates only the staged
flow files, and `test-flow-tools` runs the complete local test suite. Neither hook
- Files reviewed: 16/17 changed files
- Comments generated: 4
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
🟡 Changes recommended
Two moderate pre-commit routing gaps can bypass validation or focused tests; documentation nits also remain.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (5)
.pre-commit-config.yaml:32
- When only
pyproject.tomlis staged, the root Husky hook forcespre-commit run --all-files, but thisfilespattern still excludespyproject.toml, sotest-flow-toolsis skipped even though that file controls pytest/Ruff behavior. Include it in the hook's trigger pattern so configuration changes cannot bypass the focused suite.
files: ^(flows/.*\.py|flows/requirements(-dev)?\.txt|\.pre-commit-config\.yaml)$
.pre-commit-config.yaml:19
- The
filesfilter prevents this hook from running when the final*_flow.pyis deleted. In that case the root hook'spre-commit run --all-filesinvocation has no matching path, sodiscover_flows()is never called and its empty-matrix guard is bypassed. Add a deletion/empty-inventory path that invokes this checker even when no flow filename remains, while preserving staged-path selection for normal runs.
files: ^flows/.*_flow\.py$
README.md:23
- This routing description is broader than the hook:
.husky/pre-commitinvokes the Playwright command only fore2e/tests/ande2e/test-setup/, so staged changes such ase2e/package.jsondo not run it. Please document the actual scoped paths or broaden the shell condition.
is the entry point and runs each area only when that area has staged changes. A
commit touching both directories runs both.
flows/README.md:81
- The subject here is the plural list of requirements/configuration files, but the sentence uses singular
is. Use the plural verb so the setup instructions are grammatically correct.
tool-version consistency checks. The native Metaflow check runs only for staged
flows/README.md:79
- This also overstates the Playwright scope: the Husky hook matches only
e2e/tests/ande2e/test-setup/, so changes toe2e/package.json, configuration, or other files undere2e/are intentionally skipped. Document the scoped paths here.
`flows/requirements.txt`, `flows/requirements-dev.txt`, or
- Files reviewed: 17/18 changed files
- Comments generated: 2
- Review effort level: Lite
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved Ruff failure and three moderate pre-commit validation issues block approval.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (2)
.husky/pre-commit:61
npm --prefix e2e run precommitis launched with the repository root as npm'sINIT_CWD. The Playwright utility explicitly usesINIT_CWDto resolveCONSUMER_ROOT, so this can scope lint-staged and the staged quality report to the root instead ofe2e(which has the relevantpackage.jsonand globs), causing the preserved E2E checks to skip or mis-scope their files. Run npm from the QA package directory so the utility receivese2eas its consumer root.
npm --prefix e2e run precommit || status=1
.husky/pre-commit:51
- The final-flow guard checks the index, but the manual inventory hook invokes
discover_flows()without a selection and that function globs the working tree. An untrackedflows/**/*_flow.pycan therefore satisfy the inventory check, allowing the last tracked flow to be deleted even though the resulting commit contains no flow. Make the inventory validation enumerate the staged/tracked flow paths (or otherwise exclude untracked files) before allowing this deletion.
if printf '%s\n' "$deleted" | grep -Eq '^flows/.*_flow\.py$' \
&& ! git ls-files --cached 'flows/*_flow.py' 'flows/**/*_flow.py' | grep -q .; then
"$pre_commit" run check-flow-inventory --hook-stage manual || status=1
- Files reviewed: 17/18 changed files
- Comments generated: 2
- Review effort level: Lite
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
🟡 Changes recommended
Three moderate findings remain involving rename detection, virtualenv selection, and misnamed flow discovery.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (2)
.husky/pre-commit:46
- When
flows/.venvexists but is not activated, any globally installedpre-commitwins this branch, so the documented fallback and the pinnedpre-commit==4.6.2environment are bypassed. That can run a different pre-commit version and hook environment than the one whose dependencies this PR defines; prefer the repository virtualenv path before searchingPATH.
if command -v pre-commit >/dev/null 2>&1; then
pre_commit=pre-commit
elif [ -x flows/.venv/bin/pre-commit ]; then
pre_commit=flows/.venv/bin/pre-commit
.pre-commit-config.yaml:19
- A newly added or copied flow can bypass this hook by using a non-
*_flow.pyfilename: the root hook still enters pre-commit for anyflows/change, but thisfilespattern excludesflows/foo.py, anddiscover_flows()only scans the same suffix. Such a file can contain aFlowSpecand still be committed despite the documented requirement that every executable flow use*_flow.py; inspect staged Python files for flow definitions (or otherwise reject misnamedFlowSpecfiles) rather than using the suffix as the sole discovery filter.
files: ^flows/.*_flow\.py$
- Files reviewed: 17/18 changed files
- Comments generated: 1
- Review effort level: Lite
There was a problem hiding this comment.
🟡 Changes recommended
Two moderate validation gaps remain in hook coverage and aliased FlowSpec detection.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (1)
.husky/pre-commit:30
- This gate only enters the Python checks when a staged path starts with
flows/(or matches the config pattern), so a commit that adds or moves aFlowSpecin another tracked Python path, such asrenamed.pycovered bytest_rejects_rewritten_flow_moved_to_filename_without_suffix, never invokes the validator. Thecheck-flow-filenameshook is also scoped to^flows/.*\.py$, despitevalidate_tracked_flow_filenamespromising to inspect every tracked Python file. Trigger the filename check for all staged Python files and broaden its pre-commit file selector accordingly.
if printf '%s\n' "$staged" | grep -Eq "^flows/|$config_pattern"; then
- Files reviewed: 17/18 changed files
- Comments generated: 1
- Review effort level: Lite
There was a problem hiding this comment.
🟡 Changes recommended
Two unresolved moderate findings affect flow detection and deleted-file validation.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (1)
.husky/pre-commit:63
- This filter omits pure deletions, so deleting a file under
e2e/tests/ore2e/test-setup/never invokesnpm run precommit, even though the README says staged paths in those directories activate the Playwright checks. IncludeDso the shared hook validates deletions as well.
if git diff --cached --name-only --diff-filter=ACMR | grep -Eq '^e2e/(tests|test-setup)/'; then
- Files reviewed: 17/18 changed files
- Comments generated: 1
- Review effort level: Lite
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Summary
e2e/tests/ande2e/test-setup/; deletion-only e2e changes skip the staged-file pipeline, and commits touching both project areas run both applicable check sets.flows/scripts/check_flows.pyto discover QA flows, enforce repository conventions, and run Metaflow's native definition and DAG validation without authenticating or starting workloads.flows/.FlowSpecwithout the required*_flow.pysuffix, including heavily rewritten moves that Git represents as delete/add pairs.metaflow.FlowSpec, so aliased flow classes are discovered and remain subject to staged filename validation.*_flow.pysuffix, and deleting the final tracked flow fails based on the staged index rather than working-tree files.Test plan
pre-commit run --all-files- Ruff, Metaflow validation, and flow utility tests passpytest -q flows/tests- 23 tests passnpm --prefix e2e run lintnpm --prefix e2e run validatecd e2e && npx prettier --check package.jsonsh -n .husky/pre-commitpre-commit validate-config .pre-commit-config.yamlVerified ordinary flow utility files run Ruff while skipping checker unit tests
Verified no unresolved conflict markers or whitespace errors remain
Verified the Playwright utility resolves
e2eas its QA consumer rootVerified an untracked
*_flow.pycannot satisfy final tracked-flow deletion validationVerified heavily rewritten flow moves fail both inside and outside
flows/Verified
FlowSpecaliases are recognized for discovery and misnamed-file rejectionVerified root-level Python paths activate the FlowSpec filename hook while non-Python paths do not