@@ -162,7 +162,9 @@ jobs:
162162 pip install --upgrade pip
163163 # Install from packages if available, otherwise legacy
164164 if [ -d "packages/hypergumbo-core" ]; then
165- pip install -e packages/hypergumbo-core -e packages/hypergumbo-lang-mainstream -e packages/hypergumbo-lang-common -e packages/hypergumbo-lang-extended1 -e "packages/hypergumbo[dev]"
165+ # Install test deps directly - NEVER use meta-package (it pulls from PyPI)
166+ pip install pytest pytest-cov pytest-xdist ruff bandit pip-audit jsonschema yamllint
167+ pip install -e packages/hypergumbo-core -e packages/hypergumbo-lang-mainstream -e packages/hypergumbo-lang-common -e packages/hypergumbo-lang-extended1 -e packages/hypergumbo
166168 else
167169 pip install -e .[dev]
168170 fi
@@ -178,6 +180,8 @@ jobs:
178180 timeout-minutes : 30
179181 steps :
180182 - uses : actions/checkout@v4
183+ with :
184+ fetch-depth : 0
181185 - name : Set up Python
182186 uses : actions/setup-python@v5
183187 with :
@@ -188,7 +192,9 @@ jobs:
188192 pip install --upgrade pip
189193 # Install from packages if available, otherwise legacy
190194 if [ -d "packages/hypergumbo-core" ]; then
191- pip install -e packages/hypergumbo-core -e packages/hypergumbo-lang-mainstream -e packages/hypergumbo-lang-common -e packages/hypergumbo-lang-extended1 -e "packages/hypergumbo[dev]"
195+ # Install test deps directly - NEVER use meta-package (it pulls from PyPI)
196+ pip install pytest pytest-cov pytest-xdist ruff bandit pip-audit jsonschema yamllint
197+ pip install -e packages/hypergumbo-core -e packages/hypergumbo-lang-mainstream -e packages/hypergumbo-lang-common -e packages/hypergumbo-lang-extended1 -e packages/hypergumbo
192198 else
193199 pip install -e .[dev]
194200 fi
@@ -205,94 +211,172 @@ jobs:
205211 echo "sentence-transformers not installed, embedding tests will be skipped"
206212 echo "EMBEDDINGS_AVAILABLE=false" >> "$GITHUB_ENV"
207213 fi
208- - name : Validate manifest (if present )
214+ - name : Validate test manifest (smart test selection )
209215 id : manifest
210216 run : |
217+ # Smart test selection: contributor commits .ci/affected-tests.txt
218+ # CI validates it with cheap sanity checks (no hypergumbo installation)
219+ # See ADR-0010 for design rationale
211220 MANIFEST=".ci/affected-tests.txt"
212- if [ -f "$MANIFEST" ]; then
213- echo "Manifest found: $MANIFEST"
214221
215- # Count tests per module for sanity check
216- CORE_TESTS=$(grep -c "hypergumbo-core/tests" "$MANIFEST" 2>/dev/null || echo 0)
217- MAINSTREAM_TESTS=$(grep -c "hypergumbo-lang-mainstream/tests" "$MANIFEST" 2>/dev/null || echo 0)
218- COMMON_TESTS=$(grep -c "hypergumbo-lang-common/tests" "$MANIFEST" 2>/dev/null || echo 0)
219- EXTENDED_TESTS=$(grep -c "hypergumbo-lang-extended1/tests" "$MANIFEST" 2>/dev/null || echo 0)
220- TOTAL_TESTS=$((CORE_TESTS + MAINSTREAM_TESTS + COMMON_TESTS + EXTENDED_TESTS))
222+ # For PRs, validate the committed manifest
223+ if [ "${{ github.event_name }}" = "pull_request" ]; then
224+ BASE="${{ github.event.pull_request.base.sha }}"
225+ HEAD="${{ github.event.pull_request.head.sha }}"
226+
227+ echo "=== Changed files (BASE=$BASE HEAD=$HEAD) ==="
228+ git diff --name-only "$BASE" "$HEAD" | head -20
229+
230+ # Check if core infrastructure changed (requires full suite regardless)
231+ CORE_CHANGED=false
232+ for core_file in sketch.py slice.py cli.py ir.py all_analyzers.py base.py registry.py; do
233+ if git diff --name-only "$BASE" "$HEAD" | grep -qE "$core_file$"; then
234+ CORE_CHANGED=true
235+ echo "✓ Core file changed: $core_file - will run full suite"
236+ break
237+ fi
238+ done
239+
240+ if [ "$CORE_CHANGED" = "true" ]; then
241+ echo "❌ Core infrastructure changed - manifest must include comprehensive tests"
242+ echo ""
243+ echo "When changing core files (sketch.py, slice.py, etc.), smart-test"
244+ echo "should generate a full-suite manifest. Run locally:"
245+ echo " ./scripts/smart-test --manifest"
246+ echo ""
247+ echo "If smart-test falls back to full suite, that's expected for core changes."
248+ # For bootstrap: allow if manifest exists and has full suite marker
249+ if [ -f ".ci/affected-tests.txt" ] && grep -q "^# Full test suite required" ".ci/affected-tests.txt"; then
250+ echo "✓ Found full-suite manifest (bootstrap mode)"
251+ echo "skip_tests=true" >> "$GITHUB_OUTPUT"
252+ echo "bootstrap_mode=true" >> "$GITHUB_OUTPUT"
253+ exit 0
254+ fi
255+ echo ""
256+ echo "During bootstrap (before stable hypergumbo has --files):"
257+ echo "PRs touching core files require manual merge."
258+ exit 1
259+ fi
260+
261+ # Check if manifest exists (contributor ran scripts/smart-test --manifest locally)
262+ if [ ! -f "$MANIFEST" ]; then
263+ echo "❌ No manifest committed"
264+ echo ""
265+ echo "Smart test selection requires a manifest. Run locally:"
266+ echo " ./scripts/smart-test --manifest"
267+ echo " git add .ci/affected-tests.txt"
268+ echo " git commit --amend"
269+ echo ""
270+ echo "See ADR-0010 for details."
271+ exit 1
272+ fi
273+
274+ echo "=== Validating committed manifest ==="
275+ head -10 "$MANIFEST"
221276
222- echo "Manifest tests: core=$CORE_TESTS mainstream=$MAINSTREAM_TESTS common=$COMMON_TESTS extended=$EXTENDED_TESTS total=$TOTAL_TESTS"
277+ # Check if manifest indicates full suite is required (bootstrap case)
278+ # This happens when smart-test couldn't run proper slice (no stable hypergumbo with --files)
279+ if grep -q "^# Full test suite required" "$MANIFEST"; then
280+ echo "⚠️ Bootstrap mode: stable hypergumbo lacks --files flag"
281+ echo ""
282+ echo "Smart-test fell back to full suite because:"
283+ echo " - Stable hypergumbo on PyPI doesn't have 'slice --files' yet"
284+ echo " - This is expected during the bootstrap period"
285+ echo ""
286+ echo "Options:"
287+ echo " 1. Manually merge this PR (tests will run in full-suite.yml after merge)"
288+ echo " 2. Wait for hypergumbo with --files to be published to PyPI"
289+ echo ""
290+ echo "Skipping CI tests - full-suite.yml will validate after merge."
291+ echo "skip_tests=true" >> "$GITHUB_OUTPUT"
292+ echo "bootstrap_mode=true" >> "$GITHUB_OUTPUT"
293+ exit 0
294+ fi
223295
224- # Sanity check: if total is suspiciously low, fall back to full suite
225- if [ "$TOTAL_TESTS" -lt 10 ]; then
226- echo "⚠️ Manifest has only $TOTAL_TESTS tests - falling back to full suite"
296+ # Cheap sanity check: verify manifest has reasonable coverage for changed modules
297+ # This prevents gaming (e.g., committing empty manifest to skip tests)
298+ CHANGED_MODULES=$(git diff --name-only "$BASE" "$HEAD" | grep "^packages/" | cut -d/ -f2 | sort -u || true)
299+
300+ SANITY_OK=true
301+ for mod in $CHANGED_MODULES; do
302+ case $mod in
303+ hypergumbo-core)
304+ MIN_TESTS=20 ;;
305+ hypergumbo-lang-*)
306+ MIN_TESTS=10 ;;
307+ hypergumbo)
308+ # Meta-package has no tests of its own
309+ MIN_TESTS=0 ;;
310+ *)
311+ MIN_TESTS=5 ;;
312+ esac
313+
314+ # Count tests that mention this module (very cheap heuristic)
315+ # Matches paths like packages/hypergumbo-core/tests/test_*.py
316+ ACTUAL=$(grep -c "packages/$mod/tests/" "$MANIFEST" 2>/dev/null || echo 0)
317+
318+ if [ "$ACTUAL" -lt "$MIN_TESTS" ]; then
319+ echo "⚠️ Manifest has $ACTUAL tests for $mod, expected ≥$MIN_TESTS"
320+ SANITY_OK=false
321+ else
322+ echo "✓ $mod: $ACTUAL tests (≥$MIN_TESTS required)"
323+ fi
324+ done
325+
326+ if [ "$SANITY_OK" = "false" ]; then
327+ echo "⚠️ Manifest sanity check failed - falling back to full suite"
227328 echo "use_manifest=false" >> "$GITHUB_OUTPUT"
228329 else
229- echo "✅ Manifest validated ($TOTAL_TESTS tests)"
330+ TOTAL_TESTS=$(grep -c "^packages/" "$MANIFEST" 2>/dev/null || echo 0)
331+ echo "✅ Manifest validated ($TOTAL_TESTS test files)"
230332 echo "use_manifest=true" >> "$GITHUB_OUTPUT"
231333 fi
232334 else
233- echo "No manifest found, running full suite"
234- echo "use_manifest=false" >> "$GITHUB_OUTPUT"
335+ # Push events skip pytest in ci.yml - full-suite.yml handles that
336+ echo "Push event - skipping pytest (full-suite.yml handles comprehensive validation)"
337+ echo "skip_tests=true" >> "$GITHUB_OUTPUT"
235338 fi
236339 - name : Run tests with coverage
340+ if : steps.manifest.outputs.skip_tests != 'true'
237341 run : |
238342 COV_PATHS="--cov=packages/hypergumbo-core/src --cov=packages/hypergumbo-lang-mainstream/src --cov=packages/hypergumbo-lang-common/src --cov=packages/hypergumbo-lang-extended1/src"
239343
240344 echo "=== Python version ==="
241345 python --version
242346
243347 echo "=== Installed packages ==="
244- pip list | grep -E "hypergumbo|pytest|tree-sitter"
245-
246- echo "=== Test directory structure ==="
247- # Use || true to avoid SIGPIPE exit code when head closes early
248- ls -la packages/*/tests/ | head -20 || true
249-
250- echo "=== Running full test suite ==="
251- # Run without explicit test path - pytest will auto-discover from packages/*/tests/
252- if [ -d "packages/hypergumbo-core" ]; then
253- pytest -n 2 --tb=short $COV_PATHS --cov-report=term 2>&1 | tee coverage-output.txt
254- else
255- # Legacy structure
256- pytest -n 2 --tb=short --cov=src --cov-report=term 2>&1 | tee coverage-output.txt
257- fi
258- - name : Update coverage badge
259- if : github.event_name == 'push' && (github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main')
260- run : |
261- # Extract coverage percentage from pytest output
262- COV=$(grep "^TOTAL" coverage-output.txt | awk '{print $NF}' | tr -d '%')
263- if [ -z "$COV" ]; then
264- echo "Could not extract coverage percentage"
265- exit 0 # Don't fail the build
266- fi
348+ pip list | grep -E "hypergumbo|pytest|tree-sitter" | head -20 || true
267349
268- # Determine badge color based on coverage
269- if [ "$COV" -ge 90 ]; then
270- COLOR="brightgreen"
271- elif [ "$COV" -ge 70 ]; then
272- COLOR="yellow"
350+ MANIFEST=".ci/affected-tests.txt"
351+ if [ -f "$MANIFEST" ] && [ "${{ steps.manifest.outputs.use_manifest }}" = "true" ]; then
352+ echo "=== Running affected tests only (smart test selection) ==="
353+ # Filter out comment lines (starting with #) from manifest
354+ AFFECTED_TESTS=$(grep -v '^#' "$MANIFEST" | grep -v '^$' | tr '\n' ' ')
355+ echo "Running: pytest $AFFECTED_TESTS"
356+ pytest -n 2 --tb=short $COV_PATHS --cov-report=term $AFFECTED_TESTS 2>&1 | tee coverage-output.txt
273357 else
274- COLOR="red"
358+ echo "❌ No valid manifest - cannot run tests"
359+ echo ""
360+ echo "Fast-CI requires a manifest with targeted tests."
361+ echo "Full suite runs in full-suite.yml after merge, not here."
362+ exit 1
275363 fi
276364
277- # Create shields.io endpoint JSON in temp location
278- echo "{\"schemaVersion\":1,\"label\":\"coverage\",\"message\":\"${COV}%\",\"color\":\"${COLOR}\"}" > /tmp/coverage.json
279-
280- # Configure git
281- git config user.name "CI Bot"
282- git config user.email "ci@iterabloom.com"
283-
284- # Fetch badges branch or create orphan
285- git fetch origin badges:badges 2>/dev/null || true
286- git checkout badges 2>/dev/null || git checkout --orphan badges
287-
288- # Clean branch and add only coverage.json
289- git rm -rf . 2>/dev/null || true
290- cp /tmp/coverage.json coverage.json
291-
292- # Commit and push
293- git add coverage.json
294- git commit -m "Update coverage badge: ${COV}%" || echo "No changes to commit"
295- git push origin badges --force || echo "Push failed - may need write permissions"
365+ - name : Bootstrap mode notice
366+ if : steps.manifest.outputs.bootstrap_mode == 'true'
367+ run : |
368+ echo "╔════════════════════════════════════════════════════════════╗"
369+ echo "║ BOOTSTRAP MODE - Tests skipped in fast-CI ║"
370+ echo "╚════════════════════════════════════════════════════════════╝"
371+ echo ""
372+ echo "This PR can be manually merged. After merge, full-suite.yml"
373+ echo "will run comprehensive validation."
374+ echo ""
375+ echo "To enable smart test selection, publish hypergumbo with"
376+ echo "'slice --files' to PyPI, then contributors can generate"
377+ echo "targeted manifests."
378+ # Coverage badge is updated by full-suite.yml, not here
379+ # (ci.yml skips pytest on push events)
296380
297381 # Final gate job - ensures workflow completes even when jobs are skipped
298382 # This fixes Forgejo Actions bug where skipped jobs leave workflow in "running" state
0 commit comments