fix(ownership): report a Cloudflare challenge as its own error and result

When Cloudflare answers idx.co.id with a challenge, curl fails with only
`curl: (22)`, which exits 4 like any other HTTP failure, and every profile
is tried twice. A challenge needs a browser, so no profile can solve it. The
source is monthly, so the publisher only needs one run per month to get
through: treat a challenge as "retry tomorrow" and alert on stale data.

- Add IdxError::CloudflareChallenge (CLOUDFLARECHALLENGE, exit 6), detected
  from the cf-mitigated header. curl reports it on stderr through --write-out,
  because --fail drops the response.
- A URL with a challenge gets one round of profiles; other failures keep two.
  This also covers `stocks flow --source idx`, which now exits 6 on a challenge.
- -v logs which profile fetched each URL.
- Discovery keeps error types, so a challenge survives an announcement miss.
  A source answering with something unparseable still is not called a challenge.
- `ownership discover --require-data-page` fails when the data page cannot be
  read. Default discover is unchanged: it warns and lists older announcements.
- The publisher and builder use that flag, so a failed data page can no longer
  end in a false "up-to-date". A challenge ends in `RESULT: blocked stage=<stage>`,
  exit 6; every other failure stays `RESULT: FAILED`.
- The systemd example treats exit 6 as success and runs the freshness check, so
  the unit fails on FAILED or on stale data.
- live-smoke reports a challenge as CHALLENGED, a failure and never a pass.

Golden change: completions-bash.golden gains `--require-data-page` in the
discover options, one word on one line. It is the direct result of the new flag;
no other golden file changes.

Live check on 2026-11-05 against release/v0.5.0: 19 passed, 2 failed. Both
failures are real challenges (flow-idx, discover), each exit 6 with empty stdout.
The import, releases and ticker cases did not run, because discover did not pass.

Refs #83
This commit is contained in:
hermes 2026-11-05 08:54:58 +00:00
commit b236c7f9d9
23 changed files with 797 additions and 54 deletions

View file

@ -23,8 +23,13 @@ artifact is missing, and a retry repairs the publication.
The last line is always one of:
RESULT: published <as-of>
RESULT: up-to-date <as-of>
RESULT: blocked stage=<stage>
RESULT: FAILED stage=<stage> (exit <code>)
A Cloudflare challenge from IDX is "blocked", exit 6: nothing is wrong with the
run, so retry on the next schedule and alert on stale data instead. Every other
failure is FAILED.
Options:
--idx-bin <path> idx binary to use (default: ./target/debug/idx)
--output-dir <dir> Directory to write the copied snapshot and manifest
@ -66,6 +71,14 @@ fail() {
exit "$status"
}
# idx's exit status for a Cloudflare challenge.
BLOCKED_STATUS=6
blocked() {
printf 'RESULT: blocked stage=%s\n' "$STAGE" >&2
exit "$BLOCKED_STATUS"
}
# Unexpected command failures (set -e). errtrace is deliberately off: with
# it, a failure inside $(...) would run this in the subshell and the parent.
on_error() {
@ -291,8 +304,14 @@ remote_asset_sha256() {
# asked for more history than the source has does not rebuild forever.
# Returns non-zero when discovery is unusable; callers then build.
compute_requested_coverage() {
local json latest available wanted
if ! json="$("$IDX_BIN" -o json ownership discover --family above1 --limit 50 2>/dev/null)"; then
local json latest available wanted status=0
# --require-data-page: the newest reports are only on the data page, so
# without it a failed page would read as "up-to-date" against older results.
json="$("$IDX_BIN" -v -o json ownership discover --family above1 --limit 50 --require-data-page)" || status=$?
if (( status == BLOCKED_STATUS )); then
blocked
fi
if (( status != 0 )); then
return 1
fi
@ -410,7 +429,14 @@ if [[ "$KEEP_WORKDIR" == "1" ]]; then
fi
printf 'Preparing latest ownership snapshot artifacts...\n'
"$BUILDER" "${build_args[@]}"
build_status=0
"$BUILDER" "${build_args[@]}" || build_status=$?
if (( build_status == BLOCKED_STATUS )); then
blocked
fi
if (( build_status != 0 )); then
fail "$build_status" "snapshot build failed"
fi
STAGED_MANIFEST_PATH="$PUBLISH_WORKDIR/ownership-snapshot-manifest.json"
if [[ ! -f "$STAGED_MANIFEST_PATH" ]]; then