-
Notifications
You must be signed in to change notification settings - Fork 0
319 lines (278 loc) · 11 KB
/
Copy pathci.yml
File metadata and controls
319 lines (278 loc) · 11 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
# SPDX-License-Identifier: LicenseRef-OpenLBM-Docs-1.0
# SPDX-FileCopyrightText: 2026 FutureBuild, Inc. and OpenLBM contributors
name: CI
on:
push:
branches: [main, staging]
pull_request:
branches: [main, staging]
# The nightly trigger exists for the `vulnerabilities` job only: a new CVE
# can land in a dependency without anyone touching the repo, and we want to
# hear about it then rather than on the next unrelated PR. Every other job is
# gated on `github.event_name != 'schedule'` so the nightly run does not
# rebuild Docker images for nothing.
schedule:
- cron: "17 6 * * *" # 06:17 UTC daily
permissions:
contents: read
concurrency:
group: ci-${{ github.ref }}-${{ github.event_name }}
cancel-in-progress: true
env:
AUTH_MODE: dev
# Pinned tool versions. Anything installed with `@latest` in CI makes the
# build non-deterministic — see the comment on the `vulnerabilities` job.
GO_VERSION: "1.25"
NODE_VERSION: "20"
GOVULNCHECK_VERSION: "v1.1.4"
REUSE_VERSION: "6.2.0"
jobs:
# ---------------------------------------------------------------------------
# Merge gates. These must be green for a PR to land.
# ---------------------------------------------------------------------------
backend:
name: Backend
if: github.event_name != 'schedule'
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: gable_user
POSTGRES_PASSWORD: gable_password
POSTGRES_DB: gable_test
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U gable_user -d gable_test"
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
DATABASE_URL: postgres://gable_user:gable_password@localhost:5432/gable_test?sslmode=disable
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
cache-dependency-path: backend/go.sum
- name: Vet
working-directory: backend
run: go vet ./...
- name: Build
working-directory: backend
run: go build ./...
- name: Migrate test database
working-directory: backend
run: go run ./cmd/migrate
# Coverage is collected here rather than in a separate run so the suite is
# only executed once. `-covermode=atomic` is required by `-race`.
- name: Test (with coverage)
working-directory: backend
run: go test -race -coverprofile=coverage.out -covermode=atomic ./...
# REPORTING ONLY — there is deliberately NO coverage threshold, here or
# anywhere else in this workflow. Baseline coverage is low; a failing gate
# would block contributions rather than improve them. If a threshold is
# ever introduced it should ratchet upward from the measured baseline, not
# be imposed as a round number.
- name: Backend coverage summary
if: always()
working-directory: backend
run: |
if [ ! -f coverage.out ]; then
echo "## Backend coverage" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "No coverage profile was produced (the test step did not complete)." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
go tool cover -func=coverage.out > coverage-func.txt
total="$(awk '/^total:/ {print $NF}' coverage-func.txt)"
{
echo "## Backend coverage"
echo ""
echo "**Total: ${total:-unknown}**"
echo ""
echo "Reporting only — no threshold is enforced."
echo ""
echo "<details><summary>40 least-covered functions</summary>"
echo ""
echo '```'
grep -v '^total:' coverage-func.txt | sort -k3 -n | head -40
echo '```'
echo ""
echo "</details>"
} >> "$GITHUB_STEP_SUMMARY"
- name: Upload backend coverage
if: always()
uses: actions/upload-artifact@v4
with:
name: backend-coverage
path: |
backend/coverage.out
backend/coverage-func.txt
if-no-files-found: warn
retention-days: 14
frontend:
name: Frontend
if: github.event_name != 'schedule'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: npm
cache-dependency-path: app/package-lock.json
- name: Install dependencies
working-directory: app
run: npm ci
- name: Type check
working-directory: app
run: npx tsc --noEmit
- name: Lint
working-directory: app
run: npm run lint
# Prefers the coverage-instrumented script. The fallback keeps this job
# green on branches where `test:coverage` has not been added to
# app/package.json yet.
- name: Test (with coverage)
working-directory: app
run: |
if node -e "const s=require('./package.json').scripts||{};process.exit(s['test:coverage']?0:1)"; then
npm run test:coverage
else
echo "::warning::app/package.json has no \"test:coverage\" script; running \"npm run test\" without coverage."
npm run test -- --run
fi
# REPORTING ONLY — see the note on the backend coverage step. No threshold.
- name: Frontend coverage summary
if: always()
working-directory: app
run: |
{
echo "## Frontend coverage"
echo ""
echo "Reporting only — no threshold is enforced."
echo ""
} >> "$GITHUB_STEP_SUMMARY"
if [ -f coverage/coverage-summary.json ]; then
node -e "const t=require('./coverage/coverage-summary.json').total; console.log('| Metric | % | Covered / Total |'); console.log('|---|---|---|'); for (const [k,v] of Object.entries(t)) console.log('| '+k+' | '+v.pct+'% | '+v.covered+' / '+v.total+' |');" >> "$GITHUB_STEP_SUMMARY"
elif [ -d coverage ]; then
echo "Coverage report generated — see the \`frontend-coverage\` artifact." >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "_Tip: add the \`json-summary\` reporter to the vitest coverage config to get a table here._" >> "$GITHUB_STEP_SUMMARY"
else
echo "No coverage report was produced." >> "$GITHUB_STEP_SUMMARY"
fi
- name: Upload frontend coverage
if: always()
uses: actions/upload-artifact@v4
with:
name: frontend-coverage
path: app/coverage
if-no-files-found: warn
retention-days: 14
- name: Build
working-directory: app
run: npm run build
license:
name: License (REUSE)
if: github.event_name != 'schedule'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install reuse
run: |
pipx install "reuse==${REUSE_VERSION}"
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
# Full human-readable report for the log. The pass/fail decision is made
# by the gate step below.
- name: reuse lint (report)
run: reuse lint || true
# `reuse lint` also fails on "unused licenses", i.e. texts in LICENSES/
# that no file is licensed under. That check is wrong for this repo:
# LICENSE-MAP.md deliberately catalogues licenses that are applied
# per-work rather than per-directory. The gate below enforces every other
# REUSE check — including "every file has copyright + licensing info" —
# and downgrades only the unused-license finding to a note. See
# .github/scripts/reuse_gate.py for the full reasoning.
- name: REUSE gate
run: python3 .github/scripts/reuse_gate.py
docker:
name: Docker Build
if: github.event_name != 'schedule'
runs-on: ubuntu-latest
needs: [backend, frontend, license]
steps:
- uses: actions/checkout@v4
- name: Build backend image
run: docker build -f backend/Dockerfile .
- name: Build frontend image
run: docker build -f app/Dockerfile .
# ---------------------------------------------------------------------------
# Advisory. NOT a merge gate.
# ---------------------------------------------------------------------------
# govulncheck used to run inside the `backend` job, installed with `@latest`.
# Two problems with that:
#
# 1. `@latest` is non-deterministic. The same commit could pass on Monday
# and fail on Tuesday because a new tool release or a new entry in the Go
# vulnerability database landed overnight.
# 2. Even pinned, the vulnerability database is a moving target. A freshly
# disclosed CVE in a transitive dependency would turn every contributor's
# PR red for a problem they did not introduce and cannot fix in their PR.
#
# So: the tool version is pinned (GOVULNCHECK_VERSION), the scan lives in its
# own job, and that job is `continue-on-error` — it reports loudly in the job
# summary and in the run's job list, but it does not fail the workflow.
#
# MAINTAINERS: do not add "Vulnerabilities" to the required status checks for
# branch protection; that would re-introduce exactly the blocking behaviour
# this split exists to avoid. Triage findings from the nightly run instead.
vulnerabilities:
name: Vulnerabilities (advisory)
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
cache-dependency-path: backend/go.sum
- name: Install govulncheck (pinned)
run: go install "golang.org/x/vuln/cmd/govulncheck@${GOVULNCHECK_VERSION}"
- name: Run govulncheck
id: govulncheck
working-directory: backend
run: |
set +e
govulncheck ./... > govulncheck.txt 2>&1
status=$?
set -e
cat govulncheck.txt
{
echo "## govulncheck (advisory)"
echo ""
echo "Tool: \`${GOVULNCHECK_VERSION}\` — exit code \`${status}\`."
echo ""
if [ "$status" -eq 0 ]; then
echo "No vulnerabilities called by this module's code."
else
echo "> [!WARNING]"
echo "> govulncheck reported findings. This does **not** block the merge gate,"
echo "> but a maintainer should triage it."
fi
echo ""
echo '```'
tail -n 200 govulncheck.txt
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
exit "$status"
- name: Upload govulncheck report
if: always()
uses: actions/upload-artifact@v4
with:
name: govulncheck-report
path: backend/govulncheck.txt
if-no-files-found: warn
retention-days: 30