-
Notifications
You must be signed in to change notification settings - Fork 2
339 lines (311 loc) · 16.5 KB
/
Copy pathci-quality-gates.yml
File metadata and controls
339 lines (311 loc) · 16.5 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
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
name: Quality
on:
workflow_call:
inputs:
should_skip:
description: "Whether to skip the workflow"
required: false
type: string
default: "false"
webapp_changed:
description: "Whether webapp files changed (or should run for other reasons)"
required: false
type: string
default: "true"
application_server_changed:
description: "Whether application-server files changed (or should run for other reasons)"
required: false
type: string
default: "true"
tooling_changed:
description: "Whether repository tooling, docs, or instruction files changed"
required: false
type: string
default: "true"
postgres_image_changed:
description: "Whether the PostgreSQL image inputs changed"
required: false
type: string
default: "false"
contracts_changed:
description: "Whether server contracts or their PostgreSQL image changed"
required: false
type: string
default: "true"
pmd_canary:
description: "Whether PMD wiring inputs changed"
required: false
type: string
default: "false"
jobs:
quality-gates:
name: "${{ matrix.display-name }}"
runs-on: ubuntu-latest
permissions:
contents: read
checks: write
if: inputs.should_skip != 'true'
timeout-minutes: ${{ matrix.check == 'server-contracts' && 25 || 15 }}
strategy:
fail-fast: false
matrix:
include:
- check: application-server-quality
display-name: "App Server"
- check: tooling-quality
display-name: "Tooling and Docs"
- check: webapp-quality
display-name: "Webapp"
- check: server-contracts
display-name: "API and Database"
steps:
- name: Determine if check should run
id: should_run
run: |
case "${{ matrix.check }}" in
"webapp-quality")
echo "run=${{ inputs.webapp_changed }}" >> $GITHUB_OUTPUT
;;
"application-server-quality")
echo "run=${{ inputs.application_server_changed }}" >> $GITHUB_OUTPUT
;;
"server-contracts")
echo "run=${{ inputs.contracts_changed }}" >> $GITHUB_OUTPUT
;;
"tooling-quality")
echo "run=${{ inputs.tooling_changed }}" >> $GITHUB_OUTPUT
;;
*)
echo "::error::Unknown check type '${{ matrix.check }}' in ci-quality-gates.yml matrix."
echo "::error::Please add a case for this check in the 'Determine if check should run' step."
echo "run=false" >> $GITHUB_OUTPUT
exit 1
;;
esac
- name: Skip notification
if: steps.should_run.outputs.run != 'true'
run: echo "⏭️ Skipping ${{ matrix.check }} - no relevant files changed"
- name: Checkout repository
if: steps.should_run.outputs.run == 'true'
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# The depths are QUOTED. Unquoted, `cond && 0 || 1` always yields 1, because the number 0
# is falsy in GitHub expressions; the non-empty string '0' is truthy.
fetch-depth: ${{ matrix.check == 'application-server-quality' && '0' || '1' }}
persist-credentials: false
- name: Setup Bun
if: steps.should_run.outputs.run == 'true'
uses: ./.github/actions/setup-bun
- name: Setup application-server caches
if: steps.should_run.outputs.run == 'true' && matrix.check == 'application-server-quality'
uses: ./.github/actions/setup-caches
with:
cache-type: application-server-quality
os: ${{ runner.os }}
- name: Setup server-contract caches
if: steps.should_run.outputs.run == 'true' && matrix.check == 'server-contracts'
uses: ./.github/actions/setup-caches
with:
cache-type: server-contracts
os: ${{ runner.os }}
- name: Application-server quality
if: steps.should_run.outputs.run == 'true' && matrix.check == 'application-server-quality'
run: |
bun install --frozen-lockfile
ISSUES_FOUND=()
SERVER_OK=true; PMD_CANARY_OK=true; NULLNESS_OK=true; CONTRACTS_OK=true; ENV_OK=true
bun run check:server || { SERVER_OK=false; ISSUES_FOUND+=("Server formatting or PMD failed. Run: bun run check:server"); }
bun run check:java-nullness \
|| { NULLNESS_OK=false; ISSUES_FOUND+=("Java nullness policy failed. Run: bun run check:java-nullness"); }
if [ "$SERVER_OK" = "true" ] && [ "${{ inputs.pmd_canary }}" = "true" ]; then
PMD_CANARY=server/application/src/main/java/de/tum/cit/aet/hephaestus/Application.java
cp "$PMD_CANARY" "$RUNNER_TEMP/Application.java"
trap 'cp "$RUNNER_TEMP/Application.java" "$PMD_CANARY"' EXIT
sed -i '/public class Application {/a\ private int deliberatelyUnusedPmdCanary;' "$PMD_CANARY"
if bun run lint:java; then
PMD_CANARY_OK=false
ISSUES_FOUND+=("PMD accepted a planted UnusedPrivateField violation; the gate is not analyzing current main sources")
fi
cp "$RUNNER_TEMP/Application.java" "$PMD_CANARY"
trap - EXIT
fi
bun run check:contracts || { CONTRACTS_OK=false; ISSUES_FOUND+=("Artifact-source contract validation failed. Run: bun run check:contracts"); }
bun run check:env || { ENV_OK=false; ISSUES_FOUND+=("A setting does not reach a container that can read it. Run: bun run check:env"); }
if [ ${#ISSUES_FOUND[@]} -gt 0 ]; then
echo "### App Server Quality" >> $GITHUB_STEP_SUMMARY
echo "| Check | Status | Fix |" >> $GITHUB_STEP_SUMMARY
echo "|-------|--------|-----|" >> $GITHUB_STEP_SUMMARY
[[ "$SERVER_OK" == "false" ]] && echo "| Server formatting and PMD | :x: Failed | \`bun run check:server\` |" >> $GITHUB_STEP_SUMMARY
[[ "$PMD_CANARY_OK" == "false" ]] && echo "| PMD canary | :x: Failed | Inspect the planted-violation check |" >> $GITHUB_STEP_SUMMARY
[[ "$NULLNESS_OK" == "false" ]] && echo "| Java nullness policy | :x: Failed | \`bun run check:java-nullness\` |" >> $GITHUB_STEP_SUMMARY
[[ "$CONTRACTS_OK" == "false" ]] && echo "| Artifact-source contracts | :x: Failed | \`bun run check:contracts\` |" >> $GITHUB_STEP_SUMMARY
[[ "$ENV_OK" == "false" ]] && echo "| Env delivery by runtime role | :x: Failed | \`bun run check:env\` |" >> $GITHUB_STEP_SUMMARY
for issue in "${ISSUES_FOUND[@]}"; do echo "::error::$issue"; done
exit 1
fi
- name: Tooling and docs quality
if: steps.should_run.outputs.run == 'true' && matrix.check == 'tooling-quality'
run: |
bun install --frozen-lockfile
bun run check:package-manager
bun run check:agent-runtime-pins
bun run check:agents
bun run format:load:check
bun run test:load:syntax
bun run test:agents
bun run check:instructions
bun run docs:lint
bun run check:diagrams
- name: Webapp quality
if: steps.should_run.outputs.run == 'true' && matrix.check == 'webapp-quality'
working-directory: ./webapp
run: |
cd ..
bun install --frozen-lockfile
cd webapp
ISSUES_FOUND=()
CLIENT_OK=true
COMPONENTS_OK=true
STORIES_OK=true
SORT_OK=true
TOKENS_OK=true
BUILD_OK=true
ROUTES_OK=true
TESTS_OK=true
(cd .. && bun run check:client) || {
CLIENT_OK=false
ISSUES_FOUND+=("Webapp lint, formatting, or typecheck failed. Run: bun run check:client")
}
(cd .. && bun run check:components) || { COMPONENTS_OK=false; ISSUES_FOUND+=("Components must take data as props. Run: bun run check:components"); }
(cd .. && bun run check:stories) || { STORIES_OK=false; ISSUES_FOUND+=("Story prose renders HTML paragraphs. Run: bun run check:stories"); }
(cd .. && bun run check:story-sort) || { SORT_OK=false; ISSUES_FOUND+=("Storybook sidebar order is stale. Run: bun run check:story-sort"); }
(cd .. && bun run check:docs-tokens) || { TOKENS_OK=false; ISSUES_FOUND+=("The docs site copies a web app colour that has since changed. Run: bun run check:docs-tokens"); }
timeout --kill-after=30s 10m bun run test \
|| { TESTS_OK=false; ISSUES_FOUND+=("Unit tests failed. Run: cd webapp && bun run test"); }
# Vite generates src/routeTree.gen.ts; compare it with the committed copy after building.
bun run vite build || { BUILD_OK=false; ISSUES_FOUND+=("The webapp does not build. Run: cd webapp && bun run build"); }
if [ "$BUILD_OK" = "true" ] && ! git diff --exit-code -- src/routeTree.gen.ts; then
ROUTES_OK=false
ISSUES_FOUND+=("routeTree.gen.ts is stale. Run: cd webapp && bun run build, then commit the result")
fi
if [ ${#ISSUES_FOUND[@]} -gt 0 ]; then
echo "### Webapp Quality" >> $GITHUB_STEP_SUMMARY
echo "| Check | Status | Fix |" >> $GITHUB_STEP_SUMMARY
echo "|-------|--------|-----|" >> $GITHUB_STEP_SUMMARY
[[ "$CLIENT_OK" == "false" ]] && echo "| Lint, format, and TypeScript | :x: Failed | \`bun run check:client\` |" >> $GITHUB_STEP_SUMMARY
[[ "$COMPONENTS_OK" == "false" ]] && echo "| Presentational components | :x: Failed | \`bun run check:components\` |" >> $GITHUB_STEP_SUMMARY
[[ "$STORIES_OK" == "false" ]] && echo "| Story prose | :x: Failed | \`bun run check:stories\` |" >> $GITHUB_STEP_SUMMARY
[[ "$SORT_OK" == "false" ]] && echo "| Storybook sidebar order | :x: Failed | \`bun run check:story-sort\` |" >> $GITHUB_STEP_SUMMARY
[[ "$TOKENS_OK" == "false" ]] && echo "| Docs colour tokens | :x: Failed | \`bun run check:docs-tokens\` |" >> $GITHUB_STEP_SUMMARY
[[ "$BUILD_OK" == "false" ]] && echo "| Vite build | :x: Failed | \`cd webapp && bun run build\` |" >> $GITHUB_STEP_SUMMARY
[[ "$ROUTES_OK" == "false" ]] && echo "| routeTree.gen.ts up to date | :x: Stale | \`cd webapp && bun run build\` and commit |" >> $GITHUB_STEP_SUMMARY
[[ "$TESTS_OK" == "false" ]] && echo "| Unit tests | :x: Failed | \`cd webapp && bun run test\` |" >> $GITHUB_STEP_SUMMARY
for issue in "${ISSUES_FOUND[@]}"; do echo "::error::$issue"; done
exit 1
fi
- name: Upload webapp unit-test results
if: steps.should_run.outputs.run == 'true' && matrix.check == 'webapp-quality' && always()
uses: dorny/test-reporter@a43b3a5f7366b97d083190328d2c652e1a8b6aa2 # v3.0.0
with:
name: "Test Results - Webapp Unit"
path: "webapp/test-results/junit-*.xml"
reporter: java-junit
fail-on-error: false
- name: Summarize webapp unit tests
if: steps.should_run.outputs.run == 'true' && matrix.check == 'webapp-quality' && always()
continue-on-error: true
run: bun run summarize:test-results "Webapp Unit" webapp/test-results ci-metrics/webapp-unit.json
- name: Upload webapp test metrics
if: steps.should_run.outputs.run == 'true' && matrix.check == 'webapp-quality' && always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: test-metrics-webapp-unit
path: ci-metrics/webapp-unit.json
if-no-files-found: ignore
retention-days: 14
- name: OpenAPI validation
id: openapi
continue-on-error: true
if: steps.should_run.outputs.run == 'true' && matrix.check == 'server-contracts'
env:
DATABASE_URL: postgresql://fake:fake@localhost:5432/fake
MODEL_NAME: fake:model
run: |
bun install --frozen-lockfile
bun run generate:api
git add .
if ! git diff --cached --quiet; then
echo "::error::OpenAPI out of sync. Run: bun run generate:api"
echo "### OpenAPI Validation" >> $GITHUB_STEP_SUMMARY
echo "| Check | Status | Fix |" >> $GITHUB_STEP_SUMMARY
echo "|-------|--------|-----|" >> $GITHUB_STEP_SUMMARY
echo "| API specs sync | :x: Out of sync | \`bun run generate:api\` |" >> $GITHUB_STEP_SUMMARY
exit 1
fi
- name: Database validation (schema + ERD + models)
id: database
continue-on-error: true
if: steps.should_run.outputs.run == 'true' && matrix.check == 'server-contracts' && always()
run: |
if [[ "${{ inputs.postgres_image_changed }}" == "true" ]]; then
docker build -t hephaestus-postgres:ci docker/postgres
else
docker pull ghcr.io/ls1intum/hephaestus/postgres:${{ github.event.pull_request.base.sha || github.sha }}
docker tag ghcr.io/ls1intum/hephaestus/postgres:${{ github.event.pull_request.base.sha || github.sha }} hephaestus-postgres:ci
fi
docker run -d --name postgres-db \
-e POSTGRES_DB=hephaestus -e POSTGRES_PASSWORD=root -e POSTGRES_USER=root \
-p 5432:5432 hephaestus-postgres:ci
trap 'docker stop postgres-db 2>/dev/null; docker rm postgres-db 2>/dev/null' EXIT
until docker exec postgres-db pg_isready -U root -d hephaestus; do sleep 2; done
export SPRING_DATASOURCE_URL="jdbc:postgresql://localhost:5432/hephaestus"
export SPRING_DATASOURCE_USERNAME="root"
export SPRING_DATASOURCE_PASSWORD="root"
if ! (cd server && ./mvnw test -pl application -am -Dsurefire.includedGroups=database -DskipCoverage=true --quiet); then
echo "::error::Maven compilation or database contract tests failed. Fix the reported error before continuing."
echo "### Database Validation" >> $GITHUB_STEP_SUMMARY
echo "| Check | Status | Fix |" >> $GITHUB_STEP_SUMMARY
echo "|-------|--------|-----|" >> $GITHUB_STEP_SUMMARY
echo "| Maven compile | :x: Failed | Fix compilation errors in \`server\` |" >> $GITHUB_STEP_SUMMARY
exit 1
fi
ISSUES_FOUND=()
# draft-changelog first applies the full master.xml chain to the empty DB via
# liquibase:update — so this step is ALSO the empty→head migration-chain replay gate (a
# changelog that doesn't apply fails here). Keep that apply intact.
bun run db:draft-changelog || ISSUES_FOUND+=("Schema check / migration-chain apply failed. Run: bun run db:draft-changelog")
if [ -f "server/application/src/main/resources/db/changelog_new.xml" ]; then
echo "::error::Schema drift detected. Run: bun run db:draft-changelog"
cat server/application/src/main/resources/db/changelog_new.xml
ISSUES_FOUND+=("Schema drift detected")
fi
bun run db:generate-erd-docs || ISSUES_FOUND+=("ERD generation failed. Run: bun run db:generate-erd-docs")
git add docs/contributor/erd/schema.mmd
if ! git diff --cached --quiet docs/contributor/erd/schema.mmd; then
echo "::error::ERD outdated. Run: bun run db:generate-erd-docs"
ISSUES_FOUND+=("ERD outdated")
fi
if [ ${#ISSUES_FOUND[@]} -gt 0 ]; then
echo "### Database Validation" >> $GITHUB_STEP_SUMMARY
echo "| Check | Status | Fix |" >> $GITHUB_STEP_SUMMARY
echo "|-------|--------|-----|" >> $GITHUB_STEP_SUMMARY
for issue in "${ISSUES_FOUND[@]}"; do
echo "::error::$issue"
if [[ "$issue" == *"Run:"* ]]; then
echo "| ${issue%%.*} | :x: Failed | \`${issue##*Run: }\` |" >> $GITHUB_STEP_SUMMARY
else
echo "| $issue | :x: Failed | See logs |" >> $GITHUB_STEP_SUMMARY
fi
done
exit 1
fi
- name: Evaluate API and database contracts
if: steps.should_run.outputs.run == 'true' && matrix.check == 'server-contracts' && always()
env:
OPENAPI: ${{ steps.openapi.outcome }}
DATABASE: ${{ steps.database.outcome }}
run: |
if [ "$OPENAPI" != "success" ] || [ "$DATABASE" != "success" ]; then
echo "::error::OpenAPI or database validation failed; review both independently reported steps."
exit 1
fi