Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 76 additions & 20 deletions .github/workflows/api-pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,85 @@ permissions:
env:
NODE_VERSION: '20.x'

# Three job groups run in parallel, so PR feedback is ~max(test-shard, checks, coverage)
# instead of the sum of every step. `test` is additionally split into shards.
jobs:
build:
name: Build and test
if: github.head_ref != 'develop'
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

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:
Expand Down Expand Up @@ -50,22 +121,7 @@ jobs:
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

- name: Run tests
run: npm run test
- name: Run tests (shard ${{ matrix.shard }}/3)
run: npm test -- --shard=${{ matrix.shard }}/3
env:
MIGRATION_TEST_PG: postgres://postgres:postgres@localhost:5432/postgres

- name: Enforce Bank Frick coverage
run: npm run test:frick:cov

- name: Security audit
run: npm audit --audit-level=critical
4 changes: 2 additions & 2 deletions .github/workflows/auto-release-pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ on:

permissions:
contents: read
pull-requests: write
pull-requests: read

concurrency:
group: auto-release-pr
Expand Down Expand Up @@ -47,7 +47,7 @@ jobs:
- name: Create Release PR
if: steps.check-pr.outputs.pr_exists == 'false' && steps.check-diff.outputs.has_changes == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_TOKEN: ${{ secrets.RELEASE_PR_TOKEN }}
COMMIT_COUNT: ${{ steps.check-diff.outputs.commit_count }}
run: |
printf '%s\n' \
Expand Down
10 changes: 8 additions & 2 deletions docs/bank-frick-operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,12 +249,18 @@ sandbox credentials:

## 7. Coverage gate on the shared `iso20022.service.ts`

`package.json`'s `coverageThreshold` holds `src/integration/bank/services/iso20022.service.ts` to
100% even though this file is shared with Yapeal/Raiffeisen parsing, not Frick-only. This is a
`jest.frick.config.js`'s `coverageThreshold` holds `src/integration/bank/services/iso20022.service.ts`
to 100% even though this file is shared with Yapeal/Raiffeisen parsing, not Frick-only. This is a
deliberate trade-off, not an oversight: the money-critical fixes in this PR (malformed-entry
rejection, the missing-bank-reference guard, and bank-charge parsing) live in exactly this file,
and 100% branch coverage is the only mechanical guarantee that a future change cannot silently
regress them. The cost - a future, unrelated Yapeal/Raiffeisen-only change could fail CI on an
uncovered branch it didn't intend to touch - is accepted deliberately in exchange for that
protection. If this ever becomes a real blocker, the long-term fix is to split the Frick-specific
strict-mode parsing into its own file with its own gate, not to lower this threshold.

The `test:frick:cov` gate compiles with full type information (`tsconfig.coverage.json`, which sets
`isolatedModules: false`), unlike the main test run. The main suite uses ts-jest transpile-only
(`isolatedModules`) for speed, but transpile-only emits the `emitDecoratorMetadata` helpers
differently and adds phantom uncovered branches on dependency-injected constructors, which would red
this 100% gate. Compiling the coverage run the same way as the production build keeps the gate exact.
5 changes: 5 additions & 0 deletions jest.frick.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ const base = require('./package.json').jest;

module.exports = {
...base,
// Coverage instrumentation must match the production build's emit. The main suite runs ts-jest in
// transpile-only mode (isolatedModules), which emits the emitDecoratorMetadata helpers differently
// and produces phantom uncovered branches on dependency-injected constructors. Compile with full
// type info here (tsconfig.coverage.json sets isolatedModules: false) so the 100% gate stays exact.
transform: { '^.+\\.(t|j)s$': ['ts-jest', { tsconfig: 'tsconfig.coverage.json' }] },
coverageThreshold: {
'src/integration/bank/dto/frick.dto.ts': { branches: 100, functions: 100, lines: 100, statements: 100 },
'src/integration/bank/services/frick.service.ts': { branches: 100, functions: 100, lines: 100, statements: 100 },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Config } from 'src/config/config';
import { BitcoinBasedFeeService, FeeConfig } from './bitcoin-based-fee.service';
import { BitcoinNodeType, BitcoinService } from './bitcoin.service';

export { TxFeeRateResult, TxFeeRateStatus } from './bitcoin-based-fee.service';
export type { TxFeeRateResult, TxFeeRateStatus } from './bitcoin-based-fee.service';

@Injectable()
export class BitcoinFeeService extends BitcoinBasedFeeService {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import { TradingOrderConsumer } from './consumers/trading-order.consumer';
import { LedgerBootstrapService } from './ledger-bootstrap.service';

// watermark helpers live in a consumer-free file to keep the job-service↔consumer import graph acyclic (§11.3)
export { getLedgerWatermark, LedgerWatermark, setLedgerWatermark } from './consumers/ledger-watermark.helper';
export { getLedgerWatermark, setLedgerWatermark } from './consumers/ledger-watermark.helper';
export type { LedgerWatermark } from './consumers/ledger-watermark.helper';

const CUTOVER_LOG_ID_KEY = 'ledgerCutoverLogId';

Expand Down
6 changes: 6 additions & 0 deletions tsconfig.coverage.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"isolatedModules": false
}
}
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"paths": {
"swissqrbill/svg": ["node_modules/swissqrbill/lib/esm/svg/index.d.ts"],
"swissqrbill/pdf": ["node_modules/swissqrbill/lib/esm/pdf/index.d.ts"],
Expand Down
Loading