-
Notifications
You must be signed in to change notification settings - Fork 26
227 lines (201 loc) · 9.09 KB
/
Copy pathapi-pr.yaml
File metadata and controls
227 lines (201 loc) · 9.09 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
name: API PR CI
on:
pull_request:
branches:
- main
- develop
workflow_dispatch:
permissions:
contents: read
env:
NODE_VERSION: '20.x'
# Four job groups run in parallel, so PR feedback is ~max(test-shard, checks, coverage,
# coverage-gate) instead of the sum of every step. `test` is additionally split into shards.
# The ratchet runs the whole suite under full TypeScript compilation, which is what makes its
# per-file numbers exact.
jobs:
checks:
name: Build and checks
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Use Node.js ${{ env.NODE_VERSION }}
uses: actions/setup-node@v5
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install packages
uses: nick-fields/retry@v4
with:
timeout_minutes: 10
max_attempts: 3
retry_on: error
command: npm ci
- name: Run linter
run: npm run lint
- name: Format check
run: npm run format:check
- name: Build code
run: npm run build
# ts-jest runs transpile-only (isolatedModules), so it no longer type-checks specs, and
# nest build excludes *.spec.ts. This project-wide `tsc --noEmit` is the single place that
# type-checks the test files - one pass instead of the former per-suite type-check.
- name: Type-check (incl. tests)
run: npm run type-check
- name: Security audit
run: npm audit --audit-level=critical
coverage:
name: Coverage
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Use Node.js ${{ env.NODE_VERSION }}
uses: actions/setup-node@v5
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install packages
uses: nick-fields/retry@v4
with:
timeout_minutes: 10
max_attempts: 3
retry_on: error
command: npm ci
# Runs with full compilation (tsconfig.coverage.json, isolatedModules: false) so the strict
# 100% branch gate is not skewed by transpile-only decorator-metadata emit.
- name: Run coverage
run: npm run test:frick:cov
# Same mechanics for the staff KYC gate: the files deciding who reaches an elevated endpoint are
# pinned at 100%, so an uncovered branch in the authorization path fails the PR.
- name: Run staff gate coverage
run: npm run test:staff-gate:cov
# Runs on a self-hosted runner for branches of this repository. The gate executes the whole suite
# under full compilation and is CPU-bound; a hosted runner gives a public repo four vCPUs, so Jest
# defaults to three workers and the gate alone decided how long a PR run took. The self-hosted
# pool has more cores and does not count against the account's concurrent-job limit.
#
# Pull requests from forks stay on a hosted runner. A self-hosted runner executes the workflow and
# the code of the PR head, so anyone able to open a fork PR would otherwise run arbitrary code on
# it. The repository additionally requires approval for all outside contributors, but that is a
# settings-level control someone can change; this guard lives in the reviewed diff.
#
# Deliberately NOT an `if:` on the job - a skipped check counts as passing, which would let a fork
# PR bypass the gate entirely. Forks run the same gate, just slower.
coverage-gate:
name: Coverage ratchet
runs-on: >-
${{ (github.event_name != 'pull_request'
|| github.event.pull_request.head.repo.full_name == github.repository)
&& fromJSON('["self-hosted","dfx-api"]') || 'ubuntu-latest' }}
timeout-minutes: 30
# Serialise the gate, but only where that is physically necessary. Two of these on the same
# self-hosted machine do not split it, they block each other: measured 16.6 min each against
# 1.5 min for a single run, so waiting is the cheaper outcome. A fork PR runs on a throwaway
# hosted runner instead and shares nothing, so it gets a group of its own and never queues
# behind - or ahead of - an internal run. `queue: max` keeps pending runs queued rather than
# cancelling all but the newest, so a third PR does not turn a waiting run into a red check.
concurrency:
group: >-
${{ (github.event_name != 'pull_request'
|| github.event.pull_request.head.repo.full_name == github.repository)
&& 'coverage-ratchet-self-hosted'
|| format('coverage-ratchet-hosted-{0}', github.run_id) }}
cancel-in-progress: false
queue: max
steps:
- name: Checkout
uses: actions/checkout@v5
# No `cache: 'npm'` here, unlike the hosted jobs. The runner's ~/.npm survives between jobs,
# so the cache would only be redundant - and its post-run upload cost 5.3 min, more than the
# gate itself saves by running here.
- name: Use Node.js ${{ env.NODE_VERSION }}
uses: actions/setup-node@v5
with:
node-version: ${{ env.NODE_VERSION }}
- name: Install packages
uses: nick-fields/retry@v4
with:
timeout_minutes: 10
max_attempts: 3
retry_on: error
command: npm ci
# Repo-wide ratchet: every file already at 100% is pinned, so coverage cannot regress.
# Runs the whole suite (a file is often covered by specs other than its own) with the same
# full compilation as the Frick gate. Deliberately without the Postgres service: no pinned
# file belongs to the migration suites that need it, and enabling those suites can only
# raise coverage, never lower it.
#
# maxWorkers is set here rather than in the npm script so the script stays machine-agnostic:
# a contributor running it locally keeps Jest's own default.
#
# 8 is measured, and the direction is counter-intuitive: fewer workers, not more. At 20 the
# gate took 8.4 min, at 16 it swung between 1.5 and 5.4 min. Host CPU never exceeded ~70%
# in any of those runs - not even with two jobs and 32 workers on 28 cores - so the workers
# were never short of cores. Under full compilation each holds its own TypeScript program,
# and the machine's free memory varies with what else runs on it. Re-measure before changing.
- name: Run coverage ratchet
id: ratchet
run: npm run test:gate:cov -- --maxWorkers=${{ (github.event_name != 'pull_request'
|| github.event.pull_request.head.repo.full_name == github.repository) && '8' || '3' }}
# The ratchet only protects files that someone entered by hand, so a file that reaches 100% stays
# unguarded until a human notices. This reads the run's own summary and names those files. Advisory
# on purpose: a pull request can complete a file it never touched, and failing it for that would
# train the team to ignore a red gate. The checkout clears the ignored coverage-gate/ directory
# before every run, and this step additionally waits for the gate to reach a verdict - so a report
# from an earlier build on the persistent runner cannot be read as describing this commit.
- name: Report complete but unpinned files
if: ${{ always() && (steps.ratchet.outcome == 'success' || steps.ratchet.outcome == 'failure') }}
continue-on-error: true
run: node scripts/coverage-unpinned-complete.js
# A red gate names the file and the metric but not the uncovered lines. Uploading the lcov
# report turns diagnosis into a download instead of a 15-minute local rerun.
- name: Upload coverage report
if: failure()
uses: actions/upload-artifact@v7
with:
name: coverage-gate
path: coverage-gate/
retention-days: 7
test:
name: Test (shard ${{ matrix.shard }}/3)
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3]
services:
# Throwaway Postgres so the Aktionariat-registration backfill migration suite runs on every PR.
# The suite is skipped when MIGRATION_TEST_PG is unset; all other tests use mocks and ignore it.
# Every shard keeps the service because Jest distributes the migration suites across shards.
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Use Node.js ${{ env.NODE_VERSION }}
uses: actions/setup-node@v5
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install packages
uses: nick-fields/retry@v4
with:
timeout_minutes: 10
max_attempts: 3
retry_on: error
command: npm ci
- name: Run tests (shard ${{ matrix.shard }}/3)
run: npm test -- --shard=${{ matrix.shard }}/3
env:
MIGRATION_TEST_PG: postgres://postgres:postgres@localhost:5432/postgres