Skip to content

Commit 873d2ec

Browse files
TexasCodingclaude
andauthored
harden(spec-drift): stop the recurring #467/#470 spam + close drift-test false-negatives (#471)
* ci(spec-drift): stop weekly/daily issue spam — ack/snooze, auto-close, content titles The recurring #467 (nightly) / #470 (weekly) issues are two detectors on one real, deterministic upstream drift — but the reporting mechanics amplified the pain. Fix the mechanics without weakening the read-only security model. Nightly (spec-drift.yml): - Comment only when the failing-test-ID set changes (a fingerprint), not every failing night — a deterministic upstream produced identical spam indefinitely. - `spec-drift-ack` / `snoozed` label silences comments while a reconcile is in flight, without closing the tracker. - New report-success job auto-closes the tracker on a green scheduled run (the workflow previously only ever opened/commented). Weekly (spec-sync.yml): - Title leads with a content signal ([content-changed] + path delta + short fingerprint) instead of a bare version — Kalshi changes content in place without bumping info.version, so "3.23.0 → 3.23.0" read as a false alarm. - Supersede stale weekly snapshots so exactly one weekly drift issue stays open instead of one per upstream micro-revision. Scoped to spec-sync-bot so the nightly tracker is never touched. Refs #467, #470 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(contracts): de-brittle drift suite + close false-negative gaps Direction 1 — less brittle (stop benign churn redding CI against a fast-moving, loosely-versioned upstream): - Additive drift split: a spec-REQUIRED field the SDK lacks still hard-fails (act now); a spec-OPTIONAL field becomes a non-blocking AdditiveOptionalDriftWarning. It deliberately subclasses Warning (not UserWarning) so the nightly `-W error::UserWarning` gate cannot promote benign optional drift to a red build. +3 regression tests lock the contract. - test_schema_coverage batches all unresolved schemas into one failure instead of failing on the first (one-at-a-time reruns on multi-removal drift). Direction 2 — close false-negatives (catch real drift the suite missed): - test_every_spec_endpoint_is_mapped asserts every spec OPERATION (core+perps+perps_scm) is in a METHOD_ENDPOINT_MAP, minus a reasoned _UNIMPLEMENTED_ENDPOINTS allowlist. Catches ADDED endpoints/operations the schema/path-count diff misses — verified against live 3.24.0, which had it caught the two new /margin/* endpoints AND a new GET on an existing /communications/rfqs/.../quotes path that the weekly path diff reported as "Added endpoints: (none)". - kwarg_rename exclusions are now spec-anchored (test_exclusion_map_is_current): the spec-side param must still exist for the endpoint, so a rename can't go stale and silently mask an upstream param removal/rename. Docstring updated to the actual post-#171/#172 behavior and to document why response-side field REMOVAL is intentionally NOT gated: the vendored "Kalshi Trade API Manual Endpoints" spec is a curated subset, so SDK response models are a superset by design; gating removals would red CI on every intentional SDK field. Request-BODY drift already gates removals (_check_model_drift), which is where a rename's dropped old name is caught. Refs #467, #470 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent be0c5fb commit 873d2ec

3 files changed

Lines changed: 381 additions & 40 deletions

File tree

.github/workflows/spec-drift.yml

Lines changed: 81 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,14 @@ jobs:
8585
> /tmp/drift.log 2>&1 || true
8686
# Keep the last 200 lines so the body stays inside GitHub's limits.
8787
tail -n 200 /tmp/drift.log > /tmp/drift.tail
88+
# Failing-set fingerprint: sha256 over the sorted, unique failing test
89+
# IDs. Deterministic upstream yields an identical set every night, so
90+
# the tracker step below re-comments only when this fingerprint CHANGES
91+
# (kills the daily "Still failing" spam that made #467 noisy).
92+
grep -oE '^FAILED [^ ]+' /tmp/drift.log | awk '{print $2}' | sort -u \
93+
> /tmp/fail_ids.txt || true
94+
fail_fp=$(sha256sum /tmp/fail_ids.txt | awk '{print $1}')
95+
echo "fail_fp=${fail_fp}" >> "$GITHUB_OUTPUT"
8896
8997
- name: Ensure spec-drift label exists
9098
env:
@@ -100,11 +108,12 @@ jobs:
100108
env:
101109
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
102110
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
111+
FAIL_FP: ${{ steps.repro.outputs.fail_fp }}
103112
run: |
104113
set -euo pipefail
105114
106115
# Stable marker (NOT a content fingerprint): exactly one open tracker
107-
# at a time. Re-runs append a dated comment instead of duplicating.
116+
# at a time. Re-runs comment only when the FAILING SET changes.
108117
readonly MARKER='nightly-spec-drift-tracker'
109118
today=$(date -u '+%Y-%m-%d')
110119
@@ -120,34 +129,99 @@ jobs:
120129
# heredoc) so upstream-sourced log text is never shell-expanded.
121130
{
122131
printf '## Nightly strict contract tests failed\n\n'
123-
printf 'The scheduled `Spec Drift Detection` run promoted additive drift to an error.\n'
124-
printf 'This means upstream OpenAPI/AsyncAPI changed and the SDK contract suite no longer matches.\n\n'
125-
printf '**To resolve:** locally run `uv run python scripts/sync_spec.py` + `uv run python scripts/generate.py`, reconcile models/maps, and open a PR with `Closes #<this issue>`.\n\n'
132+
printf 'The scheduled `Spec Drift Detection` run found the SDK contract suite no longer matches upstream OpenAPI/AsyncAPI.\n\n'
133+
printf '**To resolve:** locally run `uv run python scripts/sync_spec.py` + `uv run python scripts/generate.py`, reconcile models/maps, and open a PR with `Closes #<this issue>`. This tracker auto-closes on the next green scheduled run.\n\n'
134+
printf '**To silence** while a reconcile is in flight, add the `spec-drift-ack` label — nightly runs then stop commenting until the failing set changes.\n\n'
126135
printf -- '- Failing run: %s\n\n' "${RUN_URL}"
127136
printf '### Failing test output (last 200 lines)\n\n```\n'
128137
cat /tmp/drift.tail
129138
printf '\n```\n'
130139
} > /tmp/body.md
131140
132141
if [ -n "${existing}" ]; then
142+
# Ack/snooze gate: a maintainer already on this drift can add the
143+
# `spec-drift-ack` (or `snoozed`) label to silence nightly noise
144+
# without closing the tracker.
145+
labels=$(gh issue view "${existing}" --repo "${GITHUB_REPOSITORY}" \
146+
--json labels --jq '[.labels[].name] | join(",")')
147+
case ",${labels}," in
148+
*,spec-drift-ack,*|*,snoozed,*)
149+
echo "Tracker #${existing} is ack'd/snoozed; not commenting."
150+
exit 0 ;;
151+
esac
152+
153+
# Only comment when the FAILING SET changed. A deterministic upstream
154+
# produces an identical set every night; re-posting it is spam.
155+
last_fp=$(gh issue view "${existing}" --repo "${GITHUB_REPOSITORY}" \
156+
--json body,comments --jq '[.body, (.comments[].body)] | .[]' \
157+
| grep -oE 'fail-fingerprint:[0-9a-f]+' | tail -1 | cut -d: -f2 || true)
158+
if [ "${last_fp}" = "${FAIL_FP}" ]; then
159+
echo "Failing set unchanged (fingerprint ${FAIL_FP}); no new comment."
160+
exit 0
161+
fi
162+
133163
{
134-
printf 'Still failing as of **%s** ([run](%s)).\n\n' "${today}" "${RUN_URL}"
164+
printf 'Failing set changed as of **%s** ([run](%s)).\n\n' "${today}" "${RUN_URL}"
135165
printf '### Latest failing output (last 200 lines)\n\n```\n'
136166
cat /tmp/drift.tail
137167
printf '\n```\n'
168+
printf '\n<!-- fail-fingerprint:%s -->\n' "${FAIL_FP}"
138169
} > /tmp/comment.md
139-
echo "Tracker already open as #${existing}; appending dated comment."
170+
echo "Failing set changed (${last_fp:-none} -> ${FAIL_FP}); appending comment."
140171
gh issue comment "${existing}" \
141172
--repo "${GITHUB_REPOSITORY}" \
142173
--body-file /tmp/comment.md
143174
exit 0
144175
fi
145176
146-
# Hidden marker so the dedup query above matches future runs.
177+
# Hidden markers so future runs match this tracker (MARKER) and can
178+
# diff the failing set (fail-fingerprint).
147179
printf '\n<!-- spec-drift-bot: do not edit\n%s\n-->\n' "${MARKER}" >> /tmp/body.md
180+
printf '<!-- fail-fingerprint:%s -->\n' "${FAIL_FP}" >> /tmp/body.md
148181
149182
gh issue create \
150183
--repo "${GITHUB_REPOSITORY}" \
151184
--label spec-drift \
152185
--title "Nightly spec-drift: strict contract tests failing (since ${today})" \
153186
--body-file /tmp/body.md
187+
188+
# Companion to report-failure: when a scheduled strict run goes GREEN, the
189+
# drift has been reconciled — close the open nightly tracker so it doesn't
190+
# linger open forever (the old workflow only ever opened/commented). Least
191+
# privilege: issues:write lives only on this job, and it runs no third-party
192+
# actions and no untrusted upstream code (pure `gh` against the repo), so it
193+
# needs neither a checkout nor SHA-pinning.
194+
report-success:
195+
needs: drift-check
196+
if: success() && github.event_name == 'schedule'
197+
runs-on: ubuntu-latest
198+
permissions:
199+
contents: read
200+
issues: write
201+
steps:
202+
- name: Close resolved nightly drift tracker
203+
env:
204+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
205+
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
206+
run: |
207+
set -euo pipefail
208+
readonly MARKER='nightly-spec-drift-tracker'
209+
today=$(date -u '+%Y-%m-%d')
210+
211+
existing=$(gh issue list \
212+
--repo "${GITHUB_REPOSITORY}" \
213+
--state open \
214+
--label spec-drift \
215+
--limit 200 \
216+
--json number,body \
217+
--jq '[.[] | select(.body | contains("'"${MARKER}"'"))] | first | .number // empty')
218+
219+
if [ -z "${existing}" ]; then
220+
echo "No open nightly tracker; nothing to close."
221+
exit 0
222+
fi
223+
224+
gh issue close "${existing}" \
225+
--repo "${GITHUB_REPOSITORY}" \
226+
--reason completed \
227+
--comment "Resolved — strict contract suite green as of ${today} ([run](${RUN_URL})). Auto-closed by the nightly spec-drift workflow."

.github/workflows/spec-sync.yml

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -225,14 +225,16 @@ jobs:
225225
"${PERPS_ASYNCAPI_SHA}" "${PERPS_SCM_SHA}" | sha256sum | awk '{print $1}')
226226
export FINGERPRINT
227227
228-
# Dedup: skip if an open spec-drift issue already embeds this fingerprint.
228+
# Dedup: skip if an open weekly (spec-sync-bot) drift issue already
229+
# embeds this fingerprint. Scoped to spec-sync-bot so the nightly
230+
# tracker (spec-drift-bot) is never mistaken for a weekly snapshot.
229231
existing=$(gh issue list \
230232
--repo "${GITHUB_REPOSITORY}" \
231233
--state open \
232234
--label spec-drift \
233235
--limit 200 \
234236
--json number,body \
235-
--jq '[.[] | select(.body | contains("fingerprint:'"${FINGERPRINT}"'"))] | first | .number // empty')
237+
--jq '[.[] | select(.body | contains("spec-sync-bot")) | select(.body | contains("fingerprint:'"${FINGERPRINT}"'"))] | first | .number // empty')
236238
237239
if [ -n "${existing}" ]; then
238240
echo "Drift fingerprint ${FINGERPRINT} already tracked in issue #${existing}; nothing to do."
@@ -246,10 +248,40 @@ jobs:
246248
python3 scripts/render_drift_body.py > "${body_file}"
247249
248250
today=$(date -u '+%Y-%m-%d')
249-
title="Spec drift ${today}: openapi ${OLD_VERSION} → ${NEW_VERSION}"
251+
short_fp=${FINGERPRINT:0:8}
252+
# Lead the title with a CONTENT signal, not just info.version: Kalshi
253+
# changes spec content in place without bumping the version, so a bare
254+
# "3.23.0 → 3.23.0" reads like a no-op false alarm. Tag content-only
255+
# changes and always surface the path delta + short fingerprint.
256+
if [ "${OLD_VERSION}" = "${NEW_VERSION}" ]; then
257+
title="Spec drift ${today}: openapi ${NEW_VERSION} [content-changed] (paths ${OLD_PATH_COUNT}→${NEW_PATH_COUNT}, fp ${short_fp})"
258+
else
259+
title="Spec drift ${today}: openapi ${OLD_VERSION} → ${NEW_VERSION} (paths ${OLD_PATH_COUNT}→${NEW_PATH_COUNT}, fp ${short_fp})"
260+
fi
250261
251-
gh issue create \
262+
new_url=$(gh issue create \
252263
--repo "${GITHUB_REPOSITORY}" \
253264
--label spec-drift \
254265
--title "${title}" \
255-
--body-file "${body_file}"
266+
--body-file "${body_file}")
267+
echo "Opened ${new_url}"
268+
269+
# Supersede older weekly drift snapshots whose fingerprint no longer
270+
# matches live upstream — keep exactly one open weekly issue instead of
271+
# piling up one per upstream micro-revision. Scoped to spec-sync-bot so
272+
# the nightly tracker is never touched here; the freshly-created issue
273+
# carries the current fingerprint and is therefore excluded.
274+
stale=$(gh issue list \
275+
--repo "${GITHUB_REPOSITORY}" \
276+
--state open \
277+
--label spec-drift \
278+
--limit 200 \
279+
--json number,body \
280+
--jq '[.[] | select(.body | contains("spec-sync-bot")) | select((.body | contains("fingerprint:'"${FINGERPRINT}"'")) | not) | .number] | .[]')
281+
for n in ${stale}; do
282+
gh issue close "${n}" \
283+
--repo "${GITHUB_REPOSITORY}" \
284+
--reason "not planned" \
285+
--comment "Superseded by ${new_url} — upstream specs moved again, so this drift snapshot is stale. Reconcile against current upstream."
286+
echo "Superseded #${n}"
287+
done

0 commit comments

Comments
 (0)