Releases: web-casa/webcasa
v0.19.0 — Fork PR preview support
v0.19.0 — Fork PR preview support (Vercel-style approval gate)
Release date: 2026-05-06
Runtime: Podman 5.6 — unchanged
Supported OS: AlmaLinux / Rocky Linux / RHEL / CentOS Stream / Fedora — 9 and 10 series
No breaking changes. Fork PR support is opt-in per project
(accept_fork_pr_previews, default off). Existing v0.14-v0.18
deployments see no behavior change.
What's new
The v0.14 Preview Deploy webhook handler rejected fork PRs because
the build pipeline wasn't safe (clone target was always the base
repo + head.ref, which doesn't exist in the fork). v0.19 lifts that
restriction with a security-first design.
Vercel-style approval gate (Option A — gate at build, not URL)
When accept_fork_pr_previews=true is set on a project, the webhook
flow becomes:
- PR opens → preview row created
(is_fork_pr=true, approved=false, status=awaiting_approval).
No clone, no build, no container — the fork's code does NOT
execute. - Admin reviews the PR diff on GitHub + clicks Approve in the
Previews tab. - Build runs against the fork's clone_url, pinned to the
exact SHA admin approved (viagit fetch <url> <sha>+
rev-parseverification — notgit clone --branch, which would
race fork-author force-pushes). - Container starts, Caddy host created, URL goes live.
- Subsequent pushes to the same PR are gated against the previously
approved SHA: a force-push resets approval AND tears down the
host immediately. Admin must re-review the new SHA before any
new code runs. - Admin can Revoke at any time → host torn down, container
kept for inspection.
Strict gate semantics: the URL stops resolving the moment status
flips to awaiting_approval — no Vercel-style "old version stays
live until new approval" trade-off. Safety over availability.
Secret env var marking
Marking an env var as secret=true excludes it from fork PR builds
even after approval. Use for API keys / signing secrets that fork
authors must not be able to read by adding logging code:
// In a malicious fork PR's build:
console.log(process.env.STRIPE_SECRET_KEY); // empty if marked secretUI: per-row Switch in the env vars tab. Value field auto-masks
(type=password) when toggled on.
Twelve review rounds, 23 findings landed
9 rounds of Codex review + 1 round of Claude /security-review +
2 follow-up Codex rounds. All findings (10 H + 9 M + 2 L + gofmt)
fixed in-tree.
| Round | Severity | Description |
|---|---|---|
| R1-H1 | High | ApprovePreview race left approved=true, host_id=0 — fixed with cheap pre-gate re-read of approved column |
| R1-H2 | High | head.repo.clone_url not validated as github.com — could leak GitHub App token to attacker-controlled host |
| R1-M1 | Medium | createApprovedHost failure left row stuck approved-but-hostless |
| R1-M2 | Medium | "Preview is live" PR comment posted before host actually existed |
| R1-L1 | Low | env var value input plain text even when secret=true |
| R2-H1 | High | Race window between gate re-read and final status write — fixed with post-finalize reconciliation under per-PR lock |
| R2-H2 | High | extractHost stripped @ everywhere, allowing https://evil.test/a@github.com/repo past the github.com guard — fixed with net/url.Parse + Hostname() |
| R2-H3 | High | RevokePreview cleared host_id even on DeleteHost failure — orphan Caddy host serving traffic |
| R2-M1 | Medium | nixpacks_installer SSE writer not goroutine-safe — interleaved corrupted frames |
| R3-H1 | High | Main host-create branch ran lock-free — RevokePreview race could land approved=false, host_id>0 |
| R4-H1 | High | Design: approval only gated URL, not build/run — fork code executed with non-secret env vars regardless of approval. Fixed by deferring goroutine spawn until ApprovePreview triggers spawnPreviewBuild (Option A) |
| R4-H2 | High | RevokePreview retry skipped when approved=false && host_id>0 — fixed idempotency check to require both |
| R4-H3 | High | Approval persisted across force-push — fork author could approve-then-push-malicious. Fixed with ApprovedHeadSHA per-SHA approval |
| R4-M1 | Medium | clone_url scheme/path not strictly validated — required https://github.com/<head.repo.full_name> exact match |
| R5-M1 | Medium | CreatePreviewWithFork released lock before status write — first approval click could be silently lost |
| R6-H1 | High | Drain of previous build job ran AFTER unlock — second runPreview could spawn against the same preview |
| R7-H1 | High | Deadlock between runPreview holding per-PR lock at host gate and CreatePreviewWithFork waiting to drain. Fixed by removing lock from runPreview entirely; CAS-style conditional UPDATEs (WHERE host_id=0 AND generation=gen AND approved=true) with DeleteHost-orphan rollback |
| R8-H1 | High | Webhook handler validated head.ref non-empty but not head.sha — empty SHA bypassed R4-H3 force-push fence |
| R8-M1 | Medium | DeleteHost ran AFTER 30s drain — URL stayed live up to 30s after status flipped to awaiting_approval |
| R9-M1 | Medium | DeleteHost moved under per-PR lock before drain (strict gate semantics) |
| R9-M2 | Medium | DeleteHost failure path lost host_id reference — admin couldn't retry. Fixed: only clear host_id on success |
| R10-H1 | High | Claude /security-review finding: git clone --depth 1 --branch fetches HEAD-of-branch at clone-execution time, not the approved SHA. Fork author force-push between admin approval and the clone could substitute unapproved code into the build with project's non-secret env vars. The Caddy host CAS-gate kept the URL dark, but the container ran the substituted code on the panel's network. Fix: new CloneAtSHA pins clone via git fetch <url> <sha> + git rev-parse HEAD verification |
| R11-H1 | High | runPreview's first DB-read of HeadSHA could see a force-push update committed AFTER goroutine spawn. Fix: capture spawnGen + spawnSHA at goroutine spawn; runPreview uses snapshots over DB-read for clone target |
| R11-L1 | Low | CloneAtSHA failure paths left partial dstDir — fixed with defer cleanup-on-error flag pattern |
| R12 | (clean) | v019-R12 clean — ready to ship |
Compatibility
- Go 1.26+ (unchanged)
- React 19 / Vite 6 (unchanged)
- Podman 5.6 (unchanged from v0.13)
- SQLite (unchanged)
No new system dependencies. Fork PR support is OFF by default per
project — existing deployments unaffected.
Migration
AutoMigrate adds new columns (accept_fork_pr_previews,
is_fork_pr, head_repo, head_clone_url, head_sha, approved,
approved_at, approved_by_user_id, approved_head_sha) with
safe defaults. EnvVars are stored as JSON; the new secret field
is optional and defaults to false on parse.
Upgrade path
curl -sSL https://web.casa/install.sh | bash -s -- --upgradeTo enable fork PR previews on a project:
- Audit env vars — mark API keys / signing secrets as
secretfirst - Project Webhook tab → toggle Accept fork PR previews ON
- (Optional) Set up a notification webhook so you're alerted when
a fork PR needs approval — v0.20+ scope; for now, the Previews
tab badge is the surface
Known scope (v0.20+)
- Per-PR notification when a fork PR is awaiting approval
(Discord / email / Slack) - Fork allowlist / approver delegation — restricting approval
rights to specific GitHub usernames or org members - Build queue UI persistence (env var → DB setting was v0.17;
per-project caps not yet)
Full Changelog: v0.18.0...v0.19.0
Full Changelog: v0.18.0...v0.19.0
Full Changelog: v0.18.0...v0.19.0
v0.18.0
Full Changelog: v0.17.0...v0.18.0
Full Changelog: v0.17.0...v0.18.0
v0.17.0
v0.17.0 — Build queue UI + Preview Deploy operator guide
Release date: 2026-05-05
Runtime: Podman 5.6 — unchanged
Supported OS: AlmaLinux / Rocky Linux / RHEL / CentOS Stream / Fedora — 9 and 10 series
No breaking changes. v0.17 is two small UX follow-ups to v0.16
defer-cleanup. Upgrade in place.
What's new
Build queue UI
The v0.16 panel-wide concurrent-build cap was env-var only
(WEBCASA_MAX_CONCURRENT_BUILDS). v0.17 surfaces it in the panel:
- Settings → General → Max concurrent builds field, range 1-64
- Persisted in the panel DB
- Takes effect on next panel restart (UI shows a clear callout)
- Env var still wins — useful for systemd unit pinning that survives
DB resets
Backend allowlist now accepts max_concurrent_builds with
strict 1-64 integer validation (internal/handler/setting.go).
The deploy plugin's parseMaxConcurrentBuilds resolves precedence
env var > DB setting > default 3.
Preview Deploy operator guide
New docs/preview-deploy-guide.md walks through end-to-end Preview
Deploy setup for a fresh install:
- Wildcard DNS record
- Panel
wildcard_domainsetting - Per-project
preview_enabledtoggle + GitHub token - GitHub webhook event subscription
- Verifying with a real PR
Plus tuning concurrency, troubleshooting common errors, and brief
architecture notes pointing at the v0.14/v0.15/v0.16 audit trail (62
findings landed across 18 Codex review rounds).
Migration
None required. v0.16 → v0.17 is purely additive.
Compatibility
- Go 1.26+ (unchanged)
- React 19 / Vite 6 (unchanged)
- Podman 5.6 (unchanged from v0.13)
- SQLite (unchanged)
Upgrade path
# Pre-built binary
curl -sSL https://web.casa/install.sh | bash -s -- --upgrade
# From source
curl -sSL https://web.casa/install.sh | bash -s -- --upgrade-from-sourceFull Changelog: v0.16.0...v0.17.0
Full Changelog: v0.16.0...v0.17.0
v0.16.0
v0.16.0 — Defer-cleanup batch
Release date: 2026-05-05
Runtime: Podman 5.6 (RHEL AppStream) + podman-compose 1.5 (EPEL) — unchanged
Supported OS: AlmaLinux / Rocky Linux / RHEL / CentOS Stream / Fedora — 9 and 10 series
No breaking changes. v0.16 is concurrency / security hardening on
the deploy plugin. No new features, no config changes required.
Upgrade in place.
Three deferred items shipped
v0.14 (Phase A) and v0.15 (Phase B) each closed long Codex review
loops but consciously deferred a handful of Medium-severity items to
keep the release scope manageable. v0.16 is the cleanup pass — no
new features, just collecting all three deferrals into one ship.
#7 — Panel-wide concurrent-build cap (NEW)
Problem: Build() per-project dedup (the v0.11 SingleFlight) only
prevents the SAME project from running twice concurrently. An
unrelated webhook flood — say, 20 projects' git pollers all firing
after a GitHub outage — would spawn N goroutines that each git clone + docker build, OOM-ing a 2 GiB minimum-spec host.
Fix:
- New
WEBCASA_MAX_CONCURRENT_BUILDSenv var, default 3, capped at 64. Build()does a non-blocking semaphore acquire; on full returns the
newErrBuildQueueFullwhich API handlers map to HTTP 503.- GitHub webhooks retry on 503 per their backoff schedule, so we never
grow an in-memory queue (the failure mode this fixes). - Slot held for the entire coalesced loop — a same-project pending
rebuild reuses our slot rather than re-acquiring (its Docker context- clone dir are still committed).
- Released exactly once via
deferinbuildLoop.
Operator notes: tune with WEBCASA_MAX_CONCURRENT_BUILDS=N in
your systemd unit / env file. Default 3 is conservative for a 2 GiB
host. Upper bound (64) prevents runaway misconfiguration.
#5 — Per-(project_id, pr_number) preview lock (R11-M1)
Problem: the v0.14 createMu was a single global mutex covering
ALL preview Create + Delete operations. DeletePreview's destructive
cleanup phase (Caddy DeleteHost + container removal + image rmi +
RemoveAll) holds the lock for several seconds. At scale (many active
PRs across many projects) this serialized the entire panel.
Fix:
previewLocks sync.Mapkeyed bypreviewKey{ProjectID, PRNumber}.- Lazily LoadOrStore'd per PR via
previewLock(projectID, prNumber);
evicted in DeletePreview after the row is successfully deleted (PR
is terminal, no more webhooks expected for that number). - All R10/R11/R12 critical sections preserved — same scope, just
per-key rather than global.
Two webhooks for different previews now run in parallel; only
same-PR Create/Delete serialize.
#6 — Main Build() token-via-env (R8-M4)
Problem: preview deploy used GIT_CONFIG_COUNT env-var token
delivery since v0.14 R6-H1, but the main project Build path still
used ConvertToHTTPS which embeds the GitHub App / OAuth token
directly in the URL. The token surfaced in git remote -v output and
was visible on disk in the repo's .git/config.
Fix:
GitClient.CloneandGitClient.PulltakehttpsTokenas a
separate parameter (was: token embedded in URL).injectHTTPSTokenEnvhelper extracted; reused by all four call
sites:Clone,Pull,CloneToDir, andlsRemoteHead.Builder.Buildsignature: takeshttpsToken.service.go runBuildOnceandpoller.go lsRemoteHeadnow resolve
credentials, callConvertSSHToCleanHTTPSto derive the clean URL,
and pass the token separately.- Token never appears in argv (visible to
ps), URL (visible to
git remote -v), or on-disk git config.
Migration: existing v0.15 installs have GitHub tokens already
embedded in the repo's git remote origin URL. The first Pull after
upgrading silently overwrites the remote with the clean URL. No
manual migration needed.
GitHub-host guard (v0.16-R1-H1)
Added in Codex review Round 1. ConvertSSHToCleanHTTPS accepts any
SSH/HTTPS host. With auth_method=github_app/github_oauth paired
with a misconfigured non-GitHub git_url (e.g.
git@gitlab.com:owner/repo), our code would have injected the
GitHub installation token into Authorization headers sent to
gitlab.com — a token leak via that host's access logs.
Fix: hard-error when extractHost(converted) != "github.com"
before any git command runs. Both runBuildOnce and lsRemoteHead
apply the guard.
Codex review summary
Two rounds, both small (defer items had already been Codex-reviewed
when originally identified):
| Round | Findings landed |
|---|---|
| R1 | 1 High + 1 Low (2) |
| R2 | (clean — verification pass) |
Compatibility
- Go 1.26+ (unchanged)
- React 19 / Vite 6 (unchanged)
- Podman 5.6 (unchanged from v0.13)
- SQLite (unchanged)
No new system dependencies. No new mandatory configuration.
WEBCASA_MAX_CONCURRENT_BUILDS is optional (default 3).
Upgrade path
# Pre-built binary
curl -sSL https://web.casa/install.sh | bash -s -- --upgrade
# From source
curl -sSL https://web.casa/install.sh | bash -s -- --upgrade-from-sourceWhat's NOT in this release
- Fork PR previews (still v0.17+ scope — needs admin-approval UI
gate + per-PR clone URL + security review). - Build queue settings UI —
WEBCASA_MAX_CONCURRENT_BUILDSis an
env var only; a panel setting can land in v0.17 if there's demand.
Full Changelog: v0.15.0...v0.16.0
Full Changelog: v0.15.0...v0.16.0
v0.15.0
v0.15.0 — Preview Deploy Phase B
Release date: 2026-05-05
Runtime: Podman 5.6 (RHEL AppStream) + podman-compose 1.5 (EPEL) — unchanged from v0.13/v0.14
Supported OS: AlmaLinux / Rocky Linux / RHEL / CentOS Stream / Fedora — 9 and 10 series
No breaking changes. v0.15 layers UI + observability on top of
the v0.14 backend. Upgrade in place.
Headline: Preview deploys are now usable from the panel
v0.14 shipped the backend pipeline (webhook → ephemeral container →
Caddy host → daily GC) but had no UI — admins had to poke the API or
watch the Go logs to see what was happening. v0.15 closes the loop:
- a Previews tab on every project shows live + historical preview
deployments, - per-preview build logs stream live to the browser via Server-
Sent Events, - successful deploys comment the live URL on the GitHub PR (one
comment per PR, edited on rebuilds, deleted on teardown), - wildcard domain + per-project preview settings are
configurable from the UI — no SQL required.
Six features (B1–B6)
B1 — Previews tab on the project detail page
Gated by the project's preview_enabled flag. Renders a table with
PR number, branch, the live URL (clickable when status=running),
status badge, allocated host port, expiry date, and per-row
View build log + Delete actions. Auto-polls every 3 s while
any preview is mid-build so the badge updates without a manual
refresh.
B2 — Per-project preview settings (Webhook tab)
Inline switches + inputs:
preview_enabled— gate the feature per projectpreview_expiry(days, default 7) — tuned per projectgithub_token— optional PAT or App installation token used for
PR comments
All save on blur — no extra Save button per field.
B3 — Wildcard preview domain (Settings → General)
Required for preview deploys. Validated client + server with the
RFC 1035 rule (≤253 total chars, ≤63 per label, no scheme / path /
wildcard syntax). Empty value disables previews panel-wide.
B4 — Static build log fetch
GET /api/plugins/deploy/previews/:id/log returns the raw
build.log, capped at 2 MiB tail (with a […truncated to last N bytes…] marker). Used for terminal-state previews where streaming
is unnecessary.
B5 — Live build log streaming via SSE
GET /api/plugins/deploy/previews/:id/log/stream opens a Server-
Sent Events connection that:
- ships the existing log content as
event: loglines, - polls the file every 500 ms for new bytes (caps each poll at 2
MiB; emits a marker when truncating), - polls the row's status every 2 s; emits
event: statuson change, - emits
event: resetwhen the file shrinks (PR rebuild rotated
build.log), - emits
event: erroron real IO errors (instead of silently
hanging), - emits
event: donewhen status leaves the in-progress set, then
closes, - has a 20-minute hard ceiling so a stuck client can't pin a
goroutine forever.
The frontend uses a hand-rolled streamSSE helper (fetch +
ReadableStream + tiny SSE parser) so the JWT can be sent via
Authorization: Bearer ... header — browser-native EventSource
doesn't support custom headers. The React state buffer is itself
capped at 1 MiB (with a head-drop marker) to prevent the page from
being OOMed by an exploding build.
B6 — Automatic PR comments
After a successful deploy, the bot POSTs a "🚀 Preview deployment is
live" comment to the GitHub PR with the URL. The comment ID is
persisted on the row so subsequent rebuilds PATCH the same comment
instead of spamming the PR thread. PR close → DELETE the comment.
Comment posting:
- best-effort — failures (rate limit, transient errors, missing
token) never fail the deploy - only falls back to POST when GitHub clearly says the comment is
gone (404/410); other PATCH failures retry on next deploy to
avoid duplicate comments - delete runs as the LAST step of teardown, AFTER the row delete
succeeds, so partial-cleanup-failure paths preserve the comment
ID for retry
Fork PR rejection
pull_request webhooks where head.repo.full_name !=
base.repo.full_name are rejected explicitly with a clear message:
{
"ok": true,
"message": "fork PR previews are not supported; only same-repo PRs trigger preview deploys",
"head": "fork-user/repo",
"base": "owner/repo"
}The clone path uses the project's base repo URL + head.ref, which
silently breaks for fork PRs (head branch doesn't exist in the base
repo). Cross-repo support is intentionally v0.16+ scope — it
requires per-PR clone URLs, a security review of running fork code
with the project's secrets, and an admin-approval gate.
Six rounds of Codex review
| Round | Findings landed |
|---|---|
| R1 | 2 High + 5 Medium + 1 Low (8) |
| R2 | 2 High + 2 Medium (4) |
| R3 | 1 Medium + 2 Low (3) |
| R4 | clean ✅ |
| R5 | 2 High + 1 Medium + 1 Low (4) |
| R6 | 2 Low (2) |
19 findings landed across the run, 0 deferred.
The non-trivial ones (worth knowing if you operate previews):
- R1-H1 SSE log streaming didn't handle
build.logtruncation
(a rebuild that re-creates the log file): the stream silently
stopped emitting bytes - R1-H2 / M2 unbounded log read could OOM the panel and the
React tab — both now capped (2 MiB backend, 1 MiB frontend) - R2-H1 SSE
resetevent was emitted but the frontend SSE
helper had noonResetcallback — silently dropped - R2-H2 the
/settingsPUT endpoint had a hardcoded allowlist
that rejectedwildcard_domain— UI saves silently 400'd - R3-M1 removing the
binding:"required"tag to allow empty
wildcard_domainregressed ALL settings (including
auto_reload) — fixed by switching to*stringto distinguish
missing from empty - R5-H1 / H2 fork PR rejection + FQDN total-length validation
(described above)
The full per-round commit history is preserved on
feat/v0.15-preview-phase-b. The squashed result on main keeps
git blame readable.
Compatibility
- Go 1.26+ (unchanged)
- React 19 / Vite 6 (unchanged)
- Podman 5.6 (unchanged from v0.13)
- SQLite (unchanged)
No new system dependencies. No new mandatory configuration.
Migration
- Pre-v0.14
PreviewDeploymentrows are dropped on first start by
the v0.14 guard inInit()— unchanged from v0.14. - The
pr_comment_idcolumn is added byAutoMigrateon first
start; default 0 means "no comment posted yet" and the next
successful deploy will POST one. Existing v0.14 previews keep
working.
Upgrade path
# Pre-built binary
curl -sSL https://web.casa/install.sh | bash -s -- --upgrade
# From source
curl -sSL https://web.casa/install.sh | bash -s -- --upgrade-from-sourceIf you don't intend to use preview deployments, no further action is
needed — the feature stays gated by per-project preview_enabled
(default false).
Known scope (v0.16+)
- Fork PR previews — requires admin-approval UI gate and a
separate clone URL per preview - Per-(project, PR) lock — DeletePreview's destructive cleanup
phase still briefly blocks unrelated CreatePreview webhooks via a
panel-widecreateMu. Acceptable at low PR volumes; planned
sync.Map-keyed lock will land in v0.16 - Main
Build()token-via-env migration — preview path uses
GIT_CONFIG_COUNTenv var so the token never appears in argv;
main project Build still usesConvertToHTTPSwhich embeds the
token in the URL. Not a v0.14/v0.15 regression — a v0.16 cleanup - Build queue depth knob — no panel-wide concurrent-build cap
yet; a flood ofsynchronizewebhooks could thrash the host
Full Changelog: v0.14.0...v0.15.0
Full Changelog: v0.14.0...v0.15.0
v0.14.0
v0.14.0 — Preview Deploy Phase A
Release date: 2026-04-19
Runtime: Podman 5.6 (RHEL AppStream) + podman-compose 1.5 (EPEL) — unchanged from v0.13
Supported OS: AlmaLinux / Rocky Linux / RHEL / CentOS Stream / Fedora — 9 and 10 series
No breaking changes. v0.14 adds a new opt-in feature (preview deployments)
on the deploy plugin without touching existing project / Docker / Caddy
behavior. Upgrade in place from v0.13.
Headline feature: ephemeral per-PR preview environments
GitHub pull_request webhook → ephemeral subdomain
(pr-N-slug-id.<wildcard>) → isolated container build → Caddy reverse-
proxy host. PR closed → full teardown. Backend-complete; frontend
"Previews" tab and build-log streaming arrive in Phase B.
What ships
- Webhook handler for GitHub
pull_requestevents (HMAC-verified,
shares the existing webhook token + secret). Routes:opened/reopened/synchronize→ build-and-expose pipelineclosed→ full teardown
PreviewDeploymenttable tracks per-PR state with composite unique
index(project_id, pr_number), allocatedBasePortin
[20000, 25000), two-slot alternation, and a monotonicGeneration
token for fence-style concurrency control.- Admin endpoints:
GET /api/plugins/deploy/projects/:id/previewsDELETE /api/plugins/deploy/previews/:previewId
- Daily GC sweeps preview rows past
expires_atregardless of
status (default 7 days, configurable per project viaPreviewExpiry). - Plugin lifecycle:
PreviewService.Stop()cancels in-flight
goroutines via root context +WaitGroupwith 30s drain ceiling so
plugin teardown doesn't leave zombie git/podman children.
Configuration
Per-project knobs (UI lands in Phase B; settable via API/DB today):
preview_enabled bool // gate the feature per project
preview_expiry int // days to keep preview alive (default 7)
github_token string // for posting PR comments (Phase B)
Panel-wide:
wildcard_domain string // e.g. "preview.example.com" — required
// for the subdomain pr-N-slug-id.<domain>
A wildcard DNS record must point at the panel's host. TLS is handled by
Caddy automatically once the upstream creates the host (per existing
project flow).
Concurrency model — twelve rounds of Codex review
The preview pipeline is a state machine over external resources
(container × 2 slots, Caddy host, image, on-disk source/log dirs)
driven by webhooks that can fire multiple times in milliseconds and
DB rows that can be deleted mid-build. Twelve rounds of independent
Codex review hardened the design against every race we could
enumerate:
Two-slot port alternation
Each rebuild flips to the unused slot:
slot 0 → BasePort (e.g. 20137)
slot 1 → BasePort + 5000 (e.g. 25137)
The Caddy upstream points at the currently-serving slot. New container
starts on the other slot's port; old container stops only after Caddy
traffic has moved. A failed rebuild leaves the previous version live
— no rename, no port-rebind, no 502 windows.
Generation token on every DB write
PreviewDeployment.Generation is a monotonic int. CreatePreview
(rebuild trigger) and DeletePreview both bump it. runPreview
snapshots gen on entry; every subsequent DB write
(setStatus / markFailed / host_id update / final slot transition)
is gated by WHERE generation = snapshot. A stale goroutine whose
ctx.Cancel() arrived too late finds its writes rejected with
RowsAffected==0 and tears down its own staging container instead of
corrupting state.
Lock discipline
A per-PreviewService createMu mutex serializes the critical
sections that must be atomic — upsert + jobs-map atomic swap
(CreatePreview) and bump + capture + job-snapshot + cleanup
(DeletePreview). The 30-second drain windows release the lock so
unrelated webhooks aren't blocked.
TCP readiness probe
waitForPortOpen (500ms tick / 30s cap) replaces the old fixed sleep.
A crashing container fails fast and is torn down before Caddy ever
sees it.
Token via env var
HTTPS clones for github_app / github_oauth projects deliver the
installation token via the GIT_CONFIG_COUNT env-var ladder, scoped
to http.https://<host>/.extraHeader. The token never appears in
ps output or git remote -v, and a redirect to a different origin
cannot inherit the Authorization header.
Bug-bash audit trail
12 review rounds, 43 findings landed across the run:
| Severity | Count |
|---|---|
| Critical | 3 |
| High | 22 |
| Medium | 14 |
| Low | 4 |
3 Mediums consciously deferred to v0.15:
- R8-M3:
DeleteProjectcontinued past preview-cleanup failure.
Effectively resolved by R9-M1 (abort-on-error + reordered to run
preview cleanup first). - R8-M4: main
Build()still usesConvertToHTTPSwhich embeds
the GitHub App token in the URL (visible togit remote -v).
Preview path uses the env-var approach; main-path migration is a
multi-file refactor scoped to v0.15. - R11-M1:
createMuis per-PreviewService, so DeletePreview's
destructive cleanup phase briefly blocks unrelated CreatePreview
webhooks. Acceptable at single-project / low-PR-rate volumes;
plannedsync.Map-based per-(project_id, pr_number)lock is
v0.15.
The full per-round commit history is preserved on
fix/v0.14-preview-codex-review (10 commits). The squashed result
on main keeps git blame readable.
Migration
Pre-v0.14 PreviewDeployment rows (none in production — Phase A was
unreleased) are dropped on first start by a guard in Init() that
detects the missing base_port column. Other plugin data is
unaffected.
If you've been running a pre-release Phase A build and have manually-
created preview rows, re-trigger them via PR webhook after the
upgrade — the new schema requires BasePort, Generation, Slot
columns that weren't in the prior layout.
Operator notes
- Wildcard DNS prerequisite: set
wildcard_domainin panel
settings + a*.preview.example.comA record before enabling
previews on any project. Misconfiguration causes a clear error at
webhook time, not silent failure. - Disk usage: each active preview holds one container image
(~hundreds of MB depending on builder), one source checkout, and
one log directory. The daily GC cleans expired rows; manual
pruning via the admin endpoint is also available. - Concurrent build limit: there's no panel-wide cap. A flood of
synchronizewebhooks across many projects could thrash the host;
v0.15 will add a build-queue depth knob.
Other changes
backupplugin goroutine lifecycle hardening (R5+ sweep):
AI-triggered backups now run viaService.TriggerAsyncwhich
registers with the serviceWaitGroup. PluginStop()waits up
to 60s for in-flightkopiasnapshots to finish, preventing the
prior race where a backup write could land after the DB handle
closed.deployplugin Rollback re-read (R5+ sweep):Rollbacknow
verifies the project row still exists after starting the rollback
container; if the project was deleted concurrently, the new
container is removed and a clear error returned.
Compatibility
- Go 1.26+ (unchanged)
- React 19 / Vite 6 (unchanged)
- Podman 5.6 (unchanged from v0.13)
- SQLite (unchanged)
No new system dependencies. No new mandatory configuration.
Upgrade path
# Pre-built binary
curl -sSL https://web.casa/install.sh | bash -s -- --upgrade
# From source
curl -sSL https://web.casa/install.sh | bash -s -- --upgrade-from-sourceIf you don't intend to use preview deployments, no further action is
needed — the feature is gated by the per-project preview_enabled
flag (default false).
Full Changelog: v0.13.0...v0.14.0
v0.13.0
Full Changelog: v0.12.1...v0.13.0
Full Changelog: v0.12.1...v0.13.0
v0.12.1
Full Changelog: v0.12.0...v0.12.1
Full Changelog: v0.12.0...v0.12.1
v0.12.0
Full Changelog: v0.11.0...v0.12.0
Full Changelog: v0.11.0...v0.12.0
v0.11.0
Full Changelog: v0.10.0...v0.11.0
Full Changelog: v0.10.0...v0.11.0