diff --git a/.github/workflows/docker-smoke-test.yaml b/.github/workflows/docker-smoke-test.yaml new file mode 100644 index 00000000..4b2c9dbd --- /dev/null +++ b/.github/workflows/docker-smoke-test.yaml @@ -0,0 +1,51 @@ +name: docker-smoke-test + +on: + pull_request: + +jobs: + docker: + name: Docker (${{ matrix.runtime }}) + runs-on: ubuntu-latest + timeout-minutes: 15 + strategy: + matrix: + include: + - runtime: bun + dockerfile: docker-tests/Dockerfile.bun + - runtime: node + dockerfile: docker-tests/Dockerfile.node + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Build image + run: docker build -f ${{ matrix.dockerfile }} -t chronicle-${{ matrix.runtime }} . + + - name: Run container + run: docker run -d --name chronicle-${{ matrix.runtime }} -p 3000:3000 chronicle-${{ matrix.runtime }} + + - name: Wait for server + run: | + for i in $(seq 1 30); do + if curl -sf http://localhost:3000/api/ready > /dev/null 2>&1; then + echo "Server ready" + exit 0 + fi + sleep 2 + done + echo "Server failed to start" + docker logs chronicle-${{ matrix.runtime }} + exit 1 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 24 + + - name: Smoke tests + run: node docker-tests/smoke-test.mjs + + - name: Cleanup + if: always() + run: docker rm -f chronicle-${{ matrix.runtime }} || true diff --git a/docker-tests/Dockerfile.bun b/docker-tests/Dockerfile.bun new file mode 100644 index 00000000..a419a2d0 --- /dev/null +++ b/docker-tests/Dockerfile.bun @@ -0,0 +1,20 @@ +FROM oven/bun:1.3 AS builder +WORKDIR /app +COPY package.json bun.lock ./ +COPY packages/chronicle/package.json ./packages/chronicle/ +RUN bun install --frozen-lockfile +COPY packages/chronicle ./packages/chronicle +RUN cd packages/chronicle && bun build-cli.ts +RUN cd packages/chronicle && bun pm pack --destination /app + +FROM oven/bun:1.3-slim AS runner +WORKDIR /app +RUN bun init -y +COPY --from=builder /app/raystack-chronicle-*.tgz ./chronicle.tgz +RUN bun add ./chronicle.tgz && rm chronicle.tgz +COPY examples/basic ./examples/basic +RUN bunx chronicle build --config examples/basic/chronicle.yaml --preset bun + +EXPOSE 3000 + +CMD ["bunx", "chronicle", "start", "--config", "examples/basic/chronicle.yaml", "--port", "3000", "--host", "0.0.0.0"] diff --git a/docker-tests/Dockerfile.node b/docker-tests/Dockerfile.node new file mode 100644 index 00000000..ed2d776f --- /dev/null +++ b/docker-tests/Dockerfile.node @@ -0,0 +1,20 @@ +FROM oven/bun:1.3 AS builder +WORKDIR /app +COPY package.json bun.lock ./ +COPY packages/chronicle/package.json ./packages/chronicle/ +RUN bun install --frozen-lockfile +COPY packages/chronicle ./packages/chronicle +RUN cd packages/chronicle && bun build-cli.ts +RUN cd packages/chronicle && bun pm pack --destination /app + +FROM node:24-slim AS runner +WORKDIR /app +RUN npm init -y +COPY --from=builder /app/raystack-chronicle-*.tgz ./chronicle.tgz +RUN npm install ./chronicle.tgz && rm chronicle.tgz +COPY examples/basic ./examples/basic +RUN npx chronicle build --config examples/basic/chronicle.yaml + +EXPOSE 3000 + +CMD ["npx", "chronicle", "start", "--config", "examples/basic/chronicle.yaml", "--port", "3000", "--host", "0.0.0.0"] diff --git a/docker-tests/smoke-test.mjs b/docker-tests/smoke-test.mjs new file mode 100644 index 00000000..fd4af60d --- /dev/null +++ b/docker-tests/smoke-test.mjs @@ -0,0 +1,76 @@ +import assert from 'node:assert/strict'; + +const BASE = process.env.BASE_URL || 'http://localhost:3000'; +let failed = 0; + +console.log(`Smoke tests against ${BASE}\n`); + +// index returns 200 or 307 +{ + const res = await fetch(`${BASE}/`); + assert.ok(res.status === 200 || res.status === 307, `index: expected 200 or 307, got ${res.status}`); + console.log(' ✓ index returns 200 or 307'); +} + +// page API returns frontmatter +{ + const res = await fetch(`${BASE}/api/page?slug=docs,getting-started`); + assert.equal(res.status, 200, `page API: expected 200, got ${res.status}`); + const data = await res.json(); + assert.ok(data.frontmatter, 'page API: missing frontmatter'); + assert.ok(data.frontmatter.title, 'page API: missing title'); + console.log(' ✓ page API returns frontmatter'); +} + +// search API returns results +{ + const res = await fetch(`${BASE}/api/search`); + assert.equal(res.status, 200, `search API: expected 200, got ${res.status}`); + const data = await res.json(); + assert.ok(Array.isArray(data), 'search API: expected array'); + assert.ok(data.length > 0, 'search API: expected results'); + assert.ok(data[0].url, 'search API: missing url'); + console.log(' ✓ search API returns results'); +} + +// search with query returns matches +{ + const res = await fetch(`${BASE}/api/search?query=getting`); + assert.equal(res.status, 200, `search query: expected 200, got ${res.status}`); + const data = await res.json(); + assert.ok(data.length > 0, 'search query: expected results'); + assert.ok(data[0].match, 'search query: missing match field'); + console.log(' ✓ search with query returns matches'); +} + +// image API resizes PNG +{ + const res = await fetch(`${BASE}/api/image?url=${encodeURIComponent('/_content/docs/test-image.png')}&w=320`); + assert.equal(res.status, 200, `image API: expected 200, got ${res.status}`); + const ct = res.headers.get('content-type'); + assert.ok(ct.startsWith('image/'), `image API: expected image content-type, got ${ct}`); + console.log(' ✓ image API resizes PNG'); +} + +// image API returns 400 for missing params +{ + const res = await fetch(`${BASE}/api/image`); + assert.equal(res.status, 400, `image 400: expected 400, got ${res.status}`); + console.log(' ✓ image API returns 400 for missing params'); +} + +// image API returns 400 for invalid width +{ + const res = await fetch(`${BASE}/api/image?url=${encodeURIComponent('/_content/docs/test-image.png')}&w=999`); + assert.equal(res.status, 400, `image invalid width: expected 400, got ${res.status}`); + console.log(' ✓ image API returns 400 for invalid width'); +} + +// image API returns 404 for missing image +{ + const res = await fetch(`${BASE}/api/image?url=${encodeURIComponent('/_content/does-not-exist.png')}&w=640`); + assert.equal(res.status, 404, `image 404: expected 404, got ${res.status}`); + console.log(' ✓ image API returns 404 for missing image'); +} + +console.log('\nALL PASSED'); diff --git a/examples/basic/content/docs/test-image.png b/examples/basic/content/docs/test-image.png new file mode 100644 index 00000000..62a5f8f4 Binary files /dev/null and b/examples/basic/content/docs/test-image.png differ diff --git a/packages/chronicle/src/server/vite-config.ts b/packages/chronicle/src/server/vite-config.ts index 7d96a6eb..88e2346b 100644 --- a/packages/chronicle/src/server/vite-config.ts +++ b/packages/chronicle/src/server/vite-config.ts @@ -73,6 +73,7 @@ export async function createViteConfig( nitro({ serverDir: path.resolve(packageRoot, 'src/server'), ...(preset && { preset }), + ignore: ['**/*.test.ts', '**/*.test.tsx', '**/*.spec.ts', '**/*.spec.tsx'], }), mdx({ default: defineFumadocsConfig({