Skip to content

build(make): stop reinstalling tooling per target and fail doctor on missing tools - #631

Merged
yeongseon merged 2 commits into
mainfrom
build/make-bootstrap-once-doctor
Oct 24, 2026
Merged

yeongseon merged 2 commits into
mainfrom
build/make-bootstrap-once-doctor

Conversation

@yeongseon

@yeongseon yeongseon commented Oct 24, 2026 •

Copy link
Copy Markdown
Owner

Related Issue

Refs #614

Not Closes: of #614's six criteria, five are met by #628 and this PR. The third, "make e2e-local is labelled local-only", is not met as written (see "What remains of #614"). If you read it as met by the target's name and its "against local Azurite" banner, change Refs to Closes.

Summary

make doctor could not fail, every Make target re-ran pip, and make check ran mypy twice. All three reproduced on origin/main (d3a254f, which contains #628, #629 and #630):

  • doctor. With only .venv/bin/python present (no Hatch, func, git or azurite) it printed "No hatch environment found" and "func not found" and exited 0. With a directory at .venv/bin/hatch it also exited 0. In a fresh clone before make install it failed only by accident (.venv/bin/python: not found, make exit 2).
  • bootstrap. Every target that depends on ensure-hatch ran pip install --upgrade pip and pip install hatch. Three make bootstrap runs with Hatch installed made 6 pip calls, make check-all made 16, and a fresh venv's pip went 22.0.2 → 26.2.2 as a side effect.
  • check. check ran lint and then typecheck, but Hatch's lint script (pyproject.toml) already ends with hatch run typecheck, so mypy ran twice (2 summaries in a real make check).

After this PR, with Hatch installed make check-all makes 0 pip calls and one mypy run; with Hatch missing it makes exactly one (pip install hatch); make doctor is a read-only check that fails with a MISSING line for each required tool. The same fix already landed in azure-functions-doctor-python (PR 636) and azure-functions-scaffold-python (PR 462); this follows them.

Why bootstrap is not a make file target. bootstrap stays a phony target that tests for an executable regular file. A file target ($(HATCH):) looks tidier, but make treats any existing path as up to date, so it would skip the install for a directory or a non-executable file at .venv/bin/hatch (reproduced in a scratch Makefile: see the last Verification row). -x alone has the same hole for a directory, which is why the test is -f and -x. The doctor PR hit exactly this in review.

Changes

File Change
Makefile (+19/−18) bootstrap: creates .venv only if missing; runs pip install hatch only if $(HATCH) is not an executable regular file; never upgrades pip; silent when there is nothing to do (it printed two lines per target). check: lint (one type check). doctor: read-only; required $(HATCH), python3, git print ok or MISSING <tool> (required) and any MISSING fails the target; optional func and azurite are listed and never change the status. Removed the PYTHON variable (only the old doctor used it) and the pip install --upgrade pip in the CI-only branch of docs.
tests/test_makefile_contract.py (new, 65 lines, 8 cases) make on a copy of the Makefile with PATH limited to stubs that log their arguments, so nothing is installed and no network is used (details below).
docs/development.md, docs/contributing.md (+1 row each) make doctor in the two tables that list the other targets. CONTRIBUTING.md and AGENTS.md are not touched.

Not changed, verified against origin/main: the security recipe from #629 (byte-identical), the check-all, e2e-local, e2e-azure, clean and clean-all recipes (identical), and everything under .github, pyproject.toml, tools, scripts and src (git diff --stat origin/main HEAD -- <those> is empty).

Choices to know about:

  1. python3, not python3.11, is the required interpreter. bootstrap runs python3 -m venv; the Hatch environment's Python 3.11 is resolved (and, since Hatch 1.8, downloaded if absent) by Hatch itself, and CONTRIBUTING lists 3.11–3.14. A python3.11-on-PATH check, as the doctor repo uses, would report a Hatch-managed 3.11 as MISSING; this repo has no need to accept that limitation.
  2. The old output is gone (Python version, pip list, func --version, pre-commit hook state). Nothing in the repository parses it (grep over .github, tests, tools, scripts, docs).
  3. CI=1 make docs is already broken on main, independent of this change: its mkdocstrings[python]<1.0 pin resolves to mkdocstrings 0.30.1, which fails against pymdown-extensions 12.3 (Highlight.__init__() missing ... 'md'). The same traceback occurs on main (with the pip upgrade) and on this branch (without it), so removing that one line changes nothing observable. No workflow calls that branch. The non-CI make docs (via Hatch) builds on this branch (exit 0).

Tests

tests/test_makefile_contract.py (65 lines, 8 cases) pins, with make -n for the first and real runs against stubs for the rest:

  • make -n check has exactly one hatch run lint and no typecheck; the real run makes one Hatch call (run lint) and no pip call; the lint script in pyproject.toml still contains the type check (what check: lint relies on); and the Makefile has no --upgrade (covers docs, which -n cannot dry-run because its recipe line contains $(MAKE)).
  • bootstrap calls pip install hatch only when Hatch is missing or is a directory.
  • doctor exits non-zero with a MISSING line when $(HATCH), python3 or git is missing, exits 0 with func and azurite missing (listing them), and calls no tool.

Verification

Linux, GNU Make 4.3, /bin/sh = dash, system python3 3.10.12; Python 3.11.15 for the suite. "main" = unmodified origin/main d3a254f; "head" = d468af4. Scripts and logs are kept in /tmp/opencode/logging-pr2-evidence/ on the machine that ran them.

Command Environment Result
make check-all fresh git clone of head (HEAD verified), python3 3.10.12 .venv, Hatch 1.18.1 exit 0: three workflow lints, doc-link lint, ruff, format (85 files), mypy strict (64 files, one summary), 878 passed, 4 skipped, 4 deselected, coverage 96.26% (gate 95%), Bandit "4647 lines analysed, no findings"
make build same clone exit 0 (sdist + wheel)
pytest tests/ (default addopts, coverage gate) venv pip install -e ".[dev]" head: 878 passed, 4 skipped, coverage 96.26%. main: 870 passed, 4 skipped. The +8 are the new tests; the 4 skips are the same ones
ruff check / ruff format --check (src tests tools examples benchmarks scripts); mypy src tests (strict) ruff 0.17.0, mypy 2.4.0 all clean
tools/lint_release_workflows.py, lint_workflow_pins.py, lint_hatch_matrix.py, lint_doc_links.py, check_pr_format.py origin/main HEAD; git diff --check same all exit 0
mkdocs build --strict; make docs (no CI) mkdocs 1.6.1 exit 0; both new rows render (tables of 10 rows)
make -n check, main / head scratch, no execution main: 3 bootstraps with pip install --upgrade pip, hatch run lint then hatch run typecheck. Head: one bootstrap line, one hatch run lint, no typecheck, no upgrade
stub-pip call counts, main / head (stub_counts.sh: PATH = stubs, VENV_DIR in scratch) scratch 3× bootstrap, Hatch present: 6 → 0 pip calls. 3× bootstrap, Hatch absent: 6 → 1 (install hatch). check: 6 → 0 (Hatch gets run lint, run typecheck → run lint). lint 2 → 0, test 2 → 0, check-all 16 → 0; check-all with no .venv: 16 → 1 and one python3 -m venv both times. make -n check-all: 8 upgrade pip → 0
doctor exit codes, main / head (only .venv/bin/python present; Hatch, func, git, azurite missing) scratch main: exit 0. Head: exit 2 (recipe exit 1), three MISSING lines, two optional lines
doctor, fresh clone before make install; after; with a directory at .venv/bin/hatch; after restoring it fresh clones of main / head head: MISSING .venv/bin/hatch (required) exit 2; all ok exit 0; MISSING exit 2; exit 0. main: exit 2 by accident (.venv/bin/python: not found); exit 0 with a pip list; exit 0 with the directory ("Permission denied", "No hatch environment found"); exit 0
CI=true make install in a fresh clone (the step security.yml runs after pip install hatch), then a second make bootstrap, then make security fresh clone, no .venv head: "Creating virtual environment...", "Installing Hatch...", env created, "CI detected: skipping pre-commit hook installation", exit 0; .venv pip stays 22.0.2 (main: 26.2.2); second bootstrap prints nothing (main: two lines); make security exit 0
make check fresh clones mypy summary printed once on head, twice on main
make install without CI, after the CI=true run above fresh clone of head exit 0; "Environment default already exists"; pre-commit hook installed
the tests copied onto unmodified origin/main scratch worktree 8 failed (all of them); 8 passed on head
mutations (mutate_makefile.py: a mutated Makefile and pyproject.toml in a scratch tree, real tests run there) scratch baseline 8 passed; each of 14 mutations turns at least one test red: check also runs typecheck (2 forms); bootstrap upgrades pip; reinstalls Hatch every time; never installs it; accepts a directory (-x only); doctor accepts a directory; always exit 0; fails on an optional tool; drops git; drops python3; depends on ensure-hatch (installs); docs upgrades pip again; the lint script drops the type check
the recipes of bootstrap and doctor, taken from make -n, under dash, bash, bash --posix and busybox sh for an executable file, a directory, a non-executable file and a missing path; shellcheck -s sh, -s dash, -s bash shellcheck 0.8.0 identical in all four shells: only the executable file counts (doctor exit 0, no install); the other three are MISSING (exit 1) and trigger the install. shellcheck clean
a make file target for $(HATCH) with a directory, then a non-executable file, at the path scratch make lint runs without installing in both cases (the reason for the deviation above)

What remains of #614

Criteria as currently written in the issue, against origin/main plus this PR:

# Criterion Met by State
1 Azure certification step fails on any skipped e2e test (incl. App Insights) or a version mismatch; the record is written only after that step succeeds #628 e2e-azure.yml: FAIL_ON_SKIP: "1" on the e2e step, which Record certification follows with no if:; the version check was already fail-closed
2 OTel canary fails, not skips, on an unimportable module or any skip #628 otel-nightly.yml FAIL_ON_SKIP
3 make e2e-local is labelled local-only; may skip App Insights but cannot produce or replace a certification record partly Not met as written. True today: it cannot write a record (only the Record certification step in e2e-azure.yml does) and may skip App Insights (FAIL_ON_SKIP is opt-in). Missing: nothing says it is local-only. The target prints "against local Azurite", its second line says "override to certify a different deployed build", and no user-facing doc mentions it (only the Makefile and a docstring in tests/e2e/conftest.py do)
4 Routine targets do not reinstall or upgrade present tooling; make check type-checks once; make doctor non-zero when a required tool is missing this PR tables above; no --upgrade is left in the Makefile
5 Matrix cells keep their interpreter; offline suite ≥95%; existing jobs, required contexts, matrix cells and release gates unchanged holds; unchanged by this PR this PR touches none of .github, pyproject.toml, tools, scripts, src; coverage 96.26%
6 Workflow-YAML regression tests pin the skip policy; negative runs recorded #628 tests/test_ci_skip_policy.py; negative runs in #628's description

What remains: criterion 3. One changed echo in the e2e-local recipe and one sentence in the docs (for example under "Real Azure E2E Tests") saying that a local run is not Azure certification. I left it out to keep e2e-local as it is and this PR to the bootstrap, check and doctor fix. If you want #614 closed by this PR, say so and it is a two-line addition.

Not run

  • A real Azure run: not touched here.
  • macOS, BSD make, Windows. Only GNU Make 4.3 on Linux was run; the other shells (dash, bash, busybox sh) were checked on the recipe text only.
  • Python versions other than 3.11 for the suite. CI runs 3.11–3.14; the new tests spawn make and do not depend on the interpreter.
  • A GitHub-hosted run of docs.yml and maintenance.yml in normal operation. Neither runs on pull requests (manual/scheduled), so I dispatched both on this branch at d468af4 (see "CI on the head commit"); a scheduled run on main after the merge is still the real check.
  • CI=1 make docs: broken on main already (see "Changes", point 3); not fixed here.

Review Evidence

  • Review source and reviewed commit SHA: none yet. Everything above is implementer-run, which is not independent evidence (docs/review-evidence-policy.md). Head when written: d468af4.
  • Material findings and disposition (fixed / acknowledged-wont-fix / deferred-with-issue), or "none": none (no review yet).
  • Unresolved blockers, or "none": none known; awaiting required checks and maintainer review.
  • R3 (workflow permissions, release/publishing, security-sensitive redaction/exposure, telemetry trust boundaries, review-policy files): no. No workflow, permissions:, secret, release or publishing file, no redaction or telemetry code, and no policy-bearing file (AGENTS.md, CONTRIBUTING.md, SECURITY.md, docs/agent-playbook.md) is touched. The required bandit job's make install now takes the new bootstrap path; its Bandit recipe is unchanged.

Compatibility and documentation

  • This change affects the public API, supported versions, or output format. (No package change. make doctor's own output and exit status change; that is the fix, and nothing parses it.)
  • Breaking change (describe the migration)
  • Documentation updated: one row for make doctor in docs/development.md and docs/contributing.md.

CI on the head commit (d468af4)

All runs are attempt 1 (nothing was re-run). 25 check-runs, read across all pages of the check-runs API for this exact SHA: 24 success, 1 skipped (dependabot-auto-merge, by design), 0 failed or pending. The five required contexts (bandit, semgrep, Analyze, PR title, ci-required) are each present once and succeeded; mergeable_state is clean.

  • bandit job (security.yml), a clean runner checkout: pip install hatch, then make install printed "Creating virtual environment...", "Installing Hatch...", "Creating environment: default" and "CI detected: skipping pre-commit hook installation"; make security then printed "bandit: 4647 lines analysed, no findings". So make install still bootstraps a clean checkout through the new bootstrap. (pip's own "new release available" notice, on stderr, still appears after the install: it is a notice, not an upgrade.)
  • Test (Py 3.11): 878 passed, 4 skipped, 4 deselected, coverage 96.26% (gate 95%). 3.12, 3.13, 3.14: 880 passed, 2 skipped (the two tests that need 3.12+ run there). Minimum dependencies: 878 passed, 4 skipped. The +8 over main are the new tests, in every cell.
  • Lint, type-check & security (Py 3.11): ruff and format clean, mypy strict 64 files.
  • workflow_dispatch on this branch: Maintenance Checks and Docs (strict build check) both succeeded; their logs show the same "Creating virtual environment..." / "Installing Hatch..." lines.
  • Not triggered, as expected for this change: e2e-azure.yml, publish-pypi.yml, otel-nightly.yml.

yeongseon and others added 2 commits October 25, 2026 01:09
… missing tools

Every target depends on bootstrap, which ran `pip install --upgrade pip` and `pip install hatch` each time: three `make bootstrap` runs with Hatch installed made six pip calls and `make check-all` sixteen. `make check` ran `make lint` and then `make typecheck`, but Hatch's `lint` script already ends with the type check, so mypy ran twice. `make doctor` could not fail: its Hatch and `func` probes ended in `|| echo`, so with both missing it printed "No hatch environment found" and "func not found" and exited 0.

- bootstrap creates .venv only when it is missing and runs `pip install hatch` only when $(HATCH) is not an executable regular file. It never upgrades pip and prints nothing when there is nothing to do. `-x` alone is also true for a searchable directory, so the test is `-f` and `-x`; a make file target would be wrong for the same reason, because make treats any existing path as up to date.
- check is now `check: lint`. check-all, e2e-local and clean-all are untouched, and so is the security recipe.
- doctor is read-only: $(HATCH), python3 and git are required and print `ok` or `MISSING <tool> (required)`; any MISSING fails the target. func and azurite are listed as optional and never change the status. The PYTHON variable, used only by the old doctor, is gone.
- docs: the CI-only branch no longer upgrades the ambient pip. No workflow calls it, and it was the last `--upgrade` in the file.

tests/test_makefile_contract.py runs make on a copy of the Makefile with PATH limited to stubs that log their arguments, so nothing is installed and no network is used. It pins: `make -n check` has one `hatch run lint` and no typecheck, and the real run makes one Hatch call and no pip call; bootstrap calls `pip install hatch` only when Hatch is missing or is a directory; doctor exits non-zero with a MISSING line for each required tool, exits 0 with an optional tool missing, and installs nothing; and that no `--upgrade` is left in the Makefile. All 8 cases fail on the previous Makefile.

Refs #614

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
`make doctor` was not documented anywhere, and it now has a contract worth stating: it is a read-only check that exits non-zero when Hatch, python3 or git is missing, and lists func and azurite as optional. Add one row to each of the two command tables that list the other make targets (docs/development.md and docs/contributing.md). CONTRIBUTING.md and AGENTS.md are not touched.

Refs #614

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
@yeongseon

Copy link
Copy Markdown
Owner Author

R3 authorization for head d468af4eaabfb8ba272134da15601d29276285e2.

The maintainer delegated the merge decision to an Oracle review. The Oracle (AI) reviewed this change at head d468af4eaabfb8ba272134da15601d29276285e2 through code inspection and independent probes and returned GO (fresh install/test/security/check-all pass; pip calls 16 to 0; doctor rejects missing and non-executable tools; 14 of 14 mutations detected; 878 passed 96.26%). Changes since that head are merges of main only; required checks were re-run on this exact head and are green. This is advisory AI evidence, not line-by-line human review or human approval. Refs #614 stays open: its local-only labelling criterion for e2e-local is not met here.

@yeongseon
yeongseon merged commit f1956b7 into main Oct 24, 2026
29 checks passed
@yeongseon
yeongseon deleted the build/make-bootstrap-once-doctor branch October 24, 2026 18:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant