diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml index 853beff..439785d 100644 --- a/.github/workflows/canary.yml +++ b/.github/workflows/canary.yml @@ -55,7 +55,11 @@ jobs: - name: Require a complete run with full rows run: | python3 -c " - import json, os + import json, os, sys, traceback + def annotate(kind, exc, tb): + traceback.print_exception(kind, exc, tb) + print('::error::canary row check failed: %s: %s' % (kind.__name__, str(exc)[:400].replace(chr(10), ' '))) + sys.excepthook = annotate code = int(os.environ['canary_exit']) print(f'exit {code}') if code != 0: @@ -63,6 +67,8 @@ jobs: print('::error::The direct run from the GitHub runner did not complete (exit %d). ' 'If it was refused (exit 3), set the INSTAGRAM_PROXY repo secret (a residential ' 'http://login:password@host:port proxy) so the canary can reach the site.' % code) + else: + print('::error::canary exit %d through the INSTAGRAM_PROXY secret (1 crash, 3 blocked, 4 zero rows, 5 content never obtained, 6 partial). A proxy that rejects its credential (HTTP 407) reaches Chromium as a navigation timeout, so on exit 5 check the INSTAGRAM_PROXY secret first.' % code) raise SystemExit(f'canary requires EXIT_OK=0, got {code}') meta = json.load(open('canary.json.meta.json')) print(json.dumps(meta, indent=2)) diff --git a/CHANGELOG.md b/CHANGELOG.md index a82e8c8..bdc856e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,11 @@ that changes a default is called out at the top of its entry. ### Changed +- CI only: a failed canary check now always ends with an `::error::` line. A run + through the `INSTAGRAM_PROXY` secret that exited nonzero used to end red with no + annotation, and a missing or wrong output file showed only a traceback. + The message notes that a proxy rejecting its credential (HTTP 407) shows + up in Chromium as a navigation timeout (exit 5), not as a 407. - CI only: the canary now runs weekly (Mondays 06:41 UTC) and on a manual dispatch, with its optional secret `INSTAGRAM_PROXY` passed on every run; the daily schedule is removed. ## [0.3.0] - 2026-10-15 diff --git a/smoke_test.py b/smoke_test.py index ec3b38c..3496412 100644 --- a/smoke_test.py +++ b/smoke_test.py @@ -19,6 +19,7 @@ import asyncio import inspect as _inspect import json +import os import tempfile from pathlib import Path @@ -1314,6 +1315,48 @@ def _(): assert 'log.info("Fingerprint applied: user agent %s", user_agent)' in (ROOT / path).read_text(encoding="utf-8"), path +def _canary_check_snippets(): + """Every `python3 -c "..."` block in the canary workflow that reads canary_exit.""" + import textwrap + lines = (ROOT / ".github" / "workflows" / "canary.yml").read_text(encoding="utf-8").splitlines() + snippets, i = [], 0 + while i < len(lines): + if lines[i].strip() == 'python3 -c "': + body = [] + i += 1 + while lines[i].strip() != '"': + body.append(lines[i]) + i += 1 + text = textwrap.dedent("\n".join(body)) + if "canary_exit" in text: + # bash would expand these inside the double quotes; this check runs the text as-is + assert not any(ch in text for ch in "$`\\"), "canary snippet uses a bash-special character" + snippets.append(text) + i += 1 + return snippets + + +@check("every way the canary check step fails prints an ::error:: line: a nonzero exit with or without the proxy secret, and a missing or wrong output file (a red job with no annotation hid a proxy failure on 2026-10-20)") +def _(): + import subprocess + import sys + if not (ROOT / ".github").is_dir(): + return # the Docker image ships no workflows; the repository's own CI runs this check + snippets = _canary_check_snippets() + assert snippets, "no canary check step found in .github/workflows/canary.yml" + with tempfile.TemporaryDirectory() as tmp: + for snippet in snippets: + for has_proxy in ("true", "false"): + for code in ("0", "1", "3", "4", "5", "6"): + env = dict(os.environ, HAS_PROXY=has_proxy, canary_exit=code, + canary_details_exit=code, CANARY_REGION="US") + proc = subprocess.run([sys.executable, "-c", snippet], cwd=tmp, env=env, + capture_output=True, text=True, timeout=60) + # code 0 in an empty directory: the sidecar is missing, which must fail too + assert proc.returncode != 0, (has_proxy, code, "an empty directory passed") + assert "::error::" in proc.stdout, (has_proxy, code, proc.stdout[-300:], proc.stderr[-300:]) + + def run() -> int: """All @check-decorated functions above already ran at import time (that's the point — see the `check()` docstring) and self-registered