From 9e86168372011887ac9298a1dc1d0a9ea6e5a56a Mon Sep 17 00:00:00 2001 From: jhaydeeee-web Date: Mon, 19 Oct 2026 19:36:39 +0100 Subject: [PATCH 1/4] infra: add OTLP endpoint variable and collector egress rules (#1785) --- docs/DISTRIBUTED_TRACING.md | 9 ++++ infrastructure/terraform/main.tf | 1 + infrastructure/terraform/modules/ecs/main.tf | 15 +++++- infrastructure/terraform/otel_collector.tf | 48 ++++++++++++++++++++ 4 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 infrastructure/terraform/otel_collector.tf diff --git a/docs/DISTRIBUTED_TRACING.md b/docs/DISTRIBUTED_TRACING.md index c763bda9..216c30a5 100644 --- a/docs/DISTRIBUTED_TRACING.md +++ b/docs/DISTRIBUTED_TRACING.md @@ -305,3 +305,12 @@ Key metrics to monitor: - [OpenTelemetry Documentation](https://opentelemetry.io/docs/) - [Jaeger Documentation](https://www.jaegertracing.io/docs/) - [W3C Trace Context](https://www.w3.org/TR/trace-context/) + +## OTLP collector in AWS (Terraform) + +`infrastructure/terraform/otel_collector.tf` adds three variables: + +- `otlp_endpoint`: injected into the API task as `OTLP_ENDPOINT` (unset by default, which disables export). +- `otlp_collector_cidr_blocks` / `otlp_collector_sg_id`: open ECS task egress on TCP 4317 (OTLP gRPC) and 4318 (OTLP HTTP) to the collector. + +Run an ADOT or OpenTelemetry collector reachable from the private subnets, then set these in the environment's tfvars. diff --git a/infrastructure/terraform/main.tf b/infrastructure/terraform/main.tf index e697d1d2..8a92a0e8 100644 --- a/infrastructure/terraform/main.tf +++ b/infrastructure/terraform/main.tf @@ -129,6 +129,7 @@ module "ecs" { sendgrid_api_key = var.sendgrid_api_key api_signing_key = var.api_signing_key ecs_tasks_sg_id = aws_security_group.ecs_tasks.id + otlp_endpoint = var.otlp_endpoint } module "monitoring" { diff --git a/infrastructure/terraform/modules/ecs/main.tf b/infrastructure/terraform/modules/ecs/main.tf index 9179f58d..ed46a335 100644 --- a/infrastructure/terraform/modules/ecs/main.tf +++ b/infrastructure/terraform/modules/ecs/main.tf @@ -102,6 +102,12 @@ variable "api_signing_key" { sensitive = true } +variable "otlp_endpoint" { + type = string + default = "" + description = "OTLP collector endpoint injected as OTLP_ENDPOINT (empty disables export)" +} + variable "ecs_tasks_sg_id" { type = string description = "Security group ID of the ECS tasks (managed at root level)" @@ -163,12 +169,17 @@ resource "aws_ecs_task_definition" "api" { protocol = "tcp" } ] - environment = [ + environment = concat([ { name = "ENVIRONMENT" value = var.environment } - ] + ], var.otlp_endpoint == "" ? [] : [ + { + name = "OTLP_ENDPOINT" + value = var.otlp_endpoint + } + ]) # All secrets are injected from AWS Secrets Manager — no plaintext # environment variables for sensitive values. secrets = [ diff --git a/infrastructure/terraform/otel_collector.tf b/infrastructure/terraform/otel_collector.tf new file mode 100644 index 00000000..15eaf320 --- /dev/null +++ b/infrastructure/terraform/otel_collector.tf @@ -0,0 +1,48 @@ +# OpenTelemetry collector wiring for the API ECS tasks. +# +# The API exports traces over OTLP to OTLP_ENDPOINT. Point `otlp_endpoint` at +# an ADOT / OpenTelemetry collector (managed or self-hosted) that is reachable +# from the private subnets, and list the collector's CIDR ranges (or its +# security group) so the ECS tasks may reach OTLP gRPC (4317) and HTTP (4318). + +variable "otlp_endpoint" { + type = string + default = "" + description = "OTLP collector endpoint, e.g. http://otel-collector.internal:4317. Empty disables the OTLP_ENDPOINT env var." +} + +variable "otlp_collector_cidr_blocks" { + type = list(string) + default = [] + description = "CIDR blocks of the OTLP collector. Egress on 4317/4318 is opened to these ranges." +} + +variable "otlp_collector_sg_id" { + type = string + default = "" + description = "Security group ID of the OTLP collector. Egress on 4317/4318 is opened to it when set." +} + +resource "aws_security_group_rule" "ecs_tasks_egress_otlp_cidr" { + for_each = length(var.otlp_collector_cidr_blocks) > 0 ? toset(["4317", "4318"]) : toset([]) + + type = "egress" + from_port = tonumber(each.value) + to_port = tonumber(each.value) + protocol = "tcp" + security_group_id = aws_security_group.ecs_tasks.id + cidr_blocks = var.otlp_collector_cidr_blocks + description = "OTLP export to collector" +} + +resource "aws_security_group_rule" "ecs_tasks_egress_otlp_sg" { + for_each = var.otlp_collector_sg_id != "" ? toset(["4317", "4318"]) : toset([]) + + type = "egress" + from_port = tonumber(each.value) + to_port = tonumber(each.value) + protocol = "tcp" + security_group_id = aws_security_group.ecs_tasks.id + source_security_group_id = var.otlp_collector_sg_id + description = "OTLP export to collector" +} From 8c200bb76bc18eb1de8806e2557bf6f31a44077e Mon Sep 17 00:00:00 2001 From: jhaydeeee-web Date: Mon, 19 Oct 2026 19:36:58 +0100 Subject: [PATCH 2/4] ci: add least-privilege top-level permissions to six workflows (#1786) --- .github/workflows/accessibility.yml | 3 +++ .github/workflows/dependency-scan.yml | 10 ++++++++++ .github/workflows/docker.yml | 3 +++ .github/workflows/license-check.yml | 3 +++ .github/workflows/openapi-validation.yml | 3 +++ .github/workflows/protect-changelog.yml | 3 +++ 6 files changed, 25 insertions(+) diff --git a/.github/workflows/accessibility.yml b/.github/workflows/accessibility.yml index 8c08ddce..c11697c3 100644 --- a/.github/workflows/accessibility.yml +++ b/.github/workflows/accessibility.yml @@ -10,6 +10,9 @@ on: paths: - 'frontend/**' +permissions: + contents: read + env: NODE_VERSION: '18.x' # Lighthouse accessibility threshold (issue #1610). diff --git a/.github/workflows/dependency-scan.yml b/.github/workflows/dependency-scan.yml index 2df61d84..467eb7cf 100644 --- a/.github/workflows/dependency-scan.yml +++ b/.github/workflows/dependency-scan.yml @@ -11,6 +11,9 @@ on: # Pinned tool versions for reproducible scans. Bump deliberately. # cargo-audit is pinned so a new release cannot silently change scan # behavior, JSON output shape, or exit-code semantics mid-stream. +permissions: + contents: read + env: CARGO_AUDIT_VERSION: '0.21.2' @@ -103,6 +106,9 @@ jobs: scan-trivy: name: Scan with Trivy runs-on: ubuntu-latest + permissions: + contents: read + security-events: write timeout-minutes: 20 steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 @@ -129,6 +135,10 @@ jobs: timeout-minutes: 20 needs: [scan-rust, scan-npm, scan-trivy] if: always() + permissions: + contents: read + issues: write + pull-requests: write steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index e55ba95c..4a2f4e17 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -7,6 +7,9 @@ on: pull_request: branches: [main] +permissions: + contents: read + env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} diff --git a/.github/workflows/license-check.yml b/.github/workflows/license-check.yml index ae3339ee..1d0422bb 100644 --- a/.github/workflows/license-check.yml +++ b/.github/workflows/license-check.yml @@ -6,6 +6,9 @@ on: pull_request: branches: [main, develop] +permissions: + contents: read + jobs: rust-licenses-contracts: name: Rust License Check — contracts/predict-iq diff --git a/.github/workflows/openapi-validation.yml b/.github/workflows/openapi-validation.yml index 927bcb57..4e4923c8 100644 --- a/.github/workflows/openapi-validation.yml +++ b/.github/workflows/openapi-validation.yml @@ -14,6 +14,9 @@ on: - "services/api/src/**" - "services/api/tests/openapi_contract_test.rs" +permissions: + contents: read + jobs: spectral-lint: name: Spectral — OpenAPI structural lint diff --git a/.github/workflows/protect-changelog.yml b/.github/workflows/protect-changelog.yml index 99d91d13..59562c1f 100644 --- a/.github/workflows/protect-changelog.yml +++ b/.github/workflows/protect-changelog.yml @@ -5,6 +5,9 @@ on: paths: - "CHANGELOG.md" +permissions: + contents: read + jobs: block-manual-changelog: name: Block manual CHANGELOG edits From 240f8f60985fa18b07eaa081f9c536a78cd8d4b0 Mon Sep 17 00:00:00 2001 From: jhaydeeee-web Date: Mon, 19 Oct 2026 19:37:07 +0100 Subject: [PATCH 3/4] ci: bump Node 18 to 20 in workflows and frontend engines (#1787) --- .github/workflows/accessibility.yml | 2 +- .github/workflows/dependency-scan.yml | 2 +- .github/workflows/license-check.yml | 4 ++-- .github/workflows/openapi-validation.yml | 2 +- .github/workflows/test.yml | 18 +++++++++--------- frontend/package.json | 2 +- 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/accessibility.yml b/.github/workflows/accessibility.yml index c11697c3..870f7f97 100644 --- a/.github/workflows/accessibility.yml +++ b/.github/workflows/accessibility.yml @@ -14,7 +14,7 @@ permissions: contents: read env: - NODE_VERSION: '18.x' + NODE_VERSION: '20.x' # Lighthouse accessibility threshold (issue #1610). # Rationale: a zero-tolerance 100/100 gate on a synthetic Lighthouse run made # the all-tests-passed gate brittle — a single non-actionable Lighthouse quirk diff --git a/.github/workflows/dependency-scan.yml b/.github/workflows/dependency-scan.yml index 467eb7cf..3e835e2c 100644 --- a/.github/workflows/dependency-scan.yml +++ b/.github/workflows/dependency-scan.yml @@ -77,7 +77,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v6 with: - node-version: '18' + node-version: '20' - name: Install dependencies run: npm ci diff --git a/.github/workflows/license-check.yml b/.github/workflows/license-check.yml index 1d0422bb..e0e306d4 100644 --- a/.github/workflows/license-check.yml +++ b/.github/workflows/license-check.yml @@ -70,7 +70,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v6 with: - node-version: "18" + node-version: "20" cache: "npm" cache-dependency-path: frontend/package-lock.json @@ -102,7 +102,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v6 with: - node-version: "18" + node-version: "20" cache: "npm" cache-dependency-path: services/tts/package-lock.json diff --git a/.github/workflows/openapi-validation.yml b/.github/workflows/openapi-validation.yml index 4e4923c8..385655b2 100644 --- a/.github/workflows/openapi-validation.yml +++ b/.github/workflows/openapi-validation.yml @@ -28,7 +28,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v6 with: - node-version: "18" + node-version: "20" - name: Install Spectral run: npm install -g @stoplight/spectral-cli diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9edba486..58685c96 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -158,7 +158,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v6 with: - node-version: "18" + node-version: "20" - name: Compare gas benchmarks against baseline working-directory: contracts/predict-iq @@ -229,7 +229,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v6 with: - node-version: "18" + node-version: "20" - name: Save baseline for main branch working-directory: contracts/predict-iq @@ -696,7 +696,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v6 with: - node-version: "18" + node-version: "20" cache: "npm" cache-dependency-path: frontend/package-lock.json @@ -739,7 +739,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v6 with: - node-version: "18" + node-version: "20" cache: "npm" cache-dependency-path: frontend/package-lock.json @@ -818,7 +818,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v6 with: - node-version: "18" + node-version: "20" cache: "npm" cache-dependency-path: frontend/package-lock.json @@ -964,7 +964,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v3 with: - node-version: "18" + node-version: "20" - name: Parse benchmark output run: | @@ -1032,7 +1032,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v3 with: - node-version: "18" + node-version: "20" - name: Parse benchmark results run: | @@ -1073,7 +1073,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v3 with: - node-version: "18" + node-version: "20" cache: "npm" cache-dependency-path: frontend/package-lock.json @@ -1388,7 +1388,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v3 with: - node-version: "18" + node-version: "20" - name: Check API_SPEC.md is in sync with openapi.yaml run: node scripts/generate-api-spec.js --check diff --git a/frontend/package.json b/frontend/package.json index 616c5caf..ff7028fe 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -70,7 +70,7 @@ "typescript": "^7.0.2" }, "engines": { - "node": ">=18.0.0", + "node": ">=20.0.0", "npm": ">=9.0.0" } } \ No newline at end of file From e26dd97997c813bd8774752306ff3f76ad226aa8 Mon Sep 17 00:00:00 2001 From: jhaydeeee-web Date: Mon, 19 Oct 2026 19:37:28 +0100 Subject: [PATCH 4/4] ci: cache prebuilt cargo tool binaries instead of recompiling (#1788) --- .github/workflows/contract-deployment.yml | 24 ++++++- .github/workflows/dependency-scan.yml | 8 ++- .github/workflows/license-check.yml | 16 ++++- .github/workflows/test.yml | 80 ++++++++++++++++++++--- 4 files changed, 112 insertions(+), 16 deletions(-) diff --git a/.github/workflows/contract-deployment.yml b/.github/workflows/contract-deployment.yml index 94821423..e7491232 100644 --- a/.github/workflows/contract-deployment.yml +++ b/.github/workflows/contract-deployment.yml @@ -56,8 +56,14 @@ jobs: path: target key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }} + - name: Cache soroban-cli binary + uses: actions/cache@v5 + with: + path: ~/.cargo/bin/soroban + key: ${{ runner.os }}-bin-soroban-cli-v1 + - name: Install Soroban CLI - run: cargo install --locked soroban-cli --features opt + run: command -v soroban >/dev/null 2>&1 || cargo install --locked soroban-cli --features opt - name: Run unit tests run: cargo test --lib --workspace @@ -145,9 +151,15 @@ jobs: name: contract-wasm path: ./artifacts + - name: Cache soroban-cli binary + uses: actions/cache@v5 + with: + path: ~/.cargo/bin/soroban + key: ${{ runner.os }}-bin-soroban-cli-v1 + - name: Install Soroban CLI run: | - cargo install --locked soroban-cli --features opt + command -v soroban >/dev/null 2>&1 || cargo install --locked soroban-cli --features opt - name: Configure Soroban for Testnet run: | @@ -214,9 +226,15 @@ jobs: name: contract-wasm path: ./artifacts + - name: Cache soroban-cli binary + uses: actions/cache@v5 + with: + path: ~/.cargo/bin/soroban + key: ${{ runner.os }}-bin-soroban-cli-v1 + - name: Install Soroban CLI run: | - cargo install --locked soroban-cli --features opt + command -v soroban >/dev/null 2>&1 || cargo install --locked soroban-cli --features opt - name: Configure Soroban for Mainnet run: | diff --git a/.github/workflows/dependency-scan.yml b/.github/workflows/dependency-scan.yml index 3e835e2c..f0ba5691 100644 --- a/.github/workflows/dependency-scan.yml +++ b/.github/workflows/dependency-scan.yml @@ -30,8 +30,14 @@ jobs: - name: Install Rust uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 # stable + - name: Cache cargo-audit binary + uses: actions/cache@v5 + with: + path: ~/.cargo/bin/cargo-audit + key: ${{ runner.os }}-bin-cargo-audit-v1 + - name: Install cargo-audit - run: cargo install cargo-audit --version "$CARGO_AUDIT_VERSION" --locked + run: command -v cargo-audit >/dev/null 2>&1 || cargo install cargo-audit --version "$CARGO_AUDIT_VERSION" --locked - name: Audit contracts/predict-iq dependencies run: cargo audit --deny warnings diff --git a/.github/workflows/license-check.yml b/.github/workflows/license-check.yml index e0e306d4..4abfb3c3 100644 --- a/.github/workflows/license-check.yml +++ b/.github/workflows/license-check.yml @@ -28,8 +28,14 @@ jobs: ~/.cargo/git key: ${{ runner.os }}-cargo-deny-contracts-${{ hashFiles('**/Cargo.lock') }} + - name: Cache cargo-deny binary + uses: actions/cache@v5 + with: + path: ~/.cargo/bin/cargo-deny + key: ${{ runner.os }}-bin-cargo-deny-v1 + - name: Install cargo-deny - run: cargo install cargo-deny --locked + run: command -v cargo-deny >/dev/null 2>&1 || cargo install cargo-deny --locked - name: Check licenses — contracts workspace run: cargo deny check licenses @@ -53,8 +59,14 @@ jobs: ~/.cargo/git key: ${{ runner.os }}-cargo-deny-api-${{ hashFiles('services/api/Cargo.lock') }} + - name: Cache cargo-deny binary + uses: actions/cache@v5 + with: + path: ~/.cargo/bin/cargo-deny + key: ${{ runner.os }}-bin-cargo-deny-v1 + - name: Install cargo-deny - run: cargo install cargo-deny --locked + run: command -v cargo-deny >/dev/null 2>&1 || cargo install cargo-deny --locked - name: Check licenses — services/api run: cargo deny check licenses diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 58685c96..f28c3db9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -68,9 +68,15 @@ jobs: path: target key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }} + - name: Cache soroban-cli binary + uses: actions/cache@v5 + with: + path: ~/.cargo/bin/soroban + key: ${{ runner.os }}-bin-soroban-cli-v1 + - name: Install Soroban CLI run: | - cargo install --locked soroban-cli --features opt + command -v soroban >/dev/null 2>&1 || cargo install --locked soroban-cli --features opt - name: Run unit tests run: cargo test --lib --workspace @@ -99,8 +105,14 @@ jobs: target key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + - name: Cache soroban-cli binary + uses: actions/cache@v5 + with: + path: ~/.cargo/bin/soroban + key: ${{ runner.os }}-bin-soroban-cli-v1 + - name: Install Soroban CLI - run: cargo install --locked soroban-cli --features opt + run: command -v soroban >/dev/null 2>&1 || cargo install --locked soroban-cli --features opt - name: Run integration tests run: cargo test --test '*' --workspace @@ -148,8 +160,14 @@ jobs: target key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + - name: Cache soroban-cli binary + uses: actions/cache@v5 + with: + path: ~/.cargo/bin/soroban + key: ${{ runner.os }}-bin-soroban-cli-v1 + - name: Install Soroban CLI - run: cargo install --locked soroban-cli --features opt + run: command -v soroban >/dev/null 2>&1 || cargo install --locked soroban-cli --features opt - name: Run gas benchmarks run: cargo test --benches --features testutils 2>&1 | tee benchmark-output.txt @@ -219,8 +237,14 @@ jobs: target key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + - name: Cache soroban-cli binary + uses: actions/cache@v5 + with: + path: ~/.cargo/bin/soroban + key: ${{ runner.os }}-bin-soroban-cli-v1 + - name: Install Soroban CLI - run: cargo install --locked soroban-cli --features opt + run: command -v soroban >/dev/null 2>&1 || cargo install --locked soroban-cli --features opt - name: Run gas benchmarks run: cargo test --benches --features testutils @@ -268,11 +292,23 @@ jobs: with: components: llvm-tools-preview + - name: Cache cargo-llvm-cov binary + uses: actions/cache@v5 + with: + path: ~/.cargo/bin/cargo-llvm-cov + key: ${{ runner.os }}-bin-cargo-llvm-cov-v1 + - name: Install cargo-llvm-cov - run: cargo install cargo-llvm-cov --locked + run: command -v cargo-llvm-cov >/dev/null 2>&1 || cargo install cargo-llvm-cov --locked + + - name: Cache soroban-cli binary + uses: actions/cache@v5 + with: + path: ~/.cargo/bin/soroban + key: ${{ runner.os }}-bin-soroban-cli-v1 - name: Install Soroban CLI - run: cargo install --locked soroban-cli --features opt + run: command -v soroban >/dev/null 2>&1 || cargo install --locked soroban-cli --features opt - name: Prepare coverage output directory run: mkdir -p coverage-reports @@ -334,8 +370,14 @@ jobs: - name: Install Rust uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 # stable + - name: Cache cargo-audit binary + uses: actions/cache@v5 + with: + path: ~/.cargo/bin/cargo-audit + key: ${{ runner.os }}-bin-cargo-audit-v1 + - name: Install cargo-audit - run: cargo install cargo-audit + run: command -v cargo-audit >/dev/null 2>&1 || cargo install cargo-audit - name: Run security audit — contracts/predict-iq run: cargo audit @@ -428,9 +470,15 @@ jobs: with: sarif_file: trivy-results.sarif + - name: Cache cargo-audit binary + uses: actions/cache@v5 + with: + path: ~/.cargo/bin/cargo-audit + key: ${{ runner.os }}-bin-cargo-audit-v1 + - name: Scan Rust dependencies run: | - cargo install cargo-audit + command -v cargo-audit >/dev/null 2>&1 || cargo install cargo-audit cargo audit --deny warnings working-directory: contracts/predict-iq @@ -599,8 +647,14 @@ jobs: with: target: wasm32-unknown-unknown + - name: Cache soroban-cli binary + uses: actions/cache@v5 + with: + path: ~/.cargo/bin/soroban + key: ${{ runner.os }}-bin-soroban-cli-v1 + - name: Install Soroban CLI - run: cargo install --locked soroban-cli --features opt + run: command -v soroban >/dev/null 2>&1 || cargo install --locked soroban-cli --features opt - name: Build optimized WASM run: cargo build --target wasm32-unknown-unknown --release @@ -1148,8 +1202,14 @@ jobs: target key: ${{ runner.os }}-cargo-api-order-${{ hashFiles('services/api/Cargo.lock') }} + - name: Cache cargo-nextest binary + uses: actions/cache@v5 + with: + path: ~/.cargo/bin/cargo-nextest + key: ${{ runner.os }}-bin-cargo-nextest-v1 + - name: Install cargo-nextest - run: cargo install cargo-nextest --locked + run: command -v cargo-nextest >/dev/null 2>&1 || cargo install cargo-nextest --locked - name: Run tests with seed 1 (unit + doc tests) working-directory: services/api