Skip to content
Open
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
54 changes: 54 additions & 0 deletions .github/workflows/msrv.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: MSRV Check

on:
push:
branches: [main, develop]
paths:
- "services/api/**"
- ".github/workflows/msrv.yml"
pull_request:
branches: [main, develop]
paths:
- "services/api/**"
- ".github/workflows/msrv.yml"

jobs:
msrv:
name: Build with Minimum Supported Rust Version
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Read MSRV from Cargo.toml
id: msrv
run: |
MSRV=$(grep '^rust-version' services/api/Cargo.toml | sed 's/.*= *"\(.*\)"/\1/')
echo "version=$MSRV" >> "$GITHUB_OUTPUT"
echo "MSRV detected: $MSRV"

- name: Install MSRV toolchain (${{ steps.msrv.outputs.version }})
uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ steps.msrv.outputs.version }}

- name: Cache cargo registry
uses: actions/cache@v5
with:
path: |
~/.cargo/registry
~/.cargo/git
services/api/target
key: ${{ runner.os }}-cargo-msrv-${{ hashFiles('services/api/Cargo.lock') }}

- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y pkg-config libssl-dev

- name: cargo check (MSRV)
run: cargo check --all-targets
working-directory: services/api

- name: cargo build (MSRV)
run: cargo build --release
working-directory: services/api
29 changes: 29 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,35 @@ make test

---

## Minimum Supported Rust Version (MSRV)

The `services/api` crate declares a `rust-version` field in its `Cargo.toml`.
This is the **oldest** Rust toolchain version the crate is guaranteed to compile on.

### Current MSRV

| Crate | MSRV |
|-------|------|
| `predictiq-api` (`services/api`) | **1.75.0** |

### Policy

- The MSRV is set to the version required by the most-restrictive direct dependency
(currently `axum 0.7`, `sqlx 0.8`, and `tower-http 0.6`, all of which require ≥ 1.75).
- Bumping the MSRV is a **semver-minor** change and must be documented in `CHANGELOG.md`
via a `chore(api): bump MSRV to X.Y.Z` commit.
- A dedicated CI job (`.github/workflows/msrv.yml`) installs the declared MSRV toolchain
using `rustup` and runs `cargo check` + `cargo build --release` against it on every PR
that touches `services/api/`.
- To verify the MSRV locally:

```bash
rustup toolchain install 1.75
rustup run 1.75 cargo check --manifest-path services/api/Cargo.toml
```

---

## Code Style

### Rust
Expand Down
45 changes: 45 additions & 0 deletions docker-compose.tracing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,51 @@ services:
networks:
- predictiq-tracing

# ── TTS Service ──────────────────────────────────────────────────────────────
# Included so the TTS service can export traces to the OpenTelemetry Collector
# running in this compose stack.
tts:
build:
context: ./services/tts
dockerfile: Dockerfile
container_name: predictiq-tts
ports:
- "3000:3000"
environment:
- TTS_PROVIDER=${TTS_PROVIDER:-elevenlabs}
- ELEVENLABS_API_KEY=${ELEVENLABS_API_KEY:-}
- GOOGLE_APPLICATION_CREDENTIALS=${GOOGLE_APPLICATION_CREDENTIALS:-}
- TTS_OUTPUT_DIR=/tmp/tts-output
- OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
- OTEL_SERVICE_NAME=predictiq-tts
volumes:
- tts-output:/tmp/tts-output
healthcheck:
test:
- "CMD"
- "node"
- "-e"
- "require('http').get('http://localhost:3000/health/live',(r)=>{process.exit(r.statusCode===200?0:1)}).on('error',()=>process.exit(1))"
interval: 30s
timeout: 5s
start_period: 10s
retries: 3
depends_on:
- otel-collector
deploy:
resources:
limits:
cpus: '0.5'
memory: 512M
reservations:
cpus: '0.25'
memory: 256M
networks:
- predictiq-tracing

networks:
predictiq-tracing:
driver: bridge

volumes:
tts-output:
8 changes: 8 additions & 0 deletions services/api/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@ name = "predictiq-api"
version = "0.1.0"
edition = "2021"
publish = false
# Minimum Supported Rust Version (MSRV).
# Determined by the most-restrictive dependency in this crate:
# - axum 0.7 requires Rust ≥ 1.75
# - sqlx 0.8 requires Rust ≥ 1.75
# - tower-http 0.6 requires Rust ≥ 1.75
# The root Dockerfile already pins rust:1.75-slim for production builds.
# See CONTRIBUTING.md for the MSRV policy.
rust-version = "1.75"

[[bin]]
name = "predictiq-api"
Expand Down
59 changes: 41 additions & 18 deletions services/tts/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,28 +1,51 @@
# TTS Service Dockerfile
# Builds and runs the Text-to-Speech service with health checks
# Multi-stage build for PredictIQ TTS Service
# Pinned to Node.js 20 LTS (matching the "engines" field in package.json)

FROM node:20-alpine
# ── Stage 1: Builder ─────────────────────────────────────────────────────────
FROM node:20-alpine AS builder

WORKDIR /app
WORKDIR /build

# Install dependencies
COPY package*.json ./
RUN npm ci --only=production
# Copy dependency manifests first for layer caching
COPY package.json package-lock.json ./

# Copy source code
COPY src ./src
# Install all dependencies (including devDependencies needed for tsc)
RUN npm ci

# Copy source and compile TypeScript
COPY tsconfig.json ./
COPY src ./src
RUN npm run build

# ── Stage 2: Runtime ─────────────────────────────────────────────────────────
FROM node:20-alpine AS runtime

# Create a non-root user to prevent container-escape privilege escalation
RUN addgroup -S ttsgroup && adduser -S ttsuser -G ttsgroup

WORKDIR /app

# Copy dependency manifests and install production dependencies only
COPY package.json package-lock.json ./
RUN npm ci --omit=dev && npm cache clean --force

# Copy compiled output from the builder stage
COPY --from=builder /build/dist ./dist

# Set ownership to the non-root user before switching
RUN chown -R ttsuser:ttsgroup /app

# Build TypeScript
RUN npm run build 2>/dev/null || npx tsc
USER ttsuser

EXPOSE 3000

# Health check endpoint
# Checks Google Cloud TTS API connectivity and service health
# Returns 200 with {"status": "ok"} when healthy
# Returns 503 with error details when upstream is unavailable
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/health', (r) => {if (r.statusCode !== 200) throw new Error(r.statusCode)})" || exit 1
# HEALTHCHECK calls /health/live — a fast, process-level liveness probe that
# does not depend on external TTS providers and therefore never produces false
# negatives due to upstream unavailability.
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD node -e "\
require('http').get('http://localhost:3000/health/live', (r) => { \
process.exit(r.statusCode === 200 ? 0 : 1); \
}).on('error', () => process.exit(1));"

CMD ["npm", "start"]
CMD ["node", "dist/server.js"]
26 changes: 25 additions & 1 deletion services/tts/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion services/tts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
"private": true,
"main": "dist/TTSService.js",
"types": "dist/TTSService.d.ts",
"engines": {
"node": ">=20.0.0 <21.0.0"
},
"scripts": {
"build": "tsc",
"dev": "ts-node src/server.ts",
Expand All @@ -24,12 +27,14 @@
"@opentelemetry/resources": "^2.7.1",
"@opentelemetry/sdk-node": "^0.218.0",
"@opentelemetry/semantic-conventions": "^1.41.1",
"express": "^5.2.1"
"express": "^5.2.1",
"opossum": "^8.3.0"
},
"devDependencies": {
"@types/express": "^5.0.6",
"@types/jest": "30.0.0",
"@types/node": "^25.9.1",
"@types/opossum": "^8.1.8",
"jest": "^30.4.2",
"ts-jest": "^29.4.11",
"ts-node": "^10.9.0",
Expand Down
Loading