Skip to content

fix(domain): skip remote vhost for cp-http80 apps + 409 idempotent recovery - #190

Merged
samo-agent merged 10 commits into
mainfrom
fix/domain-add-cp-http80-and-409-idempotent
Jul 21, 2026
Merged

fix(domain): skip remote vhost for cp-http80 apps + 409 idempotent recovery#190
samo-agent merged 10 commits into
mainfrom
fix/domain-add-cp-http80-and-409-idempotent

Conversation

@samo-agent

Copy link
Copy Markdown
Collaborator

Summary

  • Fix 1 — cp-http80 remote vhost skip: When `app.mainListen === "cp-http80"`, the remote SSH vhost step is now skipped. The CP already proxies via `header_up Host <app.mainHost>`, so no extra vhost is needed on the app VM. This also unblocks legacy apps that pre-date the releases-based deployment (no `.samohost-active-static.json` → guard in `buildCustomDomainVhostScript` rejects the script).
  • Fix 2 — 409 idempotent re-run: When `domain add` is interrupted after CF registration but before local state is written, a re-run now recovers the existing CF hostname via `listCustomHostnames` instead of aborting on the 409.

Root-cause analysis

Triggered by connecting `gamechangers.team` to the `game-changers` app (id `bf003044`, `mainListen: "cp-http80"`). First run registered the CF custom hostname then aborted on the remote vhost guard. Second run hit 409. Both failures are fixed here. The CP vhost was written and Caddy was reloaded successfully; `domains.json` has the record with `hostnameStatus: pending / sslStatus: pending_validation` (correct — awaiting customer DNS).

Test plan

  • bun test test/domain-command.test.ts — 21 pass (2 new tests cover both fixes)
  • bun test — 2061 pass, 0 fail
  • Verify RED commit (34e2201) + GREEN commit (35fe81a) are separate SHAs
  • CP vhost at /etc/caddy/sites.d/10-domain-gamechangers-team.caddy routes to 46.225.115.31:80 with header_up Host game-changers.samo.team
  • caddy validate passes; Caddy reloaded (confirmed via journal)

🤖 Generated with Claude Code

samo-agent and others added 6 commits July 18, 2026 19:09
The custom-domain vhost script runs as the admin SSH user 'samo', but static
release checkouts are owned by the app user, so 'git rev-parse HEAD' aborted
with dubious-ownership and the guard falsely reported release drift, blocking
'domain add' for every static app. Scope git safe.directory to the already
realpath-validated checkout path so the integrity check runs regardless of
checkout ownership. Deploy path is unaffected (it SSHes as the app user).

Apply the same fix to src/app/bootstrap.ts:762 (re-bootstrap of a host with
an active static deployment runs as root, which also trips dubious-ownership
on app-user-owned release dirs). Drop 2>/dev/null in both sites so real git
errors reach the operator instead of being masked by the drift message.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
'systemctl reload caddy' can fail on the control plane with systemd
status=226/NAMESPACE when the long-running caddy service's PrivateTmp
namespace goes stale, which blocked 'domain add' at the control-plane
vhost step. Fall back to 'caddy reload --config ... --force' (the admin
API, zero-downtime, Caddy's recommended reload) when systemctl reload
fails, so control-plane routing changes still apply. Fail-closed on a
genuinely invalid config is preserved (both paths validate).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…teTmp namespace failure

Records the systemd status=226/NAMESPACE failure mode (stale PrivateTmp
staging dir on long-running caddy.service) and the admin-API reload
fallback that samohost now uses on the control plane, plus the host-side
maintenance remedy.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pins two behaviours that were missing and caused gamechangers.team
domain registration to fail:

1. mainListen=cp-http80: remote vhost script must be skipped (the CP
   reverse_proxy already routes via header_up Host <mainHost>; the
   guard in the remote script rejects legacy apps without release state).

2. 409 duplicate: createCustomHostname returns 409 on re-run (CF already
   registered the hostname on a prior attempt that failed later);
   the command must recover via listCustomHostnames instead of aborting.

Both tests are RED against the current implementation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…covery

Two root-cause fixes surfaced while connecting gamechangers.team to the
game-changers app:

Fix 1 — cp-http80 remote vhost skip
When app.mainListen === "cp-http80", the control-plane already proxies
all traffic to the app VM using `header_up Host <app.mainHost>`, so the
app VM's existing mainHost vhost handles the custom domain without any
extra Caddy block.  Writing a second vhost was also impossible: the
guard in buildCustomDomainVhostScript requires .samohost-active-static.json,
which does not exist on legacy apps that pre-date the releases-based
deployment.  The fix: gate the remote SSH step on
`app.mainListen !== "cp-http80"`.

Fix 2 — 409 idempotent re-run
When domain add is interrupted after the CF API call but before
local state is written, a re-run hits a 409 (duplicate custom hostname).
The fix: catch CloudflareError(409) and recover the existing record via
listCustomHostnames instead of aborting.  Any other error status remains
fatal (fail-closed).

Both fixes are unit-tested in the companion RED commit (34e2201).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…p git rev-parse

Adds the missing test samorev flagged for bootstrap.ts:762. The change
in this PR fixed the dubious-ownership guard in buildHostBootstrapScript
(static path) — switching from `rev-parse HEAD 2>/dev/null` (which
suppressed the git error AND lacked safe.directory, causing an empty
capture that always failed the sha comparison) to the ownership-scoped
`-c safe.directory="$SAMOHOST_CHECKOUT_REAL" rev-parse HEAD` form.

The test mirrors the pattern already established in env-script.test.ts
#ownershipfix (line 3954) for the same root cause in buildCustomDomainVhostScript.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@samo-agent samo-agent closed this Jul 21, 2026
@samo-agent samo-agent reopened this Jul 21, 2026
@samo-agent
samo-agent marked this pull request as draft July 21, 2026 05:57
@samo-agent
samo-agent marked this pull request as ready for review July 21, 2026 05:57
samo-agent and others added 3 commits July 21, 2026 05:57
…rigger CI

Merge origin/main into fix/domain-add-cp-http80-and-409-idempotent to
resolve the routing.md doc conflict (both sides added new ##-sections after
line 74) and ensure the PR has a clean merge commit for CI to run against.

All tests pass (2316 pass; packages/auth/pg.test.ts skipped locally —
requires Postgres service, provided by CI).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@samo-agent
samo-agent merged commit 5bff4c5 into main Jul 21, 2026
1 check passed
@samo-agent
samo-agent deleted the fix/domain-add-cp-http80-and-409-idempotent branch July 21, 2026 06:06
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