The 2026-05-04 redeploy-tenants-on-main run for sha 2b862f6 emitted
"HTTP 000000" and failed the deploy. Root cause: when curl exits non-
zero (connection reset → 56, --fail-with-body 4xx/5xx → 22), the
`-w '%{http_code}'` already wrote a status to stdout; the inline
`|| echo "000"` then fires AND appends another "000" to the captured
substitution stdout. Result: HTTP_CODE="<actual><000>" — fails string
comparisons against "200" while looking superficially right.
Same class of bug the synth-E2E §7c gate hit twice (PRs #2779/#2783
+ #2797). Memory feedback_curl_status_capture_pollution.md.
Mass fix in 8 workflows: route -w into a tempfile so curl's exit
code can't pollute stdout. Wrap with set +e/-e so the non-zero
curl exit doesn't trip the outer pipeline.
redeploy-tenants-on-main.yml (production-critical, caught the bug)
redeploy-tenants-on-staging.yml (sibling)
sweep-stale-e2e-orgs.yml (cleanup loop)
e2e-staging-sanity.yml (E2E safety-net teardown)
e2e-staging-saas.yml
e2e-staging-external.yml
e2e-staging-canvas.yml
canary-staging.yml
Plus a new lint workflow `lint-curl-status-capture.yml` that runs on
every PR/push touching `.github/workflows/**`. Multi-line aware:
collapses bash `\` continuations, then matches the buggy
$(curl ... -w '%{http_code}' ... || echo "000") subshell shape.
Distinguishes from the SAFE $(cat tempfile || echo "000") shape
(cat with missing file emits empty stdout, no pollution).
Verified:
- All 8 workflows pass the lint locally
- A known-bad injection is caught
- A known-safe cat-fallback passes through
- yaml.safe_load clean on all changed files
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
95 lines
4.2 KiB
YAML
95 lines
4.2 KiB
YAML
name: Lint curl status-code capture
|
|
|
|
# Pins the workflow-bash anti-pattern that produced "HTTP 000000" on the
|
|
# 2026-05-04 redeploy-tenants-on-main run for sha 2b862f6:
|
|
#
|
|
# HTTP_CODE=$(curl ... -w '%{http_code}' ... || echo "000")
|
|
#
|
|
# When curl exits non-zero (connection reset → 56, --fail-with-body 4xx/5xx
|
|
# → 22), the `-w '%{http_code}'` already wrote a status to stdout — usually
|
|
# "000" for connection failures or the actual code for HTTP errors. The
|
|
# `|| echo "000"` then fires AND appends ANOTHER "000" to the captured
|
|
# stdout, producing values like "000000" or "409000" that fail string
|
|
# comparisons against "200" while looking superficially right.
|
|
#
|
|
# Same class of bug the synth-E2E §7c gate hit twice (PRs #2779/#2783 +
|
|
# #2797). Memory: feedback_curl_status_capture_pollution.md.
|
|
#
|
|
# Fix shape (route -w into a tempfile so curl's exit code can't pollute):
|
|
#
|
|
# set +e
|
|
# curl ... -w '%{http_code}' >code.txt 2>/dev/null
|
|
# set -e
|
|
# HTTP_CODE=$(cat code.txt 2>/dev/null)
|
|
# [ -z "$HTTP_CODE" ] && HTTP_CODE="000"
|
|
|
|
on:
|
|
pull_request:
|
|
paths: ['.github/workflows/**']
|
|
push:
|
|
branches: [main, staging]
|
|
paths: ['.github/workflows/**']
|
|
merge_group:
|
|
types: [checks_requested]
|
|
|
|
jobs:
|
|
scan:
|
|
name: Scan workflows for curl status-capture pollution
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
- name: Find curl ... -w '%{http_code}' ... || echo "000" subshells
|
|
run: |
|
|
set -uo pipefail
|
|
# Multi-line aware: look for `$(curl ... -w '%{http_code}' ... || echo "000")`
|
|
# subshell where the entire command-substitution wraps a curl that
|
|
# ends with `|| echo "000"`. Must distinguish from the SAFE shape
|
|
# `$(cat tempfile 2>/dev/null || echo "000")` — `cat` with a missing
|
|
# tempfile produces empty stdout, no pollution.
|
|
python3 <<'PY'
|
|
import os, re, sys, glob
|
|
|
|
BAD_FILES = []
|
|
|
|
# Match the buggy substitution across newlines: $(curl ... -w '%{http_code}' ... || echo "000")
|
|
# The `\\n` is the bash line-continuation that lets curl flags span lines.
|
|
# We collapse continuation lines first, then look for the single-line bad pattern.
|
|
PATTERN = re.compile(
|
|
r'\$\(\s*curl\b[^)]*-w\s*[\'"]%\{http_code\}[\'"][^)]*\|\|\s*echo\s+"000"\s*\)',
|
|
re.DOTALL,
|
|
)
|
|
|
|
# Self-skip: this lint workflow contains the literal anti-pattern in
|
|
# its own docstring — that's intentional, not a bug.
|
|
SELF = ".github/workflows/lint-curl-status-capture.yml"
|
|
|
|
for f in sorted(glob.glob(".github/workflows/*.yml")):
|
|
if f == SELF:
|
|
continue
|
|
with open(f) as fh:
|
|
content = fh.read()
|
|
# Collapse bash line-continuations (\\\n + leading whitespace)
|
|
# into a single logical line so the regex can see the full
|
|
# curl invocation as one chunk.
|
|
flat = re.sub(r'\\\s*\n\s*', ' ', content)
|
|
for m in PATTERN.finditer(flat):
|
|
BAD_FILES.append((f, m.group(0)[:120]))
|
|
|
|
if not BAD_FILES:
|
|
print("✓ No curl-status-capture pollution patterns detected")
|
|
sys.exit(0)
|
|
|
|
print(f"::error::Found {len(BAD_FILES)} curl-status-capture pollution site(s):")
|
|
for f, snippet in BAD_FILES:
|
|
print(f"::error file={f}::Curl status-capture pollution: '|| echo \"000\"' inside a $(curl ... -w '%{{http_code}}' ...) subshell. On non-2xx or connection failure, curl's -w writes a status, then exits non-zero, then the || echo appends another '000' — producing 'HTTP 000000' or '409000' that fails comparisons silently. Fix: route -w into a tempfile so the exit code can't pollute stdout. See memory feedback_curl_status_capture_pollution.md.")
|
|
print(f" matched: {snippet}…")
|
|
print()
|
|
print("Fix template:")
|
|
print(' set +e')
|
|
print(' curl ... -w \'%{http_code}\' >code.txt 2>/dev/null')
|
|
print(' set -e')
|
|
print(' HTTP_CODE=$(cat code.txt 2>/dev/null)')
|
|
print(' [ -z "$HTTP_CODE" ] && HTTP_CODE="000"')
|
|
sys.exit(1)
|
|
PY
|