Skip to content

Commit d7bc089

Browse files
rsbhclaude
andauthored
ci: add Docker build and run tests for Node and Bun (#100)
* ci: add Docker build and run tests for Node and Bun Build and health-check chronicle with basic example on both runtimes. Verifies server starts, pages render, and search API responds. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: move Dockerfiles to docker/ directory Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: use node:24-slim and fix curl flags for redirect check Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: exclude test files from Nitro server bundle Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: split verify steps for better CI debug output Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update CI checks to test index, page API, and search API Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * ci: add image optimization API test Add test PNG to basic example and verify /api/image endpoint in CI. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * ci: replace curl checks with node smoke test script 8 assertions: index, page API, search API, search query, image resize, image 400, image invalid width, image 404. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: rename docker/ to docker-tests/ Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * ci: run Docker smoke tests only on pull requests Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * ci: move Docker smoke tests to separate pr.yaml workflow Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * ci: rename pr.yaml to docker-smoke-test.yaml Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: use npm/bun init + local install instead of global Pack CLI as tarball, init project in runner, install locally. Tested on both Node and Bun — all 8 smoke tests pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: rename workflow name to docker-smoke-test Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: use /api/ready instead of /api/health for server readiness Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a38e999 commit d7bc089

6 files changed

Lines changed: 168 additions & 0 deletions

File tree

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
name: docker-smoke-test
2+
3+
on:
4+
pull_request:
5+
6+
jobs:
7+
docker:
8+
name: Docker (${{ matrix.runtime }})
9+
runs-on: ubuntu-latest
10+
timeout-minutes: 15
11+
strategy:
12+
matrix:
13+
include:
14+
- runtime: bun
15+
dockerfile: docker-tests/Dockerfile.bun
16+
- runtime: node
17+
dockerfile: docker-tests/Dockerfile.node
18+
steps:
19+
- name: Checkout
20+
uses: actions/checkout@v4
21+
22+
- name: Build image
23+
run: docker build -f ${{ matrix.dockerfile }} -t chronicle-${{ matrix.runtime }} .
24+
25+
- name: Run container
26+
run: docker run -d --name chronicle-${{ matrix.runtime }} -p 3000:3000 chronicle-${{ matrix.runtime }}
27+
28+
- name: Wait for server
29+
run: |
30+
for i in $(seq 1 30); do
31+
if curl -sf http://localhost:3000/api/ready > /dev/null 2>&1; then
32+
echo "Server ready"
33+
exit 0
34+
fi
35+
sleep 2
36+
done
37+
echo "Server failed to start"
38+
docker logs chronicle-${{ matrix.runtime }}
39+
exit 1
40+
41+
- name: Setup Node
42+
uses: actions/setup-node@v4
43+
with:
44+
node-version: 24
45+
46+
- name: Smoke tests
47+
run: node docker-tests/smoke-test.mjs
48+
49+
- name: Cleanup
50+
if: always()
51+
run: docker rm -f chronicle-${{ matrix.runtime }} || true

docker-tests/Dockerfile.bun

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
FROM oven/bun:1.3 AS builder
2+
WORKDIR /app
3+
COPY package.json bun.lock ./
4+
COPY packages/chronicle/package.json ./packages/chronicle/
5+
RUN bun install --frozen-lockfile
6+
COPY packages/chronicle ./packages/chronicle
7+
RUN cd packages/chronicle && bun build-cli.ts
8+
RUN cd packages/chronicle && bun pm pack --destination /app
9+
10+
FROM oven/bun:1.3-slim AS runner
11+
WORKDIR /app
12+
RUN bun init -y
13+
COPY --from=builder /app/raystack-chronicle-*.tgz ./chronicle.tgz
14+
RUN bun add ./chronicle.tgz && rm chronicle.tgz
15+
COPY examples/basic ./examples/basic
16+
RUN bunx chronicle build --config examples/basic/chronicle.yaml --preset bun
17+
18+
EXPOSE 3000
19+
20+
CMD ["bunx", "chronicle", "start", "--config", "examples/basic/chronicle.yaml", "--port", "3000", "--host", "0.0.0.0"]

docker-tests/Dockerfile.node

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
FROM oven/bun:1.3 AS builder
2+
WORKDIR /app
3+
COPY package.json bun.lock ./
4+
COPY packages/chronicle/package.json ./packages/chronicle/
5+
RUN bun install --frozen-lockfile
6+
COPY packages/chronicle ./packages/chronicle
7+
RUN cd packages/chronicle && bun build-cli.ts
8+
RUN cd packages/chronicle && bun pm pack --destination /app
9+
10+
FROM node:24-slim AS runner
11+
WORKDIR /app
12+
RUN npm init -y
13+
COPY --from=builder /app/raystack-chronicle-*.tgz ./chronicle.tgz
14+
RUN npm install ./chronicle.tgz && rm chronicle.tgz
15+
COPY examples/basic ./examples/basic
16+
RUN npx chronicle build --config examples/basic/chronicle.yaml
17+
18+
EXPOSE 3000
19+
20+
CMD ["npx", "chronicle", "start", "--config", "examples/basic/chronicle.yaml", "--port", "3000", "--host", "0.0.0.0"]

docker-tests/smoke-test.mjs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import assert from 'node:assert/strict';
2+
3+
const BASE = process.env.BASE_URL || 'http://localhost:3000';
4+
let failed = 0;
5+
6+
console.log(`Smoke tests against ${BASE}\n`);
7+
8+
// index returns 200 or 307
9+
{
10+
const res = await fetch(`${BASE}/`);
11+
assert.ok(res.status === 200 || res.status === 307, `index: expected 200 or 307, got ${res.status}`);
12+
console.log(' ✓ index returns 200 or 307');
13+
}
14+
15+
// page API returns frontmatter
16+
{
17+
const res = await fetch(`${BASE}/api/page?slug=docs,getting-started`);
18+
assert.equal(res.status, 200, `page API: expected 200, got ${res.status}`);
19+
const data = await res.json();
20+
assert.ok(data.frontmatter, 'page API: missing frontmatter');
21+
assert.ok(data.frontmatter.title, 'page API: missing title');
22+
console.log(' ✓ page API returns frontmatter');
23+
}
24+
25+
// search API returns results
26+
{
27+
const res = await fetch(`${BASE}/api/search`);
28+
assert.equal(res.status, 200, `search API: expected 200, got ${res.status}`);
29+
const data = await res.json();
30+
assert.ok(Array.isArray(data), 'search API: expected array');
31+
assert.ok(data.length > 0, 'search API: expected results');
32+
assert.ok(data[0].url, 'search API: missing url');
33+
console.log(' ✓ search API returns results');
34+
}
35+
36+
// search with query returns matches
37+
{
38+
const res = await fetch(`${BASE}/api/search?query=getting`);
39+
assert.equal(res.status, 200, `search query: expected 200, got ${res.status}`);
40+
const data = await res.json();
41+
assert.ok(data.length > 0, 'search query: expected results');
42+
assert.ok(data[0].match, 'search query: missing match field');
43+
console.log(' ✓ search with query returns matches');
44+
}
45+
46+
// image API resizes PNG
47+
{
48+
const res = await fetch(`${BASE}/api/image?url=${encodeURIComponent('/_content/docs/test-image.png')}&w=320`);
49+
assert.equal(res.status, 200, `image API: expected 200, got ${res.status}`);
50+
const ct = res.headers.get('content-type');
51+
assert.ok(ct.startsWith('image/'), `image API: expected image content-type, got ${ct}`);
52+
console.log(' ✓ image API resizes PNG');
53+
}
54+
55+
// image API returns 400 for missing params
56+
{
57+
const res = await fetch(`${BASE}/api/image`);
58+
assert.equal(res.status, 400, `image 400: expected 400, got ${res.status}`);
59+
console.log(' ✓ image API returns 400 for missing params');
60+
}
61+
62+
// image API returns 400 for invalid width
63+
{
64+
const res = await fetch(`${BASE}/api/image?url=${encodeURIComponent('/_content/docs/test-image.png')}&w=999`);
65+
assert.equal(res.status, 400, `image invalid width: expected 400, got ${res.status}`);
66+
console.log(' ✓ image API returns 400 for invalid width');
67+
}
68+
69+
// image API returns 404 for missing image
70+
{
71+
const res = await fetch(`${BASE}/api/image?url=${encodeURIComponent('/_content/does-not-exist.png')}&w=640`);
72+
assert.equal(res.status, 404, `image 404: expected 404, got ${res.status}`);
73+
console.log(' ✓ image API returns 404 for missing image');
74+
}
75+
76+
console.log('\nALL PASSED');
69 Bytes
Loading

packages/chronicle/src/server/vite-config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ export async function createViteConfig(
7373
nitro({
7474
serverDir: path.resolve(packageRoot, 'src/server'),
7575
...(preset && { preset }),
76+
ignore: ['**/*.test.ts', '**/*.test.tsx', '**/*.spec.ts', '**/*.spec.tsx'],
7677
}),
7778
mdx({
7879
default: defineFumadocsConfig({

0 commit comments

Comments
 (0)