Skip to content

feat(deploy): one command from a fresh clone to a live Railway project - #76

Merged
jayleaton merged 1 commit into
mainfrom
feat/railway-bootstrap
Oct 22, 2026
Merged

jayleaton merged 1 commit into
mainfrom
feat/railway-bootstrap

Conversation

@jayleaton

Copy link
Copy Markdown
Owner

Summary

A fresh clone of the template now reaches a live Railway project in one command, with no manual Railway or GitHub step first. Previously, the first make railway-apply dead-ended on "You don't have access to the GitHub repo" until the Railway GitHub app was installed.

railway login
make railway-doctor   # login, workspace, SSH key, GitHub access, Doppler -> prints fixes
make railway-setup    # project, envs, services, DBs, domains, secrets, first deploy, /healthz checked
make railway-setup
├─ platform.yaml: --project/--workspace/--environments/--admin/--region/--repo (or PLATFORM_*)
│    written in place, comments and layout kept; project/repo: auto = checkout's origin
├─ ensureProject (asks once; --yes for agents, else exit 3)  -> ensureEnvironments
├─ source: gitHubRepoAccessAvailable (refused for CLI tokens) / already connected?
│    no -> graph without a source (PLATFORM_SOURCE=upload)
├─ per env: IaC apply -> DB password fix -> domains -> secrets (generated + placeholders)
├─ serviceConnect(web, repo) = the real access check
│    ok   -> re-apply with the GitHub source: deploy-on-push, same services
│    fail -> railway up the services without a live deployment
│            + next step: Railway GitHub app install URL (account preselected), re-run
└─ wait for deployments, GET /healthz -> --json summary, exit 0 / 5 (unhealthy)
  • Agent-friendly:
    • --yes, plus --json: one object on stdout (ok, project.{id,url}, source, each env's service url/deployment/health, nextSteps) with logs on stderr.
    • Exit codes 0 ok, 1 error, 2 plan pending, 3 needs --yes, 4 preflight failed, 5 unhealthy.
    • New AGENTS.md section "Deploy to Railway (for agents)". The README quick start is three commands.
  • Production boots on day one: setup provisions a RESEND_API_KEY placeholder, and the next steps tell the user to replace it.
  • New Railway findings, drafted in docs/railway-go-iac-issues.md (scratch: CI warm-run measurement (do not merge) #15–fix(auth): cheap argon2 in test binaries so -race suites stay small #17):
    • a deleted database's volume is re-attached by name to the next database;
    • GitHub queries are refused for CLI tokens;
    • project names are limited to 32 characters.

Evidence

Run in JJ's account (workspace "JJ Eaton's Projects", Pro), CLI 5.71.0, one throwaway project. The brief's name go-fast-stack-bootstrap-test-20261022 is 37 characters, and Railway rejects names over 32 (Project names must be between 1 and 32 characters.), so the project was go-fast-stack-boot-test-261022 (0490dac3-…5403). repo: jayleaton/ai-patterns-go, environments staging,production.

Run What Result
1 from zero, --yes --json (12:14→12:32) project + staging created. Railway could already read this repo, so serviceConnect succeeded and both envs deployed from GitHub (8b120a3). The production DB got Railway's literal secret(…) password, and setup replaced it. /healthz 200 on both. ok:true exitCode:0 source:"github"
2 re-run no changes in both envs, no new deployments, both healthy, exit 0
3–4 fallback: services deleted, --repo jayleaton/railway-bootstrap-no-access-probe (doesn't exist, so Railway can't read it) serviceConnect → User does not have access to the repo. Setup deployed by railway up, and both envs ended health ok, deployment SUCCESS with source:"upload". nextSteps[0]: Install Railway's GitHub app … https://github.com/apps/railway-app/installations/new/permissions?target_id=58495321 …, then re-run make railway-apply. The re-run didn't redeploy the already-live staging.
5 --repo jayleaton/ai-patterns-go again (stands in for "after installing") GitHub: connected …; switching every service to deploy-on-push. Still exactly 2 services (postgres, web). Both envs' latest deployments come from GitHub (7fcc091), /healthz ok, exit 0. The next plan reports pendingChanges: 0.
doctor --json all checks ok (ssh key matched dev-box-workers-behind-api); without --yes and with no TTY, apply exits 3 before creating anything

The fallback test turned up two problems:

  • A real code issue, now fixed: a failed upload used to abort the run. It now continues to the summary and exits 5.
  • A Railway trap, issue draft scratch: CI warm-run measurement (do not merge) #15: after my service delete, the leftover production volume came back with the old data, so web refused to boot on old 2FA keys. I cleared the 0-user throwaway DB's authKey rows and the re-run recovered. A from-zero project can't hit this.

Cost: the test project cost $0.0045 (Railway usage). It was deleted after matching its exact name and ID; Railway scheduled the removal. Railway's 2026-10-21 incident was still open (deploy queueing for Pro): one deployment sat in DEPLOYING until the 15-minute wait ran out, while /healthz stayed ok.

Checks (pre-push, per COMMON.md): gofmt clean, go vet (module + .railway/railway.go), golangci-lint on deploy/ 0 issues, deploy/platform tests (new: Resolve, byte-preserving Edit, ForceUpload, 32-char names), actionlint. Full CI on GitHub Actions.

Not verified: a real GitHub app install. Railway could already read this repo, so the switch was shown with a nonexistent repo and then the real one. The admin path wasn't run in this test.

Merge Danger

Door: two-way. Nothing touches Railway on merge. platform.yaml now defaults project/repo to auto, so a fork whose origin is owner/name gets project name instead of the old hardcoded ai-patterns-go. Revert to restore.

Blast Radius: deployment. deploy/ (its own module), the railway Make targets (now @-silent, built into bin/railway-setup), README and AGENTS. App code is unchanged.

🤖 Generated with Claude Code

make railway-setup creates the project, environments, services, databases,
domains and secrets (generated or placeholders), deploys, and waits until
every /healthz answers. No dashboard or GitHub step first:

- project and repo default to auto (the checkout's origin); --project,
  --workspace, --environments, --admin, --region, --repo (or PLATFORM_*)
  are written into platform.yaml, comments and layout kept byte for byte
- GitHub: if Railway can't read the repo yet, services are created without
  a source and the checkout is deployed by upload, with the GitHub app
  install link as the next step; a re-run after installing connects the
  repo (serviceConnect is the access check) and switches to deploy-on-push,
  no duplicate services
- --yes, --json summary (project, source, per-environment URLs, health,
  next steps) and exit codes 0-5; make railway-doctor preflight (login,
  workspace, SSH key, GitHub, Doppler) with fixes
- RESEND_API_KEY placeholder so production boots on day one
- Railway's 32-char project limit; the DB password fix uses the local
  socket; failed uploads are reported, not fatal
- AGENTS.md "Deploy to Railway (for agents)", README quick start, three
  new Railway issue drafts

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jayleaton
jayleaton marked this pull request as ready for review October 22, 2026 13:38
@jayleaton
jayleaton merged commit 4755931 into main Oct 22, 2026
5 checks passed
jayleaton added a commit that referenced this pull request Oct 22, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jayleaton added a commit that referenced this pull request Oct 22, 2026
…-loop bench (#74)

* perf(devloop): make bench-devloop prints build, generate, lint, probe and per-package test timings

Adds scripts/bench-devloop.sh (cold + warm), the Bench dev loop workflow
(manual, or push a bench/* branch) and scripts/testbudget, which renders
go test -json as plain go test output, lists the slowest tests and enforces
per-package and per-test time budgets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* perf(test): every test runs in parallel, no sleeps, crawl checks each body once

paralleltest + tparallel make t.Parallel() the rule (no exceptions were
needed) and forbidigo bans time.Sleep in tests (synctest fake-clock sleeps go
through one documented helper). Fixes that made it safe: the UI crawls no
longer share a package-level crawlMethod, admin render tests stop mutating
templ's global buffer size, TestDopplerErrors and the web services table get
per-case fakes and controllers, and packages/auth migrates a template once and
clones it (shared database and newDatabase) instead of migrating per database.
The crawls split into parallel subtests and memoize the body-only checks:
static assets come back byte-identical in every state, and re-scanning them
was most of the crawl's time under -race.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* perf(ci): test-time budgets, slowest tests in every run, probes reuse the test profile

make test and CI pipe go test -json through scripts/testbudget: plain go test
output, the slowest tests, and a failure when a package runs over 30s or a
leaf test over 5s (make lint-tests proves both trip). The coverage probes
plant their uncovered statements in the real profile instead of recompiling
three packages under -race (CI runs them after the tests). AGENTS.md
"Fast tests" documents the rules; make bench-devloop the measurements.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(devloop): bench-devloop's warm test run compiles nothing

The cold run uses a throwaway cache, so the warm run was compiling every
-race variant; an untimed run now primes the cache first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(devloop): make ci hands its coverage profile to the probes, as CI does

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* perf(test): tests added by the UI overhaul and deploy run in parallel too

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* perf(test): crawl one subtest per state; bench A/B against a base ref on one machine

Per-route crawl subtests built a handler and mock set each: twice the CPU of
the serial crawl, which 2 vCPUs cannot pay back. One parallel subtest per
state (web: and method) keeps the setup at main's level and still uses both
CPUs. Separate CI runs of identical code varied by up to 50%, so the bench
gains BENCH_BASE: base and HEAD test runs alternate on the same machine and
their medians are compared (the workflow compares with main).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* perf(test): crawl routes as parallel subtests sharing one handler per state and method

Per-state crawl subtests ran 6-11s each on CI's single job and tripped the
5s test budget. Routes are subtests again, now sharing their state's
handler: at GOMAXPROCS=2 the crawls use main's CPU or less and finish
29-38% sooner.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ci): test-time budgets at about twice today's slowest (60s package, 10s test)

5s flaked on a CPU-bound scrypt test (5.28s) while a cold build cache
compiled on the same 2 vCPUs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* perf(test): deploy tests added in #76 run in parallel

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant