-
Notifications
You must be signed in to change notification settings - Fork 21
313 lines (287 loc) · 13.4 KB
/
Copy pathnvchecker.yml
File metadata and controls
313 lines (287 loc) · 13.4 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
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
name: Upstream version drift
on:
workflow_dispatch:
schedule:
# Weekly: Mondays at 06:00 UTC.
- cron: "0 6 * * 1"
# PR / push: only the config-drift job below runs (cheap, ~10 s);
# the upstream-fetch drift-scan stays gated to schedule/dispatch.
# Path filter scopes the trigger to changes that can plausibly
# invalidate nvchecker.toml — ebuild adds/drops, generator rule
# edits, the generated config itself, and this workflow file.
push:
branches: [master]
paths:
- '*/**.ebuild'
- 'scripts/nvchecker/generate.py'
- 'scripts/nvchecker/nvchecker.toml'
- '.github/workflows/nvchecker.yml'
pull_request:
branches: [master]
paths:
- '*/**.ebuild'
- 'scripts/nvchecker/generate.py'
- 'scripts/nvchecker/nvchecker.toml'
- '.github/workflows/nvchecker.yml'
# Scope serialization by event_name + ref so a Monday-morning PR
# doesn't queue behind the weekly upstream fetch. Within the same
# event class, runs serialize (manual workflow_dispatch fired during
# the scheduled cron would race the upstream fetches and produce a
# half-fresh baseline; let one finish before the other starts).
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}
cancel-in-progress: false
jobs:
# PR / push gate: enforce that nvchecker.toml stays in sync with
# generate.py. Without it, a contributor adding or dropping a
# package and forgetting to re-run the generator means the new
# package never reaches the drift report (silent omission), and a
# dropped package leaves a ghost entry that nvchecker keeps trying
# to fetch forever.
config-drift:
if: github.event_name == 'pull_request' || github.event_name == 'push'
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: '3.13'
- name: Regenerate nvchecker.toml and verify no drift
run: |
set -euo pipefail
python3 scripts/nvchecker/generate.py
if ! git diff --quiet scripts/nvchecker/nvchecker.toml; then
echo "::error file=scripts/nvchecker/nvchecker.toml::scripts/nvchecker/nvchecker.toml is out of sync with generate.py — run 'python3 scripts/nvchecker/generate.py' locally and commit the result."
git --no-pager diff scripts/nvchecker/nvchecker.toml | head -200 || true
exit 1
fi
echo "scripts/nvchecker/nvchecker.toml is up-to-date with generate.py."
drift-scan:
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
permissions:
contents: read
# File / append / close the rolling nvchecker-drift issue.
issues: write
steps:
- uses: actions/checkout@v6
# setup-python's built-in pip cache keys against the requirements
# file's hash, so cached wheels survive across runs and only
# invalidate on a deliberate version bump. Saves ~30s per weekly
# run (the lxml/structlog/aiohttp transitive deps are the slow
# part). The pinned version lives in scripts/nvchecker/
# requirements.txt — same supply-chain discipline as the
# pkgcheck-action SHA pin and actionlint@v1.7.12. Bumps are
# deliberate and manual; this repo runs no Dependabot.
- uses: actions/setup-python@v6
with:
python-version: '3.13'
cache: 'pip'
cache-dependency-path: 'scripts/nvchecker/requirements.txt'
- name: Install nvchecker
run: pip install --user -r scripts/nvchecker/requirements.txt
# nvchecker's GitHub source needs a token to avoid the 60-req/hour
# unauthenticated rate limit (this config has about 195 GitHub entries,
# which trivially saturates that). The default GITHUB_TOKEN gets
# 1000 req/hour, which is plenty.
- name: Configure github keyfile
env:
NV_GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
mkdir -p ~/.config/nvchecker
# Write via printf with -- so the token (Bearer-format) isn't
# interpreted by the shell. The here-doc form risks the token
# accidentally being echoed by `set -x` or similar.
{
printf '[keys]\n'
printf 'github = "%s"\n' "${NV_GH_TOKEN}"
} > ~/.config/nvchecker/keyfile.toml
chmod 600 ~/.config/nvchecker/keyfile.toml
# Baseline is "current ebuild PVs", not "last week's upstream
# snapshot". See scripts/nvchecker/tree_baseline.py for the
# framing — CI cares about drift relative to the tree, not
# relative to time.
- name: Generate baseline from tree
id: baseline
run: |
set -euo pipefail
baseline="$RUNNER_TEMP/old_ver.json"
python3 scripts/nvchecker/tree_baseline.py > "$baseline"
entries=$(python3 -c "import json,sys;print(len(json.load(open('${baseline}'))['data']))")
echo "Baseline entries: ${entries}"
echo "entries=${entries}" >> "$GITHUB_OUTPUT"
# nvchecker reads oldver/newver paths from a [__config__] section,
# which the committed nvchecker.toml deliberately omits (so it
# doesn't bake a user-specific path into the repo). Compose a
# per-run config that prepends [__config__] to the committed file.
- name: Compose run config
run: |
set -euo pipefail
{
printf '[__config__]\n'
printf 'oldver = "%s/old_ver.json"\n' "$RUNNER_TEMP"
printf 'newver = "%s/new_ver.json"\n' "$RUNNER_TEMP"
printf '\n'
cat scripts/nvchecker/nvchecker.toml
} > "$RUNNER_TEMP/run.toml"
# -l error silences the per-entry INFO noise. No --failures: a
# transient PyPI/GitHub error on one entry shouldn't fail the
# whole job (the affected entry just won't appear in newver).
- name: Run nvchecker (fetch upstream)
run: |
set -euo pipefail
~/.local/bin/nvchecker \
-c "$RUNNER_TEMP/run.toml" \
-k ~/.config/nvchecker/keyfile.toml \
-l error --logger pretty
- name: Diff with nvcmp
id: drift
run: |
set -euo pipefail
drift_out="$RUNNER_TEMP/drift.txt"
# nvcmp exits non-zero if anything drifted; that's normal here.
~/.local/bin/nvcmp -c "$RUNNER_TEMP/run.toml" > "$drift_out" 2>&1 || true
count=$(wc -l < "$drift_out")
echo "Drift entries: ${count}"
echo "count=${count}" >> "$GITHUB_OUTPUT"
echo "::group::drift report"
cat "$drift_out"
echo "::endgroup::"
# Surface entries that are tracked AND in the tree but returned no
# upstream version. nvchecker logs the empty-filter case at
# WARNING (below our -l error threshold), so an upstream tag-scheme
# pivot that silently empties an include_regex would otherwise look
# "up to date" forever — no version => no drift. The local cron's
# scripts/nvchecker/silent_entries.py uses set-difference over
# consecutive runs; in CI the baseline is the tree itself, so the
# equivalent question is "in config AND in tree but missing from
# upstream". Surfaced as ::warning::, not ::error::: a single run
# can blip on a transient fetch error, and persistence detection
# would need state the stateless CI runner deliberately avoids.
- name: Detect entries returning no upstream version
env:
RUNTEMP: ${{ runner.temp }}
run: |
set -euo pipefail
silent_out="$RUNTEMP/silent.txt"
python3 - <<'PY' > "$silent_out"
import json, os, tomllib
from pathlib import Path
temp = Path(os.environ["RUNTEMP"])
config = tomllib.loads(Path("scripts/nvchecker/nvchecker.toml").read_text())
active = {
k for k, v in config.items()
if not k.startswith("__") and isinstance(v, dict) and "source" in v
}
with (temp / "old_ver.json").open() as f:
tree = set(json.load(f).get("data", {}))
with (temp / "new_ver.json").open() as f:
upstream = set(json.load(f).get("data", {}))
for atom in sorted((active & tree) - upstream):
print(atom)
PY
count=$(wc -l < "$silent_out")
if (( count > 0 )); then
echo "::warning::nvchecker: ${count} tracked entries returned no upstream version (stale include_regex or dead upstream)"
while IFS= read -r atom; do
echo "::warning:: ${atom}"
done < "$silent_out"
else
echo "All tracked tree entries returned an upstream version."
fi
- uses: actions/upload-artifact@v7
with:
name: nvchecker-drift-${{ github.run_id }}
path: |
${{ runner.temp }}/drift.txt
${{ runner.temp }}/silent.txt
${{ runner.temp }}/new_ver.json
${{ runner.temp }}/old_ver.json
retention-days: 30
# Surface drift as a rolling GH issue. The artifact above has the
# raw data, but a maintainer has to remember to fetch it — a
# rolling issue lands in the watch feed for free, mirroring the
# dusty-packages pattern in dusty.yml. Open / append on non-empty
# drift OR non-empty silent set; auto-close when both are zero.
# Ordered after the artifact upload so a transient GH-API error
# in these steps doesn't lose the week's drift data.
- name: Summarize drift and compose report body
id: summary
uses: actions/github-script@v9
env:
RUNTEMP: ${{ runner.temp }}
with:
script: |
const fs = require('fs');
const path = require('path');
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
const when = new Date().toISOString().split('T')[0];
const readLines = (p) => {
try {
return fs.readFileSync(p, 'utf8').trimEnd().split('\n').filter(l => l.length > 0);
} catch (e) {
return [];
}
};
const driftLines = readLines(path.join(process.env.RUNTEMP, 'drift.txt'));
const silentLines = readLines(path.join(process.env.RUNTEMP, 'silent.txt'));
// Parse "atom old -> new" lines from nvcmp; skip malformed
// (transient fetch-error lines slip into drift.txt because
// nvcmp captures stderr).
const driftRows = driftLines
.map(l => l.match(/^(\S+)\s+(\S+)\s+->\s+(\S+)$/))
.filter(m => m)
.map(m => ({ atom: m[1], old: m[2], next: m[3] }));
core.setOutput('when', when);
core.setOutput('drift_count', String(driftRows.length));
core.setOutput('silent_count', String(silentLines.length));
// Both lists empty → close path takes over; emit the closing
// comment and stop.
if (driftRows.length === 0 && silentLines.length === 0) {
core.setOutput('close_comment',
`Weekly scan reports zero drift and zero silent entries: ${runUrl}\n\nAuto-closing.`);
return;
}
// Compose the report body.
const parts = [`Weekly upstream-drift report for ${when}.`, ``];
if (driftRows.length > 0) {
parts.push(`### Version drift (${driftRows.length})`, ``);
parts.push(`| Package | Current | Upstream |`);
parts.push(`| ------- | ------- | -------- |`);
for (const r of driftRows) {
parts.push(`| \`${r.atom}\` | ${r.old} | ${r.next} |`);
}
parts.push(``);
}
if (silentLines.length > 0) {
parts.push(`### No upstream version returned (${silentLines.length})`, ``);
parts.push(`Tracked AND in the tree but the upstream fetch returned`);
parts.push(`nothing — usually a stale \`include_regex\` after an`);
parts.push(`upstream tag-scheme pivot, or a dead upstream. Re-check`);
parts.push(`the filter in \`scripts/nvchecker/generate.py\`.`, ``);
for (const atom of silentLines) {
parts.push(`- \`${atom}\``);
}
parts.push(``);
}
parts.push(`Run log + drift artifact: ${runUrl}`, ``);
parts.push(`Auto-generated. Close once each listed entry has been`);
parts.push(`reviewed — bumped, deliberately held (capped), or`);
parts.push(`rejected (yanked / preview).`);
core.setOutput('body', parts.join('\n'));
- name: Auto-close drift issue when all clear
if: steps.summary.outputs.drift_count == '0' && steps.summary.outputs.silent_count == '0'
uses: ./.github/actions/rolling-issue-close
with:
label: nvchecker-drift
comment: ${{ steps.summary.outputs.close_comment }}
- name: Upsert rolling drift issue
if: steps.summary.outputs.drift_count != '0' || steps.summary.outputs.silent_count != '0'
uses: ./.github/actions/rolling-issue-upsert
with:
label: nvchecker-drift
title: Upstream version drift (${{ steps.summary.outputs.when }})
body: ${{ steps.summary.outputs.body }}