fix(reports): refresh retired Yandex docs URLs + add docs-drift protection (#426)#427
Conversation
…s-drift protection (#426) Yandex retired three Reports docs paths (`/ru/reports/<page>`) in favor of `/ru/<page>` and renamed `spec.html` to `spec`. The drift checker silently fell into a Yandex SmartCaptcha rate-limit gateway for three of those pages (~14.6 KB captcha HTML instead of real docs); the existing `spec.json` snapshot survived only because it was committed pre-migration. The fix also catches five more URLs that Yandex moved from `…/ru/<group>/<group>` to `…/ru/<group>` (dynamictextadtargets, dynamicfeedadtargets, reports, smartadtargets, vcards). Protection against silent recurrence: - `fetch_reports_spec` and `fetch_wsdl`/`fetch_live_wsdl` reject responses matching SmartCaptcha markers or below a size threshold. - New tests `test_reports_cache_files_are_real_content` and `test_wsdl_cache_files_are_real_content` guard the committed caches. - New `scripts/check_all_docs_urls.py` paces requests, follows redirects, and fails hard on canonical path moves / captcha / 4xx. - `scripts/release_pypi.sh` runs the health check + cache refresh + focused pytest as a mandatory pre-release gate (no opt-out). - `CLAUDE.md` codifies "no URL literals outside the registry" and the pre-release gate. Closes #426. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
|
@claude review. Focus on critical issues: bugs, security vulnerabilities, logical errors, data loss risks, performance problems. Do NOT nitpick style, naming conventions, minor formatting, or subjective preferences — only flag issues that could break functionality or cause real harm in production. |
|
Claude finished @axisrow's task in 3m 49s —— View job PR Review — Critical Issues Only
Solid PR overall — the captcha guards, cache-content tests, and URL drift checker are the right shape. Below are the issues I'd want addressed before this becomes the gating release path. Everything else (style, naming, thresholds) I'm intentionally skipping per the review brief. 1. HEAD-only probing will produce false
|
Three FIX items from the @claude review: 1. HEAD-only probing in check_all_docs_urls.py falsely declared CDN-issued 405/403 as GONE. Now: only 5xx and 3xx-with-different-path are decided from HEAD; everything else falls through to GET, and GONE is only declared when GET also returns 4xx. Prevents Yandex CDN false positives from blocking a release. 2. release_pypi.sh no longer runs refresh_reports_cache.py, which mutated committed files mid-release. Instead it runs the read-only freshness tests and then refuses to release if tests/reports_cache or tests/wsdl_cache has uncommitted changes — the maintainer must commit the cache refresh in a separate, reviewable diff. 3. fetch_imported_xsd now validates responses against a new _assert_real_xsd guard (captcha markers + min size + xml/schema markers), closing the silent-poisoning hole for tests/wsdl_cache/imports/*.xsd. New test test_imported_xsd_cache_files_are_real_content extends coverage to those three XSDs that the original glob('*.xml') had missed. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
|
Triage applied in 1f1b8d3:
|
|
@claude review. Focus on critical issues: bugs, security vulnerabilities, logical errors, data loss risks, performance problems. Do NOT nitpick style, naming conventions, minor formatting, or subjective preferences — only flag issues that could break functionality or cause real harm in production. |
|
Claude finished @axisrow's task in 2m 5s —— View job Re-review — follow-up commit 1f1b8d3
All three FIX items look correctly implemented. Notes below are not blockers, just observations from re-reading the patch. #1 — HEAD→GET fallback (FIXED, correctly)
One minor edge case worth knowing about (not a blocker): the GET on line 79 doesn't set #3 — refresh out of release path (FIXED, with one subtlety)
One subtle behavior worth flagging (borderline minor): …would pass the gate even though the cache has uncommitted-to-HEAD changes. The intent of the gate ("maintainer must commit the refresh explicitly") is better expressed as: if ! git diff --quiet HEAD -- tests/reports_cache tests/wsdl_cache; thenThat covers both unstaged and staged-but-uncommitted changes. As-is, the practical risk is small (released wheels don't include #4 — imported XSD captcha guard + recursive test (FIXED, correctly)
Implementation matches the production guard's checks one-for-one. VerdictThe triage applied in 1f1b8d3 addresses the three FIX items I flagged. The two SKIP items (#2 escape hatch, #5 namespace prefix) are defensible per the author's reasoning — #2's underlying risk dissolves now that #1 no longer cascades HEAD failures into release blocks, and #5 is a documented-as-fragile guard with no observed drift. The two minor notes above (GET redirect handling in LGTM to merge as-is. |
- release_pypi.sh: use `git diff --quiet HEAD --` so the gate trips on staged-but-uncommitted cache changes too (was checking against index only). - check_all_docs_urls.py: pass allow_redirects=False to the GET fallback, so a canonical move that only surfaces on GET (HEAD 4xx → GET 3xx-to- different-path) is still detected as MOVED instead of silently followed. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Pre #427 the release script ran scripts/check_all_docs_urls.py inline, so a SmartCaptcha rate-limit on our IP (302 → showcaptcha, or HEAD 302/GET 404 when Yandex throttles the docs CDN) would hard-fail an otherwise valid release. Changes: - New scripts/preflight_check.sh runs the three network-dependent checks (docs URLs, cache freshness, no uncommitted cache diff) as a separate, manual step. Run before bumping the version. - scripts/release_pypi.sh no longer calls those checks — it only rebuilds the wheel, runs twine check, and uploads. Releases become deterministic; transient Yandex throttling cannot block them. - scripts/check_all_docs_urls.py downgrades CAPTCHA from hard-fail to soft-warn (same bucket as 5xx), since a persisted captcha is an external rate-limit on our IP — not evidence the URL is gone. Canonical move (3xx → different path) and confirmed 4xx remain hard-fail. - CLAUDE.md "PyPI Release" section rewritten to document the two-phase flow. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Summary
/ru/type,/ru/period,/ru/fields-list,/ru/spec) after Yandex moved them off the legacy/ru/reports/<page>layout and renamedspec.html→spec. Three of the four had silently been captcha-poisoned (~14.6 KB SmartCaptcha HTML) intests/reports_cache/raw/.RESOURCE_MAPPING_V5[*]["docs"]URLs that Yandex moved from…/ru/<group>/<group>to single-segment…/ru/<group>(dynamictextadtargets,dynamicfeedadtargets,reports,smartadtargets,vcards). Discovered by the new health checker.spec.jsonis byte-equivalent to the pre-migration snapshot save for one updatedAttributionModelsdescription string.Protection against recurrence
fetch_reports_specandfetch_wsdl/fetch_live_wsdlraise a clearRuntimeErrorwhen a response containsshowcaptcha/smartcaptcha/<title>Captcha, or is suspiciously short. No more silent cache poisoning.test_reports_cache_files_are_real_contentandtest_wsdl_cache_files_are_real_contentenforce the same invariants on committed cache files.scripts/check_all_docs_urls.py— paces requests (1.5s gap + 30s captcha retry), enumerates every URL inRESOURCE_MAPPING_V5andREPORTS_SPEC_URLS, hard-fails on 3xx-to-different-path, 4xx, or captcha body; soft-warns on 5xx.release_pypi.sh— runscheck_all_docs_urls.py+refresh_reports_cache.py+ focused pytest before building the wheel. Hard fail = no release. No--skip-checksflag.Test plan
pytest tests/test_api_coverage.py::TestReportsCoverage tests/test_api_coverage.py::TestWsdlCacheFreshness -v— 10/10 green.pytest tests/ -q --timeout=60— 1945 passed, 45 skipped, 0 failed.python scripts/check_all_docs_urls.py— all 37 URLs OK./ru/reports/typeURL back intoRESOURCE_MAPPING_V5["reports"]["docs_pages"]["type"], ran the health checker — caught it asGONE HTTP 404and exited with code 1. Restored.grep -l "showcaptcha\|smartcaptcha" tests/reports_cache/raw/*.html— empty.Out of scope
*FieldNamesparity (already closed under Wave 2: expose remaining nested *FieldNames request parameters as CLI options #402 / milestone 0.3.14).--fields→--field-namesper the no-legacy-aliases rule (docs(claude): add 'no legacy CLI flag aliases' rule #423). Tracked separately if needed.Closes #426.
🤖 Generated with Claude Code