Skip to content

Feat/git autodeploy#19

Open
Lftobs wants to merge 10 commits into
devfrom
feat/git-autodeploy
Open

Feat/git autodeploy#19
Lftobs wants to merge 10 commits into
devfrom
feat/git-autodeploy

Conversation

@Lftobs

@Lftobs Lftobs commented Jul 15, 2026

Copy link
Copy Markdown
Owner

Description

Fixes #(issue)

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Refactor (no functional changes)
  • CI / Build / Tooling
  • Other (please describe):

How Has This Been Tested?

Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce.

  • Existing tests pass (bun test in apps/api/)
  • New tests added (if applicable)
  • Manual testing performed (describe steps)

Checklist

  • My code follows the project's code style (no comments, named exports, functional components, etc.)
  • I have read the contributing guidelines
  • I have added tests that prove my fix is effective or that my feature works
  • I have updated the documentation (if applicable)
  • My changes generate no new warnings or lint errors
  • I have run bun test in apps/api/ and all tests pass
  • I have synced the VERSION file if needed (bun run sync-versions)

Summary by CodeRabbit

  • New Features
    • Added a mobile-friendly sidebar with open/close controls and backdrop dismissal.
    • Enabled horizontal scrolling for tabs and data tables on smaller screens.
    • Improved installer checks for Docker access and added clearer setup guidance.
    • Enhanced the update command to fetch the latest release and refresh configuration automatically.
  • Bug Fixes
    • Improved failed deployment cleanup and image removal behavior.
    • Repository listings now focus on repositories affiliated with the signed-in account.
  • Maintenance
    • Added scheduled cleanup for managed Docker resources and queued failed jobs.
    • Added automatic restart policy for core services after interruptions.

Lftobs added 4 commits July 14, 2026 23:58
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
Copilot AI review requested due to automatic review settings July 15, 2026 01:40
@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 09ab981b-be1c-4416-8635-d0cb4e07ba89

📥 Commits

Reviewing files that changed from the base of the PR and between e6d0e69 and e925856.

📒 Files selected for processing (1)
  • apps/api/src/orchestrator/pipeline.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/api/src/orchestrator/pipeline.ts

📝 Walkthrough

Walkthrough

The 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.

Changes

Backend runtime and orchestration

Layer / File(s) Summary
Managed container lifecycle
apps/api/src/utils/dequel-labels.ts, apps/api/src/databases/manager.ts, apps/api/src/orchestrator/runtime.ts, apps/api/src/scaling/engine.ts
Provisioned, deployed, and scaled containers receive com.dequel.managed=true; runtime helpers generate container names and remove failed resources and images.
Deployment failure handling
apps/api/src/orchestrator/pipeline.ts, apps/api/src/orchestrator/__tests__/pipeline-cleanup.test.ts
Deployment state tracks whether the running state was reached and conditionally cleans up failed deployments, with tests covering build and post-deploy failures.
Scheduled cleanup and Redis workers
apps/api/src/orchestrator/cleanup.ts, apps/api/src/orchestrator/queue.ts, apps/api/src/orchestrator/__tests__/cleanup.test.ts
Docker resources and the Redis DLQ are periodically pruned; workers use dedicated Redis clients and DLQ removal covers all retry attempts.
Startup and repository retrieval
apps/api/src/index.ts, apps/api/src/api/github/index.ts
Startup launches build cleanup instead of the removed Git watcher, and repository retrieval uses owner-affiliated /user/repos results directly.

Responsive web interface

Layer / File(s) Summary
Mobile sidebar navigation
apps/web/src/components/Layout.tsx, apps/web/src/components/layout/Header.tsx, apps/web/src/components/layout/Sidebar.tsx
Layout state, route resets, header controls, backdrop behavior, and close controls support mobile sidebar navigation.
Responsive tables and tabs
apps/web/src/components/project/..., apps/web/src/routes/ProjectDetail.tsx, apps/web/src/routes/Settings.tsx
Project and settings tables support horizontal scrolling and minimum widths, while project tabs remain in a single scrollable row.
Responsive notification placement
apps/web/src/components/layout/NotificationBanner.tsx
Notification banners use responsive horizontal margins and breakpoint-specific right alignment.

Deployment tooling and service configuration

Layer / File(s) Summary
Service restart configuration
docker-compose.yml
Application, monitoring, and supporting services use restart: unless-stopped.
Installer Docker access checks
scripts/install.sh
The installer checks Docker daemon access and attempts Docker group setup when permissions are insufficient.
CLI installation and update flow
scripts/dequel
The CLI derives its installation path and version, reports environment-specific URLs, downloads release assets during updates, and refreshes version and help output.

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
Loading

Possibly related PRs

  • Lftobs/dequel#13: Both changes overlap in PipelineOrchestrator.runDeployment cleanup-related control flow.
  • Lftobs/dequel#15: Both changes modify Docker prerequisite handling in scripts/install.sh.
  • Lftobs/dequel#17: Both changes modify PipelineOrchestrator.runDeployment and deployment build or cleanup state.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and broadly matches the deployment/autodeploy changes, including git watcher removal and cleanup work.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/git-autodeploy

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 dequel CLI update flow (download latest config + restart) and installer prerequisite checks; add restart: unless-stopped across 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.

Comment thread scripts/dequel
Comment on lines +82 to +83
# shellcheck disable=SC2064
trap "rm -rf '$tmp_dir'" EXIT
Comment thread scripts/dequel
Comment on lines +103 to +104
mv "$tmp_dir/docker-compose.yml" "$DEQUEL_HOME/docker-compose.yml"
mv "$tmp_dir/Caddyfile" "$DEQUEL_HOME/infra/caddy/Caddyfile"
Comment thread scripts/dequel
cmd up -d
success "Dequel updated."

success "Dequel updated to $tag"
tryRun: mockTryRun,
}));

mock.module(fileUrl('../utils/config'), () => ({
},
}));

mock.module(fileUrl('../utils/docker-bin'), () => ({
Comment on lines 42 to +46
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++) {
Comment thread scripts/dequel
| grep '"tag_name"' | head -1 | sed -E 's/.*"([^"]+)".*/\1/') || true

if [ -z "$tag" ]; then
warn "Could not determine latest release, using main branch"
Comment on lines 553 to +557
},
);

deployed = true;

Comment thread apps/api/src/index.ts
import { alertEvaluator } from './monitoring/evaluator';
import { loadOrCreateJwtSecret } from './utils/secrets';
import { initAuth, cleanupExpiredTokens } from './utils/auth';
import { startBuildCleanup } from './orchestrator/cleanup';

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🧹 Nitpick comments (1)
apps/api/src/orchestrator/queue.ts (1)

46-48: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Use a pipeline for batch Redis operations.

Executing lrem in a loop creates an N+1 query issue, which increases round-trip latency. Use an ioredis pipeline 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

📥 Commits

Reviewing files that changed from the base of the PR and between b21922e and e6d0e69.

📒 Files selected for processing (25)
  • apps/api/src/api/github/index.ts
  • apps/api/src/databases/manager.ts
  • apps/api/src/git/watcher.ts
  • apps/api/src/index.ts
  • apps/api/src/orchestrator/__tests__/cleanup.test.ts
  • apps/api/src/orchestrator/__tests__/pipeline-cleanup.test.ts
  • apps/api/src/orchestrator/cleanup.ts
  • apps/api/src/orchestrator/pipeline.ts
  • apps/api/src/orchestrator/queue.ts
  • apps/api/src/orchestrator/runtime.ts
  • apps/api/src/scaling/engine.ts
  • apps/api/src/utils/dequel-labels.ts
  • apps/web/src/components/Layout.tsx
  • apps/web/src/components/layout/Header.tsx
  • apps/web/src/components/layout/NotificationBanner.tsx
  • apps/web/src/components/layout/Sidebar.tsx
  • apps/web/src/components/project/deployments/deployment-history.tsx
  • apps/web/src/components/project/domains/DomainsTab.tsx
  • apps/web/src/components/project/envtab/EnvVarTable.tsx
  • apps/web/src/components/project/volumes/VolumesTab.tsx
  • apps/web/src/routes/ProjectDetail.tsx
  • apps/web/src/routes/Settings.tsx
  • docker-compose.yml
  • scripts/dequel
  • scripts/install.sh
💤 Files with no reviewable changes (1)
  • apps/api/src/git/watcher.ts

Comment on lines +154 to +156
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) => ({

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Suggested change
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.

Comment on lines +20 to +21
await tryRun(dockerBin, ['image', 'prune', '-f']);
await tryRun(dockerBin, ['buildx', 'prune', '-f']);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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.

Suggested change
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.

Comment on lines +635 to +650
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),
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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: Pass undefined instead of imageTag to cleanupFailedDeployment when deployment.sourceType === 'image'.
  • apps/api/src/orchestrator/pipeline.ts#L162-L164: Guard the docker rmi -f execution so it only runs if deployment.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".

Comment on lines +62 to +81
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();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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.

Comment on lines +41 to +47
{/* 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)}
/>
)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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.

Suggested change
{/* 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

Comment on lines +109 to +134
<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">

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.tsx

Repository: 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.tsx

Repository: 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.

Comment thread scripts/dequel
Comment on lines +85 to +120
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"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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.

Comment thread scripts/dequel
Comment on lines +91 to +97
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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"
 	done

Also 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.

Comment thread scripts/install.sh
Comment on lines +43 to +55
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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}')
PY

Repository: 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}')
PY

Repository: 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.

Lftobs and others added 6 commits July 15, 2026 02:54
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
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.

2 participants