-
Notifications
You must be signed in to change notification settings - Fork 0
TIM-46: drop Cloudflare, run as Node service in Docker Compose #72
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| # Build artefacts | ||
| node_modules | ||
| .pnpm-store | ||
| build | ||
| .svelte-kit | ||
| .svelte-kit-test | ||
| .output | ||
| .vercel | ||
| .netlify | ||
| .wrangler | ||
|
|
||
| # Vite caches | ||
| .vite | ||
| .cache | ||
| *.tsbuildinfo | ||
| vite.config.js.timestamp-* | ||
| vite.config.ts.timestamp-* | ||
|
|
||
| # Tests, coverage, editor noise | ||
| coverage | ||
| coverage-* | ||
| *.lcov | ||
| .nyc_output | ||
| .idea | ||
| .vscode/* | ||
| !.vscode/extensions.json | ||
| !.vscode/settings.json | ||
| *.swp | ||
| *.swo | ||
| *~ | ||
|
|
||
| # OS junk | ||
| .DS_Store | ||
| Thumbs.db | ||
|
|
||
| # Logs / temp | ||
| logs | ||
| *.log | ||
| npm-debug.log* | ||
| yarn-debug.log* | ||
| yarn-error.log* | ||
| pnpm-debug.log* | ||
| lerna-debug.log* | ||
| tmp | ||
| temp | ||
| *.tmp | ||
| *.temp | ||
|
|
||
| # Env / secrets — must never be baked into the image | ||
| .env | ||
| .env.* | ||
| !.env.example | ||
| !.env.test | ||
|
|
||
| # Source control | ||
| .git | ||
| .gitignore | ||
| .github | ||
| .worktrees | ||
| .gitattributes | ||
|
|
||
| # Project-local supabase / docker artefacts | ||
| supabase/.temp | ||
| supabase/.env | ||
| supabase/.env.* | ||
| docker-compose*.yml | ||
| Dockerfile | ||
| .dockerignore | ||
|
|
||
| # Docs that don't belong in the runtime image (kept locally only) | ||
| README.md | ||
| CHANGELOG.md | ||
| CONTRIBUTING.md | ||
| LICENSE | ||
| SECURITY.md | ||
| osv-scanner.toml | ||
| .releaserc.cjs | ||
| .cursor | ||
| .clusterfuzzlite | ||
|
|
||
| # Test fixtures / mocks | ||
| src/mocks | ||
| *.test.ts | ||
| *.test.js | ||
| *.spec.ts | ||
| *.spec.js | ||
| __tests__ | ||
| tests |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,21 +1,74 @@ | ||
| PUBLIC_SUPABASE_URL=https://your-project.supabase.co | ||
| PUBLIC_SUPABASE_ANON_KEY=your-anon-key | ||
| # Required for workflow runs cache writes (server-only; bypasses RLS so cache table can be written) | ||
| SUPABASE_SERVICE_ROLE_KEY=your-service-role-key | ||
|
|
||
| # Production only: set to your app URL (e.g. https://your-project.pages.dev) so OAuth redirects back to the app instead of localhost | ||
| # PUBLIC_APP_URL=https://your-project.pages.dev | ||
|
|
||
| # ─── GitHub App (AI Optimization → Apply as PR) ──────────────────────────── | ||
| # Create a GitHub App at https://github.com/settings/apps/new with: | ||
| # Permissions: Contents (Read & Write), Pull requests (Read & Write), Workflows (Read & Write), Actions (Read) | ||
| # Callback URL: https://your-project.pages.dev/auth/github-app/callback | ||
| # For local dev: http://localhost:5173/auth/github-app/callback | ||
| # | ||
| # GITHUB_APP_ID: the numeric App ID shown on the App settings page | ||
| # GITHUB_APP_PRIVATE_KEY: the RSA private key downloaded from the App settings page. | ||
| # Store the full PEM content (multi-line), or collapse newlines to literal \n for single-line secrets. | ||
| # GITHUB_APP_SLUG: the URL slug of your App (e.g. "workflow-metrics-bot" from github.com/apps/workflow-metrics-bot) | ||
| # ============================================================================= | ||
| # DATABASE — pick ONE of the two blocks below and delete the other. | ||
| # ============================================================================= | ||
|
|
||
| # --- (A) Bundled Postgres (docker compose with the postgres overlay) ------- | ||
| # Use this when `docker compose -f docker-compose.yml -f docker-compose.postgres.yml up`. | ||
| # The hostname `postgres` is the service name in docker-compose.postgres.yml. | ||
| # POSTGRES_PASSWORD is also defined below for the postgres container itself. | ||
| DATABASE_URL=postgres://workflow_metrics:${POSTGRES_PASSWORD}@postgres:5432/workflow_metrics | ||
|
|
||
| # --- (B) External Postgres (managed, K8s, or your own instance) ----------- | ||
| # Uncomment and replace the placeholders. sslmode=require is recommended for | ||
| # managed Postgres providers (Neon, RDS, Crunchy Bridge, Supabase pooled, …). | ||
| # DATABASE_URL=postgres://workflow_metrics:CHANGE_ME@db.example.com:5432/workflow_metrics?sslmode=require | ||
|
|
||
| # Required only when using bundled mode (block A). The compose overlay reads | ||
| # this to provision the postgres container. | ||
| POSTGRES_PASSWORD=change-me-local-dev-password | ||
| # Optional — defaults are workflow_metrics / workflow_metrics if unset. | ||
| # POSTGRES_DB=workflow_metrics | ||
| # POSTGRES_USER=workflow_metrics | ||
|
|
||
| # ============================================================================= | ||
| # BETTER AUTH — Stage 2a wires these. Listed here so docker compose / Helm | ||
| # can inject them now without a second round of edits. | ||
| # Generate BETTER_AUTH_SECRET with: openssl rand -base64 32 | ||
| # ============================================================================= | ||
| BETTER_AUTH_SECRET=change-me-run-openssl-rand-base64-32 | ||
| # The public URL where the app is reachable. Used as the OAuth callback base. | ||
| BETTER_AUTH_URL=http://localhost:3000 | ||
|
|
||
| # ============================================================================= | ||
| # GITHUB OAUTH (login) | ||
| # Create an OAuth App at https://github.com/settings/applications/new. | ||
| # Authorization callback URL (after Better Auth lands) = ${BETTER_AUTH_URL}/api/auth/callback/github | ||
| # ============================================================================= | ||
| GITHUB_CLIENT_ID=your-github-oauth-app-client-id | ||
| GITHUB_CLIENT_SECRET=your-github-oauth-app-client-secret | ||
|
|
||
| # ============================================================================= | ||
| # GITHUB APP (Apply as PR) | ||
| # Separate from the OAuth App above. Create at https://github.com/settings/apps/new. | ||
| # Repository permissions: Contents (R/W), Pull requests (R/W), Workflows (R/W), Actions (R). | ||
| # ============================================================================= | ||
| GITHUB_APP_ID=your-github-app-id | ||
| # Multi-line PEM is fine. Or collapse newlines to literal \n for single-line secrets. | ||
| GITHUB_APP_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----" | ||
| GITHUB_APP_SLUG=your-github-app-slug | ||
|
|
||
| # ============================================================================= | ||
| # APP | ||
| # ============================================================================= | ||
| # Production URL of the app (no trailing slash). OAuth redirects back here. | ||
| PUBLIC_APP_URL=http://localhost:3000 | ||
| # Node adapter binds to HOST:PORT (defaults 0.0.0.0:3000). Override per-env here. | ||
| HOST=0.0.0.0 | ||
| PORT=3000 | ||
|
|
||
| # ============================================================================= | ||
| # MISTRAL AI (optional) | ||
| # Per-user keys are entered in Settings and stored in the DB. This server-wide | ||
| # fallback is used when no per-user key is set (e.g. demos). | ||
| # ============================================================================= | ||
| # MISTRAL_API_KEY=your-mistral-api-key | ||
|
|
||
| # ============================================================================= | ||
| # DEPRECATED — Supabase | ||
| # The app still calls `locals.supabase.*`. These are required until Stage 4 | ||
| # removes the Supabase wiring. Do not delete yet. | ||
| # ============================================================================= | ||
| PUBLIC_SUPABASE_URL=https://your-project.supabase.co | ||
| PUBLIC_SUPABASE_ANON_KEY=your-anon-key | ||
| # Server-only; bypasses RLS for cache writes. Never expose to the browser. | ||
| SUPABASE_SERVICE_ROLE_KEY=your-service-role-key | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| # syntax=docker/dockerfile:1.7 | ||
| ARG NODE_VERSION=24-alpine | ||
|
|
||
| # ============================================================================ | ||
| # Stage 1 — install all dependencies (dev + prod) using the frozen lockfile | ||
| # ============================================================================ | ||
| FROM node:${NODE_VERSION} AS deps | ||
| WORKDIR /app | ||
|
|
||
| # Corepack ships with Node 16+; pin the exact pnpm version from package.json | ||
| # so the lockfile interpretation matches local dev and CI. | ||
| RUN corepack enable && corepack prepare pnpm@10.30.3 --activate | ||
|
|
||
| # Copy only the manifests so this layer is reused across source-only changes. | ||
| COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./ | ||
| RUN --mount=type=cache,id=pnpm,target=/pnpm/store \ | ||
| pnpm config set store-dir /pnpm/store && \ | ||
| pnpm install --frozen-lockfile | ||
|
|
||
| # ============================================================================ | ||
| # Stage 2 — build the SvelteKit Node bundle and prune dev dependencies | ||
| # ============================================================================ | ||
| FROM node:${NODE_VERSION} AS build | ||
| WORKDIR /app | ||
|
|
||
| RUN corepack enable && corepack prepare pnpm@10.30.3 --activate | ||
|
|
||
| # Reuse node_modules from the deps stage and copy the rest of the source. | ||
| COPY --from=deps /app/node_modules ./node_modules | ||
| COPY . . | ||
|
|
||
| # SvelteKit's `$env/static/public` requires PUBLIC_* vars at build time. | ||
| # The Supabase wiring still imports them (Stage 4 will remove it). Provide | ||
| # placeholders so `pnpm build` succeeds without a `.env` in the image. Real | ||
| # values come from runtime env_file in docker-compose.yml / the Helm chart. | ||
| ENV PUBLIC_SUPABASE_URL=https://placeholder.supabase.co \ | ||
| PUBLIC_SUPABASE_ANON_KEY=placeholder-anon-key \ | ||
| PUBLIC_APP_URL=http://localhost:3000 | ||
|
Comment on lines
+32
to
+38
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift 🧩 Analysis chain🌐 Web query:
💡 Result: In SvelteKit 2.x, values from $env/static/public are substituted into your code at build time [1]. Because they are statically replaced with their values during the build process, they cannot be overridden by runtime environment variables in a built application [1][2]. If you need to access environment variables that can be changed at runtime, you should use $env/dynamic/public instead [2][3]. Unlike the static variant, $env/dynamic/public reads values from the environment at runtime (e.g., from process.env in a Node.js environment) [3]. Summary of differences: - $env/static/public: Values are baked into the bundle at build time, allowing for build-time optimizations (like dead code elimination). They are fixed once the build is complete [1][2]. - $env/dynamic/public: Values are accessed at runtime. This is the correct choice if you need to configure the application using environment variables after it has been built, such as when deploying the same build artifact to different environments [2][3]. Citations:
Avoid baking placeholder 🤖 Prompt for AI Agents |
||
|
|
||
| # Build the Node adapter output to ./build and drop dev-only deps. | ||
| # CI=1 lets `pnpm prune --prod` run non-interactively inside the build. | ||
| RUN CI=1 pnpm build && CI=1 pnpm prune --prod | ||
|
|
||
| # ============================================================================ | ||
| # Stage 3 — runtime image (small, non-root, healthchecked) | ||
| # ============================================================================ | ||
| FROM node:${NODE_VERSION} AS runtime | ||
| WORKDIR /app | ||
|
|
||
| ENV NODE_ENV=production \ | ||
| PORT=3000 \ | ||
| HOST=0.0.0.0 | ||
|
|
||
| # wget is used by the HEALTHCHECK below. node:alpine does not ship it. | ||
| # The base image already provides a non-root `node` user (uid/gid 1000), so we | ||
| # reuse it instead of creating a duplicate account. | ||
| RUN apk add --no-cache wget | ||
|
|
||
| # Copy only what we need to run the app: the built bundle, pruned node_modules, | ||
| # and the package.json so any runtime introspection (engines, etc.) still works. | ||
| COPY --from=build --chown=node:node /app/build ./build | ||
| COPY --from=build --chown=node:node /app/node_modules ./node_modules | ||
| COPY --from=build --chown=node:node /app/package.json ./package.json | ||
|
|
||
| USER node | ||
|
|
||
| EXPOSE 3000 | ||
|
|
||
| # Liveness/readiness probe target. Intentionally unauthenticated and DB-free | ||
| # — see src/routes/healthz/+server.ts. The Node process binds to HOST:PORT, | ||
| # so we probe via 127.0.0.1 to avoid any DNS surprises. | ||
| HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ | ||
| CMD wget --no-verbose --tries=1 --spider http://127.0.0.1:3000/healthz || exit 1 | ||
|
|
||
| # Default SvelteKit adapter-node entry point. PORT and HOST above are honoured. | ||
| CMD ["node", "build/index.js"] | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| # Overlay that adds a bundled Postgres to the base compose. | ||
| # | ||
| # Use ONLY when you want Postgres to run alongside the app container (local | ||
| # dev, CI smoke tests, single-node deploys). The base docker-compose.yml | ||
| # stays Postgres-free so external-DB users don't pay for an unused service. | ||
| # | ||
| # Usage: | ||
| # | ||
| # docker compose -f docker-compose.yml -f docker-compose.postgres.yml up | ||
| # | ||
| # Requires Docker Compose v2.20+ for the `include:` directive. | ||
|
|
||
| include: | ||
| - path: ./docker-compose.yml | ||
|
|
||
| services: | ||
| app: | ||
| depends_on: | ||
| postgres: | ||
| condition: service_healthy | ||
|
|
||
| postgres: | ||
| image: postgres:16-alpine | ||
| container_name: workflow-metrics-postgres | ||
| restart: unless-stopped | ||
| environment: | ||
| POSTGRES_DB: ${POSTGRES_DB:-workflow_metrics} | ||
| POSTGRES_USER: ${POSTGRES_USER:-workflow_metrics} | ||
| POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set when using bundled mode} | ||
| volumes: | ||
| - pgdata:/var/lib/postgresql/data | ||
| healthcheck: | ||
| test: | ||
| - CMD-SHELL | ||
| - pg_isready -U "$${POSTGRES_USER:-workflow_metrics}" -d "$${POSTGRES_DB:-workflow_metrics}" | ||
| interval: 5s | ||
| timeout: 5s | ||
| retries: 10 | ||
| start_period: 5s | ||
|
|
||
| volumes: | ||
| pgdata: | ||
| name: workflow-metrics-pgdata |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,37 @@ | ||||||||||||||||||||||||||||||||||
| # Base compose for workflow-metrics. | ||||||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||||||
| # This file ships the **app** service only. Postgres is intentionally not here | ||||||||||||||||||||||||||||||||||
| # so the same image works against a managed Postgres / K8s service / an existing | ||||||||||||||||||||||||||||||||||
| # instance — point `DATABASE_URL` in `.env` at it and `docker compose up`. | ||||||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||||||
| # Two ways to run: | ||||||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||||||
| # 1. Bundled Postgres (dev default) — bring in the overlay that adds one: | ||||||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||||||
| # docker compose -f docker-compose.yml -f docker-compose.postgres.yml up | ||||||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||||||
| # 2. External Postgres (managed, K8s, shared cluster) — point DATABASE_URL | ||||||||||||||||||||||||||||||||||
| # in .env at your instance, then just: | ||||||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||||||
| # docker compose -f docker-compose.yml up | ||||||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||||||
| # See `.env.example` for the matching DATABASE_URL forms. | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| services: | ||||||||||||||||||||||||||||||||||
| app: | ||||||||||||||||||||||||||||||||||
| build: | ||||||||||||||||||||||||||||||||||
| context: . | ||||||||||||||||||||||||||||||||||
| dockerfile: Dockerfile | ||||||||||||||||||||||||||||||||||
| image: workflow-metrics:local | ||||||||||||||||||||||||||||||||||
| container_name: workflow-metrics-app | ||||||||||||||||||||||||||||||||||
| env_file: | ||||||||||||||||||||||||||||||||||
| - .env | ||||||||||||||||||||||||||||||||||
| environment: | ||||||||||||||||||||||||||||||||||
| # Ensure the runtime picks up the right interface and port even if the | ||||||||||||||||||||||||||||||||||
| # operator forgets to export them in .env. | ||||||||||||||||||||||||||||||||||
| HOST: ${HOST:-0.0.0.0} | ||||||||||||||||||||||||||||||||||
| PORT: ${PORT:-3000} | ||||||||||||||||||||||||||||||||||
| NODE_ENV: production | ||||||||||||||||||||||||||||||||||
| ports: | ||||||||||||||||||||||||||||||||||
| - "${PORT:-3000}:3000" | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+29
to
+36
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win Keep the container's listen port fixed unless the rest of the stack follows it. Line 32-L33 can move the app to a different internal port, but Line 36 still publishes container port Suggested fix environment:
# Ensure the runtime picks up the right interface and port even if the
# operator forgets to export them in .env.
HOST: ${HOST:-0.0.0.0}
- PORT: ${PORT:-3000}
+ PORT: 3000
NODE_ENV: production
ports:
- "${PORT:-3000}:3000"📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
| restart: unless-stopped | ||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: timoa/workflow-metrics
Length of output: 221
🏁 Script executed:
Repository: timoa/workflow-metrics
Length of output: 8278
🏁 Script executed:
Repository: timoa/workflow-metrics
Length of output: 6014
Add
AI_OPTIMIZATION_MODELto.env.examplesrc/lib/server/config/app-config.tsreads this override, and the README already documents it, but the example file does not. Add a placeholder so local setup stays in sync.🧰 Tools
🪛 dotenv-linter (4.0.0)
[warning] 56-56: [UnorderedKey] The HOST key should go before the PUBLIC_APP_URL key
(UnorderedKey)
[warning] 57-57: [UnorderedKey] The PORT key should go before the PUBLIC_APP_URL key
(UnorderedKey)
[warning] 72-72: [UnorderedKey] The PUBLIC_SUPABASE_ANON_KEY key should go before the PUBLIC_SUPABASE_URL key
(UnorderedKey)
[warning] 74-74: [EndingBlankLine] No blank line at the end of the file
(EndingBlankLine)
🤖 Prompt for AI Agents
Source: Coding guidelines