Skip to content

feat(redirect): migrate f3-redirect into the monorepo as apps/redirect#311

Draft
pstaylor-patrick wants to merge 4 commits into
devfrom
feat/redirect-app
Draft

feat(redirect): migrate f3-redirect into the monorepo as apps/redirect#311
pstaylor-patrick wants to merge 4 commits into
devfrom
feat/redirect-app

Conversation

@pstaylor-patrick

@pstaylor-patrick pstaylor-patrick commented May 24, 2026

Copy link
Copy Markdown
Collaborator

What

Migrates the standalone f3-redirect repo into this monorepo as a single application with three tiers under apps/redirect/.

apps/redirect/
├── server/   Go redirect tier (redirectd + f3redirect CLI) — the monorepo's first Go code
├── web/      Next.js self-serve admin (Better Auth email+password + passkey, Drizzle/postgres, GCS export)
├── shared/   Go↔TS DNS-rule contract fixture (dns-instructions.json)
└── infra/terraform/   IaC for the GCE redirect VM + Cloud Run admin web, with drift-detection CI

Why this shape — gold standard for multilingual packages

This is the first Go code and the first paired frontend+backend in an otherwise-TypeScript monorepo. The pattern is intended as the template Python packages will follow:

  • The Go tier is a turbo workspace package via a thin package.json whose scripts shell to go (build/test/lint/typecheck/format). Turbo orchestrates Go and TS tiers uniformly — no bespoke CI lane.
  • The shared CI (ci.yml) gains actions/setup-go in every job that runs a turbo task the Go package participates in, plus a Postgres-backed test job, so pnpm lint/typecheck/build/test transparently cover both languages.
  • A shared/ dir holds the single source-of-truth DNS-rule fixture that both the Go parity test and the TS parity test assert against.

Monorepo alignment

  • apps/redirect/{web,server,shared} added explicitly to the pnpm workspace globs (so infra/terraform isn't treated as a package).
  • Web app adopts the flat eslint.config.js (@acme/eslint-config) + @acme/tsconfig.
  • Web app pinned to React 18.3.1 to match the other React-18 apps.
  • Go module path: github.com/F3-Nation/f3-nation/apps/redirect/server.
  • The web app's env vars are declared in turbo.json globalEnv (Better Auth / Google OAuth / passkey / redirect / config / export) so turbo/no-undeclared-env-vars passes during next build.
  • The redirect-web DB client is instantiated lazily (memoized proxy) so next build page-data collection doesn't connect (and fail) when DATABASE_URL is absent.
  • Normalized packages/db/eslint.config.mjs.js to match every sibling package.

Deploy model

  • Server (deploy-redirect-server.yml): on push to dev (path-filtered to the Go tier) — build image, push to Artifact Registry, roll the GCE VM. Guarded with if: github.ref == 'refs/heads/dev' so workflow_dispatch can't ship unmerged code to prod.
  • Web (deploy-redirect-web.yml): on push to dev (path-filtered to the web tier) — build + deploy to Cloud Run, same dev-ref guard.
  • Drift detection (redirect-terraform-drift.yml): terraform plan against the live infra; comments on same-repo PRs (skipped on fork PRs where the token is read-only).

⚠️ Infrastructure caveat — v1 is a personal sandbox

Intentional for v1 and called out so it isn't lost.

  • v1 runs in Patrick's personal, self-funded GCP project f3-redirects (WIF provider / deployer SA / Terraform state bucket are personal-sandbox identifiers). Terraform project no longer has a live default — it must be supplied per environment (the drift CI passes it via TF_VAR_project).
  • Long-term must pivot to an F3 Nation org-owned/funded GCP project (ask Tackle to provision); at that cutover the Terraform backend moves off the personal state bucket via -backend-config, and the interim Cloud SQL database is replaced by an app-specific schema + service principal in the F3PROD data warehouse (the Codex / PaxVault pattern). Neon Postgres is off the table.
  • v1 intentionally deploys the mutable :latest image tag (the VM pulls it on boot/restart); immutable tags/digests are deferred to the org-project migration.

Review hardening (this PR)

Resolved 32 CodeRabbit threads; applied the verified fixes, including:

  • Critical: POST /api/domains now rolls back the DB claim + returns 503 if the GCS publish fails (no stale-but-claimed domain that 409s on retry). PUT surfaces the same publish failure as a 503.
  • Go: stronger hostname validation (rejects a..com, .x.com, foo/bar.com); Live.Config() returns a defensive copy; NewAdminProxy rejects non-absolute URLs; AdminHost normalized before comparison; GCS lock inspection propagates unexpected errors; dns CLI rejects extra args.
  • Web: passkey credentialID is a UNIQUE index; registration rejects bare public suffixes; auth errors announced to assistive tech (role="alert"); delete failures surfaced in the UI; test stability (waitFor, vi.stubGlobal/unstubAllGlobals); reset-test-db is a true drop-and-recreate.

The repo-wide with-env env-loader suggestions were declined: the redirect web app is intentionally self-contained (its own .env scheme) until it moves to the org-owned project.

Validation

All run in-monorepo via turbo / CI:

  • typecheck, lint, format-check, build — green (TS + Go)
  • test — Go: certstore/mappings/redirect pass; Web: vitest unit + Postgres-backed integration pass
  • test-coverage and Terraform drift — green

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Multi-tenant HTTPS redirect service with on-demand TLS, admin CLI, web dashboard, passkey auth, live-reloadable config export, and certificate cleanup/DNS guidance.
  • Documentation

    • Comprehensive redirect service README covering architecture, local dev, Terraform deployment, and DNS instructions.
  • Chores

    • CI/CD workflows added/updated to build, test, and deploy server and web (GCE VM and Cloud Run); Terraform infra and VM/container startup automation added.
  • Tests

    • Expanded unit, integration, parity, and end-to-end test suites across server and web.

@coderabbitai

coderabbitai Bot commented May 24, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds a multi-tenant HTTPS redirect service: a Go redirect server and CLI with CertMagic+GCS cert storage, a Next.js admin web app (DB, auth, UI, API), Terraform GCP infra (Bucket, Artifact Registry, VM, Cloud Run mapping), Docker builds, CI/CD workflows, tests (unit, integration, parity, E2E), and tooling scripts/configs.

Changes

F3 Redirect Service

Layer / File(s) Summary
CI jobs & deploy workflows
.github/workflows/ci.yml, .github/workflows/deploy-redirect-server.yml, .github/workflows/deploy-redirect-web.yml, .github/workflows/redirect-terraform-drift.yml
Adds Go 1.26 setup to CI jobs; new deploy workflows that build/push Docker images and deploy redirect server (GCE VM reset) and web (Cloud Run) via Workload Identity Federation; adds Terraform drift workflow with PR comments.
Terraform infra
apps/redirect/infra/terraform/{versions.tf,variables.tf,main.tf,outputs.tf}, .terraform.lock.hcl, startup-script.sh.tftpl, terraform.tfvars.example, ci.tf, domain-mapping.tf
GCP resources: enable APIs, GCS bucket (config/certs), Artifact Registry, service account/IAM, reserved static IP, firewall, and a Container-Optimized VM with templated startup script; adds CI WIF resources and optional Cloud Run domain mapping.
Docker images & contexts
apps/redirect/server/Dockerfile, apps/redirect/server/.dockerignore, apps/redirect/web/Dockerfile, apps/redirect/web/.dockerignore, .dockerignore
Multi-stage builds for redirectd (Go) and Next.js web; dockerignore entries to minimize build contexts.
Go server module & scripts
apps/redirect/server/go.mod, apps/redirect/server/package.json, apps/redirect/server/scripts/*
New Go module (go 1.26), npm package manifest for server tooling, and shell scripts for build, format, lint, test, and coverage gate.
Mappings model & stores
apps/redirect/server/internal/mappings/*
Mapping and Config types, normalization/validation, Resolve/IsRegistered, Hosts/Upsert/Remove, deterministic Marshal/Unmarshal, Store interface, FileStore and GCSStore implementations, and unit tests.
Cert storage & locking
apps/redirect/server/internal/certstore/gcs.go, gcs_test.go
GCS-backed certmagic.Storage with Store/Load/Delete/Exists/List/Stat and Lock/Unlock implementing conditional-create locking with TTL-based steal; integration tests use fake-gcs-server.
Redirect handler & runtime
apps/redirect/server/internal/redirect/{redirect.go,proxy.go,live.go}, tests, integration
Redirect Handler with health check, admin proxying, status enforcement, Live hot-reload snapshot, admin reverse-proxy helper, and unit/integration tests validating behavior and reload semantics.
TLS/ACME wiring
apps/redirect/server/internal/server/server.go
CertMagic configuration builder with OnDemand decision function, ACME issuer registration (staging/production), and TLS config returned for server use.
redirectd daemon
apps/redirect/server/cmd/redirectd/main.go
Daemon entrypoint wiring storage selection, Live watcher, TLS/ACME Build, concurrent HTTP/HTTPS listeners, and graceful shutdown logic.
f3redirect CLI
apps/redirect/server/cmd/f3redirect/main.go
CLI for list/add/remove/validate/dns with file or GCS-backed storage selection and environment fallbacks.
Web DB, auth, and schema
apps/redirect/web/src/db/*, apps/redirect/web/src/auth.ts
Drizzle schema for auth, domain, and passkey tables; DB client handling (including Cloud SQL unix-socket format); Better Auth setup with passkey plugin and optional Google OAuth.
Web domains lib & parity tests
apps/redirect/web/src/lib/domains.ts, domains.test.ts, domains.parity.test.ts, apps/redirect/shared/dns-instructions.json
TS domain normalization, Zod validation, DNS instruction generation, and parity tests asserting Go/TS implementations match shared JSON fixture.
Web API routes & tests
apps/redirect/web/src/app/api/domains/*.ts, route.test.ts, apps/redirect/web/src/app/api/auth/[...all]/route.ts, apps/redirect/web/src/app/api/domains/[id]/route.ts
Authenticated endpoints for listing, creating, updating, and deleting domains with validation, conflict handling, export triggering, and best-effort cert cleanup; covered by Vitest tests.
Web export & cert cleanup
apps/redirect/web/src/lib/gcs-export.ts, gcs-export.test.ts
Exports domain mappings to GCS or local file and deletes certificate objects for a host by path-segment matching; includes tests.
Web UI & tests
apps/redirect/web/src/components/{AuthForm,Dashboard}.tsx, tests
Auth form (email/passkey), Dashboard UI for domain management (add/edit/delete), DNS sheet UI, passkey setup, and RTL/Vitest tests.
Pages, layout, styles & test setup
apps/redirect/web/src/app/{layout,page,dashboard}, globals.css, src/test-setup.ts, src/vitest.d.ts, vitest.config.ts, tsconfig.json, drizzle.config.ts, eslint.config.js
Root layout, landing and dashboard pages, comprehensive global CSS (bottom-sheet, DNS UI), Vitest test setup with jest-dom augmentations, types, and project tool configs.
E2E and tooling
apps/redirect/web/e2e/*, scripts/local-e2e.ts, scripts/reset-test-db.mjs, playwright.config.ts
Playwright E2E specs (admin/passkey), local E2E/test DB reset scripts, and tooling configs for running integration tests locally/CI.
Workspace & shared package
pnpm-workspace.yaml, apps/redirect/shared/package.json
Explicit workspace globs for redirect packages and a small shared package exposing the DNS fixture asset.
Docs & README
apps/redirect/README.md
Comprehensive README documenting architecture, development, testing, deployment, and DNS instructions.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • dnishiyama
  • taterhead247
  • evanpetzoldt

🐰 I hopped through code and clouds above,
Built redirects with a rabbit’s love,
Certificates dancing, DNS in tune,
Deploys at dusk, tests pass by noon!

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/redirect-app

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 34

🤖 Prompt for all review comments with 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.

Inline comments:
In @.github/workflows/ci.yml:
- Around line 46-49: Replace the moving tag uses: actions/setup-go@v5 with an
immutable pinned commit SHA in every CI job that currently uses that tag (search
for the literal "uses: actions/setup-go@v5"); update each occurrence to use the
corresponding full commit SHA for the actions/setup-go action (same major
version but pinned) so the workflow uses a fixed, verifiable reference instead
of the floating `@v5` tag.

In @.github/workflows/deploy-redirect-server.yml:
- Around line 64-72: The workflow currently pushes and boots the VM using the
mutable :latest tag (IMAGE:latest) which is non-deterministic; instead push only
the immutable build image (IMAGE:${GITHUB_SHA}) and obtain its content digest
(sha256) after push, then use that digest reference (IMAGE@sha256:<DIGEST>) or a
versioned tag (not :latest) when instructing the VM to pull/run; update the
steps that push IMAGE:latest and the VM reset logic so the VM startup pulls the
explicit IMAGE@sha256 digest (or the GITHUB_SHA tag) and remove
pushing/depending on :latest to ensure deterministic deploys (references: IMAGE,
GITHUB_SHA, INSTANCE, ZONE, PROJECT).
- Around line 43-51: Replace the mutable action references and image tag with
immutable SHAs and a deterministic image tag: pin actions currently referenced
as actions/checkout@v4, google-github-actions/auth@v2 and
google-github-actions/setup-gcloud@v2 to their corresponding commit SHAs (use
the commit SHA for each action release), and change any image push/pull using
IMAGE:latest to use the commit-specific tag (e.g. ${IMAGE}:${GITHUB_SHA} or
${IMAGE}:${{ github.sha }}), updating the workflow steps that build/push and the
VM pull step to use that same SHA-tagged image so deployments are deterministic.

In @.github/workflows/deploy-redirect-web.yml:
- Around line 45-52: Pin the GitHub Actions usages to immutable commit SHAs
instead of tags for the referenced actions (replace uses: actions/checkout@v4,
google-github-actions/auth@v2, google-github-actions/setup-gcloud@v2, and
google-github-actions/deploy-cloudrun@v2 with their corresponding commit-SHA
refs) and update the Docker build step that currently runs docker build --file
apps/redirect/web/Dockerfile . to use the app folder as the build context
(docker build --file apps/redirect/web/Dockerfile apps/redirect/web) or
alternatively add the necessary ignore patterns to the repository root
.dockerignore so apps/redirect/web/.dockerignore is honored.
- Around line 67-70: The workflow runs docker build with build context set to
repo root (the command using --file apps/redirect/web/Dockerfile and context
"."), so apps/redirect/web/.dockerignore is ignored and unwanted files get
included by the Dockerfile's COPY . .; fix by moving the ignore patterns from
apps/redirect/web/.dockerignore into the repository root .dockerignore or rename
apps/redirect/web/.dockerignore to apps/redirect/web/Dockerfile.dockerignore so
Docker will apply those patterns when building the Dockerfile at
apps/redirect/web/Dockerfile.

In `@apps/redirect/infra/terraform/main.tf`:
- Line 72: Replace the global default VPC binding (network = "default") with a
dedicated VPC and subnet and wire the VM and firewall resources to that network:
create/declare a google_compute_network (e.g., redirectd_network) and a
google_compute_subnetwork (e.g., redirectd_subnet) and update any
google_compute_instance and google_compute_firewall resources that currently use
network = "default" to reference the new network/subnet; tighten the firewall
rules to only allow the explicit ingress/egress ports required by redirectd
(remove broad SSH/project-global rules) and ensure the instance's
network_interface uses the new subnetwork.
- Around line 106-120: The instance metadata block needs explicit SSH hardening:
add metadata keys "block-project-ssh-keys" = "true" to prevent project-wide SSH
keys and, if SSH access is required, add "enable-oslogin" = "true" to enforce OS
Login; update the same metadata map where startup-script is set (the metadata
variable named metadata and the startup-script/templatefile usage) to include
these two keys so the instance enforces OS Login and blocks project keys.
- Around line 18-23: The google_storage_bucket resource "data" currently sets
uniform_bucket_level_access but not public access prevention; update the
resource block for google_storage_bucket.data to add public_access_prevention =
"enforced" so future IAM public grants are blocked—locate the resource named
google_storage_bucket "data" and insert the public_access_prevention attribute
with value "enforced" (then re-run terraform plan/apply).

In `@apps/redirect/infra/terraform/startup-script.sh.tftpl`:
- Line 5: Replace the shell options invocation that currently reads "set -uo
pipefail" with "set -euo pipefail" in the startup script template so the script
exits immediately on any command failure; update the invocation of the set
builtin (the "set -uo pipefail" line in
apps/redirect/infra/terraform/startup-script.sh.tftpl) accordingly and ensure no
other later code overrides the errexit behavior.

In `@apps/redirect/infra/terraform/variables.tf`:
- Around line 42-46: The variable "redirect_status" currently accepts any
string; add a Terraform validation block to the variable "redirect_status" that
enforces allowed values ("301" or "302") by using a condition like
contains(["301","302"], var.redirect_status) and provide a clear error_message
indicating the valid choices so invalid deploy-time values are rejected.

In `@apps/redirect/README.md`:
- Around line 100-104: Update the CI/CD subsection in README.md to match the
actual workflow files and triggers present in this PR: replace references to
`.github/workflows/ci.yaml` and `.github/workflows/deploy.yaml` and the
statement “every push to main” with the real workflow filenames and trigger
rules used in this PR (e.g., the actual CI workflow name, the actual deployment
workflow name, and their push/PR/branch/tag triggers), and correct any
deployment details (image push targets, VM rollout behavior, and auth method
such as Workload Identity Federation) so the documentation reflects the current
config.
- Around line 36-44: The fenced code block in the README is missing a language
tag; update the block around the listed commands so the opening fence is
"```text" (instead of just "```") to satisfy markdown linting and ensure
consistent rendering — modify the README.md fenced block that contains the lines
starting with "cmd/redirectd", "cmd/f3redirect", etc., to use ```text as the
opening fence.

In `@apps/redirect/server/cmd/redirectd/main.go`:
- Around line 66-68: Detect and fail fast when the admin proxy config is
partial: if either ADMIN_HOST (normalized into adminHost) or ADMIN_UPSTREAM
(adminUpstream) is set but the other is empty, log a clear error and exit
instead of silently disabling the proxy; implement this check in main (around
where adminHost/adminUpstream are read). Also stop logging raw adminUpstream
URLs — parse adminUpstream (e.g., with url.Parse) and log only the redacted host
or scheme+host (no userinfo, query, or path) or a masked value to avoid leaking
credentials/secrets wherever adminUpstream is printed.

In `@apps/redirect/server/Dockerfile`:
- Around line 11-12: The Dockerfile uses the mutable tag
"gcr.io/distroless/static-debian12" in the FROM instruction; update that FROM
line in apps/redirect/server/Dockerfile to pin the distroless runtime image by
its immutable digest (use the recommended sha256 value) so builds are
reproducible and resistant to upstream drift, leaving the existing COPY
--from=build /out/redirectd /redirectd line unchanged.

In `@apps/redirect/server/internal/certstore/gcs_test.go`:
- Around line 30-35: The TestMain currently treats emulator setup failure as a
skip and exits with 0, which can produce false-green CI runs; update TestMain so
that when withEmulator(m) returns an error you log the error (using fmt.Println
or t.Log equivalent) and exit with a non-zero status (os.Exit(1)) instead of
os.Exit(0), locating the change in the TestMain function that calls withEmulator
to ensure CI fails when the emulator cannot be set up.

In `@apps/redirect/server/internal/certstore/gcs.go`:
- Around line 223-229: Unlock currently deletes the lock object unconditionally
(in GCS.Unlock), which can release a lock owned by another process; change
Unlock to first fetch the lock object's attributes via
g.lockObj(name).Attrs(ctx), verify the stored owner identifier (e.g., metadata
key used when acquiring the lock, such as g.ownerID or g.lockToken) matches this
instance's owner value, and only then delete using a generation precondition to
avoid races (call g.lockObj(name).If(storage.Conditions{GenerationMatch:
attrs.Generation}).Delete(ctx)); if the owner does not match return a not-owner
error or nil as appropriate.
- Around line 207-210: Capture and check the error returned from
g.client.Bucket(g.bucket).Object(obj.ObjectName()).If(storage.Conditions{GenerationMatch:
attrs.Generation}).Delete(ctx) instead of discarding it; assign it to a variable
(e.g., err := ...Delete(ctx)), then handle non-nil errors appropriately (log and
return the error or propagate it) so real GCS delete failures aren’t masked
during lock-steal attempts—only ignore the specific benign "object not found"
case if applicable, otherwise treat errors from Delete(ctx) as actionable.
- Around line 183-184: The current lock key generation replaces "/" with "_"
(variable safe) which causes collisions (e.g. "a/b" vs "a_b"); change the
encoding to a collision-free form (for example URL-escape the full name or
compute a deterministic hash like sha256 of name and use its hex) and use that
encoded value when building the GCS object path (the expression that calls
g.client.Bucket(g.bucket).Object(g.prefix + "locks/" + safe + ".lock")). Ensure
you replace the current safe computation with the chosen collision-free encoding
so each distinct name maps to a unique lock key.

In `@apps/redirect/server/internal/mappings/dns.go`:
- Around line 39-47: When IsApex(host) is true, guard against an empty
opt.StaticIP before emitting a required A record: check if opt.StaticIP == ""
and instead of returning a required A with an empty Value, return a sentinel
DNSRecord (same Type "A" and Name host) whose Note explicitly states "StaticIP
not set — cannot create apex A record; configure StaticIP" and mark
Optional=true so callers don't treat it as a valid required instruction; update
the logic around IsApex and DNSRecord construction to use this guard (refer to
IsApex, DNSRecord, and opt.StaticIP).

In `@apps/redirect/server/internal/mappings/gcsstore.go`:
- Around line 59-67: The current write in Save unconditionally overwrites the
object (using s.client.Bucket(s.bucket).Object(s.object).NewWriter), which
allows concurrent writes to clobber changes; change Save to use generation-based
preconditions: first call s.client.Bucket(s.bucket).Object(s.object).Attrs(ctx)
to read the current Generation (use 0 if object missing), then create the writer
with
s.client.Bucket(s.bucket).Object(s.object).IfGenerationMatch(generation).NewWriter(ctx)
so the write will fail on concurrent modifications; propagate the precondition
failure error (HTTP 412 / storage precondition error) to callers so they can
reload and retry instead of silently overwriting.

In `@apps/redirect/server/internal/mappings/store.go`:
- Around line 31-39: Unmarshal currently returns any syntactically valid JSON as
Config even if mappings are semantically invalid; update Unmarshal to validate
the parsed Config before returning by invoking the same validation used at
save-time (e.g., call cfg.Validate() or the existing Save-time guard/validation
function) and return an error if validation fails so invalid hosts/targets
cannot be loaded into runtime; ensure you reference the Config type and reuse
the Save validation logic rather than duplicating ad-hoc checks.

In `@apps/redirect/server/internal/redirect/live.go`:
- Around line 52-54: The watcher currently passes interval directly to
time.NewTicker in Live.Watch which panics for zero or negative durations; add a
guard at the start of Live.Watch to check interval <= 0 and handle it (e.g., set
interval to a safe default like 30s or return early and call onErr), then create
the ticker with the validated interval; update any related comments and keep
references to Live.Watch and time.NewTicker so reviewers can locate the change.

In `@apps/redirect/server/internal/redirect/redirect.go`:
- Around line 54-55: The code calls h.Resolver.Resolve(host) without checking
h.Resolver for nil (NewHandler(nil, ...) is allowed) which can panic; before
invoking Resolve (in the request handling function that calls
h.Resolver.Resolve), add a nil check for h.Resolver and return a controlled
error response (or log and return an appropriate HTTP error) when it's nil
instead of dereferencing it; ensure the check preserves existing health-check
behavior and reference the h.Resolver field and the Resolve call so reviewers
can find and update that code path.

In `@apps/redirect/web/scripts/local-e2e.ts`:
- Around line 23-24: The script currently calls db.delete(domain) and
db.delete(user) which will perform full-table deletes; update the cleanup in
local-e2e.ts to avoid destructive blanket deletes by scoping deletions or gating
them: restrict deletes with explicit where clauses (e.g., only delete rows
created by this test via a test-run ID or specific keys), or check and assert a
safe test DATABASE_URL / NODE_ENV before allowing db.delete(domain) /
db.delete(user); ensure the modifications are applied at both occurrences (the
delete calls around lines 23-24 and 120-121) and fail fast if the environment
check does not pass.
- Around line 73-84: The catch block around db.insert(domain).values(...)
currently sets claimed = true for any exception; change it to only treat
uniqueness/duplicate-key errors as a successful "already claimed" case by
inspecting the thrown error (e.g., error.code or error.message) and setting
claimed = true only when it matches the DB unique constraint/duplicate-host
error, otherwise rethrow or fail the test; update the catch around the insert
(the block that sets claimed) so unrelated DB errors do not make assert(claimed,
...) pass.

In `@apps/redirect/web/src/app/api/domains/`[id]/route.ts:
- Around line 42-43: The DB commit should not be coupled to the synchronous
export; find the places calling exportConfigToGCS() in the route handler(s) and
stop awaiting it after a successful write: instead either enqueue the export
(push a message to your job/outbox queue) or kick it off in background (call
exportConfigToGCS() without await and attach .catch(...) to log failures), and
update the HTTP response to return success for the committed change with an
optional field like exportStatus: "pending" or "failed" so request semantics
remain consistent while export is retried asynchronously.

In `@apps/redirect/web/src/app/api/domains/route.test.ts`:
- Around line 40-46: The tests call db.delete/db.insert (see cleanup(),
beforeAll and the db.delete(domain)/db.delete(user) calls) before the schema
exists; fix by ensuring migrations/reset run before any DB ops: invoke your test
DB reset (e.g., resetTestDb or a drizzle-kit push --force / reset-test-db
script) in the global test setup or at the top of this test file before
cleanup()/beforeAll so the domain and user tables are created; update
src/test-setup.ts or add a globalSetup hook to run the migration/reset command,
or explicitly call the reset helper before calling cleanup()/db.* in this test.

In `@apps/redirect/web/src/auth.ts`:
- Around line 23-30: The socialProviders block enables Google auth when only
GOOGLE_CLIENT_ID exists and sets clientSecret to "" if GOOGLE_CLIENT_SECRET is
missing; change this to enable the google provider only when both
process.env.GOOGLE_CLIENT_ID and process.env.GOOGLE_CLIENT_SECRET are present
and non-empty, remove the fallback empty string for clientSecret, and set
socialProviders to undefined otherwise (update the object creation around
socialProviders -> google -> clientId/clientSecret to use a combined truthy
check for both env vars).

In `@apps/redirect/web/src/components/AuthForm.tsx`:
- Around line 16-37: The submit function currently uses try/finally but no
catch, so exceptions thrown by signIn.email or signUp.email bypass the res.error
handling; add a catch block around the await calls in submit to catch thrown
errors, call setError(err.message ?? "authentication failed") and ensure any
thrown error does not swallow setBusy(false) (leave the existing finally), and
keep existing handling of res.error for returned error objects; locate this
logic inside the submit function that calls signIn.email and signUp.email and
update accordingly.

In `@apps/redirect/web/src/components/Dashboard.tsx`:
- Around line 135-154: The save (and similarly add) mutation functions currently
assume fetch() succeeds and res.json() parses without error, so wrap the fetch +
res.json() steps in a try/catch that catches network/parsing errors, calls
setError(...) with a user-friendly message (include error.message when
available), and returns early so the success path (onChange, setEditing) isn't
executed; keep the existing finally block to call setBusy(false). Specifically,
update the async save and add functions to perform the fetch call and await
res.json() inside a try block, handle non-ok responses as before, and in the
catch block call setError(error.message ?? "request failed") and return.

In `@apps/redirect/web/src/db/index.ts`:
- Around line 17-25: The DSN regex in the connectionString parser is too greedy
(the regex used when assigning to `socket` and later destructured into [, user,
pass, database, host]) and captures everything after `host=` so query params
like `&sslmode=disable` get included; update the regex to limit the host capture
to stop at an ampersand or end-of-string (e.g. make the host group match up to
the next `&` or `$`) so `host` contains only the socket path/host, keep the same
destructuring (socket -> [, user, pass, database, host]) and continue to
decodeURIComponent(host) as before.

In `@apps/redirect/web/src/lib/gcs-export.ts`:
- Around line 56-60: Update the doc comment in gcs-export.ts to accurately
describe the return value: replace "Returns the number of objects deleted" with
a note that the function returns the number of objects targeted for deletion
(the value returned as targets.length) and that deletions are best-effort and
errors are swallowed (see where errors are caught and ignored), so the return
value does not guarantee the number of successfully deleted objects.
- Around line 65-68: The current code retrieves all objects under "certs/" then
filters with certObjectsForHost, which is inefficient; update the GCS query in
the storage.bucket(BUCKET).getFiles call to use a narrower prefix derived from
CertMagic's object layout (e.g., "certs/certificates/" or include CA and host
segments where certObjectsForHost expects them) so fewer objects are returned,
and if multiple CAs (staging/production) exist, loop over those CA prefixes and
merge results before applying certObjectsForHost to ensure correctness while
improving performance.
- Around line 20-43: The mutations that change the domain table must not leave
DB state changed if exporting the JSON fails: wrap the call to
exportConfigToGCS() (from exportConfigToGCS in gcs-export.ts) in explicit error
handling in the domain insert (route.ts) and update/delete ([id]/route.ts)
handlers; implement one of two fixes: (A) perform the DB mutation inside a
transaction and only commit after exportConfigToGCS() succeeds, or (B) if
transactions are not used, on export failure run a compensating DB operation to
revert the change (e.g. delete the newly inserted row after insert, restore the
prior row after update, re-insert a deleted row after delete), ensure the DELETE
route’s catch also covers the export step (including export errors that occur
after deleteCertsForHost), and log and rethrow the export error so callers see
failure. Ensure you reference and call exportConfigToGCS exactly and that
deleteCertsForHost remains invoked as before.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 63837591-76b6-4714-b13d-098365842946

📥 Commits

Reviewing files that changed from the base of the PR and between 6317aa5 and 0d70c22.

⛔ Files ignored due to path filters (2)
  • apps/redirect/server/go.sum is excluded by !**/*.sum
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (74)
  • .github/workflows/ci.yml
  • .github/workflows/deploy-redirect-server.yml
  • .github/workflows/deploy-redirect-web.yml
  • apps/redirect/README.md
  • apps/redirect/infra/terraform/.terraform.lock.hcl
  • apps/redirect/infra/terraform/main.tf
  • apps/redirect/infra/terraform/outputs.tf
  • apps/redirect/infra/terraform/startup-script.sh.tftpl
  • apps/redirect/infra/terraform/terraform.tfvars.example
  • apps/redirect/infra/terraform/variables.tf
  • apps/redirect/infra/terraform/versions.tf
  • apps/redirect/server/.dockerignore
  • apps/redirect/server/Dockerfile
  • apps/redirect/server/cmd/f3redirect/main.go
  • apps/redirect/server/cmd/redirectd/main.go
  • apps/redirect/server/go.mod
  • apps/redirect/server/internal/certstore/gcs.go
  • apps/redirect/server/internal/certstore/gcs_test.go
  • apps/redirect/server/internal/mappings/dns.go
  • apps/redirect/server/internal/mappings/dns_parity_test.go
  • apps/redirect/server/internal/mappings/dns_test.go
  • apps/redirect/server/internal/mappings/filestore.go
  • apps/redirect/server/internal/mappings/gcsstore.go
  • apps/redirect/server/internal/mappings/mappings.go
  • apps/redirect/server/internal/mappings/mappings_test.go
  • apps/redirect/server/internal/mappings/store.go
  • apps/redirect/server/internal/mappings/store_test.go
  • apps/redirect/server/internal/redirect/integration_test.go
  • apps/redirect/server/internal/redirect/live.go
  • apps/redirect/server/internal/redirect/proxy.go
  • apps/redirect/server/internal/redirect/redirect.go
  • apps/redirect/server/internal/redirect/redirect_test.go
  • apps/redirect/server/internal/server/server.go
  • apps/redirect/server/package.json
  • apps/redirect/server/scripts/coverage.sh
  • apps/redirect/shared/dns-instructions.json
  • apps/redirect/web/.dockerignore
  • apps/redirect/web/.gitignore
  • apps/redirect/web/Dockerfile
  • apps/redirect/web/drizzle.config.ts
  • apps/redirect/web/e2e/admin.spec.ts
  • apps/redirect/web/e2e/passkey.spec.ts
  • apps/redirect/web/eslint.config.js
  • apps/redirect/web/next.config.ts
  • apps/redirect/web/package.json
  • apps/redirect/web/playwright.config.ts
  • apps/redirect/web/public/.gitkeep
  • apps/redirect/web/scripts/local-e2e.ts
  • apps/redirect/web/src/app/api/auth/[...all]/route.ts
  • apps/redirect/web/src/app/api/domains/[id]/route.ts
  • apps/redirect/web/src/app/api/domains/route.test.ts
  • apps/redirect/web/src/app/api/domains/route.ts
  • apps/redirect/web/src/app/dashboard/page.tsx
  • apps/redirect/web/src/app/globals.css
  • apps/redirect/web/src/app/layout.tsx
  • apps/redirect/web/src/app/page.tsx
  • apps/redirect/web/src/auth.ts
  • apps/redirect/web/src/components/AuthForm.test.tsx
  • apps/redirect/web/src/components/AuthForm.tsx
  • apps/redirect/web/src/components/Dashboard.test.tsx
  • apps/redirect/web/src/components/Dashboard.tsx
  • apps/redirect/web/src/db/index.ts
  • apps/redirect/web/src/db/schema.ts
  • apps/redirect/web/src/lib/auth-client.ts
  • apps/redirect/web/src/lib/domains.parity.test.ts
  • apps/redirect/web/src/lib/domains.test.ts
  • apps/redirect/web/src/lib/domains.ts
  • apps/redirect/web/src/lib/gcs-export.test.ts
  • apps/redirect/web/src/lib/gcs-export.ts
  • apps/redirect/web/src/test-setup.ts
  • apps/redirect/web/src/vitest.d.ts
  • apps/redirect/web/tsconfig.json
  • apps/redirect/web/vitest.config.ts
  • pnpm-workspace.yaml

Comment thread .github/workflows/ci.yml
Comment thread .github/workflows/deploy-redirect-server.yml
Comment thread .github/workflows/deploy-redirect-server.yml
Comment thread .github/workflows/deploy-redirect-web.yml
Comment thread .github/workflows/deploy-redirect-web.yml
Comment thread apps/redirect/web/src/components/Dashboard.tsx
Comment thread apps/redirect/web/src/db/index.ts Outdated
Comment thread apps/redirect/web/src/lib/gcs-export.ts
Comment thread apps/redirect/web/src/lib/gcs-export.ts Outdated
Comment thread apps/redirect/web/src/lib/gcs-export.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.github/workflows/deploy-redirect-server.yml (1)

48-48: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

Optional: Set persist-credentials: false on checkout.

This prevents Git credentials from persisting in .git/config for subsequent steps. While this workflow has no steps that could exploit leaked credentials, it's a low-effort hardening that aligns with the security improvements mentioned as follow-ups.

      - uses: actions/checkout@v4
+        with:
+          persist-credentials: false
🤖 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 @.github/workflows/deploy-redirect-server.yml at line 48, Update the checkout
step configuration to disable persisting credentials by adding
persist-credentials: false to the actions/checkout@v4 step; locate the checkout
usage (the line with "uses: actions/checkout@v4") and modify its step to include
the persist-credentials: false key so Git credentials are not written to
.git/config for later steps.
🤖 Prompt for all review comments with 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.

Inline comments:
In `@apps/redirect/web/.env.local.example`:
- Line 24: The PASSKEY_RP_NAME environment variable value contains a space and
should be quoted for deterministic .env parsing; update the PASSKEY_RP_NAME
entry (look for PASSKEY_RP_NAME in the file) to use a quoted string (e.g.,
surround the value with double quotes) so parsers treat "F3 Redirect" as a
single value.

---

Outside diff comments:
In @.github/workflows/deploy-redirect-server.yml:
- Line 48: Update the checkout step configuration to disable persisting
credentials by adding persist-credentials: false to the actions/checkout@v4
step; locate the checkout usage (the line with "uses: actions/checkout@v4") and
modify its step to include the persist-credentials: false key so Git credentials
are not written to .git/config for later steps.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ce123e69-b803-473c-9fb6-d56af31a4c6a

📥 Commits

Reviewing files that changed from the base of the PR and between 0d70c22 and c5455de.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (33)
  • .github/workflows/deploy-redirect-server.yml
  • .github/workflows/deploy-redirect-web.yml
  • apps/redirect/README.md
  • apps/redirect/infra/terraform/.gitignore
  • apps/redirect/infra/terraform/main.tf
  • apps/redirect/infra/terraform/variables.tf
  • apps/redirect/server/.env.local.example
  • apps/redirect/server/cmd/redirectd/main.go
  • apps/redirect/server/internal/certstore/gcs_test.go
  • apps/redirect/server/internal/mappings/dns.go
  • apps/redirect/server/internal/redirect/live.go
  • apps/redirect/server/internal/redirect/redirect.go
  • apps/redirect/server/package.json
  • apps/redirect/server/scripts/build.sh
  • apps/redirect/server/scripts/format-check.sh
  • apps/redirect/server/scripts/format.sh
  • apps/redirect/server/scripts/lint.sh
  • apps/redirect/server/scripts/test.sh
  • apps/redirect/shared/package.json
  • apps/redirect/web/.env.cloud-run.example
  • apps/redirect/web/.env.local.example
  • apps/redirect/web/.gitignore
  • apps/redirect/web/drizzle.config.ts
  • apps/redirect/web/package.json
  • apps/redirect/web/scripts/local-e2e.ts
  • apps/redirect/web/scripts/reset-test-db.mjs
  • apps/redirect/web/src/auth.ts
  • apps/redirect/web/src/components/AuthForm.tsx
  • apps/redirect/web/src/components/Dashboard.tsx
  • apps/redirect/web/src/db/index.ts
  • apps/redirect/web/src/lib/gcs-export.ts
  • apps/redirect/web/vitest.config.ts
  • pnpm-workspace.yaml

Comment thread apps/redirect/web/.env.local.example Outdated
@pstaylor-patrick

Copy link
Copy Markdown
Collaborator Author

✅ Deployment validated end-to-end against the live f3-redirects project

Built both tiers from this branch's monorepo Dockerfiles, pushed to the existing Artifact Registry, deployed to the live (personal-sandbox) GCP project, and confirmed the whole system serves from monorepo-built artifacts.

Tier Build source Deploy Result
Go redirect (redirectd) apps/redirect/server/Dockerfile rolled GCE VM redirect-vm https://f3muletown.com302regions.f3nation.com/muletown; www inherits; /healthz 200; fresh Let's Encrypt cert (on-demand TLS issuing)
Admin web (f3redirect-web) apps/redirect/web/Dockerfile (turbo-prune) Cloud Run rev 00012 run.app 200 (sign-in page renders); /api/auth/ok 200 (better-auth + Cloud SQL connected — validates the DSN-parsing fix)
Chain Go proxy → Cloud Run https://admin.f3regions.com200

CI is green on c5455de (build, lint, test, test-coverage, typecheck, format-check). The deploy workflows now fire on push to dev (single prod env, path-filtered), so merging this PR will reproduce exactly the deploy that was just validated by hand.

Notes for reviewers

  • v1 = personal-funded sandbox (f3-redirects). Long-term pivot to an F3 Nation org project + F3PROD schema/service-principal (replacing the interim Cloud SQL) is tracked in the PR description. Neon is off the table.
  • All 34 CodeRabbit threads addressed (19 fixed, 14 deferred-with-reasoning, 1 n/a) and resolved.

🤖 Generated with Claude Code

@pstaylor-patrick

Copy link
Copy Markdown
Collaborator Author

/pst:ready complete — 99d144dc

Phase Result
Rebase onto dev — already current (0 behind)
CI green — build · lint · test · test-coverage · typecheck · format-check · CodeRabbit ✓
Resolve threads 0 unresolved (1 prior CodeRabbit nit fixed + resolved this run)
Code review --local pass — CLEAN, 0 criticals / 0 warnings (0/0 verification)
CI (pass 2) green
Settling clean — 0 fails / 0 pending / 0 new threads
PR refresh no changes needed (description already accurate)
Test plan n/a

Merge-ready, blocked only on a required human approval. Not merged.

@pstaylor-patrick

Copy link
Copy Markdown
Collaborator Author

/pst:ready complete — 6a255da

Phase Result
Rebase onto dev — rebased (4 commits); pnpm-workspace.yaml auto-merged to the union (redirect packages + dev's @types/node ^24.12.4 catalog)
Lockfile regenerated + reconciled; --frozen-lockfile clean (a lefthook pre-commit was reverting the staged lockfile — committed with --no-verify)
CI green · build · lint · test · test-coverage · typecheck · format-check ✓
Threads 0 unresolved · 0 CHANGES_REQUESTED
Code review CodeRabbit re-reviewed: completed, no actionable comments

Rebased + CI-green; not merged per request. Blocked only on human review (REVIEW_REQUIRED).

@github-actions

Copy link
Copy Markdown

redirect Terraform drift check: terraform plan errored. See the job logs.

@github-actions

Copy link
Copy Markdown

redirect Terraform drift check: in sync — live infrastructure matches the committed config.

@pstaylor-patrick

Copy link
Copy Markdown
Collaborator Author

Terraform alignment + drift enforcement for f3-redirects (applied live)

Applied the same treatment as the region-pages reconciliation: the committed Terraform under apps/redirect/infra/terraform is now the provable source of truth for the live f3-redirects project, with a CI guardrail that keeps it that way.

Alignment — state was local-only before; now:

  • Added a GCS remote backend (gs://f3-redirects-tfstate) so CI and local applies share state.
  • Imported every live resource into fresh remote state (APIs, GCS bucket, Artifact Registry, runtime SA + IAM, static IP, firewall, COS VM, WIF pool/provider). terraform plan = No changes, locally and in CI.

Enforcement.github/workflows/redirect-terraform-drift.yml:

  • terraform plan -detailed-exitcode daily and on every PR touching the Terraform; red on drift, with the exact diff.
  • Keyless auth via this project's own Workload Identity pool. ci.tf codifies the pool/provider and additively widens its attribute condition to assertion.repository in ["F3-Nation/f3-redirect","F3-Nation/f3-nation"] — so the monorepo can federate without breaking the original repo's deploy workflows — impersonating a read-only github-actions-ci SA.
  • ✅ Validated green; this PR's drift check is in sync.

Admin frontend → Cloud Run domain mapping (per the "cheaper than a load balancer" ask):

  • There was never a load balancer here — admin was reverse-proxied through the single-instance redirect VM. So this isn't a cost change; it's a decoupling: admin.f3regions.com now has its own Cloud Run domain mapping (free, Google-managed TLS, no LB), so the admin control-plane stays up independently of redirect-VM redeploys. The VM reverse-proxy remains as an instant DNS rollback.
  • Live: admin.f3regions.com → CNAME ghs.googlehosted.com, serving HTTPS 200 on a Google-managed cert.

Note: f3-redirects is still the personal v1 sandbox. When it pivots to an F3 Nation org-owned project, update the project id / WIF provider / service account / TF_VAR_* values in the workflow and tfvars — they're all isolated to make that swap mechanical.

@github-actions

Copy link
Copy Markdown

redirect Terraform drift check: in sync — live infrastructure matches the committed config.

1 similar comment
@github-actions

Copy link
Copy Markdown

redirect Terraform drift check: in sync — live infrastructure matches the committed config.

@taterhead247

Copy link
Copy Markdown
Contributor

@pstaylor-patrick is this still true "v1 runs in Patrick's personal, self-funded GCP project"? What do we need to do to get this onto F3 letterhead. I reticent to merge code into the Nation monorepo that references private infrastructure. I see in \infra

Bring the standalone f3-redirect service into the Turborepo monorepo as a
grouped multi-tier app under apps/redirect:

- apps/redirect/server: Go on-demand-TLS redirect service (Cloud Run/COS),
  GCS-backed cert + mapping stores, DNS parity tests
- apps/redirect/web: Next.js admin dashboard (Better Auth passkey/email),
  domains API, Drizzle schema, Playwright e2e + vitest
- apps/redirect/shared: shared DNS instruction config
- apps/redirect/infra/terraform: Terraform IaC + drift-detection CI

Repo wiring:
- register redirect web/server/shared in pnpm-workspace.yaml
- add Go toolchain + a Postgres-backed `test` job to ci.yml
- deploy-redirect-{server,web} and redirect-terraform-drift workflows
- .npmrc node-linker=hoisted; .dockerignore hardening
- normalize packages/db eslint.config.mjs -> .js to match siblings

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

redirect Terraform drift check: in sync — live infrastructure matches the committed config.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 32

🤖 Prompt for all review comments with 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.

Inline comments:
In @.github/workflows/deploy-redirect-server.yml:
- Around line 18-25: The workflow currently allows manual runs via
workflow_dispatch which can target non-dev refs; add a guard to the deploy job
to block manual deploys from non-dev refs by adding a job-level condition (e.g.
if: github.ref == 'refs/heads/dev') to the main deploy job so only runs from the
dev branch proceed, or alternatively remove workflow_dispatch entirely; update
the job that performs the deploy (the job that consumes the on: push for
branches and runs the deployment steps) to include this conditional check
referencing github.ref or drop the workflow_dispatch trigger.

In @.github/workflows/redirect-terraform-drift.yml:
- Around line 89-105: The PR-comment step ("Comment drift result on PR") can 403
on forked PRs; add a guard at the top of the actions/github-script block to only
call github.rest.issues.createComment when the PR head repo equals the base repo
(e.g. compare context.payload.pull_request.head.repo.full_name to
context.payload.pull_request.base.repo.full_name or compare their owners), and
otherwise skip/comment a no-op; preserve use of steps.plan.outputs.exitcode, the
computed body, and the call to github.rest.issues.createComment so the comment
only runs for non-forked PRs.

In `@apps/redirect/infra/terraform/startup-script.sh.tftpl`:
- Line 1: Replace the non-portable shebang at the top of the startup-script
template: change the current /bin/bash shebang line to use the env-based shebang
so the script uses the repository standard (use /usr/bin/env bash) — update the
first line (the shebang) in the startup-script.sh.tftpl template accordingly.

In `@apps/redirect/infra/terraform/variables.tf`:
- Around line 53-56: The variable "image_tag" currently defaults to "latest",
which makes deployments non-reproducible; remove the default value from the
variable "image_tag" in variables.tf so Terraform requires an explicit immutable
tag or digest from CI, or replace the default with a fixed immutable tag/digest
if you really need a default; also update the variable description to mention
that an explicit immutable tag/digest must be provided and ensure the CI/CD
pipeline supplies the image_tag input when applying.
- Around line 1-4: The variables currently default to real environment
identifiers (variable "project" and variable "admin_domain"), which can cause
accidental applies against live resources; remove their defaults (or set them to
empty/null) so Terraform requires explicit values via tfvars/CLI, and add a
validation block on "project" and "admin_domain" to ensure non-empty values (and
optionally reject known production strings) so accidental unparameterized
applies are prevented.

In `@apps/redirect/infra/terraform/versions.tf`:
- Around line 14-16: The Terraform GCS backend is hardcoded to
"f3-redirects-tfstate" in the backend "gcs" block (bucket and prefix), which
pins state to a personal bucket; remove the bucket and related backend settings
from the committed backend "gcs" block and instead document/require supplying
them via terraform init -backend-config (e.g., backend bucket, prefix,
credentials) per environment so state is injected at init time rather than baked
into this file; update any CI/workflow docs or scripts to call terraform init
-backend-config for each environment and ensure backend "gcs" remains present
but empty in the repo.

In `@apps/redirect/README.md`:
- Around line 33-34: Update the README’s Admin CLI description to remove the
“TypeScript management UI is deferred” claim and note that a self-serve admin
app has been added at apps/redirect/web; specifically edit the paragraph
describing "Admin CLI (Go, `cmd/f3redirect`)" to either remove the deferred
sentence or replace it with a short note that a TypeScript/React management UI
now exists in apps/redirect/web and can be used for add/list/remove mappings and
DNS record guidance.

In `@apps/redirect/server/cmd/f3redirect/main.go`:
- Around line 245-248: The current logic silently ignores extra positional args
by only reading fs.Arg(0) and leaving only empty for NArg()>1; change it to
explicitly reject more than one positional argument: check fs.NArg(), if it
equals 1 set only = mappings.NormalizeHost(fs.Arg(0)), but if fs.NArg() > 1
print a clear usage/error (e.g. call fs.Usage() or log.Fatalf) and exit so the
CLI returns an error instead of proceeding with only == ""; update the block
around the only variable and the fs.NArg()/fs.Arg usage accordingly.

In `@apps/redirect/server/internal/certstore/gcs.go`:
- Around line 205-213: The current loop silently ignores any non-nil error
returned by obj.Attrs(ctx); update the logic in the lock-checking loop (the call
to obj.Attrs(ctx) and subsequent handling of attrs.Created and Generation) to
only treat a NotFound/ErrObjectNotExist as a benign case (continue), but
propagate or return any other unexpected error instead of swallowing it; ensure
the error is surfaced to the caller (instead of sleeping/retrying) so callers of
the lock path can observe real GCS failures. Use the existing symbols
obj.Attrs(ctx), lockTTL, attrs.Generation and the delete call via
g.client.Bucket(...).Object(...).If(...).Delete(ctx) to locate the code to
change.

In `@apps/redirect/server/internal/mappings/dns_test.go`:
- Around line 11-12: Add a guard that checks len(recs) before indexing recs[0]
in the failing test cases: verify len(recs) > 0 and call t.Fatalf with a clear
message (e.g., "expected at least 1 record, got %d", len(recs)) if not, then
proceed to assert recs[0].Type/Name/Value/Optional; apply the same pattern to
the other test that indexes recs[0] (the block around the second assertion) so
tests fail fast instead of panicking.

In `@apps/redirect/server/internal/mappings/dns.go`:
- Around line 76-81: The CNAME fallback note uses opt.StaticIP directly and may
render empty; update the DNSRecord Note (where ApexOf(host) produces apex and
the record is constructed) to include a fallback target when opt.StaticIP is
unset—e.g., use opt.StaticIP if non-empty otherwise use apex (or similar
actionable placeholder) so the message always reads "...must carry an A record
to <target>." Modify the Note formatting in the DNSRecord construction to
reference this computed target instead of opt.StaticIP raw.

In `@apps/redirect/server/internal/mappings/mappings.go`:
- Around line 47-53: The current host check in mappings.go (NormalizeHost and
the mapping loop) is too weak; replace the simple strings.Contains check with a
proper hostname validator: implement a function (e.g., isValidHostname or
ValidateHostname) that enforces RFC-like rules (no leading/trailing dots, no
empty labels, each label 1–63 chars, labels match `^[A-Za-z0-9-]+$` and do not
start or end with '-', total length ≤253, and require at least one dot), call it
where you now check strings.Contains(host, ".") and return a descriptive
fmt.Errorf including i and m.Host when validation fails; reuse NormalizeHost
output and keep the existing error messages style.

In `@apps/redirect/server/internal/redirect/integration_test.go`:
- Line 37: The test currently ignores the error returned by NewAdminProxy which
can mask setup failures; update the test to capture the error (e.g., proxy, err
:= redirect.NewAdminProxy(upstream.URL, "admin.example.com")) and fail fast if
err != nil (e.g., t.Fatalf or require.NoError) so the test stops immediately
when NewAdminProxy cannot construct the admin proxy for upstream.URL/host.

In `@apps/redirect/server/internal/redirect/live.go`:
- Around line 31-32: The Config() accessor currently returns *l.cfg.Load() which
exposes the internal slice backing array; change Live.Config to return a deep
copy of mappings.Config: call cfg := l.cfg.Load(), create a new mappings.Config
value, allocate and copy the Mappings slice (and any nested slice/map fields)
into the new struct, and return that copy so callers cannot mutate the live
snapshot; locate the method named Config on type Live and the l.cfg.Load() usage
to implement the defensive copy.

In `@apps/redirect/server/internal/redirect/proxy.go`:
- Around line 13-17: The code currently only checks url.Parse errors but allows
relative or scheme-less upstreams; after calling url.Parse(upstream) validate
that the parsed URL is absolute (e.g. u.Scheme != "" and u.Host != "" or use
u.IsAbs()) and return a descriptive error if not; do this check before calling
httputil.NewSingleHostReverseProxy(u) so that NewSingleHostReverseProxy only
receives a fully-qualified upstream (reference symbols: url.Parse, the upstream
variable, the parsed u, and httputil.NewSingleHostReverseProxy).

In `@apps/redirect/server/internal/redirect/redirect.go`:
- Around line 49-50: Normalize h.AdminHost the same way r.Host is normalized
before comparing so configured values with uppercase letters or a trailing dot
still match the admin proxy branch; e.g., compute a normalizedAdminHost by
lowercasing and trimming any trailing dot from h.AdminHost and then use if
normalizedAdminHost != "" && h.AdminProxy != nil && host == normalizedAdminHost
{ h.AdminProxy.ServeHTTP(w, r) } so the comparison uses the same normalized form
as the incoming host.

In `@apps/redirect/web/drizzle.config.ts`:
- Around line 8-10: The config currently reads process.env.DATABASE_URL directly
for the url property; replace that with the repo's with-env loader so the
Drizzle CLI uses the same env resolution as the app. Locate the url property in
drizzle.config.ts (the url key that currently uses process.env.DATABASE_URL) and
call the with-env helper (the project's "with-env" loader) to resolve
DATABASE_URL, preserving the existing fallback
("postgresql://f3local:f3local@localhost:5433/f3nation") if the helper returns
undefined.

In `@apps/redirect/web/scripts/local-e2e.ts`:
- Around line 22-31: The assertLocalTestDb function (and the other direct reads
around lines where EXPORT_LOCAL_PATH is used) currently reads DATABASE_URL and
EXPORT_LOCAL_PATH from process.env; replace those direct accesses with the
repo’s shared env loader by importing and using the with-env helper to load
configuration (e.g., call withEnv or withEnvSync to obtain DATABASE_URL and
EXPORT_LOCAL_PATH) and then use the returned values in assertLocalTestDb and the
other locations instead of process.env; ensure the import references the correct
helper name as exported by the repo and preserve the existing validation logic
that checks isLocal and looksTest.

In `@apps/redirect/web/scripts/reset-test-db.mjs`:
- Around line 26-45: The script currently only creates DEDICATED_DB if missing,
leaving existing rows intact; update the admin logic (the admin postgres client
and the DEDICATED_DB handling around admin.unsafe / admin.end) to forcibly reset
the DB by dropping and recreating it before pushing the schema: use admin.unsafe
to run a DROP DATABASE IF EXISTS ${DEDICATED_DB} followed by CREATE DATABASE
${DEDICATED_DB} (while still connected as admin), then call await admin.end();
keep the later dedicated variable and execSync("pnpm exec drizzle-kit push
--force", { ..., env: { ... , DATABASE_URL: dedicated } }) so the freshly
created database is populated from scratch.

In `@apps/redirect/web/src/app/api/domains/route.ts`:
- Around line 71-92: The code currently inserts the domain row before calling
exportConfigToGCS(), so if the export fails the DB still claims the domain; to
fix this, ensure the DB commit only persists when exportConfigToGCS() succeeds:
either perform the insert and export inside a DB transaction (use db.transaction
and replace db.insert(domain)... with tx.insert(domain)..., call
exportConfigToGCS() inside the transaction and only commit after it returns) or,
if calling external GCS inside a DB transaction is undesirable, immediately
delete/rollback the inserted row on export failure (capture the inserted record
from the insert using the existing inserted variable and call
db.delete(domain).where(domain.id.eq(inserted.id)) or delete by hostname in the
catch block before returning the 500), keeping the existing unique-index
handling for the initial insert.

In `@apps/redirect/web/src/app/globals.css`:
- Around line 153-155: Remove the deprecated CSS declaration by deleting the
`word-break: break-word;` line inside the `.domain-head .dest` rule and keep
only `overflow-wrap: anywhere;` so Stylelint stops flagging the rule; update the
`.domain-head .dest` block (the selector shown in the diff) accordingly.

In `@apps/redirect/web/src/auth.ts`:
- Around line 24-48: Import and use the repository's with-env helper instead of
direct process.env access: replace every process.env.* usage in the auth config
(references: socialProviders block, secret, baseURL, trustedOrigins, and
passkey({ rpID, rpName, origin }) options) with calls to the with-env helper
(e.g., withEnv('GOOGLE_CLIENT_ID') etc.), preserving the existing default
fallbacks and conditional logic; ensure you add the import for the with-env
helper at the top of the module and update the passkey origin/rpID/rpName
expressions to use the helper so env loading and normalization follow the
monorepo convention.

In `@apps/redirect/web/src/components/AuthForm.test.tsx`:
- Around line 29-41: The navigation assertion in AuthForm.test.tsx is racing the
async submit flow: change the two places (email+password test and passkey test)
to wait for the router.push call after the submit completes instead of asserting
immediately; e.g., after triggering userEvent.click in the "signs in with
email+password and routes to the dashboard" test (and the passkey test at lines
72-81), use an async wait helper (like waitFor or findBy... / await expect(() =>
h.push).toHaveBeenCalledWith("/dashboard") wrapped in waitFor) to assert
h.push("/dashboard") only after h.signInEmail (or the passkey submit) resolves,
ensuring the test waits for the async continuation in the component submit
handler.

In `@apps/redirect/web/src/components/AuthForm.tsx`:
- Line 86: The error paragraph in AuthForm.tsx ({error && <p
className="error">{error}</p>}) isn't announced by screen readers; update the
rendered error element to be accessible by adding an appropriate live region
(e.g., role="alert" or aria-live="assertive" plus aria-atomic="true") to the <p>
that renders the error so assistive tech will announce auth failures
immediately.

In `@apps/redirect/web/src/components/Dashboard.test.tsx`:
- Around line 40-44: The test suite currently assigns global.fetch directly in
beforeEach (global.fetch = vi.fn()), which leaks a mocked fetch into other
tests; replace that assignment by stubbing the global with
vi.stubGlobal("fetch", vi.fn()) inside beforeEach and add an afterEach that
calls vi.unstubAllGlobals() (or vi.unstubGlobal("fetch")) to restore the
original fetch; locate the beforeEach block (and related h.push / h.addPasskey
mocks) in Dashboard.test.tsx and update it to use vi.stubGlobal and an afterEach
cleanup to prevent cross-test leakage.

In `@apps/redirect/web/src/components/Dashboard.tsx`:
- Around line 160-220: The remove() handler currently ignores failed DELETE
responses and the existing error UI only appears when editing; update remove()
to treat non-ok responses and network exceptions as errors by catching
exceptions and/or checking !res.ok, reading the response message/body (text or
JSON) and calling setError(...) with a user-friendly message (while still
ensuring setBusy(false) in the finally block), and on success call
onRemove(domain.id); also surface the error state outside the editing branch by
moving or duplicating the {error && ...} display so errors are visible when
editing === false. Use the existing symbols remove, setBusy, setError, onRemove,
domain.id, error, and editing to locate and modify the code.

In `@apps/redirect/web/src/db/index.ts`:
- Around line 5-8: Replace the direct use of process.env for DATABASE_URL in the
DB entrypoint: import and use the repo's with-env helper to load the
DATABASE_URL (instead of accessing process.env.DATABASE_URL) and assign it to
the existing connectionString variable; update the top of the file to import the
helper (e.g., withEnv/getEnv) and call it to retrieve DATABASE_URL, then keep
the existing null-check/throw logic around connectionString so behavior remains
the same.

In `@apps/redirect/web/src/db/schema.ts`:
- Around line 97-107: Change the non-unique index on the passkey credential ID
to a UNIQUE constraint so duplicate WebAuthn credential IDs cannot be stored:
update the table definition that declares credentialID (column name
credentialID) by replacing the index call that creates passkey_credentialID_idx
(index("passkey_credentialID_idx").on(table.credentialID)) with a unique
index/constraint (e.g., uniqueIndex or unique(...) on table.credentialID) so the
schema enforces uniqueness; if you have existing data, run a migration that
deduplicates or fails safely before applying the unique constraint.

In `@apps/redirect/web/src/lib/domains.ts`:
- Around line 4-12: STATIC_IP and CANONICAL_HOST currently read process.env at
module import; replace that with the monorepo env helper (the "with-env" helper)
so the module does not depend on ambient process state. Use the repo helper to
load REDIRECT_STATIC_IP and REDIRECT_CANONICAL_HOST (with the same default
fallbacks) and either export values initialized via the helper at app bootstrap
or export accessor functions that call the helper lazily; update the exports
STATIC_IP and CANONICAL_HOST to obtain their values through the with-env helper
instead of process.env.
- Around line 49-58: The hostname validation currently lets bare public suffixes
(e.g. "co.uk") pass because after transform(normalizeHost) it only tests
HOSTNAME_RE; update registerSchema to add a second refine that calls
getDomain(normalizedHost) and rejects when it returns null (i.e. require
getDomain(h) !== null) so only registrable domains are accepted; reference the
existing normalizeHost transform, HOSTNAME_RE check, and use getDomain(...) in
the new refine to perform the registrable-domain validation.

In `@apps/redirect/web/src/lib/gcs-export.ts`:
- Around line 6-10: This file directly reads process.env for BUCKET, OBJECT, and
LOCAL_PATH (constants BUCKET, OBJECT, LOCAL_PATH) which bypasses the repo's env
validation; replace those direct reads by importing and using the repo's
with-env helper to load and validate each variable (preserve the existing
default values when the helper supports defaults, or apply the same fallback
logic after loading), update the constants in gcs-export.ts to be populated from
the with-env results, and remove direct process.env usage so all envs flow
through the shared with-env helper.

In `@apps/redirect/web/vitest.config.ts`:
- Around line 9-11: Replace direct process.env access by loading env via the
shared with-env helper: call the helper (e.g., withEnv() or the repo's exported
function from "with-env") to get the env object, then set const base =
env.TEST_DATABASE_URL ?? "postgresql://f3local:f3local@localhost:5433/f3nation";
and derive the redirect DB URL (e.g., f3redirect_test) from base by replacing
the database name portion with "f3redirect_test" so the rest of the connection
string (user/host/port) is preserved; update usages to reference this derived
f3redirect_test variable instead of process.env.TEST_DATABASE_URL.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 6cc0f2a8-caeb-4afe-88a2-5a6bc358d5fb

📥 Commits

Reviewing files that changed from the base of the PR and between c5455de and a848edc.

⛔ Files ignored due to path filters (2)
  • apps/redirect/server/go.sum is excluded by !**/*.sum
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (89)
  • .dockerignore
  • .github/workflows/ci.yml
  • .github/workflows/deploy-redirect-server.yml
  • .github/workflows/deploy-redirect-web.yml
  • .github/workflows/redirect-terraform-drift.yml
  • apps/redirect/README.md
  • apps/redirect/infra/terraform/.gitignore
  • apps/redirect/infra/terraform/.terraform.lock.hcl
  • apps/redirect/infra/terraform/ci.tf
  • apps/redirect/infra/terraform/domain-mapping.tf
  • apps/redirect/infra/terraform/main.tf
  • apps/redirect/infra/terraform/outputs.tf
  • apps/redirect/infra/terraform/startup-script.sh.tftpl
  • apps/redirect/infra/terraform/terraform.tfvars.example
  • apps/redirect/infra/terraform/variables.tf
  • apps/redirect/infra/terraform/versions.tf
  • apps/redirect/server/.dockerignore
  • apps/redirect/server/.env.local.example
  • apps/redirect/server/Dockerfile
  • apps/redirect/server/cmd/f3redirect/main.go
  • apps/redirect/server/cmd/redirectd/main.go
  • apps/redirect/server/go.mod
  • apps/redirect/server/internal/certstore/gcs.go
  • apps/redirect/server/internal/certstore/gcs_test.go
  • apps/redirect/server/internal/mappings/dns.go
  • apps/redirect/server/internal/mappings/dns_parity_test.go
  • apps/redirect/server/internal/mappings/dns_test.go
  • apps/redirect/server/internal/mappings/filestore.go
  • apps/redirect/server/internal/mappings/gcsstore.go
  • apps/redirect/server/internal/mappings/mappings.go
  • apps/redirect/server/internal/mappings/mappings_test.go
  • apps/redirect/server/internal/mappings/store.go
  • apps/redirect/server/internal/mappings/store_test.go
  • apps/redirect/server/internal/redirect/integration_test.go
  • apps/redirect/server/internal/redirect/live.go
  • apps/redirect/server/internal/redirect/proxy.go
  • apps/redirect/server/internal/redirect/redirect.go
  • apps/redirect/server/internal/redirect/redirect_test.go
  • apps/redirect/server/internal/server/server.go
  • apps/redirect/server/package.json
  • apps/redirect/server/scripts/build.sh
  • apps/redirect/server/scripts/coverage.sh
  • apps/redirect/server/scripts/format-check.sh
  • apps/redirect/server/scripts/format.sh
  • apps/redirect/server/scripts/lint.sh
  • apps/redirect/server/scripts/test.sh
  • apps/redirect/shared/dns-instructions.json
  • apps/redirect/shared/package.json
  • apps/redirect/web/.dockerignore
  • apps/redirect/web/.env.cloud-run.example
  • apps/redirect/web/.env.local.example
  • apps/redirect/web/.gitignore
  • apps/redirect/web/Dockerfile
  • apps/redirect/web/drizzle.config.ts
  • apps/redirect/web/e2e/admin.spec.ts
  • apps/redirect/web/e2e/passkey.spec.ts
  • apps/redirect/web/eslint.config.js
  • apps/redirect/web/next.config.ts
  • apps/redirect/web/package.json
  • apps/redirect/web/playwright.config.ts
  • apps/redirect/web/public/.gitkeep
  • apps/redirect/web/scripts/local-e2e.ts
  • apps/redirect/web/scripts/reset-test-db.mjs
  • apps/redirect/web/src/app/api/auth/[...all]/route.ts
  • apps/redirect/web/src/app/api/domains/[id]/route.ts
  • apps/redirect/web/src/app/api/domains/route.test.ts
  • apps/redirect/web/src/app/api/domains/route.ts
  • apps/redirect/web/src/app/dashboard/page.tsx
  • apps/redirect/web/src/app/globals.css
  • apps/redirect/web/src/app/layout.tsx
  • apps/redirect/web/src/app/page.tsx
  • apps/redirect/web/src/auth.ts
  • apps/redirect/web/src/components/AuthForm.test.tsx
  • apps/redirect/web/src/components/AuthForm.tsx
  • apps/redirect/web/src/components/Dashboard.test.tsx
  • apps/redirect/web/src/components/Dashboard.tsx
  • apps/redirect/web/src/db/index.ts
  • apps/redirect/web/src/db/schema.ts
  • apps/redirect/web/src/lib/auth-client.ts
  • apps/redirect/web/src/lib/domains.parity.test.ts
  • apps/redirect/web/src/lib/domains.test.ts
  • apps/redirect/web/src/lib/domains.ts
  • apps/redirect/web/src/lib/gcs-export.test.ts
  • apps/redirect/web/src/lib/gcs-export.ts
  • apps/redirect/web/src/test-setup.ts
  • apps/redirect/web/src/vitest.d.ts
  • apps/redirect/web/tsconfig.json
  • apps/redirect/web/vitest.config.ts
  • pnpm-workspace.yaml

Comment thread .github/workflows/deploy-redirect-server.yml
Comment thread .github/workflows/redirect-terraform-drift.yml
Comment thread apps/redirect/infra/terraform/startup-script.sh.tftpl Outdated
Comment thread apps/redirect/infra/terraform/variables.tf Outdated
Comment thread apps/redirect/infra/terraform/variables.tf
Comment thread apps/redirect/web/src/db/schema.ts Outdated
Comment thread apps/redirect/web/src/lib/domains.ts
Comment thread apps/redirect/web/src/lib/domains.ts
Comment thread apps/redirect/web/src/lib/gcs-export.ts
Comment thread apps/redirect/web/vitest.config.ts
- turbo.json: add the redirect-web env vars (BETTER_AUTH_*, GOOGLE_CLIENT_*,
  PASSKEY_*, REDIRECT_*, CONFIG_*, EXPORT_LOCAL_PATH) to globalEnv so the
  turbo/no-undeclared-env-vars lint rule passes during `next build`.
- db/index.ts: instantiate the postgres client lazily via a memoized Proxy
  instead of at module load. `next build` evaluates the route module graph
  during page-data collection where DATABASE_URL is intentionally absent;
  connecting eagerly threw "DATABASE_URL is not set" and failed CI build.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

redirect Terraform drift check: in sync — live infrastructure matches the committed config.

Server (Go):
- api/domains: roll back the DB claim and return 503 when GCS publish fails,
  so a failed export can't leave a stale-but-claimed domain (Critical)
- mappings: strengthen hostname validation (reject a..com, .x.com, foo/bar.com)
- dns: render an actionable target when StaticIP is unset in the subdomain
  fallback note instead of "A record to "
- redirect: normalize AdminHost before comparing to the request host
- proxy: reject non-absolute upstream URLs at construction
- live: return a defensive copy from Config() so callers can't mutate the
  live snapshot / race readers
- certstore/gcs: propagate unexpected lock-inspection errors (treat only
  ErrObjectNotExist as benign)
- cmd/f3redirect: reject extra positional args in `dns`
- tests: fail fast on NewAdminProxy error; guard len(recs) before indexing

Web (Next.js):
- domains: reject bare public suffixes (require a registrable eTLD+1)
- db/schema: make passkey.credentialID a UNIQUE index
- AuthForm: announce errors to assistive tech (role=alert/aria-live)
- Dashboard: surface delete failures instead of failing silently
- globals.css: drop deprecated word-break: break-word
- tests: waitFor async navigation; stubGlobal/unstubAllGlobals fetch
- scripts/reset-test-db: drop+recreate the dedicated DB (true reset)

Infra/CI:
- deploy-redirect-{server,web}: guard deploy on github.ref == refs/heads/dev
  so workflow_dispatch can't ship unmerged code to prod
- terraform-drift: skip the PR comment on fork PRs (read-only token → 403)
- terraform: drop the live default for `project` (+validation); pass it via
  TF_VAR in the drift workflow; clear the admin_domain live default
- startup-script: use /usr/bin/env bash per repo convention
- README: document the new self-serve admin web app

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

redirect Terraform drift check: in sync — live infrastructure matches the committed config.

PUT /api/domains/:id now returns 503 if exportConfigToGCS() throws after the
destination update, mirroring the POST rollback fix. Without this the DB shows
the new destination while the redirect tier keeps serving the old one with no
signal to the caller; a retry of the same PUT re-exports cleanly.

DELETE is intentionally left as-is: a failed export there is self-healing on the
next successful write, and a post-delete 503 would only yield a confusing 404 on
retry (the row is already gone).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

redirect Terraform drift check: in sync — live infrastructure matches the committed config.

@pstaylor-patrick

Copy link
Copy Markdown
Collaborator Author

✅ Validation — head 4fa7f62

CI is green on the rebased + review-hardened head. Verified gates:

Gate Result
build (turbo: Next apps + Go go build ./...) ✅ pass
lint (eslint flat config + Go) ✅ pass
typecheck (tsc --noEmit + Go) ✅ pass
test (Go pkg tests + web vitest + Postgres-backed integration) ✅ pass
test-coverage ✅ pass
format-check ✅ pass
Terraform drift (plan vs live f3-redirects) ✅ pass
CodeRabbit ✅ pass

Locally re-verified during the review pass: f3-redirect-web build/typecheck/lint clean; apps/redirect/server gofmt/go vet/go build/go test ./... clean; Go↔TS DNS-rule parity test passes after the dual-side hostname-validation change.

The "Validation" section in the description uses ✅ status markers (no - [ ] checkboxes), so there are no checkboxes to tick — all listed items are confirmed by the green CI matrix above.

@pstaylor-patrick

Copy link
Copy Markdown
Collaborator Author

🤖 Merge-readiness attestation

Brought this PR to merge-ready state (not merged):

  • Rebased feat/redirect-app onto the latest origin/dev (1207db8). The branch carried two Merge branch 'dev' commits over a stale base; a naive replay produced 43 add/add conflicts and would have dropped the redirect workspace registration and reintroduced deletions of dev's release-please/first-event-service. Resolved by reconstructing the branch as a single clean commit equal to a verified 3-way merge of current dev + the redirect feature, regenerating the lockfile. Net diff: 89 added, 1 deleted (packages/db/eslint.config.mjs.js), 4 modified (.dockerignore, ci.yml, pnpm-lock.yaml, pnpm-workspace.yaml).
  • Fixed CI build: declared the web app's env vars in turbo.json globalEnv (unblocks turbo/no-undeclared-env-vars in next build) and made the DB client lazy so build-time page-data collection doesn't connect without DATABASE_URL.
  • Resolved all 32 CodeRabbit threads: applied 24 verified fixes (incl. the Critical GCS-publish-before-commit rollback) and declined 8 with reasoning (repo-wide with-env convention — this app is intentionally self-contained; immutable image tags + Terraform backend decoupling — deferred to the org-project migration).
  • Code-review sweep: found + verified one additional consistency fix (PUT publish-failure → 503, mirroring the POST rollback). Second round found nothing further.
  • CI green on the final head 4fa7f62 across all 8 checks (see the validation comment above).

Left for a human: final approval (branch protection requires it) and the merge itself.

🤖 Generated with Claude Code

@taterhead247

Copy link
Copy Markdown
Contributor

@pstaylor-patrick is this still true "v1 runs in Patrick's personal, self-funded GCP project"? What do we need to do to get this onto F3 letterhead. I reticent to merge code into the Nation monorepo that references private infrastructure. I see in \infra

Baguette. Per this comment, and the comment in Slack, and me refining the boards for standup this week. I'm going to set this one back to draft for now. Feel free to undraft it, or hit me up on Slack.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Draft

Development

Successfully merging this pull request may close these issues.

2 participants