From 0d2b7685660d3e0d52caf726462c84358823e940 Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Fri, 24 Jul 2026 20:55:56 +0200 Subject: [PATCH 1/2] perf(ci): speed up PR feedback with transpile-only jest and parallel jobs (#4374) * perf(ci): faster PR feedback via transpile-only jest and parallel jobs ts-jest re-ran a full type-check on every suite, which is redundant with the pipeline's type-check and was the dominant test cost. Enable transpile-only via tsconfig isolatedModules, add a project-wide type-check step so spec files stay type-checked (nest build excludes them), and split api-pr.yaml into parallel checks and test jobs. Every test still runs on every PR. * fix(ci): keep Bank Frick coverage gate on full compilation Transpile-only (isolatedModules) emits the emitDecoratorMetadata helpers differently, adding phantom uncovered branches on dependency-injected constructors that red the 100% Frick coverage gate. Compile the coverage run with full type info (tsconfig.coverage.json, isolatedModules: false) so the gate stays exact; the main suite keeps transpile-only. * perf(ci): shard the test job across 3 parallel runners Split the full suite into 3 disjoint jest shards and move the Frick coverage gate into its own parallel job, so PR feedback is ~max(shard, checks, coverage) instead of one ~9 min test job. Every suite still runs exactly once (shards are disjoint and cover all 300 suites). * docs(frick): document the coverage-gate compilation config Correct the coverageThreshold location (jest.frick.config.js, not package.json) and record why test:frick:cov compiles with full type info (tsconfig.coverage.json) while the main suite runs transpile-only. --- .github/workflows/api-pr.yaml | 97 +++++++++++++++---- docs/bank-frick-operations.md | 10 +- jest.frick.config.js | 5 + .../bitcoin/services/bitcoin-fee.service.ts | 2 +- .../services/ledger-booking-job.service.ts | 3 +- tsconfig.coverage.json | 6 ++ tsconfig.json | 1 + 7 files changed, 101 insertions(+), 23 deletions(-) create mode 100644 tsconfig.coverage.json diff --git a/.github/workflows/api-pr.yaml b/.github/workflows/api-pr.yaml index 0f720ad483..b1c7728d86 100644 --- a/.github/workflows/api-pr.yaml +++ b/.github/workflows/api-pr.yaml @@ -13,14 +13,88 @@ 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 + checks: + name: Build and checks if: github.head_ref != 'develop' 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: Bank Frick coverage + if: github.head_ref != 'develop' + 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: Enforce Bank Frick coverage + run: npm run test:frick:cov + + test: + name: Test (shard ${{ matrix.shard }}/3) + if: github.head_ref != 'develop' + 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: @@ -50,22 +124,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 diff --git a/docs/bank-frick-operations.md b/docs/bank-frick-operations.md index cf87df9eea..8a77413bd0 100644 --- a/docs/bank-frick-operations.md +++ b/docs/bank-frick-operations.md @@ -249,8 +249,8 @@ 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 @@ -258,3 +258,9 @@ regress them. The cost - a future, unrelated Yapeal/Raiffeisen-only change could 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. diff --git a/jest.frick.config.js b/jest.frick.config.js index 3fb238757a..a0b6dce2d8 100644 --- a/jest.frick.config.js +++ b/jest.frick.config.js @@ -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 }, diff --git a/src/integration/blockchain/bitcoin/services/bitcoin-fee.service.ts b/src/integration/blockchain/bitcoin/services/bitcoin-fee.service.ts index 08620eff47..512ba4ede9 100644 --- a/src/integration/blockchain/bitcoin/services/bitcoin-fee.service.ts +++ b/src/integration/blockchain/bitcoin/services/bitcoin-fee.service.ts @@ -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 { diff --git a/src/subdomains/core/accounting/services/ledger-booking-job.service.ts b/src/subdomains/core/accounting/services/ledger-booking-job.service.ts index 39ea8b7f0b..9000445e74 100644 --- a/src/subdomains/core/accounting/services/ledger-booking-job.service.ts +++ b/src/subdomains/core/accounting/services/ledger-booking-job.service.ts @@ -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'; diff --git a/tsconfig.coverage.json b/tsconfig.coverage.json new file mode 100644 index 0000000000..39a46bc7da --- /dev/null +++ b/tsconfig.coverage.json @@ -0,0 +1,6 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "isolatedModules": false + } +} diff --git a/tsconfig.json b/tsconfig.json index 75583a82e8..538bf77419 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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"], From a418b0f90c4f98e3442d5a363950a5c42c14f127 Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Fri, 24 Jul 2026 22:21:28 +0200 Subject: [PATCH 2/2] fix(ci): enforce release checks (#4380) * fix(ci): enforce release checks * fix(ci): trigger release PR workflows * fix(ci): reduce release workflow permissions --- .github/workflows/api-pr.yaml | 7 ++----- .github/workflows/auto-release-pr.yaml | 4 ++-- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/.github/workflows/api-pr.yaml b/.github/workflows/api-pr.yaml index b1c7728d86..03d7f0f888 100644 --- a/.github/workflows/api-pr.yaml +++ b/.github/workflows/api-pr.yaml @@ -18,7 +18,6 @@ env: jobs: checks: name: Build and checks - if: github.head_ref != 'develop' runs-on: ubuntu-latest steps: - name: Checkout @@ -57,8 +56,7 @@ jobs: run: npm audit --audit-level=critical coverage: - name: Bank Frick coverage - if: github.head_ref != 'develop' + name: Coverage runs-on: ubuntu-latest steps: - name: Checkout @@ -80,12 +78,11 @@ jobs: # 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: Enforce Bank Frick coverage + - name: Run coverage run: npm run test:frick:cov test: name: Test (shard ${{ matrix.shard }}/3) - if: github.head_ref != 'develop' runs-on: ubuntu-latest strategy: fail-fast: false diff --git a/.github/workflows/auto-release-pr.yaml b/.github/workflows/auto-release-pr.yaml index cd7f74555c..b5fb4b20be 100644 --- a/.github/workflows/auto-release-pr.yaml +++ b/.github/workflows/auto-release-pr.yaml @@ -7,7 +7,7 @@ on: permissions: contents: read - pull-requests: write + pull-requests: read concurrency: group: auto-release-pr @@ -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' \