Skip to content

Conversation

@webNeat
Copy link
Owner

@webNeat webNeat commented Jan 19, 2025

Summary by CodeRabbit

  • New Features

    • Containerized reverse proxy with Caddy, including per-service domain setup and templated configs.
    • New Docker-based dev environment and docker-compose service for running the app.
    • Directory creation now supports owner/group/mode overrides.
    • Optional SSH key passphrase and shallow Git clone support.
  • Refactor

    • Replaced legacy domain/port allocation with centralized reverse proxy flow.
    • Removed deprecated domain, port allocation, and Caddy install logic.
  • Documentation

    • Added comprehensive developer guide and minor README typo fix.
  • Tests

    • Parallel, dockerized test runner with CI mode; stabilized tests with startup waits.
  • Chores

    • CI test command update, dependency upgrades, workspace config tweaks.

@webNeat webNeat force-pushed the develop branch 3 times, most recently from 2329e6e to 75635ee Compare January 26, 2025 22:06
@webNeat webNeat force-pushed the develop branch 2 times, most recently from 76f746a to ec34e16 Compare April 20, 2025 19:53
@webNeat webNeat force-pushed the develop branch 2 times, most recently from 77bdd14 to 7355368 Compare May 26, 2025 18:34
@webNeat
Copy link
Owner Author

webNeat commented Aug 16, 2025

@gemini-cli /review

Copy link

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

📋 Review Summary

This pull request introduces a major refactoring of the testing framework to use Docker for isolated test runs and moves the Caddy reverse proxy to run as a Docker container instead of being installed directly on the host. These are great improvements for consistency and reliability.

🔍 General Feedback

  • The move to a Docker-based Caddy service is a solid architectural change that improves encapsulation and simplifies server setup.
  • The new test runner using Docker is a significant enhancement that will improve test reliability and prevent pollution of the host environment.
  • The codebase is cleaner now with the separation of concerns for the reverse proxy.
  • I've left a few comments on potential improvements regarding security in the compose.yaml, destructive commands in package.json, and the use of sleep in tests.

Overall, this is a high-quality pull request with significant improvements.

@coderabbitai
Copy link

coderabbitai bot commented Aug 18, 2025

Walkthrough

Replaces legacy Caddy installation and domain handling with a Docker-based reverse proxy module, adds templated Caddyfiles, integrates reverse proxy into server and app flows, updates types and Ansible task attrs, streamlines ports handling, introduces Docker-based test runner and CI script, adds Dockerfile/compose, and updates documentation.

Changes

Cohort / File(s) Summary
Reverse proxy (Caddy via Docker)
src/reverse_proxy/*, src/blocks/create_caddy_domain.ts, src/server.ts, src/services/app/git.ts, src/files/*, src/blocks/index.ts
Adds Caddy reverse proxy module running in Docker, templated server/service Caddyfiles, per-service domain tasks, server integration, and export updates.
Legacy domain/ports removal
src/blocks/create_domain.ts, src/blocks/install_caddy.ts, src/blocks/set_available_port.ts
Removes old Caddy install, domain creation, and port allocation utilities.
Types and task APIs
src/types.ts, src/ansible/tasks/builtin.ts, src/blocks/create_directory.ts, src/blocks/delete_domain.ts, src/blocks/generate_ssh_key.ts, src/blocks/build_repo.ts, src/services/container.ts
Adds ReverseProxy types/config, optional ssh passphrase, new attrs (unsafe_writes, depth), directory config defaults, unsafe_writes on /etc/hosts edits, optional SSH passphrase, shallow git clone, and path joins fix.
Templates and file util
src/files.ts, src/files/server_caddyfile, src/files/service_caddyfile
Introduces file templating helper and Caddyfile templates for server and services.
CI and test runner
.github/workflows/tests.yaml, package.json, test.ts
Switches CI to ci:test; enhances local tests to run in Docker with concurrency, logging, and CI mode; updates scripts and deps.
Tests updates
tests/*.test.ts, tests/utils.ts, tests/utils/Dockerfile
Adjusts test helper imports, adds startup sleeps, removes caddy systemctl assertion, simplifies SSH config, unique test names, extra chmod; removes legacy test Dockerfile.
Container environment
Dockerfile, compose.yaml
Adds dev container image with Docker-in-Docker, Node 22, pnpm, nixpacks; docker-compose service for local workflow.
Docs
about.md, README.md, task.md, tasks.todo
Adds comprehensive docs, typo fix, design plan for Dockerized Caddy, and reorganized roadmap/todos.
PNPM workspace
pnpm-workspace.yaml
Adds onlyBuiltDependencies: esbuild.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant User
  participant Hosty
  participant Server
  participant ReverseProxy as ReverseProxy (Caddy)
  participant Docker

  User->>Hosty: server({...})
  Hosty->>ReverseProxy: caddy(config?)
  Hosty->>Server: attach reverse_proxy

  User->>Hosty: deploy app.git({ domain, instances })
  Hosty->>Server: get_server_tasks()
  Server->>ReverseProxy: get_server_tasks(server)
  ReverseProxy->>Server: tasks to setup dirs, master Caddyfile, Caddy container
  Server->>Docker: create caddy service (ports 80/443, volumes)

  Hosty->>ReverseProxy: get_service_tasks(server, {service_name, domain, instances})
  ReverseProxy->>Server: write include Caddyfile, register caddyfile
  ReverseProxy->>Docker: reload caddy container when caddyfile.changed
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

In docks of code, our Caddy sails,
Templates whisper wind in sails.
Ports once scattered, now align,
Domains arise by proxy design.
Tests hop in parallel delight—
A rabbit nods: ship feels right.
(thump-thump) Onward through the night. 🐇🚢

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch develop

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

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

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 10

🔭 Outside diff range comments (1)
src/services/app/git.ts (1)

59-61: Bug: Deleting the wrong Caddyfile path on destroy

With Caddy now centralized, per-service Caddyfiles live under ${server.hosty_dir}/caddy/includes, not under the service directory. The current destroy task points at ${service_dir}/Caddyfile, which won’t clean up the include file.

Use the includes path so destroy removes the correct file:

-  if (config.domain) {
-    tasks.push(blocks.delete_domain({ domain: config.domain, caddyfile_path: path.join(service_dir, 'Caddyfile') }))
-  }
+  if (config.domain) {
+    const caddyfile_path = path.join(server.hosty_dir, 'caddy', 'includes', `${config.name}.Caddyfile`)
+    tasks.push(blocks.delete_domain({ domain: config.domain, caddyfile_path }))
+  }
🧹 Nitpick comments (42)
src/blocks/generate_ssh_key.ts (1)

11-20: Guard against passing undefined to crypto.ssh_key.

When passphrase is optional, passing { passphrase: undefined } may serialize to YAML as null/undefined (tooling-dependent), which could break Ansible or lead to unintended behavior. Include the key only when defined.

Apply this diff:

 export function generate_ssh_key({ path, passphrase }: Config): Block {
-  return block(`Generate ssh key at ${path}`, {}, [
+  return block(`Generate ssh key at ${path}`, {}, [
     builtin.stat('Check if SSH key exists', { path }, { register: 'ssh_key' }),
     builtin.file(
       'Ensure .ssh directory exists',
       { path: dirname(path), state: 'directory', owner: '{{ansible_user}}', mode: '0700' },
       { when: 'not ssh_key.stat.exists', become: true },
     ),
-    crypto.ssh_key('Generate SSH key', { type: 'rsa', path, passphrase }, { when: 'not ssh_key.stat.exists', become: true }),
+    // Build params conditionally to avoid serializing an undefined passphrase
+    crypto.ssh_key(
+      'Generate SSH key',
+      { type: 'rsa', path, ...(passphrase ? { passphrase } : {}) },
+      { when: 'not ssh_key.stat.exists', become: true },
+    ),
   ]).get()
 }

Optional: consider adding a stronger key size (e.g., size: 4096) if supported by crypto.ssh_key.

package.json (2)

12-14: Avoid destructive Docker cache pruning in local test script.

docker builder prune -f -a will wipe build cache for all users on the machine. This is surprising and slow for local dev. Keep pruning in CI-only and make local tests non-destructive.

   "scripts": {
     "build": "tsc -p tsconfig.build.json",
-    "test": "rm -rf .tests/* && docker builder prune -f -a && node --import tsx test.ts",
-    "ci:test": "NODE_ENV=ci node --import tsx test.ts"
+    "test": "rm -rf .tests/* && node --import tsx test.ts",
+    "ci:test": "NODE_ENV=ci docker builder prune -f -a && node --import tsx test.ts"
   },

32-34: Declare Node engine to match docs and tooling.

Docs say Node.js v22.x. Declaring engines helps CI and consumers.

   "homepage": "https://github.com/webNeat/hosty",
+  "engines": {
+    "node": ">=22.8"
+  },
about.md (5)

25-31: Clarify Caddy provisioning via Docker.

The PR moved Caddy into a containerized reverse proxy. “Installing ... Caddy” can mislead; suggest explicitly noting it’s provisioned via Docker.

-    *   Installing necessary software (e.g., Docker, Caddy, PostgreSQL client/server).
+    *   Installing necessary software (e.g., Docker; provisioning Caddy via a container; PostgreSQL client/server).

63-69: Add Docker to local prerequisites (tests and Caddy rely on it).

Tests and reverse proxy run through Docker. Calling this out avoids setup surprises.

 **Local machine**
 - Node.js v22.x or later
 - pnpm (preferred) or npm/yarn
 - Ansible v2.16.x
 - zx (included as a devDependency; installed via `pnpm install`)
 - TypeScript v5.x
+ - Docker Engine v24+ (with buildx), required for tests and reverse proxy (Caddy) runtime

411-413: Align “Testing” section with current scripts.

The current test script prunes Docker cache and then runs the test runner; CI uses ci:test. Reflect that here.

- - Tests are written in TypeScript using built-in assertions or frameworks
- - Execute via `pnpm test` (runs `node --import tsx test.ts`)
+ - Tests are written in TypeScript using built-in assertions or frameworks
+ - Execute locally via `pnpm test` (runs `rm -rf .tests/* && node --import tsx test.ts`)
+ - In CI, use `pnpm run ci:test` (sets `NODE_ENV=ci` and may prune Docker builder cache for clean, reproducible builds)

329-335: Remove trailing colon from heading (markdownlint MD026).

Minor formatting/lint improvement.

-## Best Practices:
+## Best Practices

1-440: Optional: Fix markdown list style/indent to satisfy markdownlint (MD004/MD007).

Static analysis highlights inconsistent list markers and indentation. If you care about zero-warnings docs, standardize unordered lists (e.g., all asterisks or all dashes) and fix indentation levels. I can run a quick pass to auto-fix via markdownlint/Prettier if you want.

test.ts (3)

60-64: Log files are keyed by container name, not test filename

You’re storing the container name in Failure.filename, so logs are saved as hosty-test-...-stdout.log. If you’d rather correlate logs to the test files, store a sanitized test basename instead.

Apply this diff:

-    failures.push({
-      filename: name,
+    failures.push({
+      filename: path.basename(filename).replace(/\.test\.ts$/, ''),
       stdout: res.stdout,
       stderr: res.stderr,
     })

74-81: Avoid fixed sleeps; wait until Docker daemon is actually ready

A hard 10s/5s sleep can be flaky and either slow or insufficient. Prefer polling for readiness (e.g., checking the Docker socket) before exec’ing tests.

Apply this diff:

-    await $`docker compose run -d --name ${name} hosty dockerd --host=unix:///var/run/docker.sock --data-root=/var/lib/docker`.nothrow()
-    await setTimeout(10_000)
+    await $`docker compose run -d --name ${name} hosty dockerd --host=unix:///var/run/docker.sock --data-root=/var/lib/docker`.nothrow()
+    // wait for the docker socket to appear instead of fixed sleep
+    for (let i = 0; i < 30; i++) {
+      const sock = await $`docker exec ${name} sh -lc 'test -S /var/run/docker.sock'`.nothrow()
+      if (sock.exitCode === 0) break
+      await setTimeout(1_000)
+    }

77-79: Quote docker inspect Go template to avoid shell/template parsing issues

Quoting is more robust across shells and avoids brace-expansion surprises.

Apply this diff:

-  const running = await $`docker inspect -f {{.State.Running}} ${name}`.nothrow()
+  const running = await $`docker inspect -f '{{.State.Running}}' ${name}`.nothrow()
tests/app-adonis-sqlite.test.ts (1)

33-34: Replace fixed sleep with readiness probe to reduce flakiness

A hard sleep increases test duration or may still be too short on slow CI. Poll the endpoint until it’s reachable.

Apply this diff:

-  assert.command(`sleep 10`, { success: true })
+  assert.command(
+    `bash -lc 'for i in {1..60}; do curl -skf https://adonis-api.local >/dev/null && exit 0; sleep 1; done; exit 1'`,
+    { success: true },
+  )
src/blocks/build_repo.ts (1)

33-35: Shallow clone is good; consider single_branch and verify history needs

Using depth: 1 will speed up clones. Consider also single_branch: true to avoid fetching other refs; and verify no builds rely on full history or tags.

Apply this diff:

-      { repo: config.repo_url, version: config.branch, accept_hostkey: true, dest: '{{clone_dir.path}}', depth: 1 },
+      { repo: config.repo_url, version: config.branch, accept_hostkey: true, dest: '{{clone_dir.path}}', depth: 1, single_branch: true },

Please confirm none of your build contexts (Dockerfile/nixpacks) or post-clone steps need tag resolution, describe, or submodules that would require full history.

.github/workflows/tests.yaml (2)

35-35: Add a trailing newline (YAMLlint error)

YAMLlint flagged “no new line character at the end of file”. Add a newline at EOF to satisfy linters and POSIX tooling expectations.


25-31: Consider pinning GitHub Actions to full-length commit SHAs

For supply-chain hardening, pin reusable actions (checkout, setup-node, pnpm/action-setup) to a specific commit SHA rather than a moving tag.

Example:

-      - uses: actions/checkout@v4
+      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4

-      - uses: actions/setup-node@v4
+      - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4

-      - uses: pnpm/action-setup@v4.0.0
+      - uses: pnpm/action-setup@3b5e8f3118fa452b5ed1ccc720236874a65cd8f4 # v4.0.0
src/files/service_caddyfile (1)

7-7: Add a trailing newline

Template currently lacks a trailing newline. Add one for consistency with tooling and to avoid EOF lint warnings.

tests/app-node-postgres.test.ts (1)

23-25: Replace fixed sleep with resilient readiness check to deflake tests

A hard sleep slows CI and can still be flaky. Prefer a retrying curl on the first readiness check.

Apply this diff to remove the sleep and make the first request resilient:

-  assert.command(`sleep 10`, { success: true })
-  assert.command(`curl -k https://todo.local`, { success: true, stdout: '[]' })
+  assert.command(`curl -k --retry 30 --retry-all-errors --retry-connrefused --connect-timeout 2 https://todo.local`, { success: true, stdout: '[]' })

Optionally, propagate the retry flags to subsequent curl calls if you still see flakes under load.

tests/app-rust-nextjs.test.ts (1)

28-32: Avoid arbitrary sleeps; prefer retrying the first HTTP check

Use curl retries on the first endpoint to wait for readiness instead of a blanket 10s pause.

Apply this diff:

-  assert.command(`sleep 10`, { success: true })
-  assert.command(`curl -k https://rust-api.local/greet/foo`, { success: true, stdout: '{"hello":"foo"}' })
+  assert.command(`curl -k --retry 30 --retry-all-errors --retry-connrefused --connect-timeout 2 https://rust-api.local/greet/foo`, { success: true, stdout: '{"hello":"foo"}' })

This will typically make the subsequent requests stable without a global delay.

src/blocks/delete_domain.ts (1)

17-17: Remove sudo from command when using become: true

Ansible will run the command as root when become: true is set; embedding sudo in cmd is redundant and can introduce failures if sudo is unavailable or configured differently.

Apply this diff:

-    builtin.command(`Reload caddy`, { cmd: `sudo systemctl reload caddy` }, { become: true, when: 'caddyfile.changed' }),
+    builtin.command(`Reload caddy`, { cmd: `systemctl reload caddy` }, { become: true, when: 'caddyfile.changed' }),
tests/app-laravel-mysql-custom-docker.test.ts (1)

80-81: Replace the fixed 20s sleep with a bounded wait for hosts entry removal

A fixed 20s delay slows CI. Waiting until the hosts entry disappears is faster and more robust.

Apply this diff:

-  assert.command(`sleep 20`, { stdout: '' })
+  assert.command(`bash -lc 'for i in {1..20}; do if ! grep -q -w "laravel.local" /etc/hosts; then exit 0; fi; sleep 1; done; exit 1'`, { success: true })

This loops up to ~20s but typically exits much sooner once /etc/hosts is updated.

src/files/server_caddyfile (1)

11-11: Consider adding per-service access logging in service templates

This file configures Caddy’s process logs. If you also want HTTP access logs, add a log block to each site (i.e., in service_caddyfile) with an output target, e.g., a separate file per domain.

Example within each site block:

log {
  format json
  output file /var/log/caddy/access-{{domain}}.log {
    roll_size 10MiB
    roll_keep 10
    roll_keep_for 720h
  }
}

Make sure the service setup creates the log directory and sets ownership to the caddy user.

src/services/container.ts (1)

23-23: Prefer server.get_service_dir(name) to avoid duplication and future regressions.

You already expose server.get_service_dir; using it here removes path-join duplication and keeps paths consistent if the layout changes later.

Apply this diff:

-      service_dir: path.join(server.hosty_dir, 'services', name),
+      service_dir: server.get_service_dir(name),
-  return [blocks.delete_service(path.join(server.hosty_dir, 'services', name))]
+  return [blocks.delete_service(server.get_service_dir(name))]

Also applies to: 30-30

Dockerfile (1)

15-22: DinD components are installed but no daemon is started—clarify intended Docker strategy.

Right now the image contains docker-ce/containerd and declares VOLUME /var/lib/docker, but nothing starts dockerd. If the compose service doesn’t mount /var/run/docker.sock, docker commands will fail.

  • If you plan to mount the host socket (recommended for CI/dev), consider removing docker-ce/containerd to slim the image.
  • If you plan to run DinD, add an entrypoint/command to start dockerd or switch to an official docker:dind base image.

Also a minor nit: ca-certificates/curl/gnupg are installed in both RUN layers; you can consolidate to reduce layers.

src/server.ts (1)

64-65: Ensure Caddy logs dir is created with correct permissions

  • In src/reverse_proxy/caddy.ts (line 32), get_server_tasks invokes
    blocks.create_directory(path.join(server.hosty_dir, 'caddy/logs')), so the logs folder will be created.
  • Please verify that the default mode/ownership applied by create_directory allows Caddy (and your log-rotation tooling) to write to that path. If not, pass explicit options (e.g. mode: 0o755, owner: 'caddy') to avoid permission issues.
src/blocks/create_caddy_domain.ts (3)

15-15: Don't force unsafe_writes; default to safe atomic updates.

unsafe_writes: true disables atomic writes and is generally discouraged unless you're targeting specific filesystems where atomic operations fail. For /etc/hosts this is not typically needed.

Apply this diff to remove unsafe_writes:

-      { path: '/etc/hosts', line: `127.0.0.1 ${config.domain}`, state: 'present', unsafe_writes: true },
+      { path: '/etc/hosts', line: `127.0.0.1 ${config.domain}`, state: 'present' },

11-19: Optionally add IPv6 localhost mapping to avoid surprises on dual-stack systems.

Many systems resolve localhost via IPv6 first. Consider adding an entry for ::1 domain alongside IPv4 to avoid inconsistent local resolution.

Example change:

   return block(`Configure Caddy domain: ${config.domain}`, {}, [
     builtin.lineinfile(
       `Ensure ${config.domain} is in /etc/hosts`,
-      { path: '/etc/hosts', line: `127.0.0.1 ${config.domain}`, state: 'present', unsafe_writes: true },
+      { path: '/etc/hosts', line: `127.0.0.1 ${config.domain}`, state: 'present' },
       { become: true },
     ),
+    builtin.lineinfile(
+      `Ensure ${config.domain} (IPv6) is in /etc/hosts`,
+      { path: '/etc/hosts', line: `::1 ${config.domain}`, state: 'present' },
+      { become: true },
+    ),
     builtin.copy(`Create Caddyfile for ${config.domain}`, { dest: config.caddyfile_path, content: config.caddyfile_content }, { register: 'caddyfile' }),
   ]).get()

18-18: Confirm whether the registered result caddyfile is used; otherwise drop it.

Registering increases task output and makes the API noisier if unused. If you plan to conditionally reload Caddy based on changes, keep it; otherwise remove for clarity.

Would you like me to wire a follow-up task/handler that reloads Caddy when caddyfile.changed is true?

src/blocks/create_directory.ts (2)

9-14: Avoid mutating the caller's config object; derive attrs immutably.

Using ||= mutates the input object and can cause side effects if the caller reuses it. Build the attrs locally with nullish coalescing.

Apply this diff:

-export function create_directory(path: string, config: Config = {}) {
-  config.owner ||= '{{ansible_user}}'
-  config.group ||= '{{ansible_user}}'
-  config.mode ||= '0755'
-  return builtin.file(`Create directory ${path}`, { path, state: 'directory', ...config }, { become: true })
-}
+export function create_directory(path: string, config: Config = {}) {
+  const attrs = {
+    path,
+    state: 'directory' as const,
+    owner: config.owner ?? '{{ansible_user}}',
+    group: config.group ?? '{{ansible_user}}',
+    mode: config.mode ?? '0755',
+  }
+  return builtin.file(`Create directory ${path}`, attrs, { become: true })
+}

3-7: Consider exporting Config to improve reusability and typing at call sites.

Callers configuring owner/group/mode will benefit from the exported type.

Example:

-type Config = {
+export type Config = {
   owner?: string
   group?: string
   mode?: string
 }
src/ansible/tasks/builtin.ts (1)

27-30: Optional: add regexp support to lineinfile for idempotent matching.

For more robust host entry management (match by domain and enforce the exact line), exposing regexp?: string helps avoid duplicates and whitespace variance.

Example change:

-export type LineInFileAttrs = { path: string; line: string; state: 'present' | 'absent'; unsafe_writes?: boolean }
+export type LineInFileAttrs = {
+  path: string
+  line: string
+  state: 'present' | 'absent'
+  unsafe_writes?: boolean
+  regexp?: string
+}
src/files.ts (2)

21-23: Avoid constructing RegExp from variable input; use string replacement.

Even though keys are type-controlled, a regex isn’t necessary here. Using split/join or replaceAll is simpler and avoids ReDoS concerns flagged by static analysis.

Apply this diff:

-  for (const [key, value] of Object.entries(data)) {
-    content = content.replace(new RegExp(`\\{\\{${key}\\}\\}`, 'g'), value)
-  }
+  for (const [key, value] of Object.entries(data)) {
+    // Replace all occurrences without regex
+    const needle = `{{${key}}}`
+    content = content.split(needle).join(value)
+  }

19-25: Optional: validate full placeholder coverage to catch template/data drift.

A simple post-check for unreplaced {{...}} tokens can fail fast when templates change but code/data doesn’t.

Example (outside the selected range, for illustration only):

if (/\{\{[^}]+\}\}/.test(content)) {
  throw new Error(`Unresolved placeholders found while rendering "${name}"`)
}
src/services/app/git.ts (1)

70-73: Avoid shared nested state across instances when cloning compose

Shallow-cloning the base compose can share nested objects (env, volumes, etc.) across instances, causing cross-instance mutation. Prefer a deep clone per instance.

Example:

-  for (let i = 0; i < config.instances!; i++) {
-    composes.push({ ...compose })
-  }
+  for (let i = 0; i < config.instances!; i++) {
+    // Deep clone to isolate nested structures between instances
+    composes.push(JSON.parse(JSON.stringify(compose)))
+  }
task.md (1)

5-5: Style nit: prefer “Currently”

Minor tone/conciseness improvement.

-Right now, Caddy is installed on the server. I would like to use it in a docker container instead.
+Currently, Caddy is installed on the server. I would like to use it in a Docker container instead.
tests/utils.ts (1)

85-85: Avoid world-writable permissions on playbooks

chmod 777 is overly permissive and unnecessary; prefer 0644 (or 0600 if stricter).

-  await $`chmod 777 ${playbook_path}`
+  await $`chmod 644 ${playbook_path}`
src/types.ts (1)

41-45: Consider adding a destroy hook to ReverseProxy

There’s no get_service_destroy_tasks, so cleanup logic has to be reimplemented per service (see src/services/app/git.ts). A symmetric destroy hook centralizes reverse-proxy concerns.

Proposed type extension:

 export type ReverseProxy = {
   get_log_path: (server: Server) => string
   get_server_tasks: (server: Server) => Tasks
   get_service_tasks: (server: Server, config: ReverseProxyConfig) => Tasks
+  get_service_destroy_tasks?: (server: Server, config: ReverseProxyConfig) => Tasks
 }

If accepted, implement in caddy.ts and switch call sites (e.g., git.ts) to use it.

src/reverse_proxy/caddy.ts (2)

39-53: Optional: restart/reload on master Caddyfile change

Right now, you register caddy_master but don’t act on it. Either pass a restart condition to create_service, or add a reload command after service creation.

Option A (restart container when master changes):

     blocks.create_service({
       name: 'caddy',
       service_dir: `${server.hosty_dir}/caddy`,
       docker_network: server.docker_network,
+      restart_conditions: ['caddy_master'],
       compose: {
         image: 'caddy:2.9-alpine',
         ports: ['80:80', '443:443'],
         networks: [server.docker_network],
         volumes: [
           `${server.hosty_dir}/caddy/config:/etc/caddy`,
           `${server.hosty_dir}/caddy/includes:/etc/caddy/includes:ro`,
           `${server.hosty_dir}/caddy/logs:/var/log/caddy`,
         ],
       },
     }),

Option B (reload config when master changes, after the service task):

   return [
     ...,
     blocks.create_service({ ... }),
+    builtin.command(
+      'Reload Caddy after master Caddyfile change',
+      { cmd: `docker exec caddy caddy reload --config /etc/caddy/Caddyfile` },
+      { become: true, when: 'caddy_master.changed' },
+    ),
   ]

Choose one; reload is generally preferable to a full restart.


44-45: Optional: make Caddy image configurable

Image tag is pinned to caddy:2.9-alpine with no override path in CaddyConfig. If you want to support other versions, extend the config type and use it here.

Sketch:

  • In src/types.ts:
 export type CaddyConfig = {
   get_server_caddyfile?: (server: Server) => string
   get_service_caddyfile?: (server: Server, config: ReverseProxyConfig) => string
+  image?: string // e.g., 'caddy:2.9-alpine'
 }
  • In this file:
-export function caddy(config: CaddyConfig = {}): ReverseProxy {
+export function caddy(config: CaddyConfig = {}): ReverseProxy {
   const normalized_config = { ...default_config, ...config }
   ...
}

-        image: 'caddy:2.9-alpine',
+        image: normalized_config.image || 'caddy:2.9-alpine',
tasks.todo (4)

1-4: Make Caddy logging explicit in the proxies section (aligns with PR title “Add caddy logs”).

Surface concrete deliverables for access/error logs and their routing so the work is trackable and testable.

Apply this diff to capture the logging work:

 proxies:
   ✔ make the proxy configurable @done
-  ✔ add caddy proxy as container (default) @done
-  ☐ add nginx proxy as container
+  ✔ add caddy proxy as container (default) @done
+  ☐ Caddy: emit structured JSON access and error logs (stdout)
+  ☐ Caddy: forward logs to Vector when monitoring is enabled
+  ☐ Caddy: per-service/per-domain log labels and naming
+  ☐ Caddy: configure log retention/rotation (Docker log-driver or Vector-managed)
+  ☐ add nginx proxy as container
+  ☐ Nginx parity: same log semantics/labels as Caddy

6-8: Clarify UFW task and Docker interplay to avoid accidental exposure.

Docker can bypass UFW unless you explicitly constrain via DOCKER-USER rules. Spell out acceptance criteria to keep the server locked down without breaking published container ports.

 server setup:
-  ☐ use ufw to only allow ssh, http and https
+  ☐ Use UFW to only allow SSH (22/tcp), HTTP (80/tcp) and HTTPS (443/tcp)
+  ☐ Ensure IPv6 rules mirror IPv4 (IPV6=yes in /etc/default/ufw)
+  ☐ Integrate with Docker: enforce rules via DOCKER-USER chain so published container ports remain constrained
+  ☐ Idempotent tasks to apply and verify firewall rules; include lockout-safe rollback instructions

9-12: Monitoring: define log pipeline deliverables (sources, sinks, PII).

Make the Vector integration actionable and privacy-safe, and include Caddy logs end-to-end.

 monitoring:
   ☐ make monitoring configurable
   ☐ add vector (default)
+  ☐ Sources: Caddy access/error logs; app/service containers (opt-in)
+  ☐ Format: structured JSON with env/app/service/request_id correlation fields
+  ☐ Sinks: stdout/file (dev) and Loki/Axiom/Datadog/OpenSearch (prod) via env config
+  ☐ Redaction: strip secrets/PII (e.g., Authorization, cookies, tokens, emails)
+  ☐ Vector container: healthcheck, restart policy, and resource limits
+  ☐ Provide sample queries/dashboards for common troubleshooting

28-28: Fix product name capitalization.

It’s “NestJS”. Also consider correcting “WordPress” on Line 30.

-  ☐ Nestjs
+  ☐ NestJS
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between fafae5d and 69a2548.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (40)
  • .github/workflows/tests.yaml (1 hunks)
  • Dockerfile (1 hunks)
  • README.md (1 hunks)
  • about.md (1 hunks)
  • compose.yaml (1 hunks)
  • package.json (2 hunks)
  • pnpm-workspace.yaml (1 hunks)
  • src/ansible/tasks/builtin.ts (2 hunks)
  • src/blocks/build_repo.ts (1 hunks)
  • src/blocks/create_caddy_domain.ts (1 hunks)
  • src/blocks/create_directory.ts (1 hunks)
  • src/blocks/create_domain.ts (0 hunks)
  • src/blocks/delete_domain.ts (1 hunks)
  • src/blocks/generate_ssh_key.ts (1 hunks)
  • src/blocks/index.ts (1 hunks)
  • src/blocks/install_caddy.ts (0 hunks)
  • src/blocks/install_git.ts (1 hunks)
  • src/blocks/set_available_port.ts (0 hunks)
  • src/files.ts (1 hunks)
  • src/files/server_caddyfile (1 hunks)
  • src/files/service_caddyfile (1 hunks)
  • src/reverse_proxy/caddy.ts (1 hunks)
  • src/reverse_proxy/index.ts (1 hunks)
  • src/server.ts (4 hunks)
  • src/services/app/git.ts (2 hunks)
  • src/services/container.ts (1 hunks)
  • src/types.ts (1 hunks)
  • task.md (1 hunks)
  • tasks.todo (2 hunks)
  • test.ts (1 hunks)
  • tests/app-adonis-sqlite.test.ts (2 hunks)
  • tests/app-laravel-mysql-custom-docker.test.ts (2 hunks)
  • tests/app-node-postgres.test.ts (2 hunks)
  • tests/app-rust-nextjs.test.ts (2 hunks)
  • tests/command-cron-host.test.ts (1 hunks)
  • tests/command-cron-service.test.ts (1 hunks)
  • tests/container.test.ts (1 hunks)
  • tests/setup.test.ts (1 hunks)
  • tests/utils.ts (3 hunks)
  • tests/utils/Dockerfile (0 hunks)
💤 Files with no reviewable changes (4)
  • tests/utils/Dockerfile
  • src/blocks/set_available_port.ts
  • src/blocks/install_caddy.ts
  • src/blocks/create_domain.ts
🧰 Additional context used
🧬 Code Graph Analysis (8)
src/blocks/create_caddy_domain.ts (2)
src/ansible/types.ts (1)
  • Block (28-28)
src/blocks/block.ts (1)
  • block (3-8)
src/services/container.ts (2)
src/server.ts (1)
  • server (8-33)
src/types.ts (2)
  • Server (58-63)
  • ContainerConfig (71-77)
tests/app-adonis-sqlite.test.ts (1)
src/ansible/tasks/builtin.ts (1)
  • assert (111-113)
src/reverse_proxy/caddy.ts (3)
src/types.ts (4)
  • CaddyConfig (53-56)
  • ReverseProxy (41-45)
  • Server (58-63)
  • ReverseProxyConfig (47-51)
src/server.ts (1)
  • server (8-33)
src/files.ts (1)
  • get_file (19-25)
tests/app-node-postgres.test.ts (1)
src/ansible/tasks/builtin.ts (1)
  • assert (111-113)
src/services/app/git.ts (3)
src/services/tasks.ts (1)
  • tasks (4-6)
src/server.ts (1)
  • server (8-33)
src/ci.ts (1)
  • server (7-18)
test.ts (2)
tests/utils.ts (1)
  • run (35-68)
src/ci.ts (1)
  • run (47-49)
src/server.ts (1)
src/reverse_proxy/caddy.ts (1)
  • caddy (19-26)
🪛 LanguageTool
about.md

[grammar] ~26-~26: There might be a mistake here.
Context: ...ocker, Caddy, PostgreSQL client/server). * Setting up users and permissions. * ...

(QB_NEW_EN)


[grammar] ~40-~40: There might be a mistake here.
Context: ...verse_proxy.caddy, deploy, and run. - **src/server.ts**: Defines the ServerConfig` and logic f...

(QB_NEW_EN)


[grammar] ~63-~63: There might be a mistake here.
Context: ...asks. ## Prerequisites Local machine - Node.js v22.x or later - pnpm (preferred...

(QB_NEW_EN)


[grammar] ~64-~64: There might be a mistake here.
Context: ...Local machine** - Node.js v22.x or later - pnpm (preferred) or npm/yarn - Ansible v...

(QB_NEW_EN)


[grammar] ~65-~65: There might be a mistake here.
Context: ... or later - pnpm (preferred) or npm/yarn - Ansible v2.16.x - zx (included as a devD...

(QB_NEW_EN)


[grammar] ~66-~66: There might be a mistake here.
Context: ...preferred) or npm/yarn - Ansible v2.16.x - zx (included as a devDependency; install...

(QB_NEW_EN)


[grammar] ~67-~67: There might be a mistake here.
Context: ...ependency; installed via pnpm install) - TypeScript v5.x Target server - Lin...

(QB_NEW_EN)


[grammar] ~71-~71: There might be a mistake here.
Context: ...ptandsystemctl(e.g., Ubuntu 22.04) - Non-root user withsudo` privileges - S...

(QB_NEW_EN)


[grammar] ~72-~72: There might be a mistake here.
Context: ...) - Non-root user with sudo privileges - SSH access ## Installation & Build ```...

(QB_NEW_EN)


[grammar] ~119-~119: There might be a mistake here.
Context: ...odules under src/. Organize by domain: - Ansible logic: src/ansible/ or `src/bl...

(QB_NEW_EN)


[grammar] ~357-~357: There might be a mistake here.
Context: ...e exact task that failed and the reason. * Inspect hosty-playbook.yaml: After `...

(QB_NEW_EN)


[style] ~396-~396: Consider using a different verb for a more formal wording.
Context: ... effects. If a run fails, you can often fix the issue and re-run it. * **Simplify...

(FIX_RESOLVE)


[grammar] ~402-~402: There might be a mistake here.
Context: ...orkflow 1. Implement or modify code in src/ 2. Add or update tests in tests/ 3. Run `...

(QB_NEW_EN)


[grammar] ~403-~403: There might be a mistake here.
Context: ...ode in src/ 2. Add or update tests in tests/ 3. Run pnpm run build to compile 4. Run `...

(QB_NEW_EN)


[grammar] ~425-~425: There might be a mistake here.
Context: ...ion.ymland implemented insrc/ci.ts: - **Inputs**: server_ip, server_user, s...

(QB_NEW_EN)


[grammar] ~426-~426: There might be a mistake here.
Context: ... server_sudo_pass, handler, vars, verbose - Steps: Set up SSH, generate playbook, ...

(QB_NEW_EN)


[grammar] ~437-~437: There might be a mistake here.
Context: ...ore merge ## Resources - Roadmap: plan.md - Quick start: README.md - **File list...

(QB_NEW_EN)


[grammar] ~438-~438: There might be a mistake here.
Context: ...Roadmap**: plan.md - Quick start: README.md - File list: files.txt

(QB_NEW_EN)

task.md

[style] ~4-~4: For conciseness, consider replacing this expression with an adverb.
Context: ...enting Caddy as a Service ## Overview Right now, Caddy is installed on the server. I wo...

(AT_THE_MOMENT)


[grammar] ~32-~32: There might be a mistake here.
Context: ... volumes, and before_start commands. - In `src/reverse_proxy/caddy.ts#get_serve...

(QB_NEW_EN)


[grammar] ~50-~50: There might be a mistake here.
Context: ... }) ``` 2. Mounting and Directories - Ensure host dirs exist via `blocks.creat...

(QB_NEW_EN)


[grammar] ~57-~57: There might be a mistake here.
Context: ...rvice routing. - Expose ports 80/443 in compose service. 4. Caddyfile Provision...

(QB_NEW_EN)


[grammar] ~57-~57: There might be a mistake here.
Context: ... Expose ports 80/443 in compose service. 4. Caddyfile Provisioning - Generate a m...

(QB_NEW_EN)


[grammar] ~58-~58: There might be a mistake here.
Context: ...mpose service. 4. Caddyfile Provisioning - Generate a master Caddyfile at `${server...

(QB_NEW_EN)


[grammar] ~74-~74: There might be a mistake here.
Context: ...t.md`) to reflect container-based Caddy. 6. Migration and Backward Compatibility ...

(QB_NEW_EN)


[grammar] ~78-~78: There might be a mistake here.
Context: ...y (hosty destroy reverse_proxy caddy). - Copying/moving existing Caddyfiles and c...

(QB_NEW_EN)


[grammar] ~81-~81: There might be a mistake here.
Context: ...ir and deprecated tasks. 7. Testing & CI - Add `tests/reverse_proxy_caddy_container...

(QB_NEW_EN)


[grammar] ~83-~83: There might be a mistake here.
Context: ...Caddy container, verify via docker ps. - Perform HTTP(S) requests to confirm rout...

(QB_NEW_EN)


[grammar] ~85-~85: There might be a mistake here.
Context: ...d stop/remove container and clean files. - Update GitHub Action to include new test...

(QB_NEW_EN)

🪛 markdownlint-cli2 (0.17.2)
about.md

40-40: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


40-40: Unordered list indentation
Expected: 0; Actual: 3

(MD007, ul-indent)


41-41: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


41-41: Unordered list indentation
Expected: 0; Actual: 3

(MD007, ul-indent)


42-42: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


42-42: Unordered list indentation
Expected: 0; Actual: 3

(MD007, ul-indent)


43-43: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


43-43: Unordered list indentation
Expected: 2; Actual: 5

(MD007, ul-indent)


44-44: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


44-44: Unordered list indentation
Expected: 2; Actual: 5

(MD007, ul-indent)


45-45: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


45-45: Unordered list indentation
Expected: 2; Actual: 5

(MD007, ul-indent)


46-46: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


46-46: Unordered list indentation
Expected: 0; Actual: 3

(MD007, ul-indent)


47-47: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


47-47: Unordered list indentation
Expected: 2; Actual: 5

(MD007, ul-indent)


50-50: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


50-50: Unordered list indentation
Expected: 0; Actual: 3

(MD007, ul-indent)


51-51: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


51-51: Unordered list indentation
Expected: 0; Actual: 3

(MD007, ul-indent)


52-52: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


52-52: Unordered list indentation
Expected: 0; Actual: 3

(MD007, ul-indent)


55-55: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


55-55: Unordered list indentation
Expected: 0; Actual: 3

(MD007, ul-indent)


56-56: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


56-56: Unordered list indentation
Expected: 0; Actual: 3

(MD007, ul-indent)


57-57: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


57-57: Unordered list indentation
Expected: 0; Actual: 3

(MD007, ul-indent)


63-63: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


64-64: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


65-65: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


66-66: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


67-67: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


68-68: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


70-70: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


71-71: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


72-72: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


73-73: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


119-119: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


120-120: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


121-121: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


122-122: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


123-123: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


124-124: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


125-125: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


126-126: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


130-130: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


131-131: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


132-132: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


133-133: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


134-134: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


135-135: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


136-136: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


137-137: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


138-138: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


139-139: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


140-140: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


141-141: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


142-142: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


143-143: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


144-144: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


145-145: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


146-146: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


150-150: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


151-151: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


152-152: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


153-153: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


154-154: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


155-155: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


166-166: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


167-167: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


167-167: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


168-168: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


168-168: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


169-169: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


169-169: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


170-170: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


170-170: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


171-171: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


185-185: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


186-186: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


186-186: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


187-187: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


187-187: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


188-188: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


188-188: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


189-189: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


189-189: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


190-190: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


190-190: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


191-191: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


205-205: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


206-206: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


206-206: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


207-207: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


207-207: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


208-208: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


208-208: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


209-209: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


209-209: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


210-210: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


210-210: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


211-211: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


211-211: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


212-212: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


212-212: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


213-213: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


213-213: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


214-214: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


214-214: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


215-215: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


238-238: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


239-239: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


239-239: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


240-240: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


240-240: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


241-241: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


248-248: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


249-249: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


250-250: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


260-260: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


261-261: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


261-261: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


262-262: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


262-262: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


263-263: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


329-329: Trailing punctuation in heading
Punctuation: ':'

(MD026, no-trailing-punctuation)


355-355: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


356-356: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


360-360: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


361-361: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


362-362: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


363-363: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


375-375: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


376-376: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


411-411: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


412-412: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


417-417: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


418-418: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


419-419: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


426-426: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


427-427: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


431-431: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


432-432: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


433-433: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


437-437: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


438-438: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


439-439: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

🪛 YAMLlint (1.37.1)
.github/workflows/tests.yaml

[error] 35-35: no new line character at the end of file

(new-line-at-end-of-file)

🪛 ast-grep (0.38.6)
src/files.ts

[warning] 21-21: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(\\{\\{${key}\\}\\}, 'g')
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

🔇 Additional comments (26)
pnpm-workspace.yaml (1)

1-2: Confirm pnpm supports onlyBuiltDependencies here (or move to .npmrc).

pnpm typically sources install-time flags like onlyBuiltDependencies from .npmrc (key: only-built-dependencies). Having it in pnpm-workspace.yaml may be ignored depending on your pnpm version/config parsing. If CI intends to rely on this, please verify it’s honored; otherwise move it to .npmrc.

Do you want me to generate a quick script to grep the repo for pnpm version constraints and validate the setting against pnpm docs, or update the config for you?

README.md (1)

53-53: LGTM – nit fix reads better.

“Node.js” spelling correction looks good.

tests/container.test.ts (1)

2-2: Helper import path update LGTM

Switching to ./utils.js matches the new test harness layout.

tests/command-cron-service.test.ts (1)

1-1: Helper import path update LGTM

Matches the unified ./utils.js entrypoint across tests.

.github/workflows/tests.yaml (1)

35-35: ci:test script verified; ready to merge

All checks pass—ci:test exists and accepts individual file paths, and the test suite is populated and invokes Docker in CI mode as expected.

  • package.json – scripts["ci:test"] = NODE_ENV=ci node --import tsx test.ts
  • test.ts (lines 31–33) reads process.argv.slice(2) to handle one test file at a time
  • tests/*.test.ts – multiple entries confirm the matrix won’t be empty
  • test.ts (lines 71–82) runs Docker commands (docker inspect, docker compose run, docker exec) in CI mode

No further action required.

tests/setup.test.ts (1)

1-1: Import path update to ./utils.js looks good

Matches the new test harness surface; removal of systemctl caddy check aligns with Docker-based proxy.

tests/command-cron-host.test.ts (1)

1-1: Import path migration to ./utils.js looks good

No other behavioral changes; the test assertions remain intact.

tests/app-node-postgres.test.ts (1)

1-1: Import path switch to ./utils.js looks consistent with the suite changes

No concerns with the ESM-style import here given the updated utilities module.

tests/app-rust-nextjs.test.ts (1)

1-1: Import path update to ./utils.js is good

Matches the broader test harness change.

src/blocks/delete_domain.ts (1)

11-15: Unsafe writes symmetry confirmed

The create_caddy_domain block in src/blocks/create_caddy_domain.ts already includes unsafe_writes: true, so behavior is symmetric with your delete task.

Optional nitpick:

  • Consider adding backup: true to these /etc/hosts edits to retain a backup before modification.
tests/app-laravel-mysql-custom-docker.test.ts (1)

1-1: Import path switch to ./utils.js looks right

Aligned with the rest of the test suite changes.

src/files/server_caddyfile (1)

1-9: Confirm Caddy global log syntax and enhance retention policy

  • Verify that your target Caddy v2.x supports the global log block as shown (the first block in the Caddyfile, only one allowed).
  • Add an explicit log level INFO to reduce verbosity.
  • Add roll_keep_for 720h (or your desired duration) to cap log age.
  • Optionally include roll_local_time if you need local‐time timestamps in rotated files.
  • Ensure the parent directory for {{log_path}} exists and is writable by the caddy user before starting the service.

Recommended snippet:

{
  log {
    level INFO
    format json
    output file {{log_path}} {
      roll_size 10MiB
      roll_keep 10
      roll_keep_for 720h
      roll_local_time
    }
  }
}
src/services/container.ts (1)

23-23: Good catch: removing the leading slash fixes the path resolution.

Using 'services' (without a leading slash) ensures the service dir stays under server.hosty_dir instead of resetting to filesystem root.

src/blocks/install_git.ts (1)

14-14: Message now correctly references the email value.

The user-facing description aligns with the actual git_config being set. Looks good.

src/server.ts (1)

29-29: Sensible defaulting to caddy() for reverse proxy.

Defaulting here keeps server setup self-contained while allowing overrides via config. Good integration point.

src/blocks/index.ts (1)

4-4: No lingering create_domain references found

Ran searches for create_domain across the codebase and found zero matches. The new create_caddy_domain symbol is correctly referenced (e.g. in src/reverse_proxy/caddy.ts:59).
Export rename is safe to merge.

src/ansible/tasks/builtin.ts (2)

27-30: LGTM: LineInFileAttrs extended with unsafe_writes.

The type aligns with Ansible's module options and unblocks the new usage.


52-55: LGTM: GitAttrs depth is a useful addition.

Shallow clones reduce bandwidth and speed up CI runs.

src/services/app/git.ts (1)

43-49: Container naming aligns with Caddy upstreams

Verified that:

  • In src/blocks/create_service.ts (lines 109–117), when compose is an array, each container is named ${name}-${instance} with instance starting at 1.
  • In src/reverse_proxy/caddy.ts (line 14), upstream hostnames use ${service_name}-${i + 1}:80, matching the 1-based suffix.
  • In src/blocks/create_caddy_domain.ts (line 18), the block registers the Caddyfile under register: 'caddyfile' as expected.

No further changes needed.

task.md (1)

63-70: Symlink plan vs. implementation divergence

The plan proposes a symlink into includes/. The implementation writes the per-service Caddyfile directly under includes/ via create_caddy_domain. Update the doc to reflect the implemented approach or adjust the code to match the symlink plan.

Would you like me to update the doc to mirror the current implementation (direct file placement) and remove the symlink step?

tests/utils.ts (3)

65-67: Conditional exit on failures only — LGTM

Cleaner CI behavior. Keeps logs visible while still failing the run when appropriate.


71-71: Unique playbook filename — LGTM

The timestamp suffix prevents collisions in parallel or repeated runs.


79-79: SSH key simplification — LGTM

Matches the passphrase?: string change in types and reduces friction in CI.

src/types.ts (1)

31-39: ServerConfig additions — LGTM

Optional SSH key passphrase and reverse proxy injection look good and are backward compatible.

src/reverse_proxy/caddy.ts (2)

14-15: Instance index base: verify 1-based suffix aligns with actual instance names

Upstreams are ${service_name}-${i + 1}:80. Confirm that blocks.create_service names instances with -1, -2, … and not -0, -1, …

Use the same script provided in src/services/app/git.ts to confirm instance naming is 1-based.


64-68: No action needed: container_name is correctly set

Verification shows that in src/reverse_proxy/caddy.ts (lines 39–53) we call:

blocks.create_service({
  name: 'caddy',
  service_dir: `${server.hosty_dir}/caddy`,
  docker_network: server.docker_network,
  compose: { /* single-object config */ },
})

And in src/blocks/create_service.ts, when compose is an object (not an array), it emits:

services[name] = {
  container_name: name,   // => 'caddy'
  networks: [docker_network],
  restart: 'unless-stopped',
  ...compose,
}

Because container_name: 'caddy' is explicitly set, docker exec caddy … will succeed as written.

Comment on lines +3 to +7
build: .
privileged: true
volumes:
- .:/app
working_dir: /app
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Docker will not be usable inside this container as-is (no daemon, no socket).

The image installs Docker tooling, but this service neither mounts the host Docker socket nor starts dockerd (DinD). As a result, docker commands will fail at runtime.

Choose one of the following fixes:

Option A (recommended: use host Docker daemon)

  • Mount the host Docker socket. You can drop privileged if not needed.
 services:
   hosty:
     build: .
-    privileged: true
     volumes:
       - .:/app
+      - /var/run/docker.sock:/var/run/docker.sock
     working_dir: /app

Option B (DinD: run a daemon inside the container)

  • Keep privileged, and start dockerd. One simple approach is to switch to the upstream DinD image, but if you keep this Dockerfile, add a command/entrypoint to start dockerd:
 services:
   hosty:
     build: .
     privileged: true
     volumes:
       - .:/app
     working_dir: /app
+    command: sh -lc 'dockerd > /var/log/dockerd.log 2>&1 & sleep 2 && tail -f /dev/null'

Note: If you go with Option A, you can simplify your Dockerfile by removing DinD packages to reduce image size.

🤖 Prompt for AI Agents
In compose.yaml around lines 3-7 the container installs Docker tooling but
neither mounts the host Docker socket nor runs a Docker daemon, so docker CLI
will fail at runtime; fix by choosing one option: Option A (recommended) — mount
the host socket into the service (add -
/var/run/docker.sock:/var/run/docker.sock) and remove privileged if unnecessary,
and optionally remove DinD packages from the image to shrink it; Option B (DinD)
— keep privileged, switch the service to run a daemon (use the official
docker:dind image or modify the Dockerfile/entrypoint to start dockerd on
container start), ensure any necessary volumes for docker data are declared, and
keep privileged only if you run dockerd.

@@ -0,0 +1,7 @@
{{domain}} {
# Use self-signed cert for local .local domains
tls internal
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Make TLS directive configurable; avoid forcing internal CA on non-local domains

Hard-coding tls internal will break public domains (internal CA isn’t publicly trusted). Gate it on *.local or parameterize via template data.

Proposed template change:

-  tls internal
+  {{tls_directive}}

Then set tls_directive to "tls internal" for *.local domains, or to an empty string (omit line) for public domains so Caddy can obtain public certificates automatically. I can follow up with the generator change (e.g., in src/reverse_proxy/caddy.ts) to inject tls_directive based on domain.

📝 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
tls internal
{{tls_directive}}
🤖 Prompt for AI Agents
In src/files/service_caddyfile around line 3, the file hard-codes "tls internal"
which forces an internal CA and will break public domains; change the template
to use a tls_directive template variable instead of the hard-coded line, so
callers can set tls_directive to "tls internal" for *.local domains or to an
empty string to omit the line and let Caddy obtain public certificates; ensure
the template renders nothing (no empty line) when tls_directive is empty and
update any generator (e.g., reverse_proxy/caddy.ts) to pass the appropriate
value based on the domain.

# Use self-signed cert for local .local domains
tls internal
reverse_proxy {{local_urls}} {
lb_policy client_ip_hash
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Invalid Caddy load-balancing policy name; use ip_hash

Caddy v2 expects lb_policy ip_hash for client IP hashing. client_ip_hash is not a valid policy and will cause config load failure.

Apply:

-    lb_policy client_ip_hash
+    lb_policy ip_hash
📝 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
lb_policy client_ip_hash
lb_policy ip_hash
🤖 Prompt for AI Agents
In src/files/service_caddyfile at line 5, the load-balancing policy name is
incorrect: replace "lb_policy client_ip_hash" with "lb_policy ip_hash" so Caddy
v2 recognizes the client IP hashing policy; update the line exactly and validate
the Caddy config after the change.

Comment on lines +7 to +17
const default_config: Required<CaddyConfig> = {
get_server_caddyfile: (server) =>
get_file('server_caddyfile', {
log_path: path.join(server.hosty_dir, 'caddy', 'logs', 'caddy.log'),
service_caddyfiles_pattern: path.join(server.hosty_dir, 'caddy', 'includes', '*.Caddyfile'),
}),
get_service_caddyfile: (server, config) => {
const local_urls = Array.from({ length: config.instances }, (_, i) => `${config.service_name}-${i + 1}:80`).join(' ')
return get_file('service_caddyfile', { domain: config.domain, local_urls })
},
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Default server Caddyfile uses host paths; use container-internal paths

get_server_caddyfile currently injects host paths (e.g., /srv/hosty/...) into the Caddyfile, which runs inside the container. Use container paths (/etc/caddy/... and /var/log/caddy/...) so the config is valid.

-  get_server_caddyfile: (server) =>
-    get_file('server_caddyfile', {
-      log_path: path.join(server.hosty_dir, 'caddy', 'logs', 'caddy.log'),
-      service_caddyfiles_pattern: path.join(server.hosty_dir, 'caddy', 'includes', '*.Caddyfile'),
-    }),
+  get_server_caddyfile: (_server) =>
+    get_file('server_caddyfile', {
+      log_path: '/var/log/caddy/caddy.log',
+      service_caddyfiles_pattern: '/etc/caddy/includes/*.Caddyfile',
+    }),

Additionally, see the next comment to actually use this hook when writing the master Caddyfile.

📝 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 default_config: Required<CaddyConfig> = {
get_server_caddyfile: (server) =>
get_file('server_caddyfile', {
log_path: path.join(server.hosty_dir, 'caddy', 'logs', 'caddy.log'),
service_caddyfiles_pattern: path.join(server.hosty_dir, 'caddy', 'includes', '*.Caddyfile'),
}),
get_service_caddyfile: (server, config) => {
const local_urls = Array.from({ length: config.instances }, (_, i) => `${config.service_name}-${i + 1}:80`).join(' ')
return get_file('service_caddyfile', { domain: config.domain, local_urls })
},
}
const default_config: Required<CaddyConfig> = {
get_server_caddyfile: (_server) =>
get_file('server_caddyfile', {
log_path: '/var/log/caddy/caddy.log',
service_caddyfiles_pattern: '/etc/caddy/includes/*.Caddyfile',
}),
get_service_caddyfile: (server, config) => {
const local_urls = Array.from({ length: config.instances }, (_, i) => `${config.service_name}-${i + 1}:80`).join(' ')
return get_file('service_caddyfile', { domain: config.domain, local_urls })
},
}
🤖 Prompt for AI Agents
In src/reverse_proxy/caddy.ts around lines 7 to 17, the get_server_caddyfile
builder injects host filesystem paths into the Caddyfile; change those to
container-internal paths so the containerized Caddy can read them — replace the
log_path with /var/log/caddy/caddy.log and the service_caddyfiles_pattern with
/etc/caddy/includes/*.Caddyfile (or equivalent container paths) and ensure any
other host-derived path here uses /etc/caddy/... for config and
/var/log/caddy/... for logs.

Comment on lines +33 to +38
builtin.copy('Create dummy Caddy include', { dest: path.join(server.hosty_dir, 'caddy/includes/.dummy.Caddyfile'), content: '' }, { become: true }),
builtin.copy(
'Create master Caddyfile',
{ content: 'import includes/*.Caddyfile', dest: path.join(server.hosty_dir, 'caddy/config/Caddyfile') },
{ register: 'caddy_master' },
),
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Master Caddyfile write ignores config hook and may need privileges

The master Caddyfile is hardcoded to 'import includes/*.Caddyfile' and does not use config.get_server_caddyfile. Also, the dummy include uses become: true but the master write does not; writing under ${hosty_dir} typically requires sudo.

-    builtin.copy(
-      'Create master Caddyfile',
-      { content: 'import includes/*.Caddyfile', dest: path.join(server.hosty_dir, 'caddy/config/Caddyfile') },
-      { register: 'caddy_master' },
-    ),
+    builtin.copy(
+      'Create master Caddyfile',
+      { content: config.get_server_caddyfile(server), dest: path.join(server.hosty_dir, 'caddy/config/Caddyfile') },
+      { become: true, register: 'caddy_master' },
+    ),

Consider adding a conditional reload step after the service is up when caddy_master.changed (see below).

@@ -0,0 +1 @@
export * from './caddy.js'
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Barrel export LGTM; confirm NodeNext/Bundler module resolution

Re-exporting from './caddy.js' is correct for ESM builds; ensure tsconfig uses moduleResolution NodeNext or Bundler so TS resolves './caddy.js' to './caddy.ts' at build time. Also confirm src/reverse_proxy/caddy.ts exists.

Run:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Verifying caddy implementation exists…"
test -f src/reverse_proxy/caddy.ts && echo "OK: src/reverse_proxy/caddy.ts found" || { echo "Missing: src/reverse_proxy/caddy.ts"; exit 1; }

echo "Checking tsconfig moduleResolution is NodeNext or Bundler…"
for f in $(fd -a 'tsconfig*.json'); do
  echo ">>> $f"
  jq -r '.compilerOptions.moduleResolution // "MISSING"' "$f"
done

Length of output: 1492


Ensure explicit NodeNext/Bundler module resolution in tsconfig.json

The barrel export is correct and src/reverse_proxy/caddy.ts exists. However, your root tsconfig.json does not specify a moduleResolution strategy, so TypeScript may not map the .js re-export to your .ts source. To guarantee that

export * from './caddy.js'

resolves to caddy.ts at build time, add one of the new resolver options under compilerOptions:

File: tsconfig.json

 {
   "compilerOptions": {
     // … your existing settings …
+    "moduleResolution": "NodeNext", // or "Bundler"
     // … other settings …
   }
 }

[must update accordingly]

🤖 Prompt for AI Agents
In src/reverse_proxy/index.ts around lines 1-1, the export uses a .js specifier
which TypeScript will only map to the local .ts file during build if
tsconfig.json sets an explicit module resolution; open your root tsconfig.json
and under compilerOptions add a moduleResolution value that supports Node-style
.js → .ts mapping (for example "nodenext" or "bundler"), then re-run the build
so the export './caddy.js' resolves to src/reverse_proxy/caddy.ts.

Comment on lines +14 to +17
reverse_proxy: reverse_proxy.caddy({
version: '2.9-alpine', // Optional, default is 2.9-alpine
config: ``, // Optional, default to some global config
}),
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Documentation/API mismatch: sample uses version and config, but the implemented caddy() takes CaddyConfig with template hooks

The current code exports caddy(config?: CaddyConfig) where CaddyConfig exposes get_server_caddyfile and get_service_caddyfile, not { version, config }. Please align the example.

Replace with something that matches the actual API surface:

-  reverse_proxy: reverse_proxy.caddy({
-    version: '2.9-alpine', // Optional, default is 2.9-alpine
-    config: ``, // Optional, default to some global config
-  }),
+  reverse_proxy: reverse_proxy.caddy({
+    // Optional hooks to customize generated Caddyfiles
+    // get_server_caddyfile: (srv) => `import /etc/caddy/includes/*.Caddyfile`,
+    // get_service_caddyfile: (srv, { domain, service_name, instances }) => `...`,
+  }),

If you want to support version/image configuration, consider extending the types to carry that information (see suggestion in src/reverse_proxy/caddy.ts review).

📝 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
reverse_proxy: reverse_proxy.caddy({
version: '2.9-alpine', // Optional, default is 2.9-alpine
config: ``, // Optional, default to some global config
}),
reverse_proxy: reverse_proxy.caddy({
// Optional hooks to customize generated Caddyfiles
// get_server_caddyfile: (srv) => `import /etc/caddy/includes/*.Caddyfile`,
// get_service_caddyfile: (srv, { domain, service_name, instances }) => `...`,
}),
🤖 Prompt for AI Agents
In task.md around lines 14 to 17, the example uses { version, config } but the
exported caddy(config?: CaddyConfig) expects a CaddyConfig with
get_server_caddyfile and get_service_caddyfile hooks; update the sample to match
the actual API by replacing the version/config usage with a CaddyConfig-style
object that supplies get_server_caddyfile and get_service_caddyfile (or if you
want to keep version/image in the example, extend the CaddyConfig type and
caddy() implementation to accept and surface version/image before updating the
docs), ensuring the doc and code agree.

Comment on lines +18 to 20
apps:
☐ add `app.dir` to deploy from a local or ssh directory

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

app.dir: specify transport, validation, and hooks to make this safe and testable.

Without constraints, a path-based deploy can open footguns (traversal, permissions) and be brittle across local/SSH flows.

 apps:
-  ☐ add `app.dir` to deploy from a local or ssh directory
+  ☐ add `app.dir` to deploy from a local or ssh directory
+    ☐ Support local paths (relative/absolute) with include/exclude patterns
+    ☐ Support SSH URIs (user@host:/path) via rsync over SSH with key management
+    ☐ Ownership/permissions and symlink handling (configurable)
+    ☐ Pre/post-deploy hooks; trigger reverse proxy reload when needed
+    ☐ Security: validate/sanitize paths; restrict to allowed base directories
+    ☐ Tests: e2e for local and SSH flows, including large directory sync
📝 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
apps:
☐ add `app.dir` to deploy from a local or ssh directory
apps:
☐ add `app.dir` to deploy from a local or ssh directory
☐ Support local paths (relative/absolute) with include/exclude patterns
☐ Support SSH URIs (user@host:/path) via rsync over SSH with key management
☐ Ownership/permissions and symlink handling (configurable)
☐ Pre/post-deploy hooks; trigger reverse proxy reload when needed
☐ Security: validate/sanitize paths; restrict to allowed base directories
☐ Tests: e2e for local and SSH flows, including large directory sync
🤖 Prompt for AI Agents
In tasks.todo around lines 18-20, the new app.dir feature needs concrete safety
and testable constraints: implement an explicit transport enum (local, ssh) and
reject unknown transports; validate and canonicalize paths (prevent ../
traversal) and enforce allowed root/whitelist directories or require a per-app
baseDir; for ssh, require explicit host/user, use key-based auth, and validate
reachable host; before deploy check file permissions and that only expected
files are included (allowlist or manifest), run pre-deploy and post-deploy hooks
for sanity checks, and add timeouts, retries and clear error handling; add unit
and integration tests covering path canonicalization, transport rejection,
permission errors, and hook execution to ensure safe, reproducible behavior.

Comment on lines +18 to +21
const failures: Array<Failure> = []
await Readable.from(filenames).forEach((filename) => docker_run_test(failures, filename), { concurrency })
if (failures.length > 0) {
for (const failure of failures) {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix invalid use of Readable.forEach; implement real concurrency control

Node’s Readable streams don’t expose a .forEach API with a concurrency option. This will throw at runtime. Replace with a simple batching approach using Promise.all.

Apply this diff:

   const failures: Array<Failure> = []
-  await Readable.from(filenames).forEach((filename) => docker_run_test(failures, filename), { concurrency })
+  for (let i = 0; i < filenames.length; i += concurrency) {
+    await Promise.all(
+      filenames.slice(i, i + concurrency).map((filename) => docker_run_test(failures, filename)),
+    )
+  }

Note: With this change, the Readable import becomes unused. If your linter enforces noUnusedLocals, consider removing it.

📝 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 failures: Array<Failure> = []
await Readable.from(filenames).forEach((filename) => docker_run_test(failures, filename), { concurrency })
if (failures.length > 0) {
for (const failure of failures) {
const failures: Array<Failure> = []
for (let i = 0; i < filenames.length; i += concurrency) {
await Promise.all(
filenames.slice(i, i + concurrency).map((filename) => docker_run_test(failures, filename)),
)
}
if (failures.length > 0) {
for (const failure of failures) {
🤖 Prompt for AI Agents
In test.ts around lines 18 to 21, the code incorrectly calls
Readable.from(...).forEach with a concurrency option (which does not exist)
causing a runtime error; replace this with a batching approach that splits
filenames into chunks of size `concurrency` and for each chunk use Promise.all
to call docker_run_test(failures, filename) for every filename in the chunk,
awaiting each Promise.all before proceeding to the next batch; remove the
Readable import if it becomes unused to satisfy linters.

Comment on lines +20 to +27
for (const failure of failures) {
await writeFile(`.tests/${failure.filename}-stdout.log`, failure.stdout)
await writeFile(`.tests/${failure.filename}-stderr.log`, failure.stderr)
}
console.error(`❌ ${failures.length} tests failed, see .tests directory for logs`)
process.exit(1)
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Ensure .tests directory exists before writing logs

writeFile will fail with ENOENT if the directory is missing. Create it first.

Apply this diff:

   if (failures.length > 0) {
+    await mkdir('.tests', { recursive: true })
     for (const failure of failures) {
       await writeFile(`.tests/${failure.filename}-stdout.log`, failure.stdout)
       await writeFile(`.tests/${failure.filename}-stderr.log`, failure.stderr)
     }
     console.error(`❌ ${failures.length} tests failed, see .tests directory for logs`)
     process.exit(1)
   }
📝 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
if (failures.length > 0) {
for (const failure of failures) {
await writeFile(`.tests/${failure.filename}-stdout.log`, failure.stdout)
await writeFile(`.tests/${failure.filename}-stderr.log`, failure.stderr)
}
console.error(`❌ ${failures.length} tests failed, see .tests directory for logs`)
process.exit(1)
}
if (failures.length > 0) {
await mkdir('.tests', { recursive: true })
for (const failure of failures) {
await writeFile(`.tests/${failure.filename}-stdout.log`, failure.stdout)
await writeFile(`.tests/${failure.filename}-stderr.log`, failure.stderr)
}
console.error(`❌ ${failures.length} tests failed, see .tests directory for logs`)
process.exit(1)
}
🤖 Prompt for AI Agents
In test.ts around lines 20 to 27, the code writes per-test logs into the .tests
directory but doesn't ensure that directory exists, causing writeFile to fail
with ENOENT; before the loop (or before any writes) create the .tests directory
(use fs.promises.mkdir or fs.mkdir with { recursive: true } and await it) and
then proceed to write the stdout/stderr files, so the writes succeed even if the
directory is missing.

Repository owner deleted a comment from chatgpt-codex-connector bot Aug 31, 2025
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