Repository navigation
716 lines (686 loc) · 34.3 KB
/
Copy pathci.yml
File metadata and controls
716 lines (686 loc) · 34.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
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
name: CI
permissions:
contents: read
on:
# Manual runs are useful for re-checking a branch without pushing to it, and
# for re-running after an infrastructure blip.
workflow_dispatch:
pull_request:
branches:
- main
- develop
push:
branches:
- main
- develop
# Deliberately no `tags:` trigger. Adding one made every job in this workflow a
# cache-poisoning finding (zizmor, 11 × high): a tag push is a publishing trigger, and a job
# that restores an npm cache on one can be fed content written from a lower-privilege context,
# which is precisely the threat that matters for a release build. The tag check lives in
# release-tag.yml instead, where it needs no dependencies and therefore no cache.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
dependencies:
name: Dependency Integrity
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
with:
persist-credentials: false
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 'lts/*'
cache: 'npm'
- name: Verify clean install
run: npm ci
lint:
name: TS/ESLint
needs: dependencies
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
with:
persist-credentials: false
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 'lts/*'
cache: 'npm'
- run: npm ci
# --prune-suppressions fails when eslint-suppressions.json lists a violation that no
# longer exists, so a fixed one cannot quietly come back and the file only shrinks.
- name: Lint TypeScript files
run: npm run lint:prune
# eslint-rules/ holds this repository's own rules, currently the cognitive-complexity rule
# that replaced the one from the removed LGPL plugin. Its tests assert exact scores, so a
# silent drift into being stricter or laxer than what it replaced fails here.
- name: Test the repository's own ESLint rules
run: npm run test:eslint-rules
# `scripts/` is outside `src`, so neither `npm run lint` nor either unit runner covers
# it — and it renders the Markdown that gets posted to pull requests. CodeQL found an
# incomplete escaping in that renderer, which is what these tests pin.
- name: Test the reporting scripts
run: npm run test:scripts
html-lint:
name: HTML Lint
needs: dependencies
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
with:
persist-credentials: false
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 'lts/*'
cache: 'npm'
- run: npm ci
- name: Lint HTML files
run: npx --no-install eslint "src/**/*.html"
format:
name: Format Check
needs: dependencies
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
with:
persist-credentials: false
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 'lts/*'
cache: 'npm'
- run: npm ci
- run: npm run format:check
i18n-check:
name: Translation Check
needs: dependencies
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
with:
persist-credentials: false
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 'lts/*'
cache: 'npm'
- run: npm ci
# Three checks: every referenced key exists, no key is interpolated without a translate
# pipe, and no catalogue holds a key en.json dropped or loses coverage. What none of them
# can see is a key assembled at runtime — the e2e suite fails on those instead, via
# ReportingMissingTranslationHandler and the failOnMissingTranslations fixture.
- name: Verify translations
run: npm run i18n:check
# The one shape i18n:check cannot see: English written straight into a template, with no
# key and no pipe. Nothing is looked up, so nothing misses and no raw key ever appears —
# the text simply stays English in every language. Per-file baseline, shrinking only.
- name: Verify no new untranslated template text
run: npm run check:template-text
- name: Verify every icon name is registered
run: npm run check:icons
- name: Verify every icon-only button has an accessible name
run: npm run check:a11y-names
# A required signal input only throws when the screen renders, and most list screens
# have no spec that renders them — so a missing `type` would reach a user rather than CI.
- name: Verify every app-button declares its type and can be named
run: npm run check:ui-primitives
# Fineract serves /v1/internal/** only under its `test` Spring profile, which upstream
# states must not be enabled in production. The few screens built on those endpoints are
# gated behind `developerToolsEnabled`; this pins that list so a new dependency on a
# test-only endpoint has to be a deliberate choice.
- name: Verify no ungated dependency on test-only Fineract endpoints
run: npm run check:internal-endpoints
# Route authorization and navigation visibility are declared in two places, and the
# failure when they disagree is silent: a menu item leading to Access Denied, or a screen
# missing from the menu that the URL still opens. This compares them.
- name: Verify route permissions agree with navigation
run: npm run check:route-permissions
# A gate on a permission code Fineract does not define can never be satisfied by any
# role, so the control it protects is disabled forever and looks exactly like one the
# viewer is not entitled to. Two had shipped that way. Checked against a committed
# snapshot, so this needs no Fineract running.
- name: Verify every permission code exists in Fineract's catalogue
run: npm run check:permission-codes
# Navigation ids are the handle a deployment's config names, in a file this repository
# never sees. A renamed or removed id silently stops matching there, so the menu a
# deployment meant to change comes back on upgrade with no error anywhere. The same check
# holds labelKey to being an actual key: `labelKey: 'Admin'` renders exactly like a
# working entry in English and stays English everywhere else.
- name: Verify navigation ids are stable and every label is a translation key
run: npm run check:nav-ids
# public/branding/ is reserved for deployments. Anything committed there becomes a merge
# conflict for every downstream that mounts its own overlay.
- name: Verify the deployment overlay path is unused upstream
run: npm run check:branding-path
# Resolves a maximally-customized reference deployment against the current tree. The unit
# tests use a three-entry fixture and the e2e suite mocks its own overlay, so both keep
# passing while an id or token a real deployment names disappears.
- name: Verify the reference downstream config still resolves
run: npm run check:reference-downstream
# The narrow layout rests on three invariants a diff cannot show: one breakpoint shared
# between the CSS and ViewportService, no 100vh (which overflows under mobile browser
# chrome), and no unbounded fixed width. The e2e `mobile` project covers behaviour; this
# covers the rules that are about to break it.
- name: Verify responsive layout invariants
run: npm run check:responsive
# `--ion-color-*-contrast` is a literal white, so the fill beside it decides whether button
# and badge labels are readable. #587 fixed that ratio and a later commit on the same branch
# reverted it, shipping every primary CTA at 3.15:1, because no check asserted it (#613).
- name: Verify theme contrast floors
run: npm run check:theme-contrast
# Nothing else type-checks e2e/: `npm run lint` covers src, and tsconfig.app.json excludes
# the suite. A helper referencing an identifier that no longer exists therefore compiled
# fine and failed only when CI ran the browser — which is a slow, expensive way to find a
# typo.
- name: Type-check the e2e suite
run: npm run typecheck:e2e
# Rejects 0.0.0 and anything that is not SemVer, and on a tag build asserts the tag names
# the same version package.json does.
- name: Verify the declared version
run: npm run check:version
test:
name: Unit Tests
needs: dependencies
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
with:
persist-credentials: false
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 'lts/*'
cache: 'npm'
- run: npm ci
# Angular's build cache, which angular.json now enables under CI (`cli.cache.environment:
# all`).
#
# Keyed on the lockfile *only*, deliberately. The obvious key includes a hash of `src/**`,
# and that is a trap: it writes a new entry on every push that touches a source file —
# which is every push — so three job-kinds' worth of a few hundred megabytes each would
# churn through the repository's 10 GB cache budget in days and evict the Playwright and
# buildx entries that the rest of CI depends on. Cache eviction is not free: it converts a
# warm job into a cold one somewhere else in the matrix.
#
# Keying on the lockfile costs nothing in hit rate, because `.angular/cache` is
# content-addressed internally: an entry built from older sources still hits for every
# file whose content has not changed, which is almost all of them. So the entry is
# rewritten when dependencies move and read the rest of the time, and the number of
# entries stays bounded at one per job-kind.
- name: Cache Angular build output
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: .angular/cache
key: ${{ runner.os }}-angular-test-${{ hashFiles('package-lock.json') }}
restore-keys: |
${{ runner.os }}-angular-test-
- name: Run unit tests on Vitest
run: npm run test:unit
# The runner's clock is UTC and playwright.config.ts pins Asia/Kolkata, so every timezone
# this project tests at is at or east of Greenwich — and a local/UTC date mix-up is
# invisible at a non-negative offset. That is how #496 shipped: a bare `YYYY-MM-DD` read as
# UTC midnight and printed back with local getters submitted the day before, for users the
# suite never simulated.
#
# A second pass costs about a minute and covers the whole application rather than the one
# helper, which no assertion running only at UTC can do.
- name: Run unit tests west of Greenwich
run: npm run test:unit
env:
TZ: America/New_York
- name: Run microfrontend tests on Vitest
run: npm run test:mfe
api-client-drift:
name: API Client Drift Check
needs: dependencies
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
with:
persist-credentials: false
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 'lts/*'
cache: 'npm'
- uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
with:
java-version: '17'
distribution: 'temurin'
- run: npm ci
# openapi-generator-cli downloads its ~25 MB generator jar on first use. The version is
# pinned in openapitools.json, so the download is the same bytes on every run.
- name: Cache the OpenAPI generator
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.openapi-generator
key: ${{ runner.os }}-openapi-generator-${{ hashFiles('openapitools.json') }}
- name: Verify generated API client matches the committed spec
run: npm run verify-api-client
build:
name: Production Build
needs: dependencies
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
with:
persist-credentials: false
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 'lts/*'
cache: 'npm'
- run: npm ci
# Angular's build cache, which angular.json now enables under CI (`cli.cache.environment:
# all`).
#
# Keyed on the lockfile *only*, deliberately. The obvious key includes a hash of `src/**`,
# and that is a trap: it writes a new entry on every push that touches a source file —
# which is every push — so three job-kinds' worth of a few hundred megabytes each would
# churn through the repository's 10 GB cache budget in days and evict the Playwright and
# buildx entries that the rest of CI depends on. Cache eviction is not free: it converts a
# warm job into a cold one somewhere else in the matrix.
#
# Keying on the lockfile costs nothing in hit rate, because `.angular/cache` is
# content-addressed internally: an entry built from older sources still hits for every
# file whose content has not changed, which is almost all of them. So the entry is
# rewritten when dependencies move and read the rest of the time, and the number of
# entries stays bounded at one per job-kind.
- name: Cache Angular build output
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: .angular/cache
key: ${{ runner.os }}-angular-build-${{ hashFiles('package-lock.json') }}
restore-keys: |
${{ runner.os }}-angular-build-
- run: npm run build
# Nothing above proves the production build can run with no network access — `build` always
# has it, so a step that quietly starts depending on a remote fetch would pass CI unnoticed.
# That is exactly how the Google Fonts dependency in #362 survived: found by an audit, not by
# a check. See #363.
offline-build:
name: Offline Production Build
needs: dependencies
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
with:
persist-credentials: false
# Both steps run inside the same node:24-alpine image deploy/Dockerfile builds from
# (pinned to the same digest), rather than installing on the runner's own Node and mounting
# the result into a container. `npm ci` resolves platform-specific native optional
# dependencies — lightningcss ships separate glibc and musl builds — so installing on the
# runner (glibc) and then running the build against that install inside Alpine (musl) would
# load the wrong binary. Installing where the build runs is what makes this a build of the
# release image's actual environment, not an approximation of it.
- name: 'Install dependencies (network allowed: registry only)'
run: |
docker run --rm -v "$PWD":/workspace -w /workspace \
node:24-alpine@sha256:d32cdf619f63fe0471182d08996dd516c6275bb5fd31ae06e55a570bd9e1ad43 \
npm ci
# `--network none` gives the container only a loopback interface — no registry, no CDN, no
# font host, nothing. A build step that reaches for any of those fails here instead of
# shipping. See DOCS/CI_CHECKS.md for why `npm ci` above does not run under the same
# restriction.
- name: Build with no network access
run: |
docker run --rm --network none -v "$PWD":/workspace -w /workspace \
node:24-alpine@sha256:d32cdf619f63fe0471182d08996dd516c6275bb5fd31ae06e55a570bd9e1ad43 \
npm run build -- --configuration production
compliance:
name: License Compliance
needs: dependencies
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
with:
persist-credentials: false
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 'lts/*'
cache: 'npm'
- run: npm ci
- name: Check Apache license headers (Custom Script)
run: ./scripts/check-license.sh
# An allow-list over what ships. Kept alongside the categorised scan below rather than
# replaced by it: the two read npm's metadata by different routes, so a package whose
# licence one of them misreads is still caught by the other.
- name: Check dependency licenses
run: npx --no-install license-checker --production --onlyAllow "MIT;Apache-2.0;BSD-2-Clause;BSD-3-Clause;ISC;0BSD"
# The tree is clean today, so a green scan proves nothing on its own about whether the
# classifier still classifies. This is what proves it — including that LGPL-3.0 still
# resolves to Category X, which is the case that removed eslint-plugin-sonarjs.
- name: Verify the licence classifier still blocks what it must
run: npm run check:licenses:selftest
# ASF policy, enforced: Category A passes, Category B is reported for PMC awareness,
# Category X and any licence nobody has classified fail the build. Runs over the whole
# tree, not just what ships: devDependencies are named in package.json, and package.json
# travels in a source release.
- name: Classify every dependency licence against ASF policy
run: npm run check:licenses
container:
name: Container Image
needs: dependencies
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
with:
persist-credentials: false
# Deliberately a plain `docker build`, with no layer cache.
#
# The obvious speed-up is buildx with `cache-to: type=gha`, which would cache this
# image's first stage — `npm ci` plus a production Angular build. It is not available
# here: the ASF enterprise restricts workflows to an allow-list of actions, and
# `docker/setup-buildx-action` and `docker/build-push-action` are not on it. A run using
# them fails before any job starts, so this is a hard constraint rather than a
# preference.
#
# The remaining route would be the buildx CLI, which ubuntu-latest already ships, with
# `--cache-to type=local` behind `actions/cache`. That was judged not worth it: only the
# dependency stage is cacheable — every source change invalidates the build stage
# regardless — so it buys roughly a minute, in exchange for the docker-container driver,
# an export step to get the image back into the daemon, and a local cache that grows
# without bound unless it is pruned by hand. Worth revisiting if this job becomes the
# critical path.
- name: Build the image
run: docker build -f deploy/Dockerfile -t fineract-backoffice-ui:ci .
# The reason this job exists. The image built and served its landing page for months while
# answering every single API call with the application's own HTML, because nginx had no
# /api/ proxy and `try_files` fell through to index.html. A 200 on `/` proves nothing; the
# assertions below are the ones that would have caught it.
- name: Start Fineract for the container to talk to
run: |
docker compose -f deploy/docker-compose-e2e.yml up -d --wait fineract-db
docker exec -i fineract-db psql -U postgres < deploy/init-db.sql
docker compose -f deploy/docker-compose-e2e.yml up -d fineract-backend
for _ in $(seq 1 60); do
if [ "$(curl -k -s -o /dev/null -w '%{http_code}' --max-time 5 \
https://localhost:8443/fineract-provider/actuator/info || true)" = "200" ]; then
echo "Fineract is up"; exit 0
fi
sleep 5
done
echo "::error::Fineract did not come up"; exit 1
- name: Run the image against it
run: |
NET=$(docker inspect fineract-backend \
--format '{{range $k,$v := .NetworkSettings.Networks}}{{$k}}{{end}}')
docker run -d --name backoffice-ui --network "$NET" -p 18080:80 \
-e FINERACT_API_URL=https://fineract-backend:8443/fineract-provider/api \
-e FINERACT_PROXY_SSL_VERIFY=off \
fineract-backoffice-ui:ci
for _ in $(seq 1 30); do
curl -sf -o /dev/null http://localhost:18080/ && break
sleep 2
done
- name: The application is served
run: |
set -eu
code=$(curl -s -o /dev/null -w '%{http_code}' http://localhost:18080/)
[ "$code" = "200" ] || { echo "::error::GET / answered $code"; exit 1; }
- name: Production HTML does not depend on a blocked inline event handler
run: |
set -eu
page=$(curl -s http://localhost:18080/)
if printf '%s' "$page" | grep -q 'onload='; then
echo "::error::index.html contains an inline onload handler that script-src blocks"
exit 1
fi
- name: config.json carries every key the application reads
run: |
set -eu
body=$(curl -s http://localhost:18080/config.json)
echo "$body"
for key in fineractApiUrl defaultTenant rbacEnabled institutionType developerToolsEnabled; do
echo "$body" | grep -q "\"$key\"" || {
echo "::error::config.json is missing $key — the entrypoint dropped it"; exit 1; }
done
- name: The API is proxied, and answers as Fineract rather than as the application
run: |
set -eu
type=$(curl -s -o /dev/null -w '%{content_type}' \
-u mifos:password -H 'Fineract-Platform-TenantId: default' \
http://localhost:18080/api/v1/offices)
code=$(curl -s -o /dev/null -w '%{http_code}' \
-u mifos:password -H 'Fineract-Platform-TenantId: default' \
http://localhost:18080/api/v1/offices)
echo "GET /api/v1/offices -> $code $type"
case "$type" in
text/html*) echo "::error::/api/v1/offices returned HTML — the SPA shell, not Fineract. \
deploy/nginx.conf.template has lost its /api/ proxy."; exit 1 ;;
esac
[ "$code" = "200" ] || { echo "::error::/api/v1/offices answered $code, expected 200"; exit 1; }
# `location ^~ /api/` is what stops the static-asset regex block claiming this. Without the
# `^~` nginx still evaluates the regex, and any API path ending in .js is served off the
# filesystem — a 404 for a request that should have reached the backend.
- name: An API path ending in .js is proxied, not served from disk
run: |
set -eu
type=$(curl -s -o /dev/null -w '%{content_type}' http://localhost:18080/api/v1/probe.js)
echo "GET /api/v1/probe.js -> $type"
case "$type" in
text/html*|application/javascript*) \
echo "::error::/api/v1/probe.js was served locally instead of proxied"; exit 1 ;;
esac
- name: Security headers survive on a proxied response
run: |
set -eu
headers=$(curl -s -D - -o /dev/null \
-u mifos:password -H 'Fineract-Platform-TenantId: default' \
http://localhost:18080/api/v1/offices)
for header in Content-Security-Policy X-Content-Type-Options X-Frame-Options; do
echo "$headers" | grep -qi "^$header:" || {
echo "::error::$header missing from the proxied response"; exit 1; }
done
- name: CSP admits only the script sources native federation needs
run: |
set -eu
headers=$(curl -s -D - -o /dev/null http://localhost:18080/)
csp=$(printf '%s\n' "$headers" | tr -d '\r' | grep -i '^Content-Security-Policy:' | head -n 1)
echo "$csp" | grep -Eiq "script-src 'self' blob:;" || {
echo "::error::CSP script-src does not contain the required self and blob sources"
exit 1
}
if echo "$csp" | grep -Eiq "script-src[^;]*(unsafe-inline|unsafe-eval)"; then
echo "::error::CSP script-src contains an unsafe execution source"
exit 1
fi
- name: Container logs
if: always()
run: docker logs backoffice-ui 2>&1 | tail -30
diagram:
name: Change Sequence Diagram
needs: dependencies
# Only a pull request has somewhere to publish the diagram to.
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
with:
persist-credentials: false
# The diagram is drawn from the files this pull request changed, so the merge base
# has to be reachable. `fetch-depth: 0` is blunt but reliable; a shallow fetch of the
# base ref alone breaks whenever the branch is behind by more than the fetch depth.
fetch-depth: 0
# No npm install: the script is dependency-free Node, and this job exists to be cheap.
- name: Render the diagram
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: node scripts/pr-sequence-diagram.mjs "$BASE_SHA" > /tmp/pr-diagram.md
- name: Show it in the run summary
run: cat /tmp/pr-diagram.md >> "$GITHUB_STEP_SUMMARY"
# Uploaded rather than posted from here, for the same reason the E2E summaries are: this
# job runs on a fork's `pull_request` with a read-only token. pr-comments.yml publishes it.
- name: Upload the diagram for the commenter
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
if: ${{ !cancelled() }}
with:
name: pr-comment-diagram
path: /tmp/pr-diagram.md
if-no-files-found: warn
retention-days: 1
rat-scan:
name: Apache RAT Scan
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
with:
persist-credentials: false
- name: Set up JDK
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
with:
java-version: '17'
distribution: 'temurin'
# The jar is immutable at a pinned version, so it is fetched once and read from the cache
# afterwards. The checksum is still verified on every run, cache hit included: a cache is
# writable from any branch, so trusting its contents unverified would be the more
# interesting half of a supply-chain problem rather than a saving.
- name: Cache Apache RAT
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: apache-rat-0.17.jar
key: ${{ runner.os }}-apache-rat-0.17
- name: Download Apache RAT
run: |
if [ ! -f apache-rat-0.17.jar ]; then
curl -sL https://repo1.maven.org/maven2/org/apache/rat/apache-rat/0.17/apache-rat-0.17.jar -o apache-rat-0.17.jar
fi
echo "401939ebe5a52c6ed524029897bf914eaaba503d36c069ebcdbd8847a9e7cf93 apache-rat-0.17.jar" | sha256sum --check --status || {
echo "::error::Apache RAT jar checksum verification failed. Aborting."
exit 1
}
- name: Run Apache RAT Scan
run: |
java -jar apache-rat-0.17.jar -E .rat-excludes -d .
security:
name: Security Audit
needs: dependencies
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
with:
persist-credentials: false
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 'lts/*'
cache: 'npm'
- run: npm ci
# --omit=dev audits only runtime (production) dependencies.
# Build tooling vulnerabilities (esbuild, vite) are excluded as they
# are never deployed and have no upstream fix available.
- run: npm audit --audit-level=high --omit=dev
ga-gate:
name: GA Readiness Gate
needs: dependencies
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
with:
persist-credentials: false
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 'lts/*'
cache: 'npm'
- run: npm ci
# The generated API surface the application actually calls, against the reviewed manifest.
# Recorded separately from ga:check so a drift here names the operations that moved.
- name: Verify the called API surface matches the manifest
run: npm run api:surface
# `scripts/ga-check.mjs` has existed, and failed, without CI ever running it. A release
# gate nobody runs is not a gate. Blocking failures fail this job; advisory ones report.
- name: Run the GA readiness gate
run: npm run ga:check
summary:
name: Build Summary
runs-on: ubuntu-latest
needs:
[
dependencies,
lint,
html-lint,
format,
i18n-check,
api-client-drift,
test,
build,
offline-build,
compliance,
container,
rat-scan,
security,
ga-gate,
]
if: always()
permissions:
contents: read
steps:
- name: Generate Summary
run: |
echo "## 🚀 Fineract Backoffice UI - CI Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### 📊 Status Overview" >> $GITHUB_STEP_SUMMARY
echo "| Check | Result |" >> $GITHUB_STEP_SUMMARY
echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| Dependencies | ${{ needs.dependencies.result == 'success' && '✅ Integrity Verified' || '❌ Fail' }} |" >> $GITHUB_STEP_SUMMARY
echo "| TS Linting | ${{ needs.lint.result == 'success' && '✅ Pass' || '❌ Fail' }} |" >> $GITHUB_STEP_SUMMARY
echo "| HTML Linting | ${{ needs.html-lint.result == 'success' && '✅ Pass' || '❌ Fail' }} |" >> $GITHUB_STEP_SUMMARY
echo "| Formatting | ${{ needs.format.result == 'success' && '✅ Pass' || '❌ Fail' }} |" >> $GITHUB_STEP_SUMMARY
echo "| Translation Check | ${{ needs.i18n-check.result == 'success' && '✅ Pass' || '❌ Fail' }} |" >> $GITHUB_STEP_SUMMARY
echo "| API Client Drift | ${{ needs.api-client-drift.result == 'success' && '✅ In Sync' || '❌ Fail' }} |" >> $GITHUB_STEP_SUMMARY
echo "| GA Readiness Gate | ${{ needs.ga-gate.result == 'success' && '✅ Pass' || '❌ Fail' }} |" >> $GITHUB_STEP_SUMMARY
echo "| Unit Tests | ${{ needs.test.result == 'success' && '✅ Pass (Artifact Uploaded)' || '❌ Fail' }} |" >> $GITHUB_STEP_SUMMARY
echo "| Prod Build | ${{ needs.build.result == 'success' && '✅ Pass' || '❌ Fail' }} |" >> $GITHUB_STEP_SUMMARY
echo "| Offline Build | ${{ needs.offline-build.result == 'success' && '✅ Pass' || '❌ Fail' }} |" >> $GITHUB_STEP_SUMMARY
echo "| Compliance | ${{ needs.compliance.result == 'success' && '✅ Pass' || '❌ Fail' }} |" >> $GITHUB_STEP_SUMMARY
echo "| Container Image | ${{ needs.container.result == 'success' && '✅ Builds and proxies the API' || '❌ Fail' }} |" >> $GITHUB_STEP_SUMMARY
echo "| RAT Scan | ${{ needs.rat-scan.result == 'success' && '✅ Pass' || '❌ Fail' }} |" >> $GITHUB_STEP_SUMMARY
echo "| Security Audit | ${{ needs.security.result == 'success' && '✅ Pass' || '❌ Fail' }} |" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### 🛡️ Security & Compliance Details" >> $GITHUB_STEP_SUMMARY
echo "- **Dependency Integrity:** Enforced via \`npm ci\`. This prevents the use of \`--legacy-peer-deps\`." >> $GITHUB_STEP_SUMMARY
echo "- **HTML Quality:** Explicitly checking templates for accessibility and structural best practices via \`angular-eslint\`." >> $GITHUB_STEP_SUMMARY
echo "- **Vulnerability Audit:** Completed with \`npm audit --audit-level=high --omit=dev\`. Production runtime is clean." >> $GITHUB_STEP_SUMMARY
echo "- **Offline Analysis:** Integrated \`SonarJS\` rules via ESLint for continuous quality tracking." >> $GITHUB_STEP_SUMMARY
echo "- **License Audit:** Apache RAT scan verified 0 unapproved files." >> $GITHUB_STEP_SUMMARY