24/ rollback - #46
Merged
Merged
Conversation
Backend: prunedAt column, per-deployment subdirs, symlink activation, pruning (KEEP_COUNT=5), POST /api/deployments/:id/rollback, Caddy root -> {appId}/current
Worker: extractStaticOutput per-deployment, fix nixpacks --env-file -> --env (v1.41.0 compat)
Frontend: rollback button with confirm dialog, GitHub URL auto-parse (owner/repo/branch/subdir), subdir dropdown for coolify-examples
Infra: API mounts shipyard_sites volume
Closes #24
- mkdir -p target before docker cp (was failing with ENOENT) - use rmSync instead of silent try/catch unlinkSync (avoids EEXIST when path is a directory, not a symlink)
API mounts sites volume :ro, can't write symlinks. Move filesystem ops to worker (has :rw). API validates only, then enqueues a type:"rollback" job. Worker runs processRollback: validates dir exists, calls activateDeployment, updates DB. Closes #24
feat(web): highlight active deployment in list API now returns activeDeploymentId alongside deployments. Frontend highlights the active row with a background tint and an ACTIVE badge. Also fixes unused path import in rollback.ts.
- storage.test.ts: 8 tests for getDeploymentDir, getCurrentSymlinkPath, activateDeployment (first/re-deploy/missing dir), pruneDeployments - rollback.test.ts: 4 tests for processRollback (not found, bad status, missing dir, success path) - deployments.test.ts: updated 6 tests — API no longer does fs ops, only validates and returns data
There was a problem hiding this comment.
Pull request overview
This PR adds rollback support by preserving per-deployment static artifacts, switching Caddy to serve a current symlink, and exposing rollback controls through the API and dashboard.
Changes:
- Adds per-deployment storage helpers, rollback worker handling, pruning metadata, and Caddy root updates.
- Adds API validation/queueing for rollback plus deployment list active-state data.
- Adds dashboard rollback confirmation UI and documentation for the symlink activation model.
Reviewed changes
Copilot reviewed 26 out of 26 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/worker/src/deployments/storage.ts | Adds deployment directory, activation symlink, and pruning helpers. |
| packages/worker/src/deployments/rollback.ts | Adds worker-side rollback processing. |
| packages/worker/src/deployments/strategies/nixpacks.ts | Writes static artifacts into deployment-specific directories and activates via symlink. |
| packages/worker/src/infrastructure/caddy/config-builder.ts | Points static file roots at sites/{appId}/current. |
| packages/worker/src/index.ts | Dispatches rollback queue jobs. |
| packages/worker/src/config/env.ts | Adds deployment retention count configuration. |
| packages/api/src/services/deployments.ts | Returns active deployment state and validates rollback targets. |
| packages/api/src/routes/deployments.ts | Adds POST /api/deployments/:id/rollback. |
| packages/api/src/routes/apps.ts | Marks deploy queue jobs with type. |
| packages/api/src/config/env.ts | Adds sites directory config for API. |
| packages/web/src/hooks/useDeployments.ts | Supports new deployments response shape and rollback mutation. |
| packages/web/src/pages/Dashboard.tsx | Adds rollback UI on deployment rows. |
| packages/shared/src/schema.ts | Adds deployments.prunedAt. |
| packages/shared/src/types/queue.ts | Adds queue job type discriminator. |
| drizzle/0008_nosy_wrecking_crew.sql | Adds pruned_at migration. |
| drizzle/meta/_journal.json | Registers migration 0008. |
| drizzle/meta/0008_snapshot.json | Adds schema snapshot for migration 0008. |
| packages/api/test/unit/deployments.test.ts | Adds rollback service unit tests. |
| packages/worker/test/unit/deployments/storage.test.ts | Adds storage helper/pruning tests. |
| packages/worker/test/unit/deployments/rollback.test.ts | Adds rollback worker tests. |
| packages/worker/test/unit/infrastructure/caddy-config.test.ts | Updates Caddy root expectations. |
| docs/adr/0014-symlink-activation-model.md | Documents symlink-based activation. |
| docs/adr/0013-volume-based-storage.md | Updates storage ADR for per-deployment artifacts. |
| docs/adr/0006-static-caddy-config.md | Updates static Caddy config ADR for symlink rollback. |
| docker-compose.yaml | Mounts sites volume into API container. |
| CONTEXT.md | Updates project context for volume storage and symlink activation. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| and( | ||
| eq(deployments.appId, appId), | ||
| eq(deployments.status, "success"), | ||
| inArray(deployments.prunedAt, [null as unknown as Date]), |
Comment on lines
+36
to
+38
| fs.rmSync(symlinkPath, { force: true, recursive: true }); | ||
|
|
||
| fs.symlinkSync(deploymentId, symlinkPath, "dir"); |
| // Activate via symlink swap, then prune old deployments | ||
| const sitesDir = env.SITES_DIR; | ||
| activateDeployment(sitesDir, app.id, deploymentId); | ||
| await pruneDeployments(db, app.id, sitesDir, env.DEPLOYMENT_KEEP_COUNT); |
Comment on lines
+122
to
+127
| if (deployment.prunedAt) { | ||
| throw Object.assign( | ||
| new Error("Cannot rollback: deployment artifacts have been pruned"), | ||
| { statusCode: 410 }, | ||
| ); | ||
| } |
Comment on lines
+114
to
+120
| if (deployment.status !== "success") { | ||
| throw Object.assign( | ||
| new Error( | ||
| `Cannot rollback: deployment has status "${deployment.status}"`, | ||
| ), | ||
| { statusCode: 422 }, | ||
| ); |
Comment on lines
+24
to
+26
| if (row.status !== "success") { | ||
| throw new Error(`Cannot rollback: deployment has status "${row.status}"`); | ||
| } |
| className={`w-1.5 h-1.5 rounded-full flex-shrink-0 ${ | ||
| STATUS_DOT[d.status] ?? STATUS_DOT.idle | ||
| deployments.map((d: Deployment) => { | ||
| const isRollbackable = d.status === "success" && !d.prunedAt; |
- fix: use isNull() instead of IN(NULL) so prune query matches rows - fix: atomic symlink swap via tmp + rename to avoid Caddy serving errors - fix: add prunedAt check in worker to close TOCTOU window - fix: defer pruneDeployments until after status=success for correct retention - fix: check artifact dir on disk in API before enqueueing rollback - fix: exclude active deployment from ROLLBACK button in UI - fix: correct stale JSDoc return type for listDeployments
Comment on lines
+132
to
+146
| const depDir = path.join(getEnv().SITES_DIR, deployment.appId, deployment.id); | ||
| if (!fs.existsSync(depDir)) { | ||
| throw Object.assign( | ||
| new Error("Cannot rollback: deployment directory does not exist on disk"), | ||
| { statusCode: 410 }, | ||
| ); | ||
| } | ||
|
|
||
| const [app] = await db | ||
| .select() | ||
| .from(apps) | ||
| .where(eq(apps.id, deployment.appId)); | ||
| if (!app || app.organizationId !== orgId) { | ||
| return { deployment: null, app: null }; | ||
| } |
Comment on lines
+113
to
+116
| res.json({ | ||
| deployment: result.deployment, | ||
| app: result.app, | ||
| }); |
Comment on lines
+148
to
+152
| if (app.activeDeploymentId === deployment.id) { | ||
| throw Object.assign(new Error("Deployment is already active"), { | ||
| statusCode: 409, | ||
| }); | ||
| } |
Comment on lines
+117
to
+123
| if (deployment.status !== "success") { | ||
| throw Object.assign( | ||
| new Error( | ||
| `Cannot rollback: deployment has status "${deployment.status}"`, | ||
| ), | ||
| { statusCode: 422 }, | ||
| ); |
| <button | ||
| type="button" | ||
| onClick={() => setRollbackTarget(d.id)} | ||
| className="font-mono text-[10px] tracking-widest text-ship-buoy/40 hover:text-ship-buoy opacity-0 group-hover:opacity-100 transition-all" |
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.
closes #24