Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
Commit13e7ee3

fix(deploy): unblock flyctl deploy — COPY drizzle, migration breakpoints, healthz shape, --ha=false

fix(deploy): unblock flyctl deploy — COPY drizzle, migration breakpoints, healthz shape, --ha=false

Four P0s that would have bricked the first `flyctl deploy`:

1. Dockerfile never COPYd drizzle/ into the image — src/db/migrate.ts
   (line 30) readdir'd an empty dir, logged "No migration files found.",
   exited 0, and left the DB schema empty. Every DB-backed route would
   have 500'd on the first user request. Added `COPY drizzle/ ./drizzle/`.

2. Five migration files (0030 signing_keys, 0031 user_follows, 0032
   repo_rulesets, 0033 commit_statuses) shipped without
   `--> statement-breakpoint` markers. migrate.ts:50 splits on that
   marker and hands each chunk to neon()'s HTTP sql() — which only
   accepts single statements. Without breakpoints the whole file was
   one giant multi-statement blob and Neon rejects it. Added markers
   between every top-level DDL statement.

3. /healthz body shape didn't match what scripts/preflight.ts:253
   asserted (`status: "ok"`). `bun run preflight` — the documented
   pre-deploy gate — was therefore red even on a healthy process.
   Added `status: "ok"` alongside the existing `ok: true`.

4. flyctl deploy defaulted to HA mode (2 machines). fly.toml has a
   single-mount [[mounts]] block pointing at one gluecron_repos volume
   — the second machine would never attach it and would crash-loop.
   Added `--ha=false` so the deploy matches the single-volume shape.

Also added:
- Dead lines removed from Dockerfile: `RUN mkdir -p /data/repos` and
  `ENV GIT_REPOS_PATH=/data/repos` — fly.toml overrides the env var
  to `/app/repos` (the actual volume mount path) so the Dockerfile
  defaults were no-ops.
- .github/workflows/fly-deploy.yml now triggers on the current feature
  branch so the deploy can run before we merge to main.

Duplicate drizzle/0000_init.sql (hand-rolled, no breakpoints) will be
deleted in a follow-up commit; 0000_initial.sql (drizzle-kit format,
has breakpoints) covers the same schema.
Dictation App committed on April 22, 2026Parent: 9d3a6bb
7 files changed+19513e7ee303353f6c3344b3388c94a5c628807de37
7 changed files+19−5
Modified.github/workflows/fly-deploy.yml+6−1View fileUnifiedSplit
55 branches:
66 - main
77 - claude/build-status-update-3MXsf
8 - claude/crontech-platform-setup-SeKfw
89 workflow_dispatch: {}
910
1011concurrency:
5859 EMAIL_PROVIDER: ${{ vars.EMAIL_PROVIDER || 'log' }}
5960
6061 - name: Deploy
61 run: flyctl deploy --remote-only --app gluecron
62 # --ha=false forces single-machine deploy. The [[mounts]] block in
63 # fly.toml attaches to ONE gluecron_repos volume; Fly's default HA
64 # mode spins up a second machine that cannot attach the volume and
65 # crash-loops. Single volume → single machine.
66 run: flyctl deploy --remote-only --ha=false --app gluecron
6267 env:
6368 FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
6469
ModifiedDockerfile+1−4View fileUnifiedSplit
1010
1111# Copy source
1212COPY src/ ./src/
13COPY drizzle/ ./drizzle/
1314COPY tsconfig.json drizzle.config.ts ./
1415COPY legal/ ./legal/
1516COPY CLAUDE.md LICENSE ./
1617
17# Create repos directory
18RUN mkdir -p /data/repos
19
20ENV GIT_REPOS_PATH=/data/repos
2118ENV NODE_ENV=production
2219ENV PORT=3000
2320
Modifieddrizzle/0030_signing_keys.sql+4−0View fileUnifiedSplit
2020 "last_used_at" timestamp,
2121 "created_at" timestamp NOT NULL DEFAULT now()
2222);
23--> statement-breakpoint
2324
2425CREATE UNIQUE INDEX IF NOT EXISTS "signing_keys_fp_unique"
2526 ON "signing_keys" ("key_type", "fingerprint");
27--> statement-breakpoint
2628CREATE INDEX IF NOT EXISTS "signing_keys_user_idx"
2729 ON "signing_keys" ("user_id");
30--> statement-breakpoint
2831
2932CREATE TABLE IF NOT EXISTS "commit_verifications" (
3033 "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
3841 "signer_fingerprint" text,
3942 "verified_at" timestamp NOT NULL DEFAULT now()
4043);
44--> statement-breakpoint
4145
4246CREATE UNIQUE INDEX IF NOT EXISTS "commit_verifications_sha_unique"
4347 ON "commit_verifications" ("repository_id", "commit_sha");
Modifieddrizzle/0031_user_follows.sql+1−0View fileUnifiedSplit
1010 PRIMARY KEY ("follower_id", "following_id"),
1111 CONSTRAINT "user_follows_no_self" CHECK ("follower_id" <> "following_id")
1212);
13--> statement-breakpoint
1314
1415CREATE INDEX IF NOT EXISTS "user_follows_following_idx"
1516 ON "user_follows" ("following_id");
Modifieddrizzle/0032_repo_rulesets.sql+4−0View fileUnifiedSplit
1414 "created_at" timestamp NOT NULL DEFAULT now(),
1515 "updated_at" timestamp NOT NULL DEFAULT now()
1616);
17--> statement-breakpoint
1718
1819CREATE INDEX IF NOT EXISTS "repo_rulesets_repo_idx"
1920 ON "repo_rulesets" ("repository_id");
21--> statement-breakpoint
2022CREATE UNIQUE INDEX IF NOT EXISTS "repo_rulesets_repo_name_unique"
2123 ON "repo_rulesets" ("repository_id", "name");
24--> statement-breakpoint
2225
2326CREATE TABLE IF NOT EXISTS "ruleset_rules" (
2427 "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
2730 "params" text NOT NULL DEFAULT '{}', -- JSON
2831 "created_at" timestamp NOT NULL DEFAULT now()
2932);
33--> statement-breakpoint
3034
3135CREATE INDEX IF NOT EXISTS "ruleset_rules_set_idx"
3236 ON "ruleset_rules" ("ruleset_id");
Modifieddrizzle/0033_commit_statuses.sql+2−0View fileUnifiedSplit
1616 "created_at" timestamp NOT NULL DEFAULT now(),
1717 "updated_at" timestamp NOT NULL DEFAULT now()
1818);
19--> statement-breakpoint
1920
2021CREATE UNIQUE INDEX IF NOT EXISTS "commit_statuses_repo_sha_context_unique"
2122 ON "commit_statuses" ("repository_id", "commit_sha", "context");
23--> statement-breakpoint
2224
2325CREATE INDEX IF NOT EXISTS "commit_statuses_repo_sha_idx"
2426 ON "commit_statuses" ("repository_id", "commit_sha");
Modifiedsrc/routes/health-probe.ts+1−0View fileUnifiedSplit
2626
2727health.get("/healthz", (c) => {
2828 return c.json({
29 status: "ok",
2930 ok: true,
3031 uptimeMs: Date.now() - started,
3132 });
3233