-
Notifications
You must be signed in to change notification settings - Fork 0
199 lines (182 loc) · 8.63 KB
/
Copy pathlive-tests.yml
File metadata and controls
199 lines (182 loc) · 8.63 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
# Geplante Live-Tests gegen api3.geo.admin.ch (DRIFT-005).
#
# Die Unit-Tests laufen gegen aufgezeichnete bzw. gemockte Antworten und
# koennen deshalb nicht bemerken, wenn die Quelle ihr Format aendert. Die
# `-m live`-Tests fragen sie wirklich ab; die CI der Pull Requests schliesst
# sie per `-m "not live"` aus, und das bleibt so.
#
# Achtung: `schedule` greift nur auf dem Default-Branch. Aenderungen an dieser
# Datei werden erst nach dem Merge wirksam. Vorher per `workflow_dispatch`
# von Hand ausloesen.
name: Live-Tests
on:
schedule:
# Woechentlich. Die ungerade Minute ist Absicht: Auf :00 legt GitHub die
# Laeufe der halben Welt, und ein verzoegerter Cron faellt bei Last aus.
# Gestaffelt gegen die uebrigen Server des Portfolios.
- cron: "53 4 * * 1"
workflow_dispatch: {}
permissions:
contents: read
issues: write
concurrency:
group: live-tests
cancel-in-progress: false
jobs:
live:
name: Live-Suite gegen api3.geo.admin.ch
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.11"
- name: Install dependencies
run: pip install uv && uv pip install -e ".[dev]" --system
# `continue-on-error`, damit die Einordnung in jedem Fall laeuft. Rot
# wird der Job weiter unten, aus dem eingeordneten Ergebnis.
- name: Live-Suite
id: suite
continue-on-error: true
run: |
set +e
# `--run-live` ist nicht optional: Ohne die Option ueberspringt sich
# jeder `-m live`-Test selbst (conftest.py, Vorgabe False), und der
# Lauf endet mit 4 uebersprungen / Exit 0. `tests/test_live_workflow.py`
# haelt fest, dass sie hier steht.
PYTHONPATH=src pytest tests/ -m live -v --run-live --junitxml=live-report.xml 2>&1 | tee live-output.txt
echo "pytest_exit=${PIPESTATUS[0]}" >> "$GITHUB_OUTPUT"
# `if: failure()` kennt rot und nicht rot. Ein Live-Lauf hat drei
# Antworten, und die dritte zaehlt: Ein gescheitertes `pip install`, ein
# Timeout oder eine umbenannte Marke sind NICHT dasselbe wie ein
# gebrochener Vertrag mit der Quelle. Und ein Lauf, in dem jeder Test
# uebersprungen wurde, sieht unter jedem Exit-Code-Check aus wie Erfolg.
- name: Ergebnis einordnen
id: verdict
run: |
python scripts/classify_live_run.py live-report.xml \
--pytest-exit "${{ steps.suite.outputs.pytest_exit }}"
{
echo "tail<<PYTEST_TAIL"
tail -n 40 live-output.txt || true
echo "PYTEST_TAIL"
} >> "$GITHUB_OUTPUT"
- name: Issue oeffnen, aktualisieren oder schliessen
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
# Ueber `env`, nicht ueber `${{ }}` im Skript: Die pytest-Ausgabe ist
# fremder Text, der sonst in einem JavaScript-Template-Literal landet.
# Ein Backtick oder ein `${` darin bricht das Skript oder fuehrt aus,
# was in einer Assertion-Meldung stand.
env:
LIVE_STATE: ${{ steps.verdict.outputs.state }}
LIVE_REASON: ${{ steps.verdict.outputs.reason }}
LIVE_TAIL: ${{ steps.verdict.outputs.tail }}
with:
script: |
const state = process.env.LIVE_STATE;
const reason = process.env.LIVE_REASON || '';
const tail = process.env.LIVE_TAIL || '(keine Ausgabe)';
// Stabiles Praefix: Damit ein zweiter roter Lauf den bestehenden
// Thread verlaengert statt einen zweiten aufzumachen. Zehn Issues
// zur selben Sache liest niemand.
const PREFIX = 'Live-Tests gegen api3.geo.admin.ch rot';
const LABEL = 'upstream';
const run = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}` +
`/actions/runs/${context.runId}`;
const heute = new Date().toISOString().slice(0, 10);
// `issues.listForRepo` statt der Such-API: Die Suche hat ein
// eigenes, knappes Rate-Limit und indexiert verzoegert — ein frisch
// geoeffnetes Issue findet sie unter Umstaenden nicht, und dann
// legt der naechste rote Lauf ein zweites an.
const issues = await github.rest.issues.listForRepo({
...context.repo, state: 'open', labels: LABEL, per_page: 100,
});
const open = issues.data.find(
(i) => !i.pull_request && i.title.startsWith(PREFIX)
);
if (state === 'unknown') {
// Weder oeffnen noch schliessen. Der Lauf hat nichts ueber den
// Vertrag mit api3.geo.admin.ch festgestellt, und ein Issue, das auf so
// einen Lauf hin zugeht, behauptet einen Vergleich, den es nicht
// gab. Sichtbar wird es ueber den roten Job.
core.warning(`Live-Suite nicht ausgewertet: ${reason}. Am Issue wurde nichts geaendert.`);
return;
}
if (state === 'finding') {
const body = [
'Die geplante Live-Suite gegen `api3.geo.admin.ch` ist rot.',
'',
`Lauf: ${run}`,
`Einordnung: ${reason}`,
'',
'**Rot heisst hier nicht zwingend «unser Fehler».** Es heisst: Der Vertrag',
'mit der Quelle hat sich geaendert, oder die Quelle ist gerade aus. Beides',
'gehoert gesehen, nur das Erste gehoert gefixt — und genau das Erste sehen',
'die Unit-Tests nicht, weil ihre Fixtures aus derselben Annahme geschrieben',
'sind wie der Code.',
'',
'Erst die Quelle abfragen, dann einordnen — nicht aus der Fehlermeldung',
'schliessen.',
'',
'<details><summary>Letzte Zeilen der pytest-Ausgabe</summary>',
'',
'```',
tail,
'```',
'',
'</details>',
].join('\n');
if (open) {
await github.rest.issues.createComment({
...context.repo, issue_number: open.number,
body: `Wieder rot am ${heute}.\n\n${body}`,
});
} else {
// Das Label anlegen, falls das Repo es noch nicht kennt: Sonst
// scheitert `issues.create` beim allerersten roten Lauf — genau
// dann, wenn das Issue gebraucht wird.
try {
await github.rest.issues.createLabel({
...context.repo, name: LABEL, color: 'd93f0b',
description: 'Vertrag mit einer externen Quelle betroffen',
});
} catch (e) {
if (e.status !== 422) throw e; // 422 = existiert bereits
}
await github.rest.issues.create({
...context.repo, title: `${PREFIX} (${heute})`, body, labels: [LABEL],
});
}
return;
}
// clear — und nur hier wird zugemacht, weil nur hier wirklich
// verglichen und nichts gefunden wurde.
if (open) {
await github.rest.issues.createComment({
...context.repo, issue_number: open.number,
body: `Die Live-Suite ist wieder gruen (${reason}). Lauf: ${run}`,
});
await github.rest.issues.update({
...context.repo, issue_number: open.number,
state: 'closed', state_reason: 'completed',
});
}
# Erst hier wird der Job rot. `clear` ist gruen; `finding` und `unknown`
# sind beide rot, aber nur `finding` hat oben ein Issue erzeugt — der
# Unterschied steht im Log, nicht im Exit-Code.
- name: Ergebnis durchreichen
if: steps.verdict.outputs.state != 'clear'
# Ueber `env`, nicht ueber `${{ }}`: Der Grund kann eine Parser-Meldung
# ueber eine fremde Datei enthalten, und `${{ }}` setzt ihn vor dem Start
# woertlich in die Shell ein — ein `$(...)` oder ein Anfuehrungszeichen
# darin liefe mit. Der github-script-Schritt macht es aus demselben Grund
# schon so.
env:
LIVE_STATE: ${{ steps.verdict.outputs.state }}
LIVE_REASON: ${{ steps.verdict.outputs.reason }}
run: |
echo "Live-Suite: $LIVE_STATE"
echo "$LIVE_REASON"
exit 1