-
Notifications
You must be signed in to change notification settings - Fork 1
227 lines (198 loc) · 9.72 KB
/
Copy pathspec-drift.yml
File metadata and controls
227 lines (198 loc) · 9.72 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
name: Spec Drift Detection
on:
pull_request:
schedule:
# Nightly at 06:00 UTC
- cron: "0 6 * * *"
workflow_dispatch:
# Least privilege: the test job only needs read. The failure-reporting job
# below re-grants issues:write at job scope (it opens a tracking issue).
permissions:
contents: read
jobs:
drift-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Install uv
uses: astral-sh/setup-uv@v7
- name: Set up Python
run: uv python install 3.12
- name: Install dependencies
run: uv sync --dev
# Nightly: download fresh spec to detect API changes
- name: Download fresh spec (nightly only)
if: github.event_name == 'schedule'
run: uv run python scripts/sync_spec.py
# PR builds use the committed pinned spec (specs/openapi.yaml)
# for deterministic, network-independent builds.
# Nightly: promote all warnings to errors (additive drift = failure)
- name: Run contract tests (nightly — strict)
if: github.event_name == 'schedule'
run: uv run pytest tests/test_contracts.py -v -W error::UserWarning
# PR: additive drift warns only, doesn't block merge
- name: Run contract tests (PR — warnings only)
if: github.event_name != 'schedule'
run: uv run pytest tests/test_contracts.py -v
# When the scheduled strict run fails, open (and dedup) a single tracking
# issue. `needs` + `if: failure()` scopes this to a failed drift-check; the
# extra `github.event_name == 'schedule'` guard keeps it off PR runs (a PR
# failure already blocks the merge — no issue needed). Least privilege:
# issues:write lives only on this job; drift-check stays read-only. Actions
# are pinned to commit SHAs here (not @v6/@v7 tags like drift-check) because
# this job holds issues:write AND runs sync_spec.py against untrusted
# upstream — same threat model as spec-sync.yml (see its header comment).
report-failure:
needs: drift-check
if: failure() && github.event_name == 'schedule'
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Install uv
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
- name: Set up Python
run: uv python install 3.12
- name: Install dependencies
run: uv sync --dev
# Re-fetch the same fresh spec drift-check saw, then re-run the strict
# suite to capture the failing tail for the issue body. continue-on-error
# so this step's (expected) failure doesn't abort the report.
- name: Reproduce failure output
id: repro
continue-on-error: true
run: |
set -uo pipefail
uv run python scripts/sync_spec.py || true
uv run pytest tests/test_contracts.py -v -W error::UserWarning \
> /tmp/drift.log 2>&1 || true
# Keep the last 200 lines so the body stays inside GitHub's limits.
tail -n 200 /tmp/drift.log > /tmp/drift.tail
# Failing-set fingerprint: sha256 over the sorted, unique failing test
# IDs. Deterministic upstream yields an identical set every night, so
# the tracker step below re-comments only when this fingerprint CHANGES
# (kills the daily "Still failing" spam that made #467 noisy).
grep -oE '^FAILED [^ ]+' /tmp/drift.log | awk '{print $2}' | sort -u \
> /tmp/fail_ids.txt || true
fail_fp=$(sha256sum /tmp/fail_ids.txt | awk '{print $1}')
echo "fail_fp=${fail_fp}" >> "$GITHUB_OUTPUT"
- name: Ensure spec-drift label exists
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh label create spec-drift \
--color 'e4e669' \
--description 'Upstream OpenAPI/AsyncAPI spec changed since last sync' \
--force \
--repo "${GITHUB_REPOSITORY}"
- name: Open or update nightly drift tracker issue
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
FAIL_FP: ${{ steps.repro.outputs.fail_fp }}
run: |
set -euo pipefail
# Stable marker (NOT a content fingerprint): exactly one open tracker
# at a time. Re-runs comment only when the FAILING SET changes.
readonly MARKER='nightly-spec-drift-tracker'
today=$(date -u '+%Y-%m-%d')
existing=$(gh issue list \
--repo "${GITHUB_REPOSITORY}" \
--state open \
--label spec-drift \
--limit 200 \
--json number,body \
--jq '[.[] | select(.body | contains("'"${MARKER}"'"))] | first | .number // empty')
# Body / comment share the captured failing tail. printf (no unquoted
# heredoc) so upstream-sourced log text is never shell-expanded.
{
printf '## Nightly strict contract tests failed\n\n'
printf 'The scheduled `Spec Drift Detection` run found the SDK contract suite no longer matches upstream OpenAPI/AsyncAPI.\n\n'
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'
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'
printf -- '- Failing run: %s\n\n' "${RUN_URL}"
printf '### Failing test output (last 200 lines)\n\n```\n'
cat /tmp/drift.tail
printf '\n```\n'
} > /tmp/body.md
if [ -n "${existing}" ]; then
# Ack/snooze gate: a maintainer already on this drift can add the
# `spec-drift-ack` (or `snoozed`) label to silence nightly noise
# without closing the tracker.
labels=$(gh issue view "${existing}" --repo "${GITHUB_REPOSITORY}" \
--json labels --jq '[.labels[].name] | join(",")')
case ",${labels}," in
*,spec-drift-ack,*|*,snoozed,*)
echo "Tracker #${existing} is ack'd/snoozed; not commenting."
exit 0 ;;
esac
# Only comment when the FAILING SET changed. A deterministic upstream
# produces an identical set every night; re-posting it is spam.
last_fp=$(gh issue view "${existing}" --repo "${GITHUB_REPOSITORY}" \
--json body,comments --jq '[.body, (.comments[].body)] | .[]' \
| grep -oE 'fail-fingerprint:[0-9a-f]+' | tail -1 | cut -d: -f2 || true)
if [ "${last_fp}" = "${FAIL_FP}" ]; then
echo "Failing set unchanged (fingerprint ${FAIL_FP}); no new comment."
exit 0
fi
{
printf 'Failing set changed as of **%s** ([run](%s)).\n\n' "${today}" "${RUN_URL}"
printf '### Latest failing output (last 200 lines)\n\n```\n'
cat /tmp/drift.tail
printf '\n```\n'
printf '\n<!-- fail-fingerprint:%s -->\n' "${FAIL_FP}"
} > /tmp/comment.md
echo "Failing set changed (${last_fp:-none} -> ${FAIL_FP}); appending comment."
gh issue comment "${existing}" \
--repo "${GITHUB_REPOSITORY}" \
--body-file /tmp/comment.md
exit 0
fi
# Hidden markers so future runs match this tracker (MARKER) and can
# diff the failing set (fail-fingerprint).
printf '\n<!-- spec-drift-bot: do not edit\n%s\n-->\n' "${MARKER}" >> /tmp/body.md
printf '<!-- fail-fingerprint:%s -->\n' "${FAIL_FP}" >> /tmp/body.md
gh issue create \
--repo "${GITHUB_REPOSITORY}" \
--label spec-drift \
--title "Nightly spec-drift: strict contract tests failing (since ${today})" \
--body-file /tmp/body.md
# Companion to report-failure: when a scheduled strict run goes GREEN, the
# drift has been reconciled — close the open nightly tracker so it doesn't
# linger open forever (the old workflow only ever opened/commented). Least
# privilege: issues:write lives only on this job, and it runs no third-party
# actions and no untrusted upstream code (pure `gh` against the repo), so it
# needs neither a checkout nor SHA-pinning.
report-success:
needs: drift-check
if: success() && github.event_name == 'schedule'
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
steps:
- name: Close resolved nightly drift tracker
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
set -euo pipefail
readonly MARKER='nightly-spec-drift-tracker'
today=$(date -u '+%Y-%m-%d')
existing=$(gh issue list \
--repo "${GITHUB_REPOSITORY}" \
--state open \
--label spec-drift \
--limit 200 \
--json number,body \
--jq '[.[] | select(.body | contains("'"${MARKER}"'"))] | first | .number // empty')
if [ -z "${existing}" ]; then
echo "No open nightly tracker; nothing to close."
exit 0
fi
gh issue close "${existing}" \
--repo "${GITHUB_REPOSITORY}" \
--reason completed \
--comment "Resolved — strict contract suite green as of ${today} ([run](${RUN_URL})). Auto-closed by the nightly spec-drift workflow."