diff --git a/.github/workflows/api-pr.yaml b/.github/workflows/api-pr.yaml index 0f720ad483..03d7f0f888 100644 --- a/.github/workflows/api-pr.yaml +++ b/.github/workflows/api-pr.yaml @@ -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: @@ -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 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' \ 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"],