-
Notifications
You must be signed in to change notification settings - Fork 2
325 lines (298 loc) · 18.3 KB
/
Copy pathci-quality-gates.yml
File metadata and controls
325 lines (298 loc) · 18.3 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
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"
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: 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"|"server-contracts")
echo "run=${{ inputs.application_server_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 caches
if: steps.should_run.outputs.run == 'true'
uses: ./.github/actions/setup-caches
with:
cache-type: ${{ matrix.check }}
os: ${{ runner.os }}
- name: Application-server formatting + agent runtime + contracts
if: steps.should_run.outputs.run == 'true' && matrix.check == 'application-server-quality'
run: |
bun install --frozen-lockfile
bun run check:package-manager
ISSUES_FOUND=()
FMT_OK=true; PMD_OK=true; NULLNESS_OK=true; LINT_OK=true; TESTS_OK=true; TYPES_OK=true; SCRIPT_TYPES_OK=true; CONTRACTS_OK=true; ENV_OK=true; DOCS_OK=true; DIAGRAMS_OK=true; INSTRUCTIONS_OK=true
bun run format:java:check || { FMT_OK=false; ISSUES_FOUND+=("Java formatting failed. Run: bun run format:java"); }
bun run check:java-nullness \
|| { NULLNESS_OK=false; ISSUES_FOUND+=("Java nullness policy failed. Run: bun run check:java-nullness"); }
bun run lint:java || { PMD_OK=false; ISSUES_FOUND+=("PMD found violations. Run: bun run lint:java:report"); }
if [ "$PMD_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_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 ci:agents \
|| { LINT_OK=false; ISSUES_FOUND+=("Lint or formatting failed outside the webapp. Run: bun run check:agents:fix"); }
bun run test:agents \
|| { TESTS_OK=false; ISSUES_FOUND+=("Agent runtime test(s) failed. Run: bun run test:agents"); }
bun run typecheck:agents \
|| { TYPES_OK=false; ISSUES_FOUND+=("Agent TypeScript errors. Run: bun run typecheck:agents"); }
bun run typecheck:scripts \
|| { SCRIPT_TYPES_OK=false; ISSUES_FOUND+=("scripts/ TypeScript errors. Run: bun run typecheck:scripts"); }
bun run check:contracts || { CONTRACTS_OK=false; ISSUES_FOUND+=("Artifact-source contract validation failed. Run: bun run check:contracts"); }
# Reads server/application.yml against docker/compose.{app,core}.yaml — the deployment that
# has to deliver each setting. Both sides are behind this job's path filter.
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"); }
# An AGENTS.md no CLAUDE.md imports is loaded by every agent except Claude Code, and nothing
# about the file says so — this is the only place that reads the wiring end to end.
bun run check:instructions \
|| { INSTRUCTIONS_OK=false; ISSUES_FOUND+=("An agent instruction file reaches no agent, or exists in two copies. Run: bun run check:instructions"); }
# docs/decisions/ and docs/runbooks/ are mounted by no Docusaurus plugin, so the site build
# never compiles or link-checks them. Markdown lint is the only gate they have.
bun run docs:lint || { DOCS_OK=false; ISSUES_FOUND+=("docs/ markdown or typecheck failed. Run: bun run docs:lint"); }
# Docusaurus renders mermaid in the browser, so a diagram it cannot parse builds green and
# publishes as an error box — the docs deployment can never be the gate for it. This leg
# owns the check because docs/** is behind this job's path filter and no other job's.
bun run check:diagrams \
|| { DIAGRAMS_OK=false; ISSUES_FOUND+=("A committed Mermaid diagram does not parse. Run: bun run check:diagrams"); }
if [ ${#ISSUES_FOUND[@]} -gt 0 ]; then
echo "### App Server Quality" >> $GITHUB_STEP_SUMMARY
echo "| Check | Status | Fix |" >> $GITHUB_STEP_SUMMARY
echo "|-------|--------|-----|" >> $GITHUB_STEP_SUMMARY
[[ "$FMT_OK" == "false" ]] && echo "| Java formatting | :x: Failed | \`bun run format:java\` |" >> $GITHUB_STEP_SUMMARY
[[ "$PMD_OK" == "false" ]] && echo "| Java lint (PMD) | :x: Failed | \`bun run lint:java:report\` |" >> $GITHUB_STEP_SUMMARY
[[ "$NULLNESS_OK" == "false" ]] && echo "| Java nullness policy | :x: Failed | \`bun run check:java-nullness\` |" >> $GITHUB_STEP_SUMMARY
[[ "$LINT_OK" == "false" ]] && echo "| Lint + format (outside webapp) | :x: Failed | \`bun run check:agents:fix\` |" >> $GITHUB_STEP_SUMMARY
[[ "$TESTS_OK" == "false" ]] && echo "| Agent runtime tests | :x: Failed | \`bun run test:agents\` |" >> $GITHUB_STEP_SUMMARY
[[ "$TYPES_OK" == "false" ]] && echo "| Agent typecheck | :x: Failed | \`bun run typecheck:agents\` |" >> $GITHUB_STEP_SUMMARY
[[ "$SCRIPT_TYPES_OK" == "false" ]] && echo "| scripts/ typecheck | :x: Failed | \`bun run typecheck:scripts\` |" >> $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
[[ "$INSTRUCTIONS_OK" == "false" ]] && echo "| Agent instruction wiring | :x: Failed | \`bun run check:instructions\` |" >> $GITHUB_STEP_SUMMARY
[[ "$DOCS_OK" == "false" ]] && echo "| Docs lint + typecheck | :x: Failed | \`bun run docs:lint\` |" >> $GITHUB_STEP_SUMMARY
[[ "$DIAGRAMS_OK" == "false" ]] && echo "| Mermaid diagrams | :x: Failed | \`bun run check:diagrams\` |" >> $GITHUB_STEP_SUMMARY
for issue in "${ISSUES_FOUND[@]}"; do echo "::error::$issue"; done
exit 1
fi
- 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=()
LINT_OK=true
TYPES_OK=true
COMPONENTS_OK=true
STORIES_OK=true
SORT_OK=true
TOKENS_OK=true
BUILD_OK=true
ROUTES_OK=true
TESTS_OK=true
bun run check || { LINT_OK=false; ISSUES_FOUND+=("Lint or formatting failed. Run: cd webapp && bun run check:fix"); }
bun run typecheck || { TYPES_OK=false; ISSUES_FOUND+=("TypeScript errors found. Run: cd webapp && bun run typecheck"); }
bun ../scripts/check-presentational-components.ts || { COMPONENTS_OK=false; ISSUES_FOUND+=("Components must take data as props. Run: bun run check:components"); }
bun ../scripts/check-story-prose.ts || { STORIES_OK=false; ISSUES_FOUND+=("Story prose renders HTML paragraphs. Run: bun run check:stories"); }
bun ../scripts/check-story-sort.ts || { SORT_OK=false; ISSUES_FOUND+=("Storybook sidebar order is stale. Run: bun run check:story-sort"); }
bun ../scripts/check-docs-tokens.ts && bun test ../scripts/check-docs-tokens.test.ts || { 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
[[ "$LINT_OK" == "false" ]] && echo "| Lint + format (oxlint + oxfmt) | :x: Failed | \`cd webapp && bun run check:fix\` |" >> $GITHUB_STEP_SUMMARY
[[ "$TYPES_OK" == "false" ]] && echo "| TypeScript | :x: Failed | \`cd webapp && bun run typecheck\` |" >> $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: |
# The pg_partman image, not stock Postgres: the auth_event changeset that calls
# partman.create_parent is preconditioned onFail="MARK_RAN" on the extension being
# available, so on a stock image the chain replays with that changeset never executed.
docker build -t hephaestus-postgres:ci docker/postgres
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