fix: five silent-failure bugs in the CLI/deploy path (SSL renewal, deploy id, server rm, install exit, re-login)#196
Open
Rish-it wants to merge 5 commits into
Conversation
A renewal that throws flips the domain to sslStatus="error", but both renewal selectors matched only "active": findExpiringSsl (the ssl:renew cron) and renewOrgCerts (the org-wide "renew all"). Once a renew failed, the domain was never picked up again by either path, so its certificate expired silently with no further attempt. Include "error" in both selectors so a failed renewal is retried on the next sweep (every 6h; well under Let's Encrypt's failed-validation rate limit). "external" (upstream-managed TLS) and "provisioning" (no cert issued yet) stay excluded.
POST /deployments responded { data: { deployment } }, but the CLI git-deploy
path reads data.deployment_id (apps/cli deploy.ts). The flat id was undefined,
breaking `openship deploy --watch` and the follow hint. The flat
{ deployment_id, project_id } shape is the convention elsewhere (dashboard
lib/api/updates.ts types it; the folder-deploy path already emits it) — this
trigger was the outlier.
Flatten deployment.id/projectId into the response. Additive: `deployment` (and
`skipped`) stay so existing readers of those keep working.
deleteServer hard-deleted a server with no occupancy check, orphaning the running containers of any project deployed to it — server delete does not enqueue orphaned-resource GC (that path is project-teardown only). Add deployment.countActiveOnServer(): projects whose CURRENT active deployment targets the server, matched on the snapshot meta serverId. deleteServer now returns 409 when the count is non-zero unless ?force=true is passed.
Two robustness gaps in the `server` command: - `rm` deleted with no confirmation; it now prompts (y/N) interactively and takes --force to skip. --force also forwards ?force=true so the API's active-deployment guard is bypassed for a deliberate teardown. - Non-streaming `install` exited 0 even when a component failed, unlike the --follow path. It now exits 1 when any component fails, so CI and scripts don't read a partial install as success.
The --api-url/--dashboard-url options carried localhost defaults, so commander set them even when unspecified. Re-authing an existing context without those flags wrote the localhost defaults over its saved endpoints, silently repointing a prod/staging context at localhost. Drop the option defaults and resolve from flag -> saved context -> localhost, so an omitted flag keeps the stored URL and an explicit flag still overrides.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Five independent fixes surfaced while auditing the CLI + its API surface as a
deployment platform. Each is a self-contained commit with a regression test.
1. SSL renewals never retry a failed cert (silent expiry)
Problem. When a renewal throws, the domain is flipped to
sslStatus="error".But both renewal selectors matched only
"active"—findExpiringSsl(
packages/db/src/repos/domain.repo.ts, thessl:renewcron) andrenewOrgCerts(
apps/api/.../domain.service.ts, the org-wide "renew all"). So once a renewfailed, the domain dropped out of both automatic renewal paths: the cron never
retried it, and its certificate expired unless the operator happened to redeploy
the project or manually re-check SSL (which route through the deploy-time tracker
that can flip it back to
active). The automatic safety net — the whole point ofthe renewal cron — no longer covered it.
Cause.
eq(domain.sslStatus, "active")/d.sslStatus !== "active"— theerror state is exactly the one that most needs retrying.
Fix. Include
"error"in both selectors. The cron runs every 6h, well underLet's Encrypt's failed-validation rate limit.
"external"(upstream-managed TLS)and
"provisioning"(no cert issued yet) stay excluded.2. Deploy trigger returns no deployment_id
Problem.
POST /deploymentsresponded{ data: { deployment } }, but the CLIgit-deploy path reads
data.deployment_id(apps/cli/deploy.ts). The flat id wasundefined, breakingopenship deploy --watchand the follow hint. The flat{ deployment_id, project_id }shape is the established convention elsewhere(
dashboard/lib/api/updates.tstypes it; the folder-deploy path already emitsit) — this trigger was the outlier.
Fix. Flatten
deployment.id/projectIdinto the response. Additive —deployment(andskipped) stay, so readers of those keep working.3.
server rmorphans running containersProblem.
deleteServerhard-deleted a server with no occupancy check,orphaning the containers of any project deployed to it — server delete does not
enqueue orphaned-resource GC (that's project-teardown only).
Fix. New
deployment.countActiveOnServer()counts projects whose currentactive deployment targets the server (matched on the snapshot
meta->>'serverId').deleteServerreturns409when that count is non-zero, unless?force=true.The CLI
rmgains a confirmation prompt and--force(which also forwards?force=true).4. Non-streaming
installexits 0 on failureProblem.
openship server install(without--follow) exited0even when acomponent failed, unlike the streaming path — CI read a partial install as
success.
Fix. Track failures and
exit(1)when any component fails.5. Re-login resets a context's endpoints to localhost
Problem.
--api-url/--dashboard-urlcarried localhost defaults, socommander set them even when unspecified. Re-authing an existing context without
those flags overwrote its saved endpoints with localhost, silently repointing a
prod/staging context.
Fix. Drop the option defaults; resolve flag → saved context → localhost. An
omitted flag keeps the stored URL; an explicit flag still overrides.
Tests
Regression test per fix, each proven RED before the fix and GREEN after:
domain.repo.test.ts—findExpiringSslselectsactive+error, excludesexternal/provisioning/fresh (PGlite).deployment.controller.test.ts— response carriesdeployment_id/project_id.deployment.repo.test.ts—countActiveOnServercounts only active-pointerdeployments (PGlite);
servers.controller.test.ts— 409 when occupied, bypasson force, delete when free.
server.test.ts— install exits 1 on component failure;rm --forcesends?force=true.login.test.ts— re-login without--api-urlkeeps the saved endpoint; anexplicit flag overrides.
Full suites green:
packages/db(34),apps/cli(81),apps/api(749);tsc --noEmitclean in all three.Not included
A sixth candidate — a preview deploy overwriting the production active-deployment
pointer — is a genuine bug but its fix depends on the intended environment model
(per-env project rows vs. a per-deployment
environmentflag), which isn't clearfrom the code. Filed as #195 (a question) rather than guessing in the
active-deployment path.