Skip to content

feat: add resource-only projects - #311

Merged
munezaclovis merged 5 commits into
mainfrom
codex/resource-only-projects
Aug 1, 2026
Merged

feat: add resource-only projects#311
munezaclovis merged 5 commits into
mainfrom
codex/resource-only-projects

Conversation

@munezaclovis

@munezaclovis munezaclovis commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add serve: false Projects that continue reconciling Managed Resources, allocations, and env without Gateway routes, TLS demand, or PHP workers
  • persist immutable Project slugs and serving mode, and derive new SQL, Redis, and RustFS allocation names from the slug
  • add safe configurable env_file targets while preserving prior PV-managed blocks on target changes and unlink
  • make serving-only config dormant in resource-only mode and omit env entries that depend on serving placeholders
  • expose mode/slug-aware CLI output and selectors, reject pv open for resource-only Projects, and keep explicit PHP available to CLI shims without creating a worker
  • preserve the last valid serving mode when a mode-changing config edit fails semantic validation

Product impact

Projects can now use PV purely for local services and generated environment values. Switching serve off preserves dormant serving configuration and hostname reservations, so switching it back on does not require deleting and recreating config. Newly generated Resource allocation names remain stable and readable even when a Project is never assigned a hostname.

Validation

  • cargo fmt --all -- --check
  • cargo clippy --workspace --all-targets -- -D warnings
  • cargo nextest run --workspace --no-fail-fast -j 4 --status-level fail --final-status-level fail — 1,080 passed, 6 environment-gated tests skipped
  • git diff --check
  • verified no pending .snap.new files

Summary by CodeRabbit

  • New Features
    • Added resource-only Projects (serve: false) for resources and runtimes without gateway routing, worker demand, or hostname reservations.
    • Added immutable Project slugs, configurable env_file support, and stable slug-based resource names.
  • Bug Fixes
    • Preserved last-good serving state and TLS artifacts during invalid or disabled serving configuration.
    • Prevented dormant serving placeholders from rewriting managed environment entries.
  • CLI
    • Updated list, status, project:env, open, unlink, and link to distinguish served and resource-only Projects.

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 08d568ca-6e3a-4950-be76-85f3e190f7dd

📥 Commits

Reviewing files that changed from the base of the PR and between 42f2b4e and f4e8d4d.

📒 Files selected for processing (2)
  • crates/cli/src/commands/php.rs
  • crates/cli/tests/php.rs

📝 Walkthrough

Walkthrough

The PR adds served and resource-only Project modes, immutable Project slugs, mode-aware persistence and daemon reconciliation, configurable environment files, slug-based allocation names, and selector-aware CLI commands.

Changes

Resource-only Project lifecycle

Layer / File(s) Summary
Configuration and environment contracts
crates/config/*, DESIGN.md
Adds serve boolean (default true) and configurable env_file path with validation. Resource-only Projects defer hostname/document-root validation and omit rendering entries containing serving placeholders. Managed environment blocks remain scoped to the configured file.
Mode and slug persistence
crates/state/*, DESIGN.md
Persists immutable Project slugs and serving modes via project_slug and serves_http columns. Hostnames become optional for resource-only Projects. Adds ProjectMode enum, link_project_with_mode for mode-aware linking, project_by_slug lookup, finalize_project_reconciliation for coordinated reconciliation, and internal .invalid hostname for representation.
Daemon reconciliation
crates/daemon/*, DESIGN.md
Preserves last-good serving state on invalid configuration. Resource-only Projects do not add Gateway routes, Project-serving workers, or TLS health scopes. Conditionally validates hostnames and resolved PHP runtime based on serving mode. TLS files remain stable until serving is re-enabled.
Slug-based allocation names
crates/resources/*, DESIGN.md
Generates SQL, Redis, and RustFS allocation names from immutable Project slugs instead of hostnames. Persists generated names once at first allocation creation and reuses them across later Project changes.
Selector-aware CLI operations
crates/cli/*, it/cli.rs, DESIGN.md
Supports project resolution by slug or hostname with ambiguity detection. Updates link validation and success reporting, unlink/open/env selector handling, list output with mode/slug display, status display names, and PHP runtime selection. Interactive pickers sort by display name and filter to served Projects only.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

  • prvious/pv#245: Earlier project configuration, discovery, and CLI project-flow foundations.
  • prvious/pv#246: Earlier managed environment rendering and daemon reconciliation paths.
  • prvious/pv#290: Overlapping daemon TLS and project environment reconciliation changes.

Poem

A rabbit links a slug in place,
With served routes or resource space.
Env files bloom, TLS stays still,
Selectors hop by name or hill.
"No browser here," the bunny sings—
Stable names guide future things. 🐰

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 43.21% 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 clearly identifies the main change: adding support for resource-only Projects.
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 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/resource-only-projects

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.

@codspeed-hq

codspeed-hq Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 7 untouched benchmarks


Comparing codex/resource-only-projects (f4e8d4d) with main (b9af4e1)

Open in CodSpeed

@munezaclovis
munezaclovis marked this pull request as ready for review July 29, 2026 15:22

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c29be2f05a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +11 to +15
CREATE TRIGGER projects_project_slug_required_insert
BEFORE INSERT ON projects
WHEN NEW.project_slug IS NULL OR NEW.project_slug = ''
BEGIN
SELECT RAISE(ABORT, 'project slug is required');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve inserts from the rollback binary

After this migration has run, the immediately previous PV binary still inserts Projects without a project_slug column, so this trigger aborts every new pv link with project slug is required. This breaks the documented migration requirement that restoring the previous binary after an update remains safe; keep the column compatible with legacy inserts or populate it without rejecting them.

AGENTS.md reference: AGENTS.md:L4-L5

Useful? React with 👍 / 👎.

ALTER TABLE projects
ADD COLUMN serves_http INTEGER NOT NULL DEFAULT 1 CHECK (serves_http IN (0, 1));

UPDATE projects SET project_slug = id WHERE project_slug IS NULL;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Backfill existing slugs from project paths

On upgrade, every existing Project receives its random internal ID as its permanent slug rather than the normalized directory basename required by the Project-slug design. Because the migration also makes slugs immutable, relinking cannot repair this: users must select migrated Projects by an opaque ID, and newly added Resource allocations inherit that ID in their generated database, bucket, or prefix names.

AGENTS.md reference: AGENTS.md:L4-L5

Useful? React with 👍 / 👎.

&project.primary_hostname,
&config_file.config.hostnames,
)?;
let serves_http = project.mode == ProjectMode::Served && config_file.config.serve;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Render pending served configurations as served

When a stored resource-only Project is changed to serve: true while the daemon is unavailable or has not reconciled yet, this conjunction remains false because the persisted mode is still ResourceOnly. Consequently pv project:env omits every ${project_url} and TLS-dependent entry and skips hostname validation, even though those entries are supposed to be restored when serving is enabled; build the preview from the candidate config mode and its slug-derived hostname rather than requiring the old persisted mode to agree.

AGENTS.md reference: AGENTS.md:L4-L5

Useful? React with 👍 / 👎.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Important

Two state compatibility issues need to be resolved before merge: existing Projects receive internal-ID slugs, and failed mode transitions can leave persisted state out of sync with the active Gateway.

Reviewed changes — This PR adds resource-only Projects that retain managed resources and generated env while opting out of HTTP serving, and introduces stable Project slugs to support that mode.

  • Add resource-only Project configurationserve: false suppresses Gateway, TLS, and worker demand while preserving dormant serving config and continuing resource reconciliation.
  • Add configurable env targetsenv_file safely resolves within the Project root, and rendering omits entries that depend on serving-only placeholders.
  • Persist Project mode and slug — migration 009 adds immutable slugs and serving mode, with internal compatibility hostnames for new resource-only records.
  • Use slugs for allocation namespaces — new SQL databases, Redis prefixes, and RustFS buckets derive names from the stable Project slug.
  • Expose mode-aware CLI behavior — list/status output and selectors support slugs, while pv open rejects resource-only Projects.
  • Preserve valid runtime state during config edits — reconciliation preflights mode-changing config before applying the candidate Project state.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using GPT Sol𝕏

ALTER TABLE projects
ADD COLUMN serves_http INTEGER NOT NULL DEFAULT 1 CHECK (serves_http IN (0, 1));

UPDATE projects SET project_slug = id WHERE project_slug IS NULL;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Existing installations get their random internal Project ID as the public slug instead of the documented basename-derived readable slug. That ID is then exposed in selectors and becomes the namespace for every allocation created after upgrade; backfill existing rows with collision-safe basename-derived slugs instead.

Technical details
# Existing Projects receive internal-ID slugs

## Affected sites
- `crates/state/src/sql/009_project_mode_and_slug.sql:5` — copies the internal `id` into `project_slug`.
- `crates/cli/src/commands/project.rs:530` — treats that migrated value as a user-facing selector.
- `crates/daemon/src/project_env.rs:661` — uses it for newly generated allocation names.
- `DESIGN.md:1260-1262` — specifies basename-derived readable slugs and allocation namespaces.

## Required outcome
- Existing Projects receive the same readable slug shape as newly linked Projects.
- Colliding basename slugs are assigned deterministic available suffixes without changing existing allocation records.

Comment thread crates/daemon/src/project_env.rs Outdated

@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: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/state/src/database.rs (1)

786-810: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Preserve the project's persisted mode instead of forcing ProjectMode::Served.

The guard only rejects projects without a hostname, but a Served→ResourceOnly project retains its primary hostname (line 673-686), so a resource-only project can reach line 810 and silently have serves_http flipped back to 1. The daemon currently gates this call behind serves_http, but the API is public and the mode flip is invisible. Pass project.mode through.

🛡️ Proposed fix
-            update_project_in_transaction(&transaction, project_id, &input, ProjectMode::Served)?;
+            update_project_in_transaction(&transaction, project_id, &input, project.mode)?;
🤖 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 `@crates/state/src/database.rs` around lines 786 - 810, Update the
LinkProjectInput construction in the hostname-update flow to preserve the
persisted project mode when calling update_project_in_transaction, instead of
forcing ProjectMode::Served. Pass project.mode through so ResourceOnly projects
remain ResourceOnly while retaining their hostname data.
🧹 Nitpick comments (9)
crates/cli/src/commands/php.rs (1)

668-677: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated project_display_name helper in two command modules. Both files define a byte-identical mode-aware display helper; the shared root cause is a missing single crate-local definition, so future changes to display rules must be made twice.

  • crates/cli/src/commands/php.rs#L668-L677: delete this copy and import the shared helper instead.
  • crates/cli/src/commands/project.rs#L577-L586: promote this definition to pub(crate) (or move it to a shared commands helper module) as the single source of truth.
🤖 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 `@crates/cli/src/commands/php.rs` around lines 668 - 677, Remove the duplicate
project_display_name helper from crates/cli/src/commands/php.rs:668-677 and
import the shared implementation. In crates/cli/src/commands/project.rs:577-586,
expose project_display_name as pub(crate) so it is the single source of truth
for both command modules.
it/cli.rs (1)

716-725: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add an explicit mode assertion alongside the snapshot.

The test name promises mode visibility, but verification is snapshot-only, so an accidental snapshot re-accept could silently drop the Mode column. A direct assert!(list.stdout.contains("resource-only")) / contains("served") pins the contract.

🤖 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 `@it/cli.rs` around lines 716 - 725, Add explicit assertions near the existing
snapshot in the mode visibility test, checking that list.stdout contains both
"resource-only" and "served". Keep the snapshot assertion unchanged so the test
directly verifies the Mode column contract.
crates/cli/src/commands/project.rs (2)

757-760: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Second config read per Project.

project_list_status already parsed ProjectConfigFile for this project; reading it again here doubles the config I/O for every project in pv list. Consider returning the parsed config (or the env_file) from project_list_status instead.

🤖 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 `@crates/cli/src/commands/project.rs` around lines 757 - 760, Update
project_list_status and its caller so the existing ProjectConfigFile parse is
reused to obtain env_file, rather than calling ProjectConfigFile::read_from_root
again for each project. Return the parsed configuration or env_file from
project_list_status while preserving the current status behavior and optional
env_file handling.

402-429: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Collapse the repeated serves_http branches.

Four separate if serves_http { … } else { String::new() } arms restate the same condition. A single branch that builds the serving fields (or returns early with empty strings) reads better.

♻️ Suggested shape
-    ProjectEnvContext {
-        primary_hostname: if serves_http {
-            context.primary_hostname.unwrap_or_default()
-        } else {
-            String::new()
-        },
-        tls_ca_path: if serves_http {
-            paths.ca_certificate().to_string()
-        } else {
-            String::new()
-        },
-        tls_cert_path: if serves_http {
-            paths.project_tls_certificate(&project_id).to_string()
-        } else {
-            String::new()
-        },
-        tls_key_path: if serves_http {
-            paths.project_tls_private_key(&project_id).to_string()
-        } else {
-            String::new()
-        },
+    let (primary_hostname, tls_ca_path, tls_cert_path, tls_key_path) = if serves_http {
+        (
+            context.primary_hostname.unwrap_or_default(),
+            paths.ca_certificate().to_string(),
+            paths.project_tls_certificate(&project_id).to_string(),
+            paths.project_tls_private_key(&project_id).to_string(),
+        )
+    } else {
+        Default::default()
+    };
+
+    ProjectEnvContext {
+        primary_hostname,
+        tls_ca_path,
+        tls_cert_path,
+        tls_key_path,
🤖 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 `@crates/cli/src/commands/project.rs` around lines 402 - 429, Refactor
project_env_context so the repeated serves_http condition is evaluated once when
constructing the serving-related fields. Build primary_hostname, tls_ca_path,
tls_cert_path, and tls_key_path together in the HTTP-serving branch, while
preserving empty-string values for all four fields when serves_http is false.
crates/cli/src/commands/status.rs (1)

307-325: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Avoid the magic "resource-only" string comparison.

display_name compares a serialized label, so any change to ProjectMode::as_str() silently degrades this to the hostname branch instead of failing to compile. Store the ProjectMode (serializing it via serde) or at minimum compare against ProjectMode::ResourceOnly.as_str().

🤖 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 `@crates/cli/src/commands/status.rs` around lines 307 - 325, Update
ProjectStatus::display_name to avoid comparing mode against the hardcoded
"resource-only" literal; use the canonical ProjectMode::ResourceOnly.as_str()
value, or store ProjectMode directly with serde serialization and compare the
enum variant. Preserve the existing slug selection for resource-only projects
and hostname fallback for all others.
crates/cli/src/error.rs (1)

24-27: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Message repeats the selector and only offers one disambiguation path.

selector is interpolated twice ("selector acme matches slug acme"), and the hint only tells the user how to reach the hostname-matched Project — there is no stated way to target the slug-matched one. Consider tightening to something like: "{selector} matches a Project slug and the hostname {hostname} on different Projects; pass {hostname} for the served Project, or rename to disambiguate." (crates/cli/tests/project_env.rs Line 269 asserts on this text.)

🤖 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 `@crates/cli/src/error.rs` around lines 24 - 27, Update the
AmbiguousProjectSelector error message to avoid repeating selector and
explicitly describe both resolution paths: passing hostname `{hostname}` selects
the served Project, while renaming disambiguates the slug match. Preserve the
wording expected by the assertion in project_env.rs.
crates/cli/src/args.rs (1)

484-503: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider renaming the field to match its new meaning.

All three positionals now accept a slug or hostname, and crates/cli/src/commands/project.rs already binds them to selector locals (Line 162, Line 499). Keeping the field named hostname is misleading for future readers; renaming to project (or selector) would align the struct with the CLI contract.

🤖 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 `@crates/cli/src/args.rs` around lines 484 - 503, Rename the positional field
hostname to selector (or project) in the relevant argument structs, including
the unlink, open, and ProjectEnvArgs definitions, and update all command
consumers to use the new field name. Preserve the existing slug-or-hostname CLI
behavior and selector bindings in project command handling.
crates/config/src/parser.rs (1)

269-280: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Import Utf8Component at the top level instead of fully qualifying it.

As per coding guidelines, "PREFER top-level imports over local imports or fully qualified names in Rust".

♻️ Proposed change
-    for component in env_file.components() {
-        match component {
-            camino::Utf8Component::Normal(_) => depth += 1,
-            camino::Utf8Component::ParentDir if depth == 0 => {
-                return Err(ConfigError::EnvFileEscapesProject { env_file });
-            }
-            camino::Utf8Component::ParentDir => depth -= 1,
-            camino::Utf8Component::CurDir => {}
-            camino::Utf8Component::RootDir | camino::Utf8Component::Prefix(_) => {
-                return Err(ConfigError::AbsoluteEnvFile { env_file });
-            }
+    for component in env_file.components() {
+        match component {
+            Utf8Component::Normal(_) => depth += 1,
+            Utf8Component::ParentDir if depth == 0 => {
+                return Err(ConfigError::EnvFileEscapesProject { env_file });
+            }
+            Utf8Component::ParentDir => depth -= 1,
+            Utf8Component::CurDir => {}
+            Utf8Component::RootDir | Utf8Component::Prefix(_) => {
+                return Err(ConfigError::AbsoluteEnvFile { env_file });
+            }
         }
     }

with use camino::{Utf8Component, Utf8PathBuf}; at the top of the file.

🤖 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 `@crates/config/src/parser.rs` around lines 269 - 280, Update the imports in
parser.rs to include Utf8Component alongside Utf8PathBuf, then replace the fully
qualified camino::Utf8Component references in the env_file component match with
Utf8Component.

Source: Coding guidelines

crates/state/src/database.rs (1)

3431-3502: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Slug generation looks correct; one note on the .invalid sentinel check.

base[..base_length] is byte-slicing, which is safe here only because project_slug_base emits ASCII alphanumerics and hyphens — worth a short comment so a future change to the character set doesn't introduce a char-boundary panic. is_internal_project_hostname matching any .invalid suffix is safe given hostnames are validated as .test.

🤖 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 `@crates/state/src/database.rs` around lines 3431 - 3502, Add a brief comment
at the byte-slice in generate_project_slug explaining that base is ASCII-only
because project_slug_base restricts generated characters to alphanumerics and
hyphens, making byte indexing safe. Do not alter slug generation or the
is_internal_project_hostname behavior.
🤖 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 `@crates/cli/src/commands/project.rs`:
- Around line 42-57: Update the primary_hostname selection in the project
command to detect when args.hostname is explicitly provided while
config_file.config.serve is false. Reject this incompatible combination or emit
a clear warning before continuing, rather than silently discarding the hostname;
preserve the existing hostname selection behavior for valid combinations.
- Around line 530-532: Update the project resolution flow around slug_project,
normalize_primary_hostname, and hostname_project so slug matches are handled
before hostname normalization errors. Treat normalization failure as no hostname
candidate, and only unwrap/use the normalized hostname on ambiguity or matched
paths where it is required; preserve ProjectNotResolved when neither candidate
resolves.

In `@crates/resources/src/allocation.rs`:
- Around line 80-91: Update the allocation-name composition logic for
ResourceAllocationKind::SqlDatabase, RedisPrefix, and RustfsBucket to encode
project_slug and allocation_name with an unambiguous boundary, preventing
distinct component pairs from producing the same physical name. Preserve each
backend’s required naming format while applying the encoding consistently, and
add regression cases covering the specified collision pairs for all three
backends.

In `@crates/state/src/sql/009_project_mode_and_slug.sql`:
- Line 5: The migration’s project_slug backfill must preserve the readable,
basename-derived slug contract instead of copying opaque project IDs. Replace
the UPDATE with a Rust-side migration/backfill that derives slugs from each
project’s path, applies the existing collision-suffixing rules, and writes
unique results before the immutable-slug trigger applies; otherwise explicitly
document and preserve the legacy behavior.

---

Outside diff comments:
In `@crates/state/src/database.rs`:
- Around line 786-810: Update the LinkProjectInput construction in the
hostname-update flow to preserve the persisted project mode when calling
update_project_in_transaction, instead of forcing ProjectMode::Served. Pass
project.mode through so ResourceOnly projects remain ResourceOnly while
retaining their hostname data.

---

Nitpick comments:
In `@crates/cli/src/args.rs`:
- Around line 484-503: Rename the positional field hostname to selector (or
project) in the relevant argument structs, including the unlink, open, and
ProjectEnvArgs definitions, and update all command consumers to use the new
field name. Preserve the existing slug-or-hostname CLI behavior and selector
bindings in project command handling.

In `@crates/cli/src/commands/php.rs`:
- Around line 668-677: Remove the duplicate project_display_name helper from
crates/cli/src/commands/php.rs:668-677 and import the shared implementation. In
crates/cli/src/commands/project.rs:577-586, expose project_display_name as
pub(crate) so it is the single source of truth for both command modules.

In `@crates/cli/src/commands/project.rs`:
- Around line 757-760: Update project_list_status and its caller so the existing
ProjectConfigFile parse is reused to obtain env_file, rather than calling
ProjectConfigFile::read_from_root again for each project. Return the parsed
configuration or env_file from project_list_status while preserving the current
status behavior and optional env_file handling.
- Around line 402-429: Refactor project_env_context so the repeated serves_http
condition is evaluated once when constructing the serving-related fields. Build
primary_hostname, tls_ca_path, tls_cert_path, and tls_key_path together in the
HTTP-serving branch, while preserving empty-string values for all four fields
when serves_http is false.

In `@crates/cli/src/commands/status.rs`:
- Around line 307-325: Update ProjectStatus::display_name to avoid comparing
mode against the hardcoded "resource-only" literal; use the canonical
ProjectMode::ResourceOnly.as_str() value, or store ProjectMode directly with
serde serialization and compare the enum variant. Preserve the existing slug
selection for resource-only projects and hostname fallback for all others.

In `@crates/cli/src/error.rs`:
- Around line 24-27: Update the AmbiguousProjectSelector error message to avoid
repeating selector and explicitly describe both resolution paths: passing
hostname `{hostname}` selects the served Project, while renaming disambiguates
the slug match. Preserve the wording expected by the assertion in
project_env.rs.

In `@crates/config/src/parser.rs`:
- Around line 269-280: Update the imports in parser.rs to include Utf8Component
alongside Utf8PathBuf, then replace the fully qualified camino::Utf8Component
references in the env_file component match with Utf8Component.

In `@crates/state/src/database.rs`:
- Around line 3431-3502: Add a brief comment at the byte-slice in
generate_project_slug explaining that base is ASCII-only because
project_slug_base restricts generated characters to alphanumerics and hyphens,
making byte indexing safe. Do not alter slug generation or the
is_internal_project_hostname behavior.

In `@it/cli.rs`:
- Around line 716-725: Add explicit assertions near the existing snapshot in the
mode visibility test, checking that list.stdout contains both "resource-only"
and "served". Keep the snapshot assertion unchanged so the test directly
verifies the Mode column contract.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7146c109-184f-496b-8f4f-b3d3ee248a4a

📥 Commits

Reviewing files that changed from the base of the PR and between d5e4532 and c29be2f.

⛔ Files ignored due to path filters (73)
  • crates/cli/tests/snapshots/doctor__doctor_fails_when_active_pf_redirects_are_missing.snap is excluded by !**/*.snap
  • crates/cli/tests/snapshots/doctor__doctor_fails_when_daemon_socket_is_stale.snap is excluded by !**/*.snap
  • crates/cli/tests/snapshots/doctor__doctor_fails_when_system_ca_trust_is_missing.snap is excluded by !**/*.snap
  • crates/cli/tests/snapshots/doctor__doctor_fails_when_system_resolver_is_missing.snap is excluded by !**/*.snap
  • crates/cli/tests/snapshots/doctor__doctor_fails_with_repair_commands.snap is excluded by !**/*.snap
  • crates/cli/tests/snapshots/doctor__doctor_passes_when_required_checks_pass.snap is excluded by !**/*.snap
  • crates/cli/tests/snapshots/doctor__doctor_warnings_do_not_fail.snap is excluded by !**/*.snap
  • crates/cli/tests/snapshots/list_json__list_json_exposes_resource_only_mode_slug_and_env_file_without_sentinel.snap is excluded by !**/*.snap
  • crates/cli/tests/snapshots/list_json__list_json_outputs_linked_projects.snap is excluded by !**/*.snap
  • crates/cli/tests/snapshots/php__php_use_latest_preserves_alias_in_config_and_records_resolved_track.snap is excluded by !**/*.snap
  • crates/cli/tests/snapshots/php__php_use_updates_project_config_state_and_reports_missing_daemon.snap is excluded by !**/*.snap
  • crates/cli/tests/snapshots/project_env__project_env_rejects_bare_slug_hostname_ambiguity.snap is excluded by !**/*.snap
  • crates/cli/tests/snapshots/project_env__project_env_resolves_resource_only_slug_and_reads_configured_env_file.snap is excluded by !**/*.snap
  • crates/cli/tests/snapshots/project_open__open_rejects_resource_only_target_and_excludes_it_from_picker.snap is excluded by !**/*.snap
  • crates/cli/tests/snapshots/project_open__open_without_current_project_fails_when_non_interactive.snap is excluded by !**/*.snap
  • crates/cli/tests/snapshots/project_unlink__unlink_resolves_resource_only_slug_and_leaves_managed_env_block.snap is excluded by !**/*.snap
  • crates/cli/tests/snapshots/status__status_prefers_ignored_php_extension_over_other_project_env_warnings.snap is excluded by !**/*.snap
  • crates/cli/tests/snapshots/status__status_reports_warning_project_env_as_success.snap is excluded by !**/*.snap
  • crates/config/tests/snapshots/project_config__project_config_discovery_validates_paths_and_conflicts.snap is excluded by !**/*.snap
  • crates/config/tests/snapshots/project_config__project_config_expands_yaml_aliases_and_merge_keys.snap is excluded by !**/*.snap
  • crates/config/tests/snapshots/project_config__project_config_parses_resource_only_controls_and_defaults.snap is excluded by !**/*.snap
  • crates/config/tests/snapshots/project_config__project_config_parses_strict_resource_env_shape.snap is excluded by !**/*.snap
  • crates/config/tests/snapshots/project_config__project_config_validates_url_placeholder_scopes.snap is excluded by !**/*.snap
  • crates/config/tests/snapshots/project_config__project_config_writer_creates_preferred_file_when_missing.snap is excluded by !**/*.snap
  • crates/config/tests/snapshots/project_config__project_config_writer_preserves_existing_config_file_mode.snap is excluded by !**/*.snap
  • crates/config/tests/snapshots/project_config__project_config_writer_preserves_resource_only_controls.snap is excluded by !**/*.snap
  • crates/config/tests/snapshots/project_config__project_config_writer_updates_php_in_alternate_file.snap is excluded by !**/*.snap
  • crates/config/tests/snapshots/project_config__project_config_writer_updates_php_in_discovered_file.snap is excluded by !**/*.snap
  • crates/config/tests/snapshots/project_config__project_config_writer_updates_symlinked_config_target.snap is excluded by !**/*.snap
  • crates/config/tests/snapshots/project_config__project_config_writer_writes_full_config_to_preferred_file.snap is excluded by !**/*.snap
  • crates/config/tests/snapshots/project_env__resource_only_env_omits_serving_placeholders_across_scopes.snap is excluded by !**/*.snap
  • crates/daemon/src/managed_resources/snapshots/daemon__managed_resources__mysql_tests__mysql_project_demand_installs_missing_fixture_track_before_start.snap is excluded by !**/*.snap
  • crates/daemon/src/managed_resources/snapshots/daemon__managed_resources__mysql_tests__mysql_reconciliation_creates_database_allocation_and_renders_env.snap is excluded by !**/*.snap
  • crates/daemon/src/managed_resources/snapshots/daemon__managed_resources__tests__async_readiness_reassigns_unowned_persisted_port_before_resource_readiness.snap is excluded by !**/*.snap
  • crates/daemon/src/managed_resources/snapshots/daemon__managed_resources__tests__demanded_resource_uses_async_readiness_and_allocation_hooks.snap is excluded by !**/*.snap
  • crates/daemon/src/managed_resources/snapshots/daemon__managed_resources__tests__postgres_project_demand_installs_missing_fixture_track_before_start.snap is excluded by !**/*.snap
  • crates/daemon/src/managed_resources/snapshots/daemon__managed_resources__tests__postgres_reconciliation_creates_database_allocation_and_renders_env.snap is excluded by !**/*.snap
  • crates/daemon/src/managed_resources/snapshots/daemon__managed_resources__tests__postgres_reconciliation_replaces_stale_admin_username_from_track_env.snap is excluded by !**/*.snap
  • crates/daemon/src/managed_resources/snapshots/daemon__managed_resources__tests__redis_port_reassignment_refreshes_ready_allocation_env.snap is excluded by !**/*.snap
  • crates/daemon/src/managed_resources/snapshots/daemon__managed_resources__tests__redis_project_demand_installs_missing_fixture_track_before_start.snap is excluded by !**/*.snap
  • crates/daemon/src/managed_resources/snapshots/daemon__managed_resources__tests__redis_reconciliation_marks_prefix_allocation_ready_and_renders_env.snap is excluded by !**/*.snap
  • crates/daemon/src/managed_resources/snapshots/daemon__managed_resources__tests__redis_reconciliation_reuses_ready_prefix_allocation.snap is excluded by !**/*.snap
  • crates/daemon/src/managed_resources/snapshots/daemon__managed_resources__tests__rustfs_allocation_failure_preserves_project_env_and_records_failed_runtime.snap is excluded by !**/*.snap
  • crates/daemon/src/managed_resources/snapshots/daemon__managed_resources__tests__rustfs_port_reassignment_renders_current_endpoint_for_ready_allocation.snap is excluded by !**/*.snap
  • crates/daemon/src/managed_resources/snapshots/daemon__managed_resources__tests__rustfs_project_demand_installs_missing_fixture_track_before_start.snap is excluded by !**/*.snap
  • crates/daemon/src/managed_resources/snapshots/daemon__managed_resources__tests__rustfs_ready_allocation_reconciliation_repairs_missing_bucket_and_preserves_env.snap is excluded by !**/*.snap
  • crates/daemon/src/managed_resources/snapshots/daemon__managed_resources__tests__rustfs_reconciliation_creates_bucket_and_renders_env.snap is excluded by !**/*.snap
  • crates/daemon/tests/snapshots/project_env_reconciliation__config_declared_hostnames_are_persisted_during_reconciliation.snap is excluded by !**/*.snap
  • crates/daemon/tests/snapshots/project_env_reconciliation__first_allocation_reconciliation_records_desired_state_before_context_failure.snap is excluded by !**/*.snap
  • crates/daemon/tests/snapshots/project_env_reconciliation__generated_allocation_name_too_long_leaves_resource_state_unchanged.snap is excluded by !**/*.snap
  • crates/daemon/tests/snapshots/project_env_reconciliation__invalid_resource_only_transition_preserves_served_mode.snap is excluded by !**/*.snap
  • crates/daemon/tests/snapshots/project_env_reconciliation__resource_only_project_uses_custom_env_file_and_no_php_worker.snap is excluded by !**/*.snap
  • crates/daemon/tests/snapshots/project_env_reconciliation__resources_and_empty_allocations_without_env_mappings_update_state_without_dotenv.snap is excluded by !**/*.snap
  • crates/resources/tests/snapshots/resource_allocations__generated_allocation_names_enforce_sixty_three_character_limit.snap is excluded by !**/*.snap
  • crates/resources/tests/snapshots/resource_allocations__resource_allocations_generate_resource_specific_names.snap is excluded by !**/*.snap
  • crates/state/tests/snapshots/state_foundation__database_runs_migrations_and_exposes_core_schema.snap is excluded by !**/*.snap
  • crates/state/tests/snapshots/state_foundation__generated_env_context_escapes_round_trip_through_state.snap is excluded by !**/*.snap
  • crates/state/tests/snapshots/state_foundation__linked_projects_preserve_ids_and_refresh_hostnames.snap is excluded by !**/*.snap
  • crates/state/tests/snapshots/state_foundation__migrated_project_resource_state_round_trips_through_public_apis.snap is excluded by !**/*.snap
  • crates/state/tests/snapshots/state_foundation__project_env_context_uses_ready_allocations_from_required_track_only.snap is excluded by !**/*.snap
  • crates/state/tests/snapshots/state_foundation__resource_allocations_preserve_generated_names_and_env_context.snap is excluded by !**/*.snap
  • it/snapshots/cli__completions_generate_bash_script.snap is excluded by !**/*.snap
  • it/snapshots/cli__completions_generate_zsh_script.snap is excluded by !**/*.snap
  • it/snapshots/cli__core_workflow_command_help_is_documented.snap is excluded by !**/*.snap
  • it/snapshots/cli__project_link_accepts_relative_path_arguments.snap is excluded by !**/*.snap
  • it/snapshots/cli__project_link_list_and_unlink_use_injected_home.snap is excluded by !**/*.snap
  • it/snapshots/cli__project_list_clears_stale_env_status_without_mappings.snap is excluded by !**/*.snap
  • it/snapshots/cli__project_list_reports_config_hostname_validation_errors.snap is excluded by !**/*.snap
  • it/snapshots/cli__project_list_reports_env_observed_status.snap is excluded by !**/*.snap
  • it/snapshots/cli__project_list_reports_env_shape_validation_errors.snap is excluded by !**/*.snap
  • it/snapshots/cli__project_list_reports_ignored_php_extensions.snap is excluded by !**/*.snap
  • it/snapshots/cli__project_list_reports_invalid_linked_config.snap is excluded by !**/*.snap
  • it/snapshots/cli__project_list_shows_project_and_mode_for_served_and_resource_only_projects.snap is excluded by !**/*.snap
📒 Files selected for processing (37)
  • DESIGN.md
  • crates/cli/src/args.rs
  • crates/cli/src/commands/php.rs
  • crates/cli/src/commands/project.rs
  • crates/cli/src/commands/status.rs
  • crates/cli/src/error.rs
  • crates/cli/tests/list_json.rs
  • crates/cli/tests/php.rs
  • crates/cli/tests/project_env.rs
  • crates/cli/tests/project_open.rs
  • crates/cli/tests/project_unlink.rs
  • crates/config/src/discovery.rs
  • crates/config/src/env.rs
  • crates/config/src/error.rs
  • crates/config/src/lib.rs
  • crates/config/src/model.rs
  • crates/config/src/parser.rs
  • crates/config/tests/project_config.rs
  • crates/config/tests/project_env.rs
  • crates/daemon/src/gateway.rs
  • crates/daemon/src/jobs.rs
  • crates/daemon/src/managed_resources/tests.rs
  • crates/daemon/src/project_env.rs
  • crates/daemon/src/server.rs
  • crates/daemon/src/watcher.rs
  • crates/daemon/tests/daemon_foundation.rs
  • crates/daemon/tests/gateway_reconciliation.rs
  • crates/daemon/tests/project_env_reconciliation.rs
  • crates/resources/src/allocation.rs
  • crates/resources/tests/resource_allocations.rs
  • crates/state/src/database.rs
  • crates/state/src/error.rs
  • crates/state/src/lib.rs
  • crates/state/src/migrations.rs
  • crates/state/src/sql/009_project_mode_and_slug.sql
  • crates/state/tests/state_foundation.rs
  • it/cli.rs

Comment on lines +42 to 57
let mode = if config_file.config.serve {
ProjectMode::Served
} else {
ProjectMode::ResourceOnly
};
let primary_hostname = match (
config_file.config.serve.then_some(args.hostname).flatten(),
existing.as_ref(),
) {
(Some(hostname), _) => config::normalize_primary_hostname(&hostname)?,
(None, Some(project)) => project.primary_hostname.clone(),
(None, Some(project)) => project
.primary_hostname
.clone()
.unwrap_or_else(|| format!("{}.test", project.slug)),
(None, None) => config::hostname_from_project_path(&project_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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

--hostname is silently ignored for resource-only Projects.

config_file.config.serve.then_some(args.hostname).flatten() discards an explicitly provided --hostname when serve: false, and the user gets no feedback. Prefer rejecting the combination (or emitting a warning line) so the ignored flag is visible.

🤖 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 `@crates/cli/src/commands/project.rs` around lines 42 - 57, Update the
primary_hostname selection in the project command to detect when args.hostname
is explicitly provided while config_file.config.serve is false. Reject this
incompatible combination or emit a clear warning before continuing, rather than
silently discarding the hostname; preserve the existing hostname selection
behavior for valid combinations.

Comment on lines +530 to +532
let slug_project = database.project_by_slug(selector)?;
let hostname = config::normalize_primary_hostname(selector)?;
let hostname_project = database.project_by_hostname(&hostname)?;

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

Hostname normalization failure masks a valid slug match / not-found.

config::normalize_primary_hostname(selector)? runs before the slug result is consumed, so an input that isn't a legal DNS label (e.g. my_project) surfaces an invalid-hostname config error instead of the intended ProjectNotResolved. Resolve the slug first and treat normalization failure as "no hostname candidate".

🐛 Suggested fix
-    let slug_project = database.project_by_slug(selector)?;
-    let hostname = config::normalize_primary_hostname(selector)?;
-    let hostname_project = database.project_by_hostname(&hostname)?;
+    let slug_project = database.project_by_slug(selector)?;
+    let hostname = config::normalize_primary_hostname(selector).ok();
+    let hostname_project = match hostname.as_deref() {
+        Some(hostname) => database.project_by_hostname(hostname)?,
+        None => None,
+    };

(subsequent uses of hostname then need the Option unwrapped only on the ambiguity/matched paths)

🤖 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 `@crates/cli/src/commands/project.rs` around lines 530 - 532, Update the
project resolution flow around slug_project, normalize_primary_hostname, and
hostname_project so slug matches are handled before hostname normalization
errors. Treat normalization failure as no hostname candidate, and only
unwrap/use the normalized hostname on ambiguity or matched paths where it is
required; preserve ProjectNotResolved when neither candidate resolves.

Comment on lines 80 to +91
ResourceAllocationKind::SqlDatabase => {
format!(
"{}_{}",
sql_hostname_slug(primary_hostname),
project_slug.replace('-', "_"),
allocation_name.replace('-', "_")
)
}
ResourceAllocationKind::RedisPrefix => {
format!(
"{}-{}-",
dash_hostname_slug(primary_hostname),
allocation_name.replace('_', "-")
)
format!("{}-{}-", project_slug, allocation_name.replace('_', "-"))
}
ResourceAllocationKind::RustfsBucket => {
format!(
"{}-{}",
dash_hostname_slug(primary_hostname),
allocation_name.replace('_', "-")
)
format!("{}-{}", project_slug, allocation_name.replace('_', "-"))

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 | 🟠 Major | ⚡ Quick win

Make allocation-name composition collision-free.

Line 83 can generate identical physical names for distinct projects: SQL maps ("a-b", "c") and ("a", "b_c") to a_b_c. Redis and RustFS similarly collide for ("a-b", "c") and ("a", "b-c"). Use an unambiguous component encoding/boundary (for example, a length-prefixed slug component) and add regression cases for all three backends.

🤖 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 `@crates/resources/src/allocation.rs` around lines 80 - 91, Update the
allocation-name composition logic for ResourceAllocationKind::SqlDatabase,
RedisPrefix, and RustfsBucket to encode project_slug and allocation_name with an
unambiguous boundary, preventing distinct component pairs from producing the
same physical name. Preserve each backend’s required naming format while
applying the encoding consistently, and add regression cases covering the
specified collision pairs for all three backends.

ALTER TABLE projects
ADD COLUMN serves_http INTEGER NOT NULL DEFAULT 1 CHECK (serves_http IN (0, 1));

UPDATE projects SET project_slug = id WHERE project_slug IS NULL;

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 | 🟠 Major | 🏗️ Heavy lift

Backfilling project_slug from the opaque project ID conflicts with the readable-slug contract.

Existing projects get slugs like k3f9x2a1b7 instead of a basename-derived slug, and because slugs are immutable (trigger at lines 18-25) this can never be corrected. DESIGN lines 1260 and 1076 state the slug is derived from the canonical directory basename and is the readable namespace shown in pv list, used by pv unlink/pv project:env selectors, and used for newly generated allocation names. Consider a Rust-side backfill that derives and collision-suffixes slugs from projects.path during migration, or explicitly document the legacy behavior.

🤖 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 `@crates/state/src/sql/009_project_mode_and_slug.sql` at line 5, The
migration’s project_slug backfill must preserve the readable, basename-derived
slug contract instead of copying opaque project IDs. Replace the UPDATE with a
Rust-side migration/backfill that derives slugs from each project’s path,
applies the existing collision-suffixing rules, and writes unique results before
the immutable-slug trigger applies; otherwise explicitly document and preserve
the legacy behavior.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Important

The transition fix still persists candidate PHP runtime state before the serving-mode commit, so a later failure can break the fallback worker for the previously active route.

Reviewed changes — The follow-up commit delays serving-mode persistence until reconciliation succeeds and tightens dormant-config and env behavior around resource-only transitions.

  • Delayed serving-mode synchronizationreconcile_loaded_project now performs candidate resource, runtime, and env work before committing mode and hostname state.
  • Preserved pending Gateway state — Gateway planning detects config/persisted-mode mismatches and retains the previously persisted runtime Project until a transition succeeds.
  • Validated dormant config shapes — Resource-only config now still normalizes and validates hostname syntax and rejects absolute or escaping document roots while deferring existence checks.
  • Cleared dormant managed env values — Empty rendering now removes values from an existing PV-managed block while leaving files untouched when no block exists.
  • Stabilized Project list order — Text and JSON list output now sort served hostnames and resource-only slugs by their displayed values.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using GPT Sol𝕏

Comment thread crates/daemon/src/project_env.rs Outdated

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 83d4ba0cab

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +58 to +68
let result = database.link_project_with_mode(
LinkProjectInput {
path: project_path.clone(),
original_path: original_project_path,
primary_hostname,
config_path: config_file.path,
desired_php_track,
additional_hostnames: config_file.config.hostnames,
},
mode,
)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Validate the candidate config before persisting its mode

When an already-linked served Project is changed to serve: false with a semantic error that ProjectConfigFile::read_from_root does not catch, such as duplicate rendered env keys, pv link reaches this call and persists ResourceOnly before the daemon runs validate_project_env_shape. Reconciliation then fails after the previous valid mode has already been lost, so a later Gateway reconciliation removes the working route instead of preserving the last valid served state; fully validate the candidate before calling link_project_with_mode, or defer the mode update until reconciliation succeeds.

AGENTS.md reference: AGENTS.md:L4-L5

Useful? React with 👍 / 👎.

Comment on lines 943 to 948
pub fn projects(&self) -> Result<Vec<ProjectRecord>, StateError> {
let mut statement = self.connection.prepare(
"SELECT id, path, original_path, primary_hostname, config_path, desired_php_track, created_at, updated_at
"SELECT id, path, original_path, primary_hostname, config_path, desired_php_track, created_at, updated_at, project_slug, serves_http
FROM projects
ORDER BY primary_hostname",
ORDER BY project_slug",
)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Sort the open picker by primary hostname

For served Projects whose custom primary hostnames sort differently from their directory-derived slugs, Database::projects now returns slug order and resolve_open_project only filters that collection without sorting it again. The numbered pv open picker therefore appears in slug order rather than primary-hostname order, so a selection can refer to a different Project than the documented ordering implies; sort the served picker entries by primary_hostname before displaying them.

AGENTS.md reference: AGENTS.md:L4-L5

Useful? React with 👍 / 👎.

@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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/config/src/parser.rs (1)

261-281: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject env_file paths resolving to the project root.

. and directory/.. leave depth == 0, so they are accepted although they target a directory, not an env file. Reject zero-depth final paths and add parser coverage.

🤖 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 `@crates/config/src/parser.rs` around lines 261 - 281, Update validate_env_file
so it rejects paths whose final normalized depth is zero, including "." and
paths such as "directory/..", while preserving existing absolute-path and
project-escape errors. Add parser tests covering these root-resolving paths and
confirming valid env-file paths remain accepted.
🧹 Nitpick comments (2)
crates/config/src/parser.rs (1)

267-277: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Import Utf8Component at module scope.

Replace the fully qualified match patterns with a top-level use camino::Utf8Component;.

As per coding guidelines, **/*.rs: “PREFER top-level imports over local imports or fully qualified names in Rust”.

🤖 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 `@crates/config/src/parser.rs` around lines 267 - 277, Update the parser module
imports to add a top-level use of camino::Utf8Component, then replace the fully
qualified camino::Utf8Component variants in the env_file component match with
Utf8Component variants. Preserve the existing path validation behavior and error
handling.

Source: Coding guidelines

crates/daemon/src/project_env.rs (1)

197-217: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prefer top-level imports over fully-qualified paths.

crate::managed_resources::reconcile_project_resources_with_catalog_and_progress / ..._with_progress are called with fully-qualified paths here rather than imported at the top of the file.

As per coding guidelines, "PREFER top-level imports over local imports or fully qualified names in Rust."

🤖 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 `@crates/daemon/src/project_env.rs` around lines 197 - 217, Import
reconcile_project_resources_with_catalog_and_progress and
reconcile_project_resources_with_progress at the module level, then call both
functions directly in the resource_result branch instead of using fully
qualified crate::managed_resources paths.

Source: Coding guidelines

🤖 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 `@crates/daemon/src/project_env.rs`:
- Around line 195-231: The project mode/hostname update in
synchronize_project_link_state can fail after resource and PHP-runtime state has
been committed, leaving persisted state inconsistent. Make the final
mode/hostname commit and PHP-runtime/environment snapshot writes atomic within
one transaction, or explicitly roll back/reconcile those writes when
synchronize_project_link_state fails; preserve Gateway reconciliation only after
the combined operation succeeds.

---

Outside diff comments:
In `@crates/config/src/parser.rs`:
- Around line 261-281: Update validate_env_file so it rejects paths whose final
normalized depth is zero, including "." and paths such as "directory/..", while
preserving existing absolute-path and project-escape errors. Add parser tests
covering these root-resolving paths and confirming valid env-file paths remain
accepted.

---

Nitpick comments:
In `@crates/config/src/parser.rs`:
- Around line 267-277: Update the parser module imports to add a top-level use
of camino::Utf8Component, then replace the fully qualified camino::Utf8Component
variants in the env_file component match with Utf8Component variants. Preserve
the existing path validation behavior and error handling.

In `@crates/daemon/src/project_env.rs`:
- Around line 197-217: Import
reconcile_project_resources_with_catalog_and_progress and
reconcile_project_resources_with_progress at the module level, then call both
functions directly in the resource_result branch instead of using fully
qualified crate::managed_resources paths.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: dfd1e4ad-2239-4873-9095-7afddeed6691

📥 Commits

Reviewing files that changed from the base of the PR and between c29be2f and 83d4ba0.

⛔ Files ignored due to path filters (4)
  • crates/config/tests/snapshots/project_config__project_config_parses_resource_only_controls_and_defaults.snap is excluded by !**/*.snap
  • crates/config/tests/snapshots/project_config__project_config_writer_preserves_resource_only_controls.snap is excluded by !**/*.snap
  • crates/config/tests/snapshots/project_env__project_env_renderer_returns_empty_output_for_no_mappings.snap is excluded by !**/*.snap
  • it/snapshots/cli__project_list_shows_project_and_mode_for_served_and_resource_only_projects.snap is excluded by !**/*.snap
📒 Files selected for processing (12)
  • crates/cli/src/commands/project.rs
  • crates/cli/tests/list_json.rs
  • crates/config/src/discovery.rs
  • crates/config/src/env.rs
  • crates/config/src/parser.rs
  • crates/config/tests/project_config.rs
  • crates/config/tests/project_env.rs
  • crates/daemon/src/gateway.rs
  • crates/daemon/src/project_env.rs
  • crates/daemon/tests/gateway_reconciliation.rs
  • crates/daemon/tests/project_env_reconciliation.rs
  • it/cli.rs
🚧 Files skipped from review as they are similar to previous changes (8)
  • crates/daemon/tests/gateway_reconciliation.rs
  • it/cli.rs
  • crates/config/src/env.rs
  • crates/config/src/discovery.rs
  • crates/config/tests/project_config.rs
  • crates/daemon/src/gateway.rs
  • crates/daemon/tests/project_env_reconciliation.rs
  • crates/cli/src/commands/project.rs

Comment thread crates/daemon/src/project_env.rs Outdated

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 343e20c018

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/cli/src/commands/php.rs Outdated
}
};
if let Some(track) = project.php_runtime.track.clone() {
let uses_project_runtime = config_file.config.serve || config_file.config.php.is_some();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve the persisted PHP runtime until reconciliation succeeds

When a served Project with a persisted PHP runtime is edited to serve: false without an explicit php: selection and the candidate config has a semantic error that read_from_root does not detect (for example, duplicate rendered env keys), this condition becomes false and the PHP/Composer shim falls back to the global runtime. Daemon reconciliation subsequently rejects the transition and preserves the last valid served state, so commands run under a different PHP version than that state requires; base shim selection on the reconciled mode or fully validate the candidate before applying its mode semantics.

AGENTS.md reference: AGENTS.md:L4-L5

Useful? React with 👍 / 👎.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Important

No new issues were found in 343e20c, and the transition-state feedback is addressed. The previously reported migration backfill issue remains unresolved, so this review does not approve the PR.

Reviewed changes — I reviewed the atomic transition finalization and CLI validation changes added since the prior Pullfrog review.

  • Atomically finalized Project state — Combined serving mode, hostname, PHP runtime, and env observation updates in one immediate SQLite transaction.
  • Preserved the fallback PHP runtime — Deferred candidate runtime persistence until all preceding reconciliation work succeeds.
  • Validated relinks before mutation — Checked rendered env-key conflicts before pv link can replace the persisted serving mode.
  • Stabilized Project selection — Sorted served Projects by primary hostname before presenting the interactive picker.
  • Added rollback coverage — Exercised transaction failure and post-PHP-resolution transition failure paths to verify prior database state remains intact.

Pullfrog  | Fix it ➔View workflow run | Using GPT Sol𝕏

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

🧹 Nitpick comments (3)
crates/daemon/src/project_env.rs (1)

578-578: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

desired_php_track: None is silently overwritten by the state layer.

Database::finalize_project_reconciliation sets input.link.desired_php_track from php_runtime (crates/state/src/database.rs:868-871), so this None never reaches the database. A brief comment would prevent a reader from concluding the track is cleared here.

🤖 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 `@crates/daemon/src/project_env.rs` at line 578, Add a brief explanatory
comment directly above desired_php_track: None in the relevant project
reconciliation input, noting that the state layer’s
Database::finalize_project_reconciliation overwrites it from php_runtime, so
this value does not clear the persisted track.
crates/state/src/database.rs (1)

1986-1990: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Naming reads as "outside a transaction" while it is also used inside one.

replace_project_php_runtime_in_connection is called with &transaction at Line 903 (via Deref<Target = Connection>), which is correct, but the _in_connection suffix conflicts with the _in_transaction convention used everywhere else in this module and obscures that the write joins the caller's transaction. Consider replace_project_php_runtime_in(...) or a short doc comment stating it participates in any active transaction.

🤖 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 `@crates/state/src/database.rs` around lines 1986 - 1990, Rename
replace_project_php_runtime_in_connection to replace_project_php_runtime_in to
reflect that it operates on the caller-provided database handle and can
participate in an active transaction. Update all call sites, including the
invocation through the transaction at Line 903, while preserving the existing
behavior.
crates/daemon/tests/project_env_reconciliation.rs (1)

213-214: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Permission restore is skipped if reconciliation returns Err.

run_project_reconciliation(...)? on Line 213 propagates before set_file_mode(&locked_directory, 0o700), leaving a 0o500 directory behind and potentially failing tempdir cleanup. Capturing the result first and restoring the mode before unwrapping keeps failures readable.

♻️ Restore before propagating
-    let lines = run_project_reconciliation(&paths, &project).await?;
-    set_file_mode(&locked_directory, 0o700)?;
+    let reconciliation = run_project_reconciliation(&paths, &project).await;
+    set_file_mode(&locked_directory, 0o700)?;
+    let lines = reconciliation?;
🤖 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 `@crates/daemon/tests/project_env_reconciliation.rs` around lines 213 - 214,
Update the test flow around run_project_reconciliation to capture its result
instead of propagating immediately, restore locked_directory permissions with
set_file_mode(..., 0o700), then unwrap or propagate the captured reconciliation
result. Preserve the existing successful lines handling and ensure permission
restoration occurs on both success and error paths.
🤖 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.

Nitpick comments:
In `@crates/daemon/src/project_env.rs`:
- Line 578: Add a brief explanatory comment directly above desired_php_track:
None in the relevant project reconciliation input, noting that the state layer’s
Database::finalize_project_reconciliation overwrites it from php_runtime, so
this value does not clear the persisted track.

In `@crates/daemon/tests/project_env_reconciliation.rs`:
- Around line 213-214: Update the test flow around run_project_reconciliation to
capture its result instead of propagating immediately, restore locked_directory
permissions with set_file_mode(..., 0o700), then unwrap or propagate the
captured reconciliation result. Preserve the existing successful lines handling
and ensure permission restoration occurs on both success and error paths.

In `@crates/state/src/database.rs`:
- Around line 1986-1990: Rename replace_project_php_runtime_in_connection to
replace_project_php_runtime_in to reflect that it operates on the
caller-provided database handle and can participate in an active transaction.
Update all call sites, including the invocation through the transaction at Line
903, while preserving the existing behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 068cee83-7bca-4178-939c-99f9096758f8

📥 Commits

Reviewing files that changed from the base of the PR and between 83d4ba0 and 343e20c.

⛔ Files ignored due to path filters (2)
  • crates/cli/tests/snapshots/project_open__link_rejects_invalid_mode_change_without_replacing_served_state.snap is excluded by !**/*.snap
  • crates/cli/tests/snapshots/project_open__open_project_picker_sorts_projects_by_primary_hostname.snap is excluded by !**/*.snap
📒 Files selected for processing (7)
  • crates/cli/src/commands/project.rs
  • crates/cli/tests/project_open.rs
  • crates/daemon/src/project_env.rs
  • crates/daemon/tests/project_env_reconciliation.rs
  • crates/state/src/database.rs
  • crates/state/src/lib.rs
  • crates/state/tests/state_foundation.rs

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Important

The new fallback is still asymmetric for failed resource-only-to-served transitions, so the last valid explicit PHP runtime can change before the transition succeeds.

Reviewed changes since the prior Pullfrog review, I reviewed the PHP shim fallback correction and its regression coverage.

  • Preserved served runtime fallback — Changed shim runtime selection to consult persisted served mode when a candidate serve: false edit has not reconciled successfully.
  • Covered failed disable transitions — Added a CLI test proving the persisted project runtime wins over the global default after semantic validation rejects a served-to-resource-only transition.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using GPT Sol𝕏

Comment thread crates/cli/src/commands/php.rs Outdated

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ No new issues found in f4e8d4d. The existing migration slug-backfill finding remains open from the prior review.

Reviewed changes since the prior Pullfrog review, I reviewed the PHP shim runtime-retention correction and its regression coverage.

  • Retained persisted Project runtimes — Removed the asymmetric mode/config gate so shims keep the last reconciled explicit runtime through failed or pending transitions in either direction.
  • Covered failed enable transitions — Added a focused test proving a resource-only Project retains its explicit runtime when an invalid served transition removes php.
  • Preserved successful global fallback — Kept the existing empty-runtime path so successfully reconciled resource-only Projects without explicit PHP continue to use the global runtime.

Pullfrog  | Fix it ➔View workflow run | Using GPT Sol𝕏

@munezaclovis
munezaclovis merged commit 3691ce4 into main Aug 1, 2026
8 checks passed
@munezaclovis
munezaclovis deleted the codex/resource-only-projects branch August 1, 2026 04:39
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.

1 participant