Skip to content

Releases: web-casa/webcasa

v0.19.0 — Fork PR preview support

05 May 17:47

Choose a tag to compare

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:

  1. 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.
  2. Admin reviews the PR diff on GitHub + clicks Approve in the
    Previews tab.
  3. Build runs against the fork's clone_url, pinned to the
    exact SHA admin approved
    (via git fetch <url> <sha> +
    rev-parse verification — not git clone --branch, which would
    race fork-author force-pushes).
  4. Container starts, Caddy host created, URL goes live.
  5. 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.
  6. 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 secret

UI: 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 -- --upgrade

To enable fork PR previews on a project:

  1. Audit env vars — mark API keys / signing secrets as
    secret first
  2. Project Webhook tab → toggle Accept fork PR previews ON
  3. (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

05 May 14:41

Choose a tag to compare

Full Changelog: v0.17.0...v0.18.0

Full Changelog: v0.17.0...v0.18.0

v0.17.0

05 May 14:25

Choose a tag to compare

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:

  1. Wildcard DNS record
  2. Panel wildcard_domain setting
  3. Per-project preview_enabled toggle + GitHub token
  4. GitHub webhook event subscription
  5. 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-source

Full Changelog: v0.16.0...v0.17.0

Full Changelog: v0.16.0...v0.17.0

v0.16.0

05 May 14:11

Choose a tag to compare

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_BUILDS env var, default 3, capped at 64.
  • Build() does a non-blocking semaphore acquire; on full returns the
    new ErrBuildQueueFull which 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 defer in buildLoop.

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.Map keyed by previewKey{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.Clone and GitClient.Pull take httpsToken as a
    separate parameter (was: token embedded in URL).
  • injectHTTPSTokenEnv helper extracted; reused by all four call
    sites: Clone, Pull, CloneToDir, and lsRemoteHead.
  • Builder.Build signature: takes httpsToken.
  • service.go runBuildOnce and poller.go lsRemoteHead now resolve
    credentials, call ConvertSSHToCleanHTTPS to 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-source

What'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 UIWEBCASA_MAX_CONCURRENT_BUILDS is 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

05 May 07:56

Choose a tag to compare

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 project
  • preview_expiry (days, default 7) — tuned per project
  • github_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: log lines,
  • 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: status on change,
  • emits event: reset when the file shrinks (PR rebuild rotated
    build.log),
  • emits event: error on real IO errors (instead of silently
    hanging),
  • emits event: done when 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.log truncation
    (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 reset event was emitted but the frontend SSE
    helper had no onReset callback — silently dropped
  • R2-H2 the /settings PUT endpoint had a hardcoded allowlist
    that rejected wildcard_domain — UI saves silently 400'd
  • R3-M1 removing the binding:"required" tag to allow empty
    wildcard_domain regressed ALL settings (including
    auto_reload) — fixed by switching to *string to 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 PreviewDeployment rows are dropped on first start by
    the v0.14 guard in Init() — unchanged from v0.14.
  • The pr_comment_id column is added by AutoMigrate on 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-source

If 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-wide createMu. 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_COUNT env var so the token never appears in argv;
    main project Build still uses ConvertToHTTPS which 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 of synchronize webhooks could thrash the host

Full Changelog: v0.14.0...v0.15.0

Full Changelog: v0.14.0...v0.15.0

v0.14.0

20 Apr 05:43

Choose a tag to compare

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_request events (HMAC-verified,
    shares the existing webhook token + secret). Routes:
    • opened / reopened / synchronize → build-and-expose pipeline
    • closed → full teardown
  • PreviewDeployment table tracks per-PR state with composite unique
    index (project_id, pr_number), allocated BasePort in
    [20000, 25000), two-slot alternation, and a monotonic Generation
    token for fence-style concurrency control.
  • Admin endpoints:
    • GET /api/plugins/deploy/projects/:id/previews
    • DELETE /api/plugins/deploy/previews/:previewId
  • Daily GC sweeps preview rows past expires_at regardless of
    status (default 7 days, configurable per project via PreviewExpiry).
  • Plugin lifecycle: PreviewService.Stop() cancels in-flight
    goroutines via root context + WaitGroup with 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: DeleteProject continued past preview-cleanup failure.
    Effectively resolved by R9-M1 (abort-on-error + reordered to run
    preview cleanup first).
  • R8-M4: main Build() still uses ConvertToHTTPS which embeds
    the GitHub App token in the URL (visible to git remote -v).
    Preview path uses the env-var approach; main-path migration is a
    multi-file refactor scoped to v0.15.
  • R11-M1: createMu is per-PreviewService, so DeletePreview's
    destructive cleanup phase briefly blocks unrelated CreatePreview
    webhooks. Acceptable at single-project / low-PR-rate volumes;
    planned sync.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_domain in panel
    settings + a *.preview.example.com A 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
    synchronize webhooks across many projects could thrash the host;
    v0.15 will add a build-queue depth knob.

Other changes

  • backup plugin goroutine lifecycle hardening (R5+ sweep):
    AI-triggered backups now run via Service.TriggerAsync which
    registers with the service WaitGroup. Plugin Stop() waits up
    to 60s for in-flight kopia snapshots to finish, preventing the
    prior race where a backup write could land after the DB handle
    closed.
  • deploy plugin Rollback re-read (R5+ sweep): Rollback now
    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-source

If 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

19 Apr 14:37

Choose a tag to compare

Full Changelog: v0.12.1...v0.13.0

Full Changelog: v0.12.1...v0.13.0

v0.12.1

19 Apr 13:44

Choose a tag to compare

Full Changelog: v0.12.0...v0.12.1

Full Changelog: v0.12.0...v0.12.1

v0.12.0

19 Apr 13:23

Choose a tag to compare

Full Changelog: v0.11.0...v0.12.0

Full Changelog: v0.11.0...v0.12.0

v0.11.0

17 Apr 07:48

Choose a tag to compare

Full Changelog: v0.10.0...v0.11.0

Full Changelog: v0.10.0...v0.11.0