Skip to content

Commit d81d182

Browse files
authored
Ci pinned services stack (#121)
* update ci tests to use pinned local services stack Stop test and coverage workflows from tunneling to production by downloading RaidHub-Services at a pinned commit, booting its docker dependencies locally, and running migrations/seeding before API tests. Made-with: Cursor * move services pin file to avoid remote path conflict Rename the pinned RaidHub-Services version file to .github/raidhub-services-version.env and update the CI setup script to use it so branch pushes succeed against the current remote. Made-with: Cursor * use services make env in ci bootstrap script Replace manual .env key injection with RaidHub-Services' make env target so CI setup follows the same environment bootstrap path as services. Made-with: Cursor * use real postgres fixtures in status integration tests Create and clean up deterministic instance/pgcr rows inside status tests so they validate route behavior against real database state instead of mocking instance reads. Made-with: Cursor * lazy-init database clients to reduce import-time coupling Avoid creating Postgres and ClickHouse clients at module import time by switching to lazy singletons, so one-off scripts like docs generation can load route modules without unnecessary DB setup. Made-with: Cursor * format * bump pinned services commit for ci schema parity Update the pinned RaidHub-Services SHA used by CI so migrations and seeded schemas match API test query expectations. Made-with: Cursor * stabilize ci tests with local-db assumptions Remove dependency on non-portable activity_version columns and update brittle tests to use local database-backed fixtures or discovered rows so the CI stack seeded from RaidHub-Services can run reliably. Made-with: Cursor * format player fixture test for ci checks Apply Prettier formatting to the updated player service fixture test so static PR checks pass. Made-with: Cursor * make dataset-dependent tests resilient for local ci data Update pgcr, player search, and weapon metrics tests to validate schema and bounds without requiring production-sized datasets, so they run reliably against locally seeded CI infrastructure. Made-with: Cursor * format pgcr service test for static checks Run Prettier on pgcr service test to satisfy CI formatting checks. Made-with: Cursor * make integration tests tolerant of seeded local datasets Relax strict non-empty expectations in dataset-dependent tests and allow nullable leaderboard search results so CI can validate shapes/behavior against the local seeded stack instead of production-sized data. Made-with: Cursor * stabilize instance service tests for local seeded db Switch instance service tests from hardcoded production instance IDs to dynamic local rows and schema checks so they pass against ephemeral CI seed data. Made-with: Cursor * remove remaining prod-data assumptions in service tests Relax atlas and reporting standing assertions for seeded CI data and fix instance metadata test hash typing to avoid integer overflow against bigint hashes. Made-with: Cursor * stabilize player and clan tests for local ci data Use DB-discovered membership IDs in player route tests and relax fixed-size clan leaderboard assertions so these tests pass against seeded local datasets. Made-with: Cursor * use discovered player ids in history and teammates route tests Replace hardcoded membership IDs with DB-discovered public/private players and make middleware assertions robust to seeded CI datasets. Made-with: Cursor * make expectOk/expectErr only assert result type Route errors now include parsed context payloads, so shared test helpers should not require null parsed values when asserting ok/err outcomes. Made-with: Cursor * add deterministic fixtures for remaining brittle route tests Use explicit postgres fixtures for profile and admin standing routes and make pantheon search assertions data-aware so tests execute fully without relying on production-only records. Made-with: Cursor * format reporting standing fixture tests for ci Apply Prettier formatting to new reporting standing fixture tests so static checks pass. Made-with: Cursor * make leaderboard search route tests data-tolerant in ci Update leaderboard route tests to assert success for list queries while allowing search queries to return not-on-leaderboard errors when seeded CI data lacks matching memberships. Made-with: Cursor * fix profile and instance-standing test setup execution Run authorized profile assertion inside a test case and split multi-statement fixture setup queries so pg prepared statements work in CI. Made-with: Cursor * set instance-standing fixture skull hashes to empty array Populate skull_hashes with an empty bigint array in the fixture instance so route response schema validation matches expected array type. Made-with: Cursor * test: add DB fixtures and drop vacuous skips Replace early returns and weak assertions with deterministic inserts via a shared fixture pool, and align teammates/history/instances tests with real instance_player rows so routes exercise real data. Made-with: Cursor * test: assert teammates payload and error codes Made-with: Cursor * test: extend review themes to leaderboards and service history Add leaderboard pagination invariants, tighten standing/history assertions with real fixture data, and replace vacuous length checks across route and service leaderboard tests. Made-with: Cursor * fix(ci): repair leaderboard TS narrowing, PGCR gzip, instance_character - Narrow LeaderboardData union before individual vs team membership asserts - Use err-branch type guard for pantheon search fallback - Store gzip-compressed minimal PGCR JSON for raw.pgcr fixtures - Seed extended.instance_character so getInstanceExtended players[].characters is an array Made-with: Cursor * test: align history final-raid and feat list with CI seed Made-with: Cursor
1 parent 927af8a commit d81d182

50 files changed

Lines changed: 2179 additions & 635 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
RAIDHUB_SERVICES_REPO=Raid-Hub/RaidHub-Services
2+
RAIDHUB_SERVICES_COMMIT=858b76cf0314c1d5547a60011bf444eb63615e88
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
5+
VERSIONS_FILE="${ROOT_DIR}/.github/raidhub-services-version.env"
6+
SERVICES_DIR="${ROOT_DIR}/RaidHub-Services"
7+
8+
if [[ ! -f "${VERSIONS_FILE}" ]]; then
9+
echo "Missing versions file at ${VERSIONS_FILE}"
10+
exit 1
11+
fi
12+
13+
set -a
14+
source "${VERSIONS_FILE}"
15+
set +a
16+
17+
if [[ -z "${RAIDHUB_SERVICES_REPO:-}" || -z "${RAIDHUB_SERVICES_COMMIT:-}" ]]; then
18+
echo "RAIDHUB_SERVICES_REPO and RAIDHUB_SERVICES_COMMIT must be set in ${VERSIONS_FILE}"
19+
exit 1
20+
fi
21+
22+
if [[ ! "${RAIDHUB_SERVICES_COMMIT}" =~ ^[0-9a-f]{40}$ ]]; then
23+
echo "RAIDHUB_SERVICES_COMMIT must be a full 40-character commit hash"
24+
exit 1
25+
fi
26+
27+
echo "Checking out ${RAIDHUB_SERVICES_REPO}@${RAIDHUB_SERVICES_COMMIT}"
28+
rm -rf "${SERVICES_DIR}"
29+
git init "${SERVICES_DIR}"
30+
git -C "${SERVICES_DIR}" remote add origin "https://github.com/${RAIDHUB_SERVICES_REPO}.git"
31+
git -C "${SERVICES_DIR}" fetch --depth=1 origin "${RAIDHUB_SERVICES_COMMIT}"
32+
git -C "${SERVICES_DIR}" checkout --detach FETCH_HEAD
33+
34+
(
35+
cd "${SERVICES_DIR}"
36+
make env
37+
)
38+
39+
docker compose -f "${SERVICES_DIR}/docker-compose.yml" --env-file "${SERVICES_DIR}/.env" up -d postgres rabbitmq clickhouse prometheus
40+
41+
wait_for() {
42+
local name="$1"
43+
local cmd="$2"
44+
local attempts="${3:-60}"
45+
local sleep_seconds="${4:-2}"
46+
47+
for _ in $(seq 1 "${attempts}"); do
48+
if eval "${cmd}" >/dev/null 2>&1; then
49+
echo "${name} is ready"
50+
return 0
51+
fi
52+
sleep "${sleep_seconds}"
53+
done
54+
55+
echo "${name} did not become ready in time"
56+
return 1
57+
}
58+
59+
echo "Waiting for PostgreSQL..."
60+
wait_for "PostgreSQL" "docker compose -f \"${SERVICES_DIR}/docker-compose.yml\" --env-file \"${SERVICES_DIR}/.env\" exec -T postgres pg_isready -U dev -d raidhub"
61+
62+
echo "Waiting for ClickHouse..."
63+
wait_for "ClickHouse" "curl -fsS \"http://localhost:8123/ping\" | grep -q '^Ok\\.$'"
64+
65+
echo "Waiting for RabbitMQ API..."
66+
wait_for "RabbitMQ API" "curl -fsS -u \"dev:password\" \"http://localhost:15672/api/overview\""
67+
68+
(
69+
cd "${SERVICES_DIR}"
70+
make migrate
71+
make seed
72+
)
73+
74+
if [[ -n "${GITHUB_ENV:-}" ]]; then
75+
echo "RAIDHUB_SERVICES_DIR=${SERVICES_DIR}" >> "${GITHUB_ENV}"
76+
fi

.github/workflows/coverage.yml

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,39 +16,29 @@ jobs:
1616
steps:
1717
- uses: actions/checkout@v4
1818
- uses: oven-sh/setup-bun@v1
19+
- uses: actions/setup-go@v5
20+
with:
21+
go-version: "1.24"
1922

2023
- name: Bun Install
2124
run: |
2225
bun install --frozen-lockfile
2326
24-
- name: Install cloudflared
25-
run: ./.github/scripts/install-cloudflared.sh
26-
27-
- name: Load SSH Key
28-
uses: shimataro/ssh-key-action@v2
29-
with:
30-
key: ${{ secrets.VPS_SSH_PRIVATE_KEY }}
31-
known_hosts: ${{ secrets.VPS_SSH_KNOWN_HOSTS }}
32-
33-
- name: Tunnel to VPS
34-
env:
35-
TUNNEL_SERVICE_TOKEN_ID: ${{ secrets.CF_GHA_CLIENT_ID }}
36-
TUNNEL_SERVICE_TOKEN_SECRET: ${{ secrets.CF_GHA_CLIENT_SECRET }}
37-
run: |
38-
ssh -N -L 5432:localhost:5432 -L 8123:localhost:8123 -L 9090:localhost:9090 -L 15672:localhost:15672 -o ProxyCommand="cloudflared access ssh --hostname %h" root@ssh.raidhub.io &
27+
- name: Start pinned local services stack
28+
run: bash ./.github/scripts/setup-local-services-ci.sh
3929

4030
- name: Run Tests
4131
env:
4232
CLIENT_SECRET: "secret-token"
4333
ADMIN_CLIENT_SECRET: "another-secret-token"
4434
JWT_SECRET: "jwt-secret"
45-
POSTGRES_USER: readonly
46-
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_READONLY_PASSWORD }}
47-
CLICKHOUSE_USER: ${{ secrets.CLICKHOUSE_READONLY_USER }}
48-
CLICKHOUSE_PASSWORD: ${{ secrets.CLICKHOUSE_READONLY_PASSWORD }}
35+
POSTGRES_USER: dev
36+
POSTGRES_PASSWORD: password
37+
CLICKHOUSE_USER: default
38+
CLICKHOUSE_PASSWORD: ""
4939
BUNGIE_API_KEY: ${{ secrets.BUNGIE_API_KEY }}
50-
RABBIT_API_USER: ${{ secrets.RABBIT_API_USER }}
51-
RABBIT_API_PASSWORD: ${{ secrets.RABBIT_API_PASSWORD }}
40+
RABBIT_API_USER: dev
41+
RABBIT_API_PASSWORD: password
5242
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
5343
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
5444
R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
@@ -57,6 +47,16 @@ jobs:
5747
run: bun test --timeout=30000 --coverage
5848
continue-on-error: true
5949

50+
- name: Dump services logs on failure
51+
if: failure()
52+
run: |
53+
docker compose -f RaidHub-Services/docker-compose.yml --env-file RaidHub-Services/.env ps
54+
docker compose -f RaidHub-Services/docker-compose.yml --env-file RaidHub-Services/.env logs --tail=200 postgres rabbitmq clickhouse
55+
56+
- name: Tear down local services stack
57+
if: always()
58+
run: docker compose -f RaidHub-Services/docker-compose.yml --env-file RaidHub-Services/.env down -v
59+
6060
- name: Upload coverage reports
6161
uses: actions/upload-artifact@v4
6262
with:

.github/workflows/test.yml

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ jobs:
1717
- name: Checkout
1818
uses: actions/checkout@v4
1919
- uses: oven-sh/setup-bun@v1
20+
- uses: actions/setup-go@v5
21+
with:
22+
go-version: "1.24"
2023

2124
- name: Download code coverage report
2225
uses: dawidd6/action-download-artifact@v3
@@ -26,45 +29,42 @@ jobs:
2629
workflow_conclusion: success
2730
name: test-coverage.info
2831

29-
- name: Install cloudflared
30-
run: ./.github/scripts/install-cloudflared.sh
31-
32-
- name: Load SSH Key
33-
uses: shimataro/ssh-key-action@v2
34-
with:
35-
key: ${{ secrets.VPS_SSH_PRIVATE_KEY }}
36-
known_hosts: ${{ secrets.VPS_SSH_KNOWN_HOSTS }}
37-
3832
- name: Install dependencies
3933
run: |
4034
bun install --frozen-lockfile
4135
42-
- name: Tunnel to VPS
43-
env:
44-
TUNNEL_SERVICE_TOKEN_ID: ${{ secrets.CF_GHA_CLIENT_ID }}
45-
TUNNEL_SERVICE_TOKEN_SECRET: ${{ secrets.CF_GHA_CLIENT_SECRET }}
46-
run: |
47-
ssh -N -L 5432:localhost:5432 -L 8123:localhost:8123 -L 9090:localhost:9090 -L 15672:localhost:15672 -o ProxyCommand="cloudflared access ssh --hostname %h" root@ssh.raidhub.io &
36+
- name: Start pinned local services stack
37+
run: bash ./.github/scripts/setup-local-services-ci.sh
4838

4939
- name: Run Tests
5040
env:
5141
CLIENT_SECRET: "secret-token"
5242
ADMIN_CLIENT_SECRET: "another-secret-token"
5343
JWT_SECRET: "jwt-secret"
54-
POSTGRES_USER: readonly
55-
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_READONLY_PASSWORD }}
56-
CLICKHOUSE_USER: ${{ secrets.CLICKHOUSE_READONLY_USER }}
57-
CLICKHOUSE_PASSWORD: ${{ secrets.CLICKHOUSE_READONLY_PASSWORD }}
44+
POSTGRES_USER: dev
45+
POSTGRES_PASSWORD: password
46+
CLICKHOUSE_USER: default
47+
CLICKHOUSE_PASSWORD: ""
5848
BUNGIE_API_KEY: ${{ secrets.BUNGIE_API_KEY }}
59-
RABBIT_API_USER: ${{ secrets.RABBIT_API_USER }}
60-
RABBIT_API_PASSWORD: ${{ secrets.RABBIT_API_PASSWORD }}
49+
RABBIT_API_USER: dev
50+
RABBIT_API_PASSWORD: password
6151
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
6252
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
6353
R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
6454
R2_BUCKET: ${{ secrets.R2_BUCKET }}
6555

6656
run: bun test --coverage --bail=5 --timeout=20000
6757

58+
- name: Dump services logs on failure
59+
if: failure()
60+
run: |
61+
docker compose -f RaidHub-Services/docker-compose.yml --env-file RaidHub-Services/.env ps
62+
docker compose -f RaidHub-Services/docker-compose.yml --env-file RaidHub-Services/.env logs --tail=200 postgres rabbitmq clickhouse
63+
64+
- name: Tear down local services stack
65+
if: always()
66+
run: docker compose -f RaidHub-Services/docker-compose.yml --env-file RaidHub-Services/.env down -v
67+
6868
# Compares two code coverage files and generates report as a comment
6969
- name: Generate Code Coverage report
7070
id: code-coverage

src/integrations/clickhouse/client.ts

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { LogFields, Logger } from "@/lib/utils/logging"
2+
import type { ClickHouseClient } from "@clickhouse/client"
23
import {
34
ClickHouseLogLevel,
45
ErrorLogParams,
@@ -58,19 +59,27 @@ class ClickhouseLogger implements ClickhouseLoggingInterface {
5859
}
5960
}
6061

61-
export const clickhouse = createClient({
62-
username: process.env.CLICKHOUSE_USER,
63-
password: process.env.CLICKHOUSE_PASSWORD,
64-
application: process.env.PROD ? "RaidHub-API-Prod" : "RaidHub-API-Dev",
65-
database: process.env.CLICKHOUSE_DATABASE ?? "default",
66-
request_timeout: 5000,
67-
log: {
68-
LoggerClass: ClickhouseLogger,
69-
level:
70-
process.env.NODE_ENV === "test"
71-
? ClickHouseLogLevel.OFF
72-
: process.env.PROD
73-
? ClickHouseLogLevel.WARN
74-
: ClickHouseLogLevel.DEBUG
62+
let clickhouseClient: ClickHouseClient | null = null
63+
64+
export const getClickhouseClient = () => {
65+
if (!clickhouseClient) {
66+
clickhouseClient = createClient({
67+
username: process.env.CLICKHOUSE_USER,
68+
password: process.env.CLICKHOUSE_PASSWORD,
69+
application: process.env.PROD ? "RaidHub-API-Prod" : "RaidHub-API-Dev",
70+
database: process.env.CLICKHOUSE_DATABASE ?? "default",
71+
request_timeout: 5000,
72+
log: {
73+
LoggerClass: ClickhouseLogger,
74+
level:
75+
process.env.NODE_ENV === "test"
76+
? ClickHouseLogLevel.OFF
77+
: process.env.PROD
78+
? ClickHouseLogLevel.WARN
79+
: ClickHouseLogLevel.DEBUG
80+
}
81+
})
7582
}
76-
})
83+
84+
return clickhouseClient
85+
}

src/integrations/postgres/index.ts

Lines changed: 47 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,50 @@ import { createTransactional } from "./transactional"
44

55
configurePostgresParsers()
66

7-
export const pgReader = createReader({
8-
user: process.env.POSTGRES_USER,
9-
password: process.env.POSTGRES_PASSWORD,
10-
database: "raidhub",
11-
min: process.env.PROD ? 5 : 1,
12-
max: process.env.PROD ? 150 : 10,
13-
idleTimeoutMillis: 30000,
14-
connectionTimeoutMillis: 10000
15-
})
16-
17-
export const pgAdmin = createTransactional({
18-
user: process.env.POSTGRES_WRITABLE_USER,
19-
password: process.env.POSTGRES_WRITABLE_PASSWORD,
20-
database: "raidhub",
21-
min: process.env.PROD ? 2 : 1,
22-
max: process.env.PROD ? 15 : 3,
23-
idleTimeoutMillis: 30000,
24-
connectionTimeoutMillis: 10000
25-
})
7+
let pgReaderClient: ReturnType<typeof createReader> | null = null
8+
let pgAdminClient: ReturnType<typeof createTransactional> | null = null
9+
10+
const getPgReader = () => {
11+
if (!pgReaderClient) {
12+
pgReaderClient = createReader({
13+
user: process.env.POSTGRES_USER,
14+
password: process.env.POSTGRES_PASSWORD,
15+
database: "raidhub",
16+
min: process.env.PROD ? 5 : 1,
17+
max: process.env.PROD ? 150 : 10,
18+
idleTimeoutMillis: 30000,
19+
connectionTimeoutMillis: 10000
20+
})
21+
}
22+
23+
return pgReaderClient
24+
}
25+
26+
const getPgAdmin = () => {
27+
if (!pgAdminClient) {
28+
pgAdminClient = createTransactional({
29+
user: process.env.POSTGRES_WRITABLE_USER,
30+
password: process.env.POSTGRES_WRITABLE_PASSWORD,
31+
database: "raidhub",
32+
min: process.env.PROD ? 2 : 1,
33+
max: process.env.PROD ? 15 : 3,
34+
idleTimeoutMillis: 30000,
35+
connectionTimeoutMillis: 10000
36+
})
37+
}
38+
39+
return pgAdminClient
40+
}
41+
42+
export const pgReader: ReturnType<typeof createReader> = {
43+
queryRow: (sql, options) => getPgReader().queryRow(sql, options),
44+
queryRows: (sql, options) => getPgReader().queryRows(sql, options),
45+
prepare: sql => getPgReader().prepare(sql)
46+
}
47+
48+
export const pgAdmin: ReturnType<typeof createTransactional> = {
49+
queryRow: (sql, options) => getPgAdmin().queryRow(sql, options),
50+
queryRows: (sql, options) => getPgAdmin().queryRows(sql, options),
51+
prepare: sql => getPgAdmin().prepare(sql),
52+
transaction: callback => getPgAdmin().transaction(callback)
53+
}

0 commit comments

Comments
 (0)