Skip to content

fix(api): keep open usage periods in step with their boxes #3400

fix(api): keep open usage periods in step with their boxes

fix(api): keep open usage periods in step with their boxes #3400

Workflow file for this run

# Run unit tests across all SDKs.
#
# This workflow runs unit tests only (no VM/integration tests).
# GitHub runners do not support nested virtualization, so we cannot run
# tests that require actual VMs.
#
# Test coverage:
# - Rust: cargo-nextest (unit tests only, with llvm-cov coverage)
# - Python: pytest with -m "not integration"
# - Node.js: vitest (all tests are unit tests)
name: Test
on:
merge_group:
push:
branches: [main]
paths:
- 'src/boxlite/**'
- 'src/shared/**'
- 'src/cli/**'
- 'src/guest/**'
- 'sdks/**'
- 'apps/**'
- '!apps/e2e/**' # Stack e2e runs in e2e-stack.yml / e2e-cloud.yml, not this unit matrix.
- '**/Cargo.toml'
- 'Cargo.lock'
- '.github/workflows/test.yml'
- '.github/workflows/config.yml'
# No path filter on pull_request: the `Test (conclusion)` job is a required
# status check, so this workflow must run on EVERY PR and report it. A
# path-skipped workflow leaves the required check stuck "Pending" → PR blocked.
# Per-suite filtering still happens in the `changes` job (jobs skip when
# unaffected); the conclusion job reports success-or-skipped.
pull_request:
branches: [main]
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: '0'
jobs:
# Load shared configuration
config:
uses: ./.github/workflows/config.yml
# Detect which files changed to conditionally run tests
changes:
runs-on: ubuntu-latest
permissions:
pull-requests: read
outputs:
rust: ${{ steps.filter.outputs.rust }}
cli: ${{ steps.filter.outputs.boxlite_cli }}
python: ${{ steps.filter.outputs.python }}
node: ${{ steps.filter.outputs.node }}
go: ${{ steps.filter.outputs.go }}
apps: ${{ steps.filter.outputs.apps }}
steps:
- uses: actions/checkout@v5
- uses: dorny/paths-filter@v4
id: filter
with:
# In a merge queue, diff against the merge-group base/head (no PR base
# exists). Empty on pull_request/push, where paths-filter ignores them.
base: ${{ github.event_name == 'merge_group' && github.event.merge_group.base_sha || '' }}
ref: ${{ github.event_name == 'merge_group' && github.event.merge_group.head_sha || '' }}
filters: |
rust:
- 'src/boxlite/**'
- 'src/shared/**'
- 'src/guest/**'
- '**/Cargo.toml'
- 'Cargo.lock'
cli:
- 'src/cli/**'
python:
- 'sdks/python/**'
node:
- 'sdks/node/**'
go:
- 'sdks/go/**'
apps:
# paths-filter ORs its rules, so a '!apps/e2e/**' rule would make
# this filter true for everything outside apps/e2e. Carve the stack
# e2e suite out inside a single pattern instead; 'apps/*' keeps
# top-level files (package.json, nx.json) in scope. Note !(e2e)
# compiles to a segment-prefix negative lookahead, so a future
# apps/e2e-<something> sibling would drop out of this filter too.
- 'apps/*'
- 'apps/!(e2e)/**'
# Rust unit tests (boxlite-shared only - boxlite requires native libs not available in CI)
rust:
name: Rust Tests (${{ matrix.platform.target }})
needs: [config, changes]
if: ${{ needs.changes.outputs.rust == 'true' }}
runs-on: ${{ matrix.platform.os }}
strategy:
fail-fast: false
matrix:
platform: ${{ fromJson(needs.config.outputs.platforms) }}
permissions:
id-token: write
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Install Rust
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: ${{ needs.config.outputs.rust-toolchain }}
- name: Install system dependencies (Linux)
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y libseccomp-dev protobuf-compiler
- name: Install system dependencies (macOS)
if: runner.os == 'macOS'
run: brew install protobuf
- name: Install cargo-nextest
uses: taiki-e/install-action@nextest
- name: Install cargo-llvm-cov
uses: taiki-e/install-action@cargo-llvm-cov
- name: Run Rust unit tests with coverage
# Only test boxlite-shared - boxlite requires libkrun/libgvproxy not available in CI
run: cargo llvm-cov nextest -p boxlite-shared --lib --profile ci --lcov --output-path lcov.info
- name: Run guest unit tests
if: runner.os == 'Linux'
run: make test:unit:guest
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
with:
file: lcov.info
flags: rust-shared
use_oidc: true
fail_ci_if_error: false
# cli unit tests (integration tests in tests/ are ignored - require VM)
cli:
name: CLI Unit Tests (${{ matrix.platform.target }})
needs: [config, changes]
if: ${{ needs.changes.outputs.cli == 'true' }}
runs-on: ${{ matrix.platform.os }}
strategy:
fail-fast: false
matrix:
platform: ${{ fromJson(needs.config.outputs.platforms) }}
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Install Rust
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: ${{ needs.config.outputs.rust-toolchain }}
- name: Install system dependencies (Linux)
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y protobuf-compiler
- name: Install system dependencies (macOS)
if: runner.os == 'macOS'
run: brew install protobuf
- name: Install cargo-nextest
uses: taiki-e/install-action@nextest
- name: Setup sccache
uses: mozilla-actions/sccache-action@v0.0.9
continue-on-error: true
- name: Export GHA cache env vars
uses: actions/github-script@v7
with:
script: |
core.exportVariable('ACTIONS_RESULTS_URL', process.env.ACTIONS_RESULTS_URL || '');
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
core.exportVariable('ACTIONS_CACHE_SERVICE_V2', process.env.ACTIONS_CACHE_SERVICE_V2 || '');
- name: Run boxlite-cli unit tests
# Only run tests in src/ (name contains "::tests::"); skip integration tests in tests/ (require VM)
env:
BOXLITE_DEPS_STUB: "1"
run: cargo nextest run -p boxlite-cli --profile ci -E 'test(::tests::)'
# Python SDK unit tests
python:
name: Python Tests (${{ matrix.platform.target }} / Python ${{ matrix.python-version }})
needs: [config, changes]
if: ${{ needs.changes.outputs.python == 'true' }}
runs-on: ${{ matrix.platform.os }}
strategy:
fail-fast: false
matrix:
platform: ${{ fromJson(needs.config.outputs.platforms) }}
python-version: ${{ fromJson(needs.config.outputs.python-versions) }}
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install test dependencies
run: |
python -m pip install --upgrade pip
pip install pytest pytest-asyncio
- name: Run Python unit tests
working-directory: sdks/python
run: python -m pytest tests/ -v -m "not integration"
# Node.js SDK unit tests
node:
name: Node.js Tests (${{ matrix.platform.target }} / Node ${{ matrix.node-version }})
needs: [config, changes]
if: ${{ needs.changes.outputs.node == 'true' }}
runs-on: ${{ matrix.platform.os }}
strategy:
fail-fast: false
matrix:
platform: ${{ fromJson(needs.config.outputs.platforms) }}
node-version: ${{ fromJson(needs.config.outputs.node-versions) }}
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
working-directory: sdks/node
run: npm install
- name: Run Node.js unit tests
working-directory: sdks/node
run: npm run test
# Go SDK unit tests
go:
name: Go Tests (${{ matrix.platform.target }})
needs: [config, changes]
if: ${{ needs.changes.outputs.go == 'true' }}
runs-on: ${{ matrix.platform.os }}
strategy:
fail-fast: false
matrix:
platform: ${{ fromJson(needs.config.outputs.platforms) }}
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.24'
- name: Install Rust
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: ${{ needs.config.outputs.rust-toolchain }}
- name: Install build dependencies
run: make setup:build
- name: Setup sccache
uses: mozilla-actions/sccache-action@v0.0.9
continue-on-error: true
- name: Export GHA cache env vars
uses: actions/github-script@v7
with:
script: |
core.exportVariable('ACTIONS_RESULTS_URL', process.env.ACTIONS_RESULTS_URL || '');
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
core.exportVariable('ACTIONS_CACHE_SERVICE_V2', process.env.ACTIONS_CACHE_SERVICE_V2 || '');
- name: Run Go unit tests
run: make test:unit:go
# apps/api (NestJS) unit tests — the nx `api` project's jest suite (org-quota,
# box-usage, box.service, organization, etc.). Pure/mocked specs, no native libs.
apps:
name: Apps API Tests
needs: [config, changes]
if: ${{ needs.changes.outputs.apps == 'true' }}
runs-on: ubuntu-latest
# Read-only token: this job runs repository-controlled code (yarn install
# build scripts, Nx/Jest), so it must not carry write scopes.
permissions:
contents: read
# Redis lets the quota engine's real-Lua integration spec run and Postgres
# lets the usage ledger's spec run (each self-skips without its env vars);
# the rest of the api suite is pure/mocked.
services:
redis:
image: redis:7
ports:
- 6379:6379
# Wait for Redis to accept connections before the job runs, so the
# real-Lua integration spec doesn't race container startup.
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: boxlite
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U postgres"
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
REDIS_HOST: localhost
DB_HOST: localhost
DB_PORT: 5432
DB_USERNAME: postgres
DB_PASSWORD: postgres
DB_DATABASE: boxlite
defaults:
run:
working-directory: apps
steps:
- name: Checkout code
uses: actions/checkout@v5
with:
submodules: recursive
# Don't persist GITHUB_TOKEN in .git/config before untrusted code runs.
persist-credentials: false
# corepack must be enabled before setup-node's yarn cache probe runs
- name: Enable corepack (yarn 4)
run: corepack enable
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: yarn
cache-dependency-path: apps/yarn.lock
- name: Install dependencies
run: yarn install --immutable
- name: Run apps/api unit tests
run: yarn nx run api:test
# Single required status check for branch protection / merge queue.
# Passes only if every job above succeeded or was skipped (path-filtered).
# `if: !cancelled()` is required: without it a failed dependency would skip
# this job, and GitHub treats a skipped required check as success.
# NOTE: keep `needs` in sync with the jobs above when adding/removing jobs.
test-conclusion:
name: Test (conclusion)
needs: [config, changes, rust, cli, python, node, go, apps]
if: ${{ !cancelled() }}
runs-on: ubuntu-latest
permissions: {}
steps:
- name: All test jobs passed or were skipped
run: |
jq -C <<< '${{ toJSON(needs) }}'
jq --exit-status 'all(.result == "success" or .result == "skipped")' <<< '${{ toJSON(needs) }}'