Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions .dockerignore
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
89 changes: 71 additions & 18 deletions .env.example
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
Comment on lines +1 to +74

Copy link
Copy Markdown

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:

#!/bin/bash
# Description: Find all env variable accesses in server code and compare with .env.example

# Extract env keys from .env.example (non-comment, non-empty lines)
grep -E '^[A-Z_]+=' .env.example | sed 's/=.*//' | sort -u > /tmp/env_example_keys.txt

# Find all env.* and env['.*'] accesses in server code
rg -nP 'env\.[A-Z_]+|env\[[\'"][A-Z_]+[\'"]\]' src/lib/server src/routes --type=ts | \
  grep -oP 'env\.\K[A-Z_]+|env\[\K[\'"]\K[A-Z_]+' | sort -u > /tmp/env_code_keys.txt

echo "=== In code but not in .env.example ==="
comm -23 /tmp/env_code_keys.txt /tmp/env_example_keys.txt

echo "=== In .env.example but maybe not in code ==="
comm -13 /tmp/env_code_keys.txt /tmp/env_example_keys.txt

Repository: timoa/workflow-metrics

Length of output: 221


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Files =="
git ls-files .env.example README* src | sed -n '1,200p'

echo
echo "== .env.example keys =="
grep -E '^[A-Z_][A-Z0-9_]*=' .env.example | sed 's/=.*//' | sort -u

echo
echo "== Server env usages (src/lib/server, src/routes) =="
python3 - <<'PY'
import re, pathlib
roots = [pathlib.Path('src/lib/server'), pathlib.Path('src/routes')]
pat = re.compile(r'\benv\.([A-Z_][A-Z0-9_]*)|\benv\[[\'"]([A-Z_][A-Z0-9_]*)[\'"]\]')
keys = set()
for root in roots:
    if not root.exists():
        continue
    for path in root.rglob('*.ts'):
        txt = path.read_text(encoding='utf-8', errors='ignore')
        for m in pat.finditer(txt):
            keys.add(m.group(1) or m.group(2))
for k in sorted(keys):
    print(k)
PY

echo
echo "== README env mentions (best effort) =="
rg -n --hidden -S 'BETTER_AUTH_SECRET|BETTER_AUTH_URL|GITHUB_CLIENT_ID|GITHUB_CLIENT_SECRET|GITHUB_APP_ID|GITHUB_APP_PRIVATE_KEY|GITHUB_APP_SLUG|PUBLIC_APP_URL|HOST|PORT|MISTRAL_API_KEY|DATABASE_URL|POSTGRES_PASSWORD|PUBLIC_SUPABASE_URL|PUBLIC_SUPABASE_ANON_KEY|SUPABASE_SERVICE_ROLE_KEY' README* .github docs 2>/dev/null || true

Repository: timoa/workflow-metrics

Length of output: 8278


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Call sites =="
rg -n -C 3 '\b(AI_OPTIMIZATION_MODEL|CACHE)\b' src/lib/server src/routes README.md .github/workflows || true

echo
echo "== README env table area =="
sed -n '150,250p' README.md

Repository: timoa/workflow-metrics

Length of output: 6014


Add AI_OPTIMIZATION_MODEL to .env.example
src/lib/server/config/app-config.ts reads 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
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.env.example around lines 1 - 74, Add the missing AI_OPTIMIZATION_MODEL
placeholder to .env.example so it matches src/lib/server/config/app-config.ts
and the README. Place it near the other app/server configuration variables,
using a clear commented example value so local setup users know to set the
override consistently.

Source: Coding guidelines

76 changes: 76 additions & 0 deletions Dockerfile
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🌐 Web query:

In SvelteKit 2.x, are $env/static/publicvalues substituted at build time, and can runtime environment variables override them in a built@sveltejs/adapter-node app?

💡 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 PUBLIC_* values into the image. Dockerfile:32-38 will inline these $env/static/public values at build time, so the placeholders ship in the bundle and runtime env_file/Helm env vars cannot override them. Switch this code to $env/dynamic/public or keep real build-time values out of the image.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Dockerfile` around lines 32 - 38, Remove the placeholder PUBLIC_* ENV block
from the Dockerfile and stop relying on build-time inlined public env values;
the issue is that these values are baked into the SvelteKit bundle via
$env/static/public, so runtime overrides cannot replace them. Update the
Supabase/public env usage in the relevant app code to use $env/dynamic/public
where appropriate, or otherwise ensure the build does not require placeholder
values in the image. Use the Dockerfile public env section and the SvelteKit env
imports as the key places to adjust.


# 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"]
43 changes: 43 additions & 0 deletions docker-compose.postgres.yml
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
37 changes: 37 additions & 0 deletions docker-compose.yml
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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 3000, and Dockerfile Line 72 probes 3000. Any PORT override makes the service unreachable and unhealthy.

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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"
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: 3000
NODE_ENV: production
ports:
- "${PORT:-3000}:3000"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docker-compose.yml` around lines 29 - 36, The container’s internal listen
port is being changed by the PORT environment override while the service mapping
and health probe still assume port 3000, so make the app listen on a fixed
container port unless the whole stack is updated together. In
docker-compose.yml, keep PORT aligned with the published container port or stop
using it to control the in-container bind port, and ensure the Dockerfile health
check/probe for the app matches the same fixed port used by the service.

restart: unless-stopped
7 changes: 3 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"deploy": "pnpm run build && pnpm exec wrangler pages deploy .svelte-kit/cloudflare --project-name=workflow-metrics",
"start": "node build/index.js",
"prepare": "svelte-kit sync || echo ''",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
Expand All @@ -43,7 +43,7 @@
},
"devDependencies": {
"@eslint/js": "9.39.3",
"@sveltejs/adapter-cloudflare": "7.2.8",
"@sveltejs/adapter-node": "5.5.7",
"@sveltejs/kit": "^2.60.1",
"@sveltejs/vite-plugin-svelte": "6.2.4",
"@tailwindcss/vite": "4.3.0",
Expand All @@ -68,7 +68,6 @@
"typescript": "5.9.3",
"typescript-eslint": "8.56.1",
"vite": "6.4.3",
"vitest": "3.2.6",
"wrangler": "4.69.0"
"vitest": "3.2.6"
}
}
Loading
Loading