From 34225a96bc413966be8ea7b5dafc0b65921416cf Mon Sep 17 00:00:00 2001 From: RlxChap2 <133292635+RlxChap2@users.noreply.github.com> Date: Sun, 4 Oct 2026 19:42:15 +0300 Subject: [PATCH 1/9] fix(deploy): run Fastify API in Cloudflare Container --- .github/workflows/ci.yml | 3 +++ README.md | 6 ++++-- package.json | 10 ++++++---- pnpm-lock.yaml | 8 ++++++++ src/cloudflare-node.d.ts | 11 ----------- src/worker.ts | 20 +++++++++++++++----- wrangler.toml | 15 ++++++++++++++- 7 files changed, 50 insertions(+), 23 deletions(-) delete mode 100644 src/cloudflare-node.d.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a963354..0ad2bfb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,6 +36,9 @@ jobs: - name: Run TypeScript build run: pnpm build + - name: Validate Cloudflare Worker and container configuration + run: pnpm cloudflare:check + - name: Run Vitest tests with coverage run: pnpm test:coverage diff --git a/README.md b/README.md index 0e8b2c1..0eb95f6 100644 --- a/README.md +++ b/README.md @@ -27,14 +27,16 @@ The default address is `http://localhost:3000`. Configuration names are listed i ### Cloudflare Workers -This is an API Worker, not a static Pages site. The repository includes `wrangler.toml` and a `src/worker.ts` entrypoint. In Cloudflare's project settings use: +This is a Fastify API deployed through a Cloudflare Container, not a static Pages site and not a Fastify process inside the Workers runtime. The repository includes `wrangler.toml`, `src/worker.ts` and the existing `DOCKERFILE`. In Cloudflare Workers Builds use: ```text Build command: pnpm run build Deploy command: pnpm run deploy:cloudflare ``` -Set production variables and secrets in the Worker settings. Do not upload `.env`. The Worker uses Node.js compatibility for Fastify's HTTP server; Cloudflare documents this integration through `httpServerHandler`. For a local deployment, run `pnpm build` before `pnpm deploy:cloudflare`. +Set production variables and secrets in the Worker and container settings. Do not upload `.env`. Cloudflare Containers require a Workers Paid plan and build the existing Docker image. For a local deployment, run `pnpm build` before `pnpm deploy:cloudflare`; local container development requires Docker. + +Use `pnpm run deploy:cloudflare` in the dashboard. The repository script pins the local Wrangler binary and explicitly selects `wrangler.toml`. Do not use the old static Pages deployment command. ## API diff --git a/package.json b/package.json index ad6c9c8..ff10bd1 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ "test:watch": "vitest", "test:coverage": "vitest run --coverage", "check:upstreams": "tsx scripts/check-upstreams.ts", - "deploy:cloudflare": "wrangler deploy", + "cloudflare:check": "pnpm exec wrangler deploy --dry-run --containers-rollout=none --config wrangler.toml", + "deploy:cloudflare": "pnpm exec wrangler deploy --config wrangler.toml", "prepare": "husky" }, "keywords": [ @@ -35,6 +36,7 @@ "license": "MIT", "packageManager": "pnpm@10.33.4", "dependencies": { + "@cloudflare/containers": "0.3.7", "@fastify/cors": "^11.2.0", "@fastify/rate-limit": "^11.0.0", "adhan": "^4.4.6", @@ -42,9 +44,9 @@ "fastify": "^5.12.1" }, "devDependencies": { + "@cloudflare/workers-types": "^5.20261004.1", "@eslint/js": "^10.0.1", "@types/node": "^24.19.1", - "@cloudflare/workers-types": "^5.20261004.1", "@vitest/coverage-v8": "^4.1.9", "ajv": "^8.20.0", "eslint": "^10.6.0", @@ -56,8 +58,8 @@ "typescript": "^6.0.3", "typescript-eslint": "^8.62.0", "vitest": "^4.1.9", - "yaml": "^2.9.1", - "wrangler": "^4.147.0" + "wrangler": "^4.147.0", + "yaml": "^2.9.1" }, "engines": { "node": ">=24 <25" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3a39f8b..fc0c81a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@cloudflare/containers': + specifier: 0.3.7 + version: 0.3.7 '@fastify/cors': specifier: ^11.2.0 version: 11.2.0 @@ -96,6 +99,9 @@ packages: resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} + '@cloudflare/containers@0.3.7': + resolution: {integrity: sha512-DM9dm3FnIBSyiSJ1FLavKwl/lk3oAmTaynCzZQ9pZR0ncRPquSxkxd8Nu2MFILxmDDsPkxKsSNEh9mHHMty4Fw==} + '@cloudflare/kv-asset-handler@0.5.0': resolution: {integrity: sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==} engines: {node: '>=22.0.0'} @@ -1790,6 +1796,8 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} + '@cloudflare/containers@0.3.7': {} + '@cloudflare/kv-asset-handler@0.5.0': {} '@cloudflare/unenv-preset@2.16.2(unenv@2.0.0-rc.24)(workerd@1.20261001.1)': diff --git a/src/cloudflare-node.d.ts b/src/cloudflare-node.d.ts deleted file mode 100644 index 32bd0d6..0000000 --- a/src/cloudflare-node.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Bonyan-API – Quran & Azkar API - * Copyright (c) 2026 BonyanOSS - * MIT License - */ - -declare module 'cloudflare:node' { - import type { Server } from 'node:http'; - - export function httpServerHandler(server: Server): unknown; -} diff --git a/src/worker.ts b/src/worker.ts index 8879e21..911212d 100644 --- a/src/worker.ts +++ b/src/worker.ts @@ -4,10 +4,20 @@ * MIT License */ -import { httpServerHandler } from 'cloudflare:node'; -import { buildApp } from './app.js'; +import { Container, getContainer } from '@cloudflare/containers'; +import type { DurableObjectNamespace } from '@cloudflare/workers-types'; -const app = await buildApp({ logger: false }); -await app.ready(); +export class BonyanApiContainer extends Container { + defaultPort = 3000; + sleepAfter = '10m'; +} -export default httpServerHandler(app.server); +interface Env { + BONYAN_API: DurableObjectNamespace; +} + +export default { + fetch(request: Request, env: Env) { + return getContainer(env.BONYAN_API).fetch(request); + }, +}; diff --git a/wrangler.toml b/wrangler.toml index 457c87c..e5d78fc 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -1,4 +1,17 @@ name = "bonyan-api" main = "dist/worker.js" compatibility_date = "2026-10-04" -compatibility_flags = ["nodejs_compat", "enable_nodejs_http_server_modules"] +compatibility_flags = ["nodejs_compat"] + +[[containers]] +class_name = "BonyanApiContainer" +image = "./DOCKERFILE" +max_instances = 3 + +[[durable_objects.bindings]] +name = "BONYAN_API" +class_name = "BonyanApiContainer" + +[[migrations]] +tag = "v1" +new_sqlite_classes = ["BonyanApiContainer"] From 967cc6f18a271504d207f29a7676cb8555c81028 Mon Sep 17 00:00:00 2001 From: RlxChap2 <133292635+RlxChap2@users.noreply.github.com> Date: Sun, 4 Oct 2026 19:47:28 +0300 Subject: [PATCH 2/9] fix(deploy): use conventional Dockerfile path --- DOCKERFILE => Dockerfile | 0 README.md | 4 ++-- wrangler.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename DOCKERFILE => Dockerfile (100%) diff --git a/DOCKERFILE b/Dockerfile similarity index 100% rename from DOCKERFILE rename to Dockerfile diff --git a/README.md b/README.md index 0eb95f6..8fabc43 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Copy `.env.example` only when creating a new configuration. Keep an existing `.e ```sh pnpm build pnpm start -docker build -f DOCKERFILE -t bonyan-api . +docker build -f Dockerfile -t bonyan-api . docker run --rm -p 3000:3000 --env-file .env bonyan-api ``` @@ -27,7 +27,7 @@ The default address is `http://localhost:3000`. Configuration names are listed i ### Cloudflare Workers -This is a Fastify API deployed through a Cloudflare Container, not a static Pages site and not a Fastify process inside the Workers runtime. The repository includes `wrangler.toml`, `src/worker.ts` and the existing `DOCKERFILE`. In Cloudflare Workers Builds use: +This is a Fastify API deployed through a Cloudflare Container, not a static Pages site and not a Fastify process inside the Workers runtime. The repository includes `wrangler.toml`, `src/worker.ts` and the existing `Dockerfile`. In Cloudflare Workers Builds use: ```text Build command: pnpm run build diff --git a/wrangler.toml b/wrangler.toml index e5d78fc..39ebb97 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -5,7 +5,7 @@ compatibility_flags = ["nodejs_compat"] [[containers]] class_name = "BonyanApiContainer" -image = "./DOCKERFILE" +image = "./Dockerfile" max_instances = 3 [[durable_objects.bindings]] From ce3cf7d5b635526147e76705a93569d07968baf0 Mon Sep 17 00:00:00 2001 From: RlxChap2 <133292635+RlxChap2@users.noreply.github.com> Date: Sun, 4 Oct 2026 19:50:12 +0300 Subject: [PATCH 3/9] docs(deploy): clarify Workers preview configuration --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8fabc43..1f04e6c 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ Deploy command: pnpm run deploy:cloudflare Set production variables and secrets in the Worker and container settings. Do not upload `.env`. Cloudflare Containers require a Workers Paid plan and build the existing Docker image. For a local deployment, run `pnpm build` before `pnpm deploy:cloudflare`; local container development requires Docker. -Use `pnpm run deploy:cloudflare` in the dashboard. The repository script pins the local Wrangler binary and explicitly selects `wrangler.toml`. Do not use the old static Pages deployment command. +Use `pnpm run deploy:cloudflare` in the production Workers Build. The repository script pins the local Wrangler binary and explicitly selects `wrangler.toml`. For preview branches, connect a separate staging Worker or Wrangler environment and use `wrangler versions upload`; preview uploads do not build or roll out container images. Do not use the old static Pages deployment command. ## API From 51255a43261b9b48a02edb34ccd1ec94a5c6c7b3 Mon Sep 17 00:00:00 2001 From: RlxChap2 <133292635+RlxChap2@users.noreply.github.com> Date: Sun, 4 Oct 2026 20:08:50 +0300 Subject: [PATCH 4/9] fix(deploy): use async container worker handler --- src/worker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/worker.ts b/src/worker.ts index 911212d..5f6c14c 100644 --- a/src/worker.ts +++ b/src/worker.ts @@ -17,7 +17,7 @@ interface Env { } export default { - fetch(request: Request, env: Env) { + async fetch(request: Request, env: Env) { return getContainer(env.BONYAN_API).fetch(request); }, }; From 1065b1403dfed92107016c080dd78a59ca705635 Mon Sep 17 00:00:00 2001 From: RlxChap2 <133292635+RlxChap2@users.noreply.github.com> Date: Sun, 4 Oct 2026 20:15:45 +0300 Subject: [PATCH 5/9] fix(deploy): use native Durable Object container API --- package.json | 1 - pnpm-lock.yaml | 8 -------- src/worker.ts | 39 +++++++++++++++++++++++++++++++++------ tsconfig.json | 1 + wrangler.toml | 2 +- 5 files changed, 35 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index ff10bd1..1152282 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,6 @@ "license": "MIT", "packageManager": "pnpm@10.33.4", "dependencies": { - "@cloudflare/containers": "0.3.7", "@fastify/cors": "^11.2.0", "@fastify/rate-limit": "^11.0.0", "adhan": "^4.4.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fc0c81a..3a39f8b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,9 +8,6 @@ importers: .: dependencies: - '@cloudflare/containers': - specifier: 0.3.7 - version: 0.3.7 '@fastify/cors': specifier: ^11.2.0 version: 11.2.0 @@ -99,9 +96,6 @@ packages: resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} - '@cloudflare/containers@0.3.7': - resolution: {integrity: sha512-DM9dm3FnIBSyiSJ1FLavKwl/lk3oAmTaynCzZQ9pZR0ncRPquSxkxd8Nu2MFILxmDDsPkxKsSNEh9mHHMty4Fw==} - '@cloudflare/kv-asset-handler@0.5.0': resolution: {integrity: sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==} engines: {node: '>=22.0.0'} @@ -1796,8 +1790,6 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} - '@cloudflare/containers@0.3.7': {} - '@cloudflare/kv-asset-handler@0.5.0': {} '@cloudflare/unenv-preset@2.16.2(unenv@2.0.0-rc.24)(workerd@1.20261001.1)': diff --git a/src/worker.ts b/src/worker.ts index 5f6c14c..92a06a9 100644 --- a/src/worker.ts +++ b/src/worker.ts @@ -4,12 +4,38 @@ * MIT License */ -import { Container, getContainer } from '@cloudflare/containers'; -import type { DurableObjectNamespace } from '@cloudflare/workers-types'; +import { DurableObject } from 'cloudflare:workers'; -export class BonyanApiContainer extends Container { - defaultPort = 3000; - sleepAfter = '10m'; +const CONTAINER_PORT = 3000; +const INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000; + +export class BonyanApiContainer extends DurableObject { + private startPromise?: Promise; + + private async ensureContainerRunning(): Promise { + const container = this.ctx.container; + if (!container) { + throw new Error('BonyanApiContainer is missing its container binding'); + } + + if (container.running) { + return; + } + + this.startPromise ??= (async () => { + container.start(); + await container.setInactivityTimeout(INACTIVITY_TIMEOUT_MS); + })().finally(() => { + this.startPromise = undefined; + }); + + await this.startPromise; + } + + async fetch(request: Request) { + await this.ensureContainerRunning(); + return this.ctx.container!.getTcpPort(CONTAINER_PORT).fetch(request); + } } interface Env { @@ -18,6 +44,7 @@ interface Env { export default { async fetch(request: Request, env: Env) { - return getContainer(env.BONYAN_API).fetch(request); + const id = env.BONYAN_API.idFromName('default'); + return env.BONYAN_API.get(id).fetch(request); }, }; diff --git a/tsconfig.json b/tsconfig.json index 4abc74b..c5af7cd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,6 +9,7 @@ "noImplicitAny": true, "noUncheckedIndexedAccess": true, "skipLibCheck": true, + "types": ["node", "@cloudflare/workers-types"], "outDir": "dist", "rootDir": "src", "resolveJsonModule": true, diff --git a/wrangler.toml b/wrangler.toml index 39ebb97..2f99ed2 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -1,5 +1,5 @@ name = "bonyan-api" -main = "dist/worker.js" +main = "src/worker.ts" compatibility_date = "2026-10-04" compatibility_flags = ["nodejs_compat"] From d3714d32626e0949815f12b58ae78751389830ca Mon Sep 17 00:00:00 2001 From: RlxChap2 <133292635+RlxChap2@users.noreply.github.com> Date: Sun, 4 Oct 2026 20:53:00 +0300 Subject: [PATCH 6/9] fix(deploy): wait for container readiness before forwarding requests --- src/worker.ts | 35 +++++++++++++++++- tests/worker.test.ts | 88 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+), 2 deletions(-) create mode 100644 tests/worker.test.ts diff --git a/src/worker.ts b/src/worker.ts index 92a06a9..9c1ac07 100644 --- a/src/worker.ts +++ b/src/worker.ts @@ -8,9 +8,11 @@ import { DurableObject } from 'cloudflare:workers'; const CONTAINER_PORT = 3000; const INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000; +const STARTUP_TIMEOUT_MS = 30_000; export class BonyanApiContainer extends DurableObject { private startPromise?: Promise; + private ready = false; private async ensureContainerRunning(): Promise { const container = this.ctx.container; @@ -18,13 +20,21 @@ export class BonyanApiContainer extends DurableObject { throw new Error('BonyanApiContainer is missing its container binding'); } - if (container.running) { + if (!container.running) { + this.ready = false; + } + + if (this.ready) { return; } this.startPromise ??= (async () => { - container.start(); + if (!container.running) { + container.start(); + } await container.setInactivityTimeout(INACTIVITY_TIMEOUT_MS); + await this.waitForReadiness(container); + this.ready = true; })().finally(() => { this.startPromise = undefined; }); @@ -32,6 +42,27 @@ export class BonyanApiContainer extends DurableObject { await this.startPromise; } + private async waitForReadiness(container: Container): Promise { + const port = container.getTcpPort(CONTAINER_PORT); + const deadline = Date.now() + STARTUP_TIMEOUT_MS; + + while (Date.now() < deadline) { + try { + const response = await port.fetch('http://container/ready', { + signal: AbortSignal.timeout(1000), + }); + const ready = response.ok; + await response.body?.cancel(); + if (ready) return; + } catch { + // The process can be running before Fastify starts listening. + } + await new Promise((resolve) => setTimeout(resolve, 250)); + } + + throw new Error('Bonyan API container did not become ready within 30 seconds'); + } + async fetch(request: Request) { await this.ensureContainerRunning(); return this.ctx.container!.getTcpPort(CONTAINER_PORT).fetch(request); diff --git a/tests/worker.test.ts b/tests/worker.test.ts new file mode 100644 index 0000000..5a9b0cf --- /dev/null +++ b/tests/worker.test.ts @@ -0,0 +1,88 @@ +/* + * Bonyan-API – Quran & Azkar API + * Copyright (c) 2026 BonyanOSS + * MIT License + */ + +import { afterEach, describe, expect, it, vi } from 'vitest'; + +vi.mock('cloudflare:workers', () => ({ + DurableObject: class { + constructor(protected ctx: unknown) {} + }, +})); + +import { BonyanApiContainer } from '../src/worker.js'; + +function createContainer(running = false) { + const port = { fetch: vi.fn().mockImplementation(async () => new Response('ok')) }; + const container = { + running, + start: vi.fn(() => { + container.running = true; + }), + setInactivityTimeout: vi.fn().mockResolvedValue(undefined), + getTcpPort: vi.fn(() => port), + }; + const object = new BonyanApiContainer({ container } as unknown as DurableObjectState, {}); + return { object, container, port }; +} + +afterEach(() => vi.useRealTimers()); + +describe('container request readiness', () => { + it('waits for readiness before forwarding concurrent requests', async () => { + const { object, container, port } = createContainer(); + let resolveReady!: (response: Response) => void; + port.fetch.mockImplementationOnce( + () => + new Promise((resolve) => { + resolveReady = resolve; + }), + ); + + const first = new Request('https://api.example/health'); + const second = new Request('https://api.example/ready'); + const requests = [object.fetch(first), object.fetch(second)]; + await vi.waitFor(() => expect(port.fetch).toHaveBeenCalledTimes(1)); + expect(container.start).toHaveBeenCalledTimes(1); + expect(port.fetch.mock.calls[0]?.[0]).toBe('http://container/ready'); + + resolveReady(new Response('ready')); + await Promise.all(requests); + expect(port.fetch).toHaveBeenCalledWith(first); + expect(port.fetch).toHaveBeenCalledWith(second); + }); + + it('checks an existing container and reapplies its inactivity timeout', async () => { + const { object, container, port } = createContainer(true); + await object.fetch(new Request('https://api.example/health')); + expect(container.start).not.toHaveBeenCalled(); + expect(container.setInactivityTimeout).toHaveBeenCalledWith(600_000); + expect(port.fetch.mock.calls[0]?.[0]).toBe('http://container/ready'); + }); + + it('rechecks readiness after a stopped container restarts', async () => { + const { object, container, port } = createContainer(); + await object.fetch(new Request('https://api.example/health')); + container.running = false; + await object.fetch(new Request('https://api.example/health')); + expect(container.start).toHaveBeenCalledTimes(2); + expect(port.fetch.mock.calls.filter(([input]) => input === 'http://container/ready')).toHaveLength(2); + }); + + it('times out without forwarding and allows a later request to retry', async () => { + vi.useFakeTimers(); + const { object, port } = createContainer(); + port.fetch.mockRejectedValue(new Error('connection refused')); + const request = new Request('https://api.example/health'); + const pending = expect(object.fetch(request)).rejects.toThrow('did not become ready'); + await vi.advanceTimersByTimeAsync(30_000); + await pending; + expect(port.fetch).not.toHaveBeenCalledWith(request); + + port.fetch.mockResolvedValue(new Response('ok')); + await object.fetch(request); + expect(port.fetch).toHaveBeenCalledWith(request); + }); +}); From cd97d02754d76399bf66f1b6c990bae682a5ead1 Mon Sep 17 00:00:00 2001 From: RlxChap2 <133292635+RlxChap2@users.noreply.github.com> Date: Sun, 4 Oct 2026 20:53:09 +0300 Subject: [PATCH 7/9] test(deploy): validate Worker startup and container health in CI --- .github/workflows/ci.yml | 13 +++++++++++++ README.md | 4 +++- package.json | 1 + scripts/check-worker-startup.ts | 29 +++++++++++++++++++++++++++++ 4 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 scripts/check-worker-startup.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0ad2bfb..551a4c8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,6 +39,19 @@ jobs: - name: Validate Cloudflare Worker and container configuration run: pnpm cloudflare:check + - name: Start Worker in the Cloudflare runtime + timeout-minutes: 2 + run: pnpm cloudflare:startup + + - name: Build and smoke test the production container + timeout-minutes: 5 + run: | + docker build -t bonyan-api-ci . + docker run --detach --name bonyan-api-ci --publish 127.0.0.1:3000:3000 bonyan-api-ci + trap 'docker logs bonyan-api-ci; docker rm --force bonyan-api-ci' EXIT + curl --fail --silent --show-error --retry 30 --retry-delay 1 --retry-connrefused --max-time 5 http://127.0.0.1:3000/health --output /tmp/bonyan-health.json + node -e "const fs = require('node:fs'); const assert = require('node:assert/strict'); assert.equal(JSON.parse(fs.readFileSync('/tmp/bonyan-health.json', 'utf8')).status, 'ok');" + - name: Run Vitest tests with coverage run: pnpm test:coverage diff --git a/README.md b/README.md index 1f04e6c..79f4c44 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,9 @@ Deploy command: pnpm run deploy:cloudflare Set production variables and secrets in the Worker and container settings. Do not upload `.env`. Cloudflare Containers require a Workers Paid plan and build the existing Docker image. For a local deployment, run `pnpm build` before `pnpm deploy:cloudflare`; local container development requires Docker. -Use `pnpm run deploy:cloudflare` in the production Workers Build. The repository script pins the local Wrangler binary and explicitly selects `wrangler.toml`. For preview branches, connect a separate staging Worker or Wrangler environment and use `wrangler versions upload`; preview uploads do not build or roll out container images. Do not use the old static Pages deployment command. +Use `pnpm run deploy:cloudflare` in the production Workers Build. The repository script pins the local Wrangler binary and explicitly selects `wrangler.toml`. To test a branch before production, connect a separate staging Worker or Wrangler environment and run a full deploy there. `wrangler versions upload` uploads Worker code only; it does not build or roll out container images or provide a full container preview. + +CI runs `pnpm cloudflare:startup` to execute the Worker in workerd, then builds the Docker image and requests `/health` from its Node.js server. A successful `tsc` build or Wrangler dry run alone does not validate Worker startup. Keep Fastify initialization in `src/server.ts` inside the container; awaiting it at module scope in `src/worker.ts` can cause `Top-level await in module is unsettled` during deployment. ## API diff --git a/package.json b/package.json index 1152282..4515a2e 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "test:coverage": "vitest run --coverage", "check:upstreams": "tsx scripts/check-upstreams.ts", "cloudflare:check": "pnpm exec wrangler deploy --dry-run --containers-rollout=none --config wrangler.toml", + "cloudflare:startup": "tsx scripts/check-worker-startup.ts", "deploy:cloudflare": "pnpm exec wrangler deploy --config wrangler.toml", "prepare": "husky" }, diff --git a/scripts/check-worker-startup.ts b/scripts/check-worker-startup.ts new file mode 100644 index 0000000..735072f --- /dev/null +++ b/scripts/check-worker-startup.ts @@ -0,0 +1,29 @@ +/* + * Bonyan-API – Quran & Azkar API + * Copyright (c) 2026 BonyanOSS + * MIT License + */ + +import { unstable_dev, unstable_readConfig } from 'wrangler'; + +const config = unstable_readConfig({ config: 'wrangler.toml' }); +if (!config.main) throw new Error('wrangler.toml must define a Worker entrypoint'); + +// A dry run bundles code but does not execute its top-level initialization. +const worker = await unstable_dev(config.main, { + config: 'wrangler.toml', + local: true, + port: 0, + inspectorPort: 0, + persist: false, + logLevel: 'error', + experimental: { + disableExperimentalWarning: true, + disableDevRegistry: true, + enableContainers: false, + watch: false, + }, +}); + +await worker.stop(); +console.log('Worker startup passed in workerd (container execution checked separately).'); From dc76f034a573e98f9f7c1cc1e92ed42472b08fa2 Mon Sep 17 00:00:00 2001 From: RlxChap2 <133292635+RlxChap2@users.noreply.github.com> Date: Sun, 4 Oct 2026 20:54:54 +0300 Subject: [PATCH 8/9] fix(docker): skip prepare scripts when pruning dev dependencies --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index dbb5d0f..99b8e2c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,7 +9,7 @@ WORKDIR /src COPY tsconfig.json ./ COPY src ./src RUN pnpm run build -RUN pnpm prune --prod +RUN pnpm prune --prod --ignore-scripts FROM node:24-alpine ENV NODE_ENV=production From ffd85e527db4f53007ad002e100f57c836da2612 Mon Sep 17 00:00:00 2001 From: RlxChap2 <133292635+RlxChap2@users.noreply.github.com> Date: Sun, 4 Oct 2026 20:56:36 +0300 Subject: [PATCH 9/9] test(ci): retry health checks during container startup --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 551a4c8..39bf2b9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,7 +49,7 @@ jobs: docker build -t bonyan-api-ci . docker run --detach --name bonyan-api-ci --publish 127.0.0.1:3000:3000 bonyan-api-ci trap 'docker logs bonyan-api-ci; docker rm --force bonyan-api-ci' EXIT - curl --fail --silent --show-error --retry 30 --retry-delay 1 --retry-connrefused --max-time 5 http://127.0.0.1:3000/health --output /tmp/bonyan-health.json + curl --fail --silent --show-error --retry 30 --retry-delay 1 --retry-all-errors --retry-max-time 60 --max-time 5 http://127.0.0.1:3000/health --output /tmp/bonyan-health.json node -e "const fs = require('node:fs'); const assert = require('node:assert/strict'); assert.equal(JSON.parse(fs.readFileSync('/tmp/bonyan-health.json', 'utf8')).status, 'ok');" - name: Run Vitest tests with coverage