Feat/git autodeploy#19
Conversation
builds and containers - Add `DEQUEL_MANAGED_LABEL` for Docker resource tracking - Implement periodic Docker and dead-letter queue garbage collection - Add `cleanupFailedDeployment` to remove artifacts on pipeline failure - Improve worker connection management in deployment queue
layouts - Implement mobile sidebar toggle - Make dashboard tables and tabs scrollable on small screens - Set restart policies for docker services - Improve CLI installation and path resolution
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughThe pull request adds managed Docker labeling and deployment cleanup, replaces the Git watcher with scheduled build cleanup, improves Redis worker handling, adds responsive mobile and table layouts, updates service restart policies, and expands CLI installation and update workflows. ChangesBackend runtime and orchestration
Responsive web interface
Deployment tooling and service configuration
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant PipelineOrchestrator
participant RuntimeCleanup
participant Docker
PipelineOrchestrator->>RuntimeCleanup: cleanupFailedDeployment(deploymentId, imageTag)
RuntimeCleanup->>Docker: disconnect and remove deployment container
RuntimeCleanup->>Docker: remove image tag
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
This PR introduces periodic Docker/Redis cleanup and more robust deployment cleanup behavior in the API, improves the CLI’s update/install experience and Compose service resilience, and makes several web UI layout adjustments for better mobile/overflow handling. It also removes the previous Git polling watcher and narrows GitHub repo listing behavior.
Changes:
- Add Dequel-managed Docker labeling + scheduled pruning, and add failed-deploy cleanup logic + queue Redis worker connection adjustments.
- Update
dequelCLI update flow (download latest config + restart) and installer prerequisite checks; addrestart: unless-stoppedacross Compose services. - Improve web UI responsiveness (scrollable tabs/tables, mobile sidebar + header/menu tweaks, banner positioning).
Reviewed changes
Copilot reviewed 25 out of 25 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| scripts/install.sh | Adds Docker socket accessibility check and attempts docker-group enrollment guidance during install prerequisites. |
| scripts/dequel | Auto-detects install directory from script path; updates update behavior to download latest config; tweaks start messaging and help/version output. |
| docker-compose.yml | Adds restart: unless-stopped to services for improved resilience. |
| apps/web/src/routes/Settings.tsx | Makes tables horizontally scrollable and form layout wrap on smaller screens. |
| apps/web/src/routes/ProjectDetail.tsx | Makes the project tabs horizontally scrollable for narrow viewports. |
| apps/web/src/components/project/volumes/VolumesTab.tsx | Makes volumes table horizontally scrollable with a minimum width. |
| apps/web/src/components/project/envtab/EnvVarTable.tsx | Makes env-var table horizontally scrollable with a minimum width. |
| apps/web/src/components/project/domains/DomainsTab.tsx | Makes domains table horizontally scrollable with a minimum width. |
| apps/web/src/components/project/deployments/deployment-history.tsx | Makes deployment history table horizontally scrollable with a minimum width. |
| apps/web/src/components/layout/Sidebar.tsx | Adds mobile sidebar open/close behavior with backdrop and close button. |
| apps/web/src/components/layout/NotificationBanner.tsx | Improves banner positioning on mobile (left/right inset) vs desktop. |
| apps/web/src/components/layout/Header.tsx | Adds mobile “open sidebar” menu button and adjusts padding. |
| apps/web/src/components/Layout.tsx | Adds sidebar state management and closes sidebar on navigation; adjusts main padding/overflow. |
| apps/api/src/utils/dequel-labels.ts | Introduces a shared Docker label constant for Dequel-managed containers. |
| apps/api/src/scaling/engine.ts | Labels scaled containers as Dequel-managed. |
| apps/api/src/orchestrator/runtime.ts | Labels deployed containers; adds helper for container naming and a failed-deployment cleanup helper. |
| apps/api/src/orchestrator/queue.ts | Uses per-worker Redis connections for blocking operations; improves DLQ removal behavior. |
| apps/api/src/orchestrator/pipeline.ts | Tracks deployment state for cleanup behavior and attempts Docker cleanup on failure. |
| apps/api/src/orchestrator/cleanup.ts | Adds periodic garbage-collection: prunes Dequel-labeled containers and clears DLQ. |
| apps/api/src/orchestrator/tests/pipeline-cleanup.test.ts | Adds coverage for “cleanup only if failure happens before deploy succeeds” behavior. |
| apps/api/src/orchestrator/tests/cleanup.test.ts | Adds tests for Docker/DLQ prune behavior. |
| apps/api/src/index.ts | Starts build cleanup on boot; removes git watcher startup. |
| apps/api/src/git/watcher.ts | Removes the previous Git polling watcher implementation. |
| apps/api/src/databases/manager.ts | Labels provisioned database containers as Dequel-managed. |
| apps/api/src/api/github/index.ts | Restricts repo listing to affiliation=owner and removes org-repo merging logic. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # shellcheck disable=SC2064 | ||
| trap "rm -rf '$tmp_dir'" EXIT |
| mv "$tmp_dir/docker-compose.yml" "$DEQUEL_HOME/docker-compose.yml" | ||
| mv "$tmp_dir/Caddyfile" "$DEQUEL_HOME/infra/caddy/Caddyfile" |
| cmd up -d | ||
| success "Dequel updated." | ||
|
|
||
| success "Dequel updated to $tag" |
| tryRun: mockTryRun, | ||
| })); | ||
|
|
||
| mock.module(fileUrl('../utils/config'), () => ({ |
| }, | ||
| })); | ||
|
|
||
| mock.module(fileUrl('../utils/docker-bin'), () => ({ |
| const allAttempts = Array.from({ length: 10 }, (_, i) => | ||
| encodeJob({ id: deploymentId, attempt: i }), | ||
| ); | ||
| await this.redis.zrem(RETRY_KEY, ...allAttempts); | ||
| await this.redis.lrem(DLQ_KEY, 0, encodeJob({ id: deploymentId, attempt: 0 })); | ||
| for (let i = 0; i <= config.queueRetryMax + 1; i++) { |
| | grep '"tag_name"' | head -1 | sed -E 's/.*"([^"]+)".*/\1/') || true | ||
|
|
||
| if [ -z "$tag" ]; then | ||
| warn "Could not determine latest release, using main branch" |
| }, | ||
| ); | ||
|
|
||
| deployed = true; | ||
|
|
| import { alertEvaluator } from './monitoring/evaluator'; | ||
| import { loadOrCreateJwtSecret } from './utils/secrets'; | ||
| import { initAuth, cleanupExpiredTokens } from './utils/auth'; | ||
| import { startBuildCleanup } from './orchestrator/cleanup'; |
There was a problem hiding this comment.
Actionable comments posted: 9
🧹 Nitpick comments (1)
apps/api/src/orchestrator/queue.ts (1)
46-48: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low valueUse a pipeline for batch Redis operations.
Executing
lremin a loop creates an N+1 query issue, which increases round-trip latency. Use anioredispipeline to batch these operations.⚡ Proposed refactor
- for (let i = 0; i <= config.queueRetryMax + 1; i++) { - await this.redis.lrem(DLQ_KEY, 0, encodeJob({ id: deploymentId, attempt: i })); - } + const pipeline = this.redis.pipeline(); + for (let i = 0; i <= config.queueRetryMax + 1; i++) { + pipeline.lrem(DLQ_KEY, 0, encodeJob({ id: deploymentId, attempt: i })); + } + await pipeline.exec();🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/api/src/orchestrator/queue.ts` around lines 46 - 48, Update the retry cleanup loop in the relevant queue method to create an ioredis pipeline, enqueue each DLQ lrem operation on that pipeline, and execute the pipeline once after the loop. Preserve the existing retry range, key, count, and encoded job arguments.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/api/src/api/github/index.ts`:
- Around line 154-156: Update the repository-fetching flow around fetchGitHub
and allRepos to iterate through successive GitHub API pages until no
repositories remain, accumulating every page before mapping results. Preserve
the existing sorting, ownership filter, and repository mapping behavior while
ensuring users with more than 100 repositories receive the complete list.
In `@apps/api/src/orchestrator/cleanup.ts`:
- Around line 20-21: Update the Docker cleanup commands in the cleanup flow to
apply an age-based filter such as until=24h to image pruning, ensuring only old
dangling images are removed and concurrent builds retain recent intermediate
layers; apply the same safety constraint to the related buildx prune command if
supported.
In `@apps/api/src/orchestrator/pipeline.ts`:
- Around line 635-650: Prevent host image deletion in cleanupFailedDeployment:
at apps/api/src/orchestrator/pipeline.ts lines 635-650, pass undefined instead
of imageTag when deployment.sourceType is "image"; at lines 162-164, guard the
docker rmi -f execution so it only runs when deployment.sourceType is not
"image".
In `@apps/api/src/orchestrator/queue.ts`:
- Around line 62-81: Update runWorker to pass workerRedis to requeueDueJobs,
retryOrDlq, and the handler-error retry path. Change both method signatures to
accept a Redis client parameter and replace their internal uses of this.redis
with that client, ensuring all worker operations continue using the open worker
connection during shutdown.
In `@apps/web/src/components/layout/Sidebar.tsx`:
- Around line 41-47: Remove the unnecessary “Mobile Backdrop” comment above the
conditional backdrop element in the Sidebar component, leaving the existing
rendering and click behavior unchanged.
In `@apps/web/src/routes/ProjectDetail.tsx`:
- Around line 109-134: Update the tab navigation in ProjectDetail around
TabsList and the TabsTrigger elements so the trigger matching activeTab is
automatically scrolled into view whenever activeTab changes, including direct
navigation to far-right tabs such as logs. Use a ref or equivalent supported by
the Tabs implementation, while preserving the existing horizontally scrollable,
hidden-scrollbar styling.
In `@scripts/dequel`:
- Around line 91-97: Prevent the two prometheus.yml downloads in the dequel
setup flow from sharing or overwriting the same temporary path. Update the
download destinations and corresponding mv operations around the monitoring and
Grafana datasource loops so each source uses a distinct temporary namespace,
preserving the intended destination for both monitoring prometheus.yml and the
datasource prometheus.yml.
- Around line 85-120: Update the configuration-download flow in the dequel
update pipeline so every curl download is validated before any files are moved
into DEQUEL_HOME. Keep all downloaded artifacts staged in tmp_dir, fail the
update on any unsuccessful curl, and perform the existing moves only after the
complete download set succeeds, preventing partial or mixed-version
installations.
In `@scripts/install.sh`:
- Around line 43-55: After the successful usermod branch in the Docker socket
access check, stop the installation flow until the user starts a fresh shell or
runs newgrp docker; do not continue to docker compose version in the current
shell. Preserve the existing guidance and failure handling for unsuccessful
group updates.
---
Nitpick comments:
In `@apps/api/src/orchestrator/queue.ts`:
- Around line 46-48: Update the retry cleanup loop in the relevant queue method
to create an ioredis pipeline, enqueue each DLQ lrem operation on that pipeline,
and execute the pipeline once after the loop. Preserve the existing retry range,
key, count, and encoded job arguments.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: e94c4b54-0a10-445b-a246-6491617e62dc
📒 Files selected for processing (25)
apps/api/src/api/github/index.tsapps/api/src/databases/manager.tsapps/api/src/git/watcher.tsapps/api/src/index.tsapps/api/src/orchestrator/__tests__/cleanup.test.tsapps/api/src/orchestrator/__tests__/pipeline-cleanup.test.tsapps/api/src/orchestrator/cleanup.tsapps/api/src/orchestrator/pipeline.tsapps/api/src/orchestrator/queue.tsapps/api/src/orchestrator/runtime.tsapps/api/src/scaling/engine.tsapps/api/src/utils/dequel-labels.tsapps/web/src/components/Layout.tsxapps/web/src/components/layout/Header.tsxapps/web/src/components/layout/NotificationBanner.tsxapps/web/src/components/layout/Sidebar.tsxapps/web/src/components/project/deployments/deployment-history.tsxapps/web/src/components/project/domains/DomainsTab.tsxapps/web/src/components/project/envtab/EnvVarTable.tsxapps/web/src/components/project/volumes/VolumesTab.tsxapps/web/src/routes/ProjectDetail.tsxapps/web/src/routes/Settings.tsxdocker-compose.ymlscripts/dequelscripts/install.sh
💤 Files with no reviewable changes (1)
- apps/api/src/git/watcher.ts
| const repos = await fetchGitHub("/user/repos?per_page=100&sort=updated&affiliation=owner", token); | ||
| const allRepos = Array.isArray(repos) ? repos : []; | ||
| const orgRepos: any[] = []; | ||
| for (const org of Array.isArray(orgs) ? orgs : []) { | ||
| try { | ||
| const orgR = await fetchGitHub(`/orgs/${org.login}/repos?per_page=100&sort=updated&type=all`, token); | ||
| if (Array.isArray(orgR)) orgRepos.push(...orgR); | ||
| } catch {} | ||
| } | ||
| const seen = new Set<number>(); | ||
| const merged = [...allRepos, ...orgRepos].filter((r) => { | ||
| if (seen.has(r.id)) return false; | ||
| seen.add(r.id); | ||
| return true; | ||
| }); | ||
| return merged.map((r: any) => ({ | ||
| return allRepos.map((r: any) => ({ |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Implement pagination to retrieve all repositories.
Fetching only the first page with per_page=100 limits the results. Users who own more than 100 repositories will not see their complete list. Consider iterating through the pages to fetch all available repositories.
💡 Proposed fix with pagination
- const repos = await fetchGitHub("/user/repos?per_page=100&sort=updated&affiliation=owner", token);
- const allRepos = Array.isArray(repos) ? repos : [];
+ let allRepos: any[] = [];
+ let page = 1;
+ while (true) {
+ const repos = await fetchGitHub(`/user/repos?per_page=100&sort=updated&affiliation=owner&page=${page}`, token);
+ if (!Array.isArray(repos) || repos.length === 0) break;
+ allRepos.push(...repos);
+ if (repos.length < 100) break;
+ page++;
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const repos = await fetchGitHub("/user/repos?per_page=100&sort=updated&affiliation=owner", token); | |
| const allRepos = Array.isArray(repos) ? repos : []; | |
| const orgRepos: any[] = []; | |
| for (const org of Array.isArray(orgs) ? orgs : []) { | |
| try { | |
| const orgR = await fetchGitHub(`/orgs/${org.login}/repos?per_page=100&sort=updated&type=all`, token); | |
| if (Array.isArray(orgR)) orgRepos.push(...orgR); | |
| } catch {} | |
| } | |
| const seen = new Set<number>(); | |
| const merged = [...allRepos, ...orgRepos].filter((r) => { | |
| if (seen.has(r.id)) return false; | |
| seen.add(r.id); | |
| return true; | |
| }); | |
| return merged.map((r: any) => ({ | |
| return allRepos.map((r: any) => ({ | |
| let allRepos: any[] = []; | |
| let page = 1; | |
| while (true) { | |
| const repos = await fetchGitHub(`/user/repos?per_page=100&sort=updated&affiliation=owner&page=${page}`, token); | |
| if (!Array.isArray(repos) || repos.length === 0) break; | |
| allRepos.push(...repos); | |
| if (repos.length < 100) break; | |
| page++; | |
| } | |
| return allRepos.map((r: any) => ({ |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/api/src/api/github/index.ts` around lines 154 - 156, Update the
repository-fetching flow around fetchGitHub and allRepos to iterate through
successive GitHub API pages until no repositories remain, accumulating every
page before mapping results. Preserve the existing sorting, ownership filter,
and repository mapping behavior while ensuring users with more than 100
repositories receive the complete list.
| await tryRun(dockerBin, ['image', 'prune', '-f']); | ||
| await tryRun(dockerBin, ['buildx', 'prune', '-f']); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Avoid pruning intermediate build layers.
Running docker image prune -f without time bounds will unconditionally delete all dangling images. If a docker build is concurrently running during this 30-minute interval, its intermediate untagged layers will be pruned, causing the build to randomly fail with "layer does not exist" errors.
Add a time filter (e.g., --filter until=24h) to safely reap old images without interrupting ongoing builds.
🛠️ Proposed fix
- await tryRun(dockerBin, ['image', 'prune', '-f']);
- await tryRun(dockerBin, ['buildx', 'prune', '-f']);
+ await tryRun(dockerBin, ['image', 'prune', '-f', '--filter', 'until=24h']);
+ await tryRun(dockerBin, ['buildx', 'prune', '-f', '--filter', 'until=24h']);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| await tryRun(dockerBin, ['image', 'prune', '-f']); | |
| await tryRun(dockerBin, ['buildx', 'prune', '-f']); | |
| await tryRun(dockerBin, ['image', 'prune', '-f', '--filter', 'until=24h']); | |
| await tryRun(dockerBin, ['buildx', 'prune', '-f', '--filter', 'until=24h']); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/api/src/orchestrator/cleanup.ts` around lines 20 - 21, Update the Docker
cleanup commands in the cleanup flow to apply an age-based filter such as
until=24h to image pruning, ensuring only old dangling images are removed and
concurrent builds retain recent intermediate layers; apply the same safety
constraint to the related buildx prune command if supported.
| if (!deployed) { | ||
| await emitLog( | ||
| deploymentId, | ||
| "system", | ||
| "Cleaning up Docker resources from failed deployment", | ||
| ); | ||
| await cleanupFailedDeployment( | ||
| deploymentId, | ||
| imageTag, | ||
| projectName, | ||
| deployment.projectId, | ||
| ).catch(e => | ||
| console.warn(`[Cleanup] Failed to clean deployment ${deploymentId}:`, e), | ||
| ); | ||
| } | ||
|
|
There was a problem hiding this comment.
🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win
Prevent accidental deletion of host Docker images.
For deployments where sourceType === 'image', imageTag refers to the original user-provided image (e.g., nginx:latest, postgres:14). Forcing removal of this tag using docker rmi -f will unintentionally delete the image from the Docker host, potentially degrading other services or projects relying on it. Image deletion should only occur for dynamically built images unique to the specific deployment.
apps/api/src/orchestrator/pipeline.ts#L635-L650: Passundefinedinstead ofimageTagtocleanupFailedDeploymentwhendeployment.sourceType === 'image'.apps/api/src/orchestrator/pipeline.ts#L162-L164: Guard thedocker rmi -fexecution so it only runs ifdeployment.sourceType !== 'image'.
📍 Affects 1 file
apps/api/src/orchestrator/pipeline.ts#L635-L650(this comment)apps/api/src/orchestrator/pipeline.ts#L162-L164
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/api/src/orchestrator/pipeline.ts` around lines 635 - 650, Prevent host
image deletion in cleanupFailedDeployment: at
apps/api/src/orchestrator/pipeline.ts lines 635-650, pass undefined instead of
imageTag when deployment.sourceType is "image"; at lines 162-164, guard the
docker rmi -f execution so it only runs when deployment.sourceType is not
"image".
| const workerRedis = createRedis(); | ||
| try { | ||
| while (!this.shuttingDown) { | ||
| await this.requeueDueJobs(); | ||
|
|
||
| const item = await this.redis.blpop(QUEUE_KEY, BLOCK_TIMEOUT_SEC); | ||
| if (!item) continue; | ||
| const job = decodeJob(item[1]); | ||
| if (!job) continue; | ||
| const item = await workerRedis.blpop(QUEUE_KEY, BLOCK_TIMEOUT_SEC); | ||
| if (!item) continue; | ||
| const job = decodeJob(item[1]); | ||
| if (!job) continue; | ||
|
|
||
| try { | ||
| const ok = await handler(job.id); | ||
| if (!ok) await this.retryOrDlq(job); | ||
| } catch (err) { | ||
| console.error(`[Queue] Worker ${workerId} handler error:`, err); | ||
| await this.retryOrDlq(job); | ||
| try { | ||
| const ok = await handler(job.id); | ||
| if (!ok) await this.retryOrDlq(job); | ||
| } catch (err) { | ||
| console.error(`[Queue] Worker ${workerId} handler error:`, err); | ||
| await this.retryOrDlq(job); | ||
| } | ||
| } | ||
| } finally { | ||
| await workerRedis.quit(); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win
Race condition causing job loss during shutdown.
The introduction of workerRedis for blpop means the worker no longer aborts its block when stop() calls this.redis.quit(). When the worker eventually unblocks (either by receiving a job or timing out), it may try to call this.retryOrDlq(job) or this.requeueDueJobs() which still rely on this.redis. Since this.redis is already closed by stop(), this will throw an exception and the popped job will be permanently lost.
Update retryOrDlq and requeueDueJobs to accept a Redis client parameter, and pass workerRedis to them so they safely use the open worker connection.
🛡️ Proposed fixes
Update the method calls inside runWorker:
- await this.requeueDueJobs();
+ await this.requeueDueJobs(workerRedis);
const item = await workerRedis.blpop(QUEUE_KEY, BLOCK_TIMEOUT_SEC);
if (!item) continue;
const job = decodeJob(item[1]);
if (!job) continue;
try {
const ok = await handler(job.id);
- if (!ok) await this.retryOrDlq(job);
+ if (!ok) await this.retryOrDlq(job, workerRedis);
} catch (err) {
console.error(`[Queue] Worker ${workerId} handler error:`, err);
- await this.retryOrDlq(job);
+ await this.retryOrDlq(job, workerRedis);
}Note: You will also need to update the method signatures of retryOrDlq and requeueDueJobs (which are outside this diff) to accept the workerRedis client and use it instead of this.redis.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/api/src/orchestrator/queue.ts` around lines 62 - 81, Update runWorker to
pass workerRedis to requeueDueJobs, retryOrDlq, and the handler-error retry
path. Change both method signatures to accept a Redis client parameter and
replace their internal uses of this.redis with that client, ensuring all worker
operations continue using the open worker connection during shutdown.
| {/* Mobile Backdrop */} | ||
| {sidebarOpen && ( | ||
| <div | ||
| className="fixed inset-0 bg-black/60 backdrop-blur-sm z-40 lg:hidden transition-opacity duration-300" | ||
| onClick={() => setSidebarOpen(false)} | ||
| /> | ||
| )} |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Remove unnecessary comment.
As per coding guidelines, source code should not contain comments unless absolutely necessary. The purpose of this backdrop is clear from its classes and behavior.
♻️ Proposed fix
- {/* Mobile Backdrop */}
{sidebarOpen && (
<div
className="fixed inset-0 bg-black/60 backdrop-blur-sm z-40 lg:hidden transition-opacity duration-300"
onClick={() => setSidebarOpen(false)}
/>
)}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| {/* Mobile Backdrop */} | |
| {sidebarOpen && ( | |
| <div | |
| className="fixed inset-0 bg-black/60 backdrop-blur-sm z-40 lg:hidden transition-opacity duration-300" | |
| onClick={() => setSidebarOpen(false)} | |
| /> | |
| )} | |
| {sidebarOpen && ( | |
| <div | |
| className="fixed inset-0 bg-black/60 backdrop-blur-sm z-40 lg:hidden transition-opacity duration-300" | |
| onClick={() => setSidebarOpen(false)} | |
| /> | |
| )} |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/web/src/components/layout/Sidebar.tsx` around lines 41 - 47, Remove the
unnecessary “Mobile Backdrop” comment above the conditional backdrop element in
the Sidebar component, leaving the existing rendering and click behavior
unchanged.
Source: Coding guidelines
| <TabsList className="mb-6 flex overflow-x-auto whitespace-nowrap justify-start h-auto p-1 bg-[#141417] border border-[#222227] rounded-lg max-w-full [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"> | ||
| <TabsTrigger value="deployments" className="text-xs px-3 py-1.5 rounded-md shrink-0 data-[state=active]:bg-zinc-800 data-[state=active]:text-zinc-100"> | ||
| Deployments | ||
| </TabsTrigger> | ||
| <TabsTrigger value="env-vars" className="text-xs px-3 py-1.5 rounded-md data-[state=active]:bg-zinc-800 data-[state=active]:text-zinc-100"> | ||
| <TabsTrigger value="env-vars" className="text-xs px-3 py-1.5 rounded-md shrink-0 data-[state=active]:bg-zinc-800 data-[state=active]:text-zinc-100"> | ||
| Env Vars | ||
| </TabsTrigger> | ||
| <TabsTrigger value="volumes" className="text-xs px-3 py-1.5 rounded-md data-[state=active]:bg-zinc-800 data-[state=active]:text-zinc-100"> | ||
| <TabsTrigger value="volumes" className="text-xs px-3 py-1.5 rounded-md shrink-0 data-[state=active]:bg-zinc-800 data-[state=active]:text-zinc-100"> | ||
| Volumes | ||
| </TabsTrigger> | ||
| <TabsTrigger value="databases" className="text-xs px-3 py-1.5 rounded-md data-[state=active]:bg-zinc-800 data-[state=active]:text-zinc-100"> | ||
| <TabsTrigger value="databases" className="text-xs px-3 py-1.5 rounded-md shrink-0 data-[state=active]:bg-zinc-800 data-[state=active]:text-zinc-100"> | ||
| Databases | ||
| </TabsTrigger> | ||
| <TabsTrigger value="domains" className="text-xs px-3 py-1.5 rounded-md data-[state=active]:bg-zinc-800 data-[state=active]:text-zinc-100"> | ||
| <TabsTrigger value="domains" className="text-xs px-3 py-1.5 rounded-md shrink-0 data-[state=active]:bg-zinc-800 data-[state=active]:text-zinc-100"> | ||
| Domains | ||
| </TabsTrigger> | ||
| <TabsTrigger value="scaling" className="text-xs px-3 py-1.5 rounded-md data-[state=active]:bg-zinc-800 data-[state=active]:text-zinc-100"> | ||
| <TabsTrigger value="scaling" className="text-xs px-3 py-1.5 rounded-md shrink-0 data-[state=active]:bg-zinc-800 data-[state=active]:text-zinc-100"> | ||
| Scaling | ||
| </TabsTrigger> | ||
| <TabsTrigger value="alerts" className="text-xs px-3 py-1.5 rounded-md data-[state=active]:bg-zinc-800 data-[state=active]:text-zinc-100"> | ||
| <TabsTrigger value="alerts" className="text-xs px-3 py-1.5 rounded-md shrink-0 data-[state=active]:bg-zinc-800 data-[state=active]:text-zinc-100"> | ||
| Alerts | ||
| </TabsTrigger> | ||
| <TabsTrigger value="observability" className="text-xs px-3 py-1.5 rounded-md data-[state=active]:bg-zinc-800 data-[state=active]:text-zinc-100"> | ||
| <TabsTrigger value="observability" className="text-xs px-3 py-1.5 rounded-md shrink-0 data-[state=active]:bg-zinc-800 data-[state=active]:text-zinc-100"> | ||
| Observability | ||
| </TabsTrigger> | ||
| <TabsTrigger value="logs" className="text-xs px-3 py-1.5 rounded-md data-[state=active]:bg-zinc-800 data-[state=active]:text-zinc-100"> | ||
| <TabsTrigger value="logs" className="text-xs px-3 py-1.5 rounded-md shrink-0 data-[state=active]:bg-zinc-800 data-[state=active]:text-zinc-100"> |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate the relevant file and the Tabs implementation/usages.
git ls-files 'apps/web/src/routes/ProjectDetail.tsx' 'apps/web/src/components/**' 'apps/web/src/**' | sed -n '1,200p'
echo
echo "=== ProjectDetail outline ==="
ast-grep outline apps/web/src/routes/ProjectDetail.tsx --view expanded
echo
echo "=== Search for Tabs components/usages ==="
rg -n "TabsList|TabsTrigger|TabsContent|from .*tabs|`@radix-ui/react-tabs`" apps/web/src -g '!**/*.map'
echo
echo "=== Inspect ProjectDetail around the tabs ==="
sed -n '1,220p' apps/web/src/routes/ProjectDetail.tsxRepository: Lftobs/dequel
Length of output: 15336
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== apps/web/src/components/ui/tabs.tsx ==="
cat -n apps/web/src/components/ui/tabs.tsxRepository: Lftobs/dequel
Length of output: 2437
Keep the active tab scrolled into view
With a horizontally scrollable, scrollbar-hidden tab strip, direct navigation to a far-right tab like ?tab=logs can leave the selected tab off-screen. Scroll the active trigger into view when activeTab changes, or keep a visible scroll affordance.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/web/src/routes/ProjectDetail.tsx` around lines 109 - 134, Update the tab
navigation in ProjectDetail around TabsList and the TabsTrigger elements so the
trigger matching activeTab is automatically scrolled into view whenever
activeTab changes, including direct navigation to far-right tabs such as logs.
Use a ref or equivalent supported by the Tabs implementation, while preserving
the existing horizontally scrollable, hidden-scrollbar styling.
| info "Downloading updated configuration..." | ||
| curl -fsSL "$base_url/docker-compose.yml" -o "$tmp_dir/docker-compose.yml" | ||
| curl -fsSL "$base_url/infra/caddy/Caddyfile" -o "$tmp_dir/Caddyfile" | ||
| curl -fsSL "$base_url/scripts/dequel" -o "$tmp_dir/dequel" | ||
| curl -fsSL "$base_url/VERSION" -o "$tmp_dir/VERSION" | ||
|
|
||
| for f in prometheus.yml loki-config.yml promtail-config.yml; do | ||
| curl -fsSL "$base_url/infra/monitoring/$f" -o "$tmp_dir/$f" | ||
| done | ||
|
|
||
| for f in loki.yml prometheus.yml; do | ||
| curl -fsSL "$base_url/infra/monitoring/grafana/datasources/$f" -o "$tmp_dir/$f" | ||
| done | ||
|
|
||
| for f in dashboards.yml deployed-apps.json; do | ||
| curl -fsSL "$base_url/infra/monitoring/grafana/dashboards/$f" -o "$tmp_dir/$f" | ||
| done | ||
|
|
||
| mv "$tmp_dir/docker-compose.yml" "$DEQUEL_HOME/docker-compose.yml" | ||
| mv "$tmp_dir/Caddyfile" "$DEQUEL_HOME/infra/caddy/Caddyfile" | ||
| mkdir -p "$DEQUEL_HOME/infra/monitoring/grafana/datasources" \ | ||
| "$DEQUEL_HOME/infra/monitoring/grafana/dashboards" | ||
|
|
||
| for f in prometheus.yml loki-config.yml promtail-config.yml; do | ||
| mv "$tmp_dir/$f" "$DEQUEL_HOME/infra/monitoring/$f" | ||
| done | ||
| for f in loki.yml prometheus.yml; do | ||
| mv "$tmp_dir/$f" "$DEQUEL_HOME/infra/monitoring/grafana/datasources/$f" | ||
| done | ||
| for f in dashboards.yml deployed-apps.json; do | ||
| mv "$tmp_dir/$f" "$DEQUEL_HOME/infra/monitoring/grafana/dashboards/$f" | ||
| done | ||
|
|
||
| mv "$tmp_dir/VERSION" "$DEQUEL_HOME/VERSION" | ||
| chmod +x "$tmp_dir/dequel" | ||
| mv "$tmp_dir/dequel" "$SCRIPT_PATH" |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
No error checking on any download in the update pipeline; partial failures leave a broken, mixed-version install.
Every curl -fsSL ... -o ... call here (compose file, Caddyfile, monitoring configs, dashboards) is unguarded, unlike the release-tag lookup which uses || true. docker-compose.yml is overwritten as early as Line 103, before any of the later downloads/moves are known to have succeeded — a single failed download (network blip, renamed asset, 404 on an older tag) leaves $DEQUEL_HOME with a mix of old and new config files and no rollback.
Consider validating all downloads succeeded (e.g. check curl exit codes, or stage everything in $tmp_dir and only start moving files once every download is confirmed) before mutating $DEQUEL_HOME.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/dequel` around lines 85 - 120, Update the configuration-download flow
in the dequel update pipeline so every curl download is validated before any
files are moved into DEQUEL_HOME. Keep all downloaded artifacts staged in
tmp_dir, fail the update on any unsuccessful curl, and perform the existing
moves only after the complete download set succeeds, preventing partial or
mixed-version installations.
| for f in prometheus.yml loki-config.yml promtail-config.yml; do | ||
| curl -fsSL "$base_url/infra/monitoring/$f" -o "$tmp_dir/$f" | ||
| done | ||
|
|
||
| for f in loki.yml prometheus.yml; do | ||
| curl -fsSL "$base_url/infra/monitoring/grafana/datasources/$f" -o "$tmp_dir/$f" | ||
| done |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win
Filename collision: two different prometheus.yml sources overwrite each other in $tmp_dir.
The loop at Line 91-93 downloads infra/monitoring/prometheus.yml to $tmp_dir/prometheus.yml, then the loop at Line 95-97 downloads infra/monitoring/grafana/datasources/prometheus.yml to the same $tmp_dir/prometheus.yml, clobbering the first file. The subsequent mv at Line 108-110 then moves the wrong (datasource) content into $DEQUEL_HOME/infra/monitoring/prometheus.yml, and the mv at Line 111-113 for the datasource copy fails outright because its source was already consumed.
🐛 Proposed fix — separate temp namespaces per source path
- for f in loki.yml prometheus.yml; do
- curl -fsSL "$base_url/infra/monitoring/grafana/datasources/$f" -o "$tmp_dir/$f"
- done
+ mkdir -p "$tmp_dir/datasources"
+ for f in loki.yml prometheus.yml; do
+ curl -fsSL "$base_url/infra/monitoring/grafana/datasources/$f" -o "$tmp_dir/datasources/$f"
+ done for f in loki.yml prometheus.yml; do
- mv "$tmp_dir/$f" "$DEQUEL_HOME/infra/monitoring/grafana/datasources/$f"
+ mv "$tmp_dir/datasources/$f" "$DEQUEL_HOME/infra/monitoring/grafana/datasources/$f"
doneAlso applies to: 108-116
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/dequel` around lines 91 - 97, Prevent the two prometheus.yml
downloads in the dequel setup flow from sharing or overwriting the same
temporary path. Update the download destinations and corresponding mv operations
around the monitoring and Grafana datasource loops so each source uses a
distinct temporary namespace, preserving the intended destination for both
monitoring prometheus.yml and the datasource prometheus.yml.
| if docker info >/dev/null 2>&1; then | ||
| success "Docker socket accessible" | ||
| else | ||
| warn "Docker socket not accessible — attempting to add user to docker group" | ||
| if sudo usermod -aG docker "$USER" 2>/dev/null; then | ||
| success "Added $USER to docker group" | ||
| warn "Log out and back in (or run 'newgrp docker') for the change to take effect" | ||
| else | ||
| warn "Could not add user to docker group automatically" | ||
| warn "Run this manually: sudo usermod -aG docker $USER && newgrp docker" | ||
| fi | ||
| fi | ||
|
|
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the installer around the reported lines and surrounding flow.
nl -ba scripts/install.sh | sed -n '1,140p'
# Find where `fail`, `success`, and docker-dependent commands are defined/used.
rg -n '^(fail|success|warn)\s*\(|docker compose version|docker info|usermod -aG docker|newgrp docker' scripts/install.sh
# Check whether the script exits or re-checks docker after adding the user to the docker group.
python3 - <<'PY'
from pathlib import Path
text = Path('scripts/install.sh').read_text()
for needle in ['usermod -aG docker', 'docker compose version', 'newgrp docker', 'fail ', 'exit ']:
print(f'\n=== {needle} ===')
for i, line in enumerate(text.splitlines(), 1):
if needle in line:
print(f'{i}: {line}')
PYRepository: Lftobs/dequel
Length of output: 191
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Read the installer with line numbers using portable tools.
sed -n '1,140p' scripts/install.sh | awk '{printf "%4d %s\n", NR, $0}'
# Show the relevant commands and control flow around docker handling.
python3 - <<'PY'
from pathlib import Path
text = Path('scripts/install.sh').read_text().splitlines()
for i, line in enumerate(text, 1):
if any(s in line for s in ['docker info', 'usermod -aG docker', 'docker compose version', 'newgrp docker', 'fail ', 'success ', 'warn ']):
print(f'{i}: {line}')
PYRepository: Lftobs/dequel
Length of output: 7663
Stop after adding the user to the docker group scripts/install.sh:43-56 — the current shell still won’t have the new group, so docker compose version can fail immediately after usermod. Require a fresh shell/newgrp before continuing.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/install.sh` around lines 43 - 55, After the successful usermod branch
in the Docker socket access check, stop the installation flow until the user
starts a fresh shell or runs newgrp docker; do not continue to docker compose
version in the current shell. Preserve the existing guidance and failure
handling for unsuccessful group updates.
builds and containers - Add `DEQUEL_MANAGED_LABEL` for Docker resource tracking - Implement periodic Docker and dead-letter queue garbage collection - Add `cleanupFailedDeployment` to remove artifacts on pipeline failure - Improve worker connection management in deployment queue
layouts - Implement mobile sidebar toggle - Make dashboard tables and tabs scrollable on small screens - Set restart policies for docker services - Improve CLI installation and path resolution
…eat/git-autodeploy
Description
Fixes #(issue)
Type of Change
How Has This Been Tested?
Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce.
bun testinapps/api/)Checklist
bun testinapps/api/and all tests passbun run sync-versions)Summary by CodeRabbit