Skip to content

Fix installer state#11624

Merged
abnegate merged 15 commits into1.9.xfrom
fix-installer-state
Mar 24, 2026
Merged

Fix installer state#11624
abnegate merged 15 commits into1.9.xfrom
fix-installer-state

Conversation

@abnegate
Copy link
Copy Markdown
Member

What does this PR do?

(Provide a description of what this PR does and why it's needed.)

Test Plan

(Write your test plan here. If you changed any code, please provide us with clear instructions on how you verified your changes work. Screenshots may also be helpful.)

Related PRs and Issues

  • (Related PR or issue)

Checklist

  • Have you read the Contributing Guidelines on issues?
  • If the PR includes a change to an API's metadata (desc, label, params, etc.), does it also include updated API specs and example docs?

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 24, 2026

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
📝 Walkthrough

Walkthrough

Adds a POST /install/reset endpoint with soft and hard reset flows; streams docker-compose output to report per-container startup progress; reduces the global installer lock timeout and introduces throttled stale-lock cleanup; persists installer lock/ID to both sessionStorage and localStorage with backup fallbacks; relaxes SSL/emailCertificates validation and falls back to accountEmail where appropriate; adds per-row install counters and global action buttons in the installer UI; removes postgresql from allowed database adapters in the compose template; bumps the appwrite-console image tag; and changes cookie-domain derivation in the install completion flow.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ❌ 3

❌ Failed checks (2 warnings, 1 inconclusive)

Check name Status Explanation Resolution
Description check ⚠️ Warning The pull request description contains only the repository's contribution template with no filled-in sections explaining the changes, test plan, or related issues. Fill in the description template sections with details about what the PR does, how it was tested, and any related issues or PRs.
Docstring Coverage ⚠️ Warning Docstring coverage is 21.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Fix installer state' is vague and generic, using a non-descriptive term that doesn't convey what specific aspect of installer state is being fixed. Make the title more specific; consider 'Fix installer stale resume redirect and account-setup phase delay' to better reflect the actual changes being made.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-installer-state

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 and usage tips.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 24, 2026

Greptile Summary

This PR fixes several installer state-management issues: it adds a /install/reset endpoint (soft and hard modes) exposed via "Start Over" / "Reset Everything" buttons, backs up install lock/ID state to localStorage so it survives tab/session loss, makes the SSL-certificate email field optional (falling back to the account email), corrects session-cookie domain scoping in Complete.php, and adds real-time container-start progress counters during docker compose up.

Key changes to note:

  • State::isValidDatabaseAdapter still lists 'postgresql' as valid even though compose.phtml removed it from $allowedDbServices; the two should stay in sync.
  • Reset.php references Validate::validateCsrf without an explicit use import; it resolves correctly via same-namespace lookup but the implicit dependency should be made explicit.
  • In progress.js, the soft-reset path silently swallows server-side HTTP errors — the user receives no feedback when the server cannot clean up state.
  • countComposeServices counts container_name: occurrences via regex, which may over-count if the pattern appears in YAML comments, making the X/Y container progress indicator inaccurate.

Confidence Score: 3/5

  • Safe to merge with minor fixes recommended, particularly the isValidDatabaseAdapter / compose inconsistency.
  • The core logic is sound and well-tested. The localStorage fallback, cookie-domain fix, and reset endpoint are all implemented correctly. The main concerns are the postgresql validator/template inconsistency (could silently misdirect users), the missing use import in Reset.php, and the swallowed soft-reset errors. None are blocking, but the database adapter inconsistency is the most likely to surface as a confusing user-facing bug.
  • src/Appwrite/Platform/Installer/Runtime/State.php (validator inconsistency with compose.phtml) and src/Appwrite/Platform/Installer/Http/Installer/Reset.php (missing explicit use import).

Important Files Changed

Filename Overview
src/Appwrite/Platform/Installer/Http/Installer/Reset.php New /install/reset endpoint with soft and hard reset modes; missing explicit use for Validate class though it resolves correctly via same-namespace lookup.
src/Appwrite/Platform/Installer/Runtime/State.php Lock timeout reduced from 3600 to 300 s, throttled stale-lock cleanup added; isValidDatabaseAdapter still allows postgresql which was removed from the compose template, creating a validator/template inconsistency.
src/Appwrite/Platform/Installer/Http/Installer/Install.php Improves re-install handling by detecting prior errors/completed runs and allowing a fresh start; makes emailCertificates optional with accountEmail fallback; removes config file deletion from the error handler (now delegated to Reset).
src/Appwrite/Platform/Installer/Http/Installer/Complete.php Adds buildCookieDomain / extractHostname helpers to set a domain-scoped session cookie matching Appwrite's convention; logic is correct for localhost, IPs, IPv6 bracket notation, and real hostnames.
app/views/install/installer/js/modules/progress.js Adds container start counter display, showGlobalActions on error, and a performReset function; soft-reset path silently swallows server-side HTTP errors without user feedback.
app/views/install/installer/js/modules/state.js Adds localStorage backup for install lock and install ID so state survives tab/session loss; read/write/clear operations are properly mirrored across both storages.
src/Appwrite/Platform/Tasks/Install.php Adds real-time container start progress tracking via proc_open; countComposeServices uses a regex that may over-count if container_name: appears in YAML comments; adds system info to analytics payload.
app/views/install/compose.phtml Removes postgresql from allowed DB services and bumps the console image to 7.8.25; inconsistent with State::isValidDatabaseAdapter which still lists postgresql as valid.
tests/unit/Platform/Modules/Installer/ModuleTest.php Adds test coverage for the new installerReset action, verifying method, path, params, injections, and type.

Comments Outside Diff (4)

  1. app/views/install/installer/js/modules/progress.js, line 175 (link)

    P2 Silent error swallow for soft reset network failures

    catch (e) {} silently discards any network-level errors on the soft-reset path. Combined with the fact that the !res.ok guard only applies when hard === true, a server-side failure (e.g., 400 CSRF mismatch, 500 internal error) on a soft reset is completely invisible to the user — the client clears state and navigates to /?step=1 regardless.

    Consider at least logging the error, or showing a toast for non-hard resets too:

        } catch (e) {
            if (!hard) console.warn('[installer] soft-reset request failed', e);
        }

    For hard resets this is already handled correctly (early return after the toast).

  2. src/Appwrite/Platform/Installer/Http/Installer/Reset.php, line 590-594 (link)

    P2 Missing explicit use import for Validate

    Validate::validateCsrf($request) works at runtime because both Reset and Validate live in the Appwrite\Platform\Installer\Http\Installer namespace, so PHP resolves the unqualified name implicitly. However, the absence of a use declaration makes the dependency invisible to static analysis tools and readers who may not notice the implicit same-namespace resolution.

    All other files that call Validate::validateCsrf (e.g. Complete.php) add an explicit import. Suggest adding:

    use Appwrite\Platform\Installer\Http\Installer\Validate;

    at the top of the file alongside the other imports.

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

  3. src/Appwrite/Platform/Installer/Runtime/State.php, line 204-205 (link)

    P2 isValidDatabaseAdapter still allows postgresql after it was removed from compose

    compose.phtml now restricts $allowedDbServices to ['mariadb', 'mongodb'] (removing 'postgresql'), but State::isValidDatabaseAdapter still returns true for 'postgresql'. If a caller relies on this method to gate the compose rendering (rather than the inline $allowedDbServices guard), a postgresql value could slip through validation and then be silently coerced to mongodb by the compose template — producing a confusing mismatch between what the user selected and what gets deployed.

    public function isValidDatabaseAdapter(string $value): bool
    {
    -    return in_array($value, ['mongodb', 'mariadb', 'postgresql'], true);
    +    return in_array($value, ['mongodb', 'mariadb'], true);
    }
  4. src/Appwrite/Platform/Tasks/Install.php, line 829-831 (link)

    P2 countComposeServices regex may produce incorrect counts

    The regex /^\s*container_name:/m will also match container_name: keys that appear inside YAML block comments (lines beginning with #) or in YAML string values, potentially over-counting the total and making the X/Y progress indicator inaccurate.

    A more defensive approach would be to skip comment lines before matching:

    private function countComposeServices(string $composeFile): int
    {
        $content = @file_get_contents($composeFile);
        if ($content === false) {
            return 0;
        }
        // Strip full-line YAML comments before counting
        $stripped = preg_replace('/^\s*#.*$/m', '', $content) ?? $content;
        $count = preg_match_all('/^\s*container_name:/m', $stripped);
        return $count !== false ? $count : 0;
    }

    This is a cosmetic counter, so it's not critical — but it could show e.g. 15/12 if comments inflate the total.

Reviews (1): Last reviewed commit: "(feat): installer improvements — reset, ..." | Re-trigger Greptile

@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 24, 2026

🔄 PHP-Retry Summary

Flaky tests detected across commits:

Commit 7668487 - 7 flaky tests
Test Retries Total Time Details
UsageTest::testVectorsDBStats 1 10.05s Logs
DocumentsDBConsoleClientTest::testTimeout 1 123.77s Logs
LegacyConsoleClientTest::testListDocumentsWithCache 1 470ms Logs
LegacyConsoleClientTest::testInvalidDocumentStructure 1 240.85s Logs
LegacyCustomClientTest::testAttributeResponseModels 1 242.36s Logs
DatabasesStringTypesTest::testListStringTypeAttributes 1 240.38s Logs
DatabasesStringTypesTest::testGetCollectionWithStringTypeAttributes 1 240.36s Logs
Commit 5ca30d3 - 2 flaky tests
Test Retries Total Time Details
UsageTest::testVectorsDBStats 1 10.01s Logs
ProxyCustomServerTest::testCreateSiteRule 1 120.09s Logs
Commit d0978d8 - 5 flaky tests
Test Retries Total Time Details
UsageTest::testVectorsDBStats 1 10.01s Logs
TablesDBConsoleClientTest::testTimeout 1 121.21s Logs
TablesDBCustomClientTest::testSelectQueries 1 120.05s Logs
TablesDBTransactionsCustomServerTest::testBulkDeleteOperations 1 120.81s Logs
TablesDBTransactionsCustomServerTest::testUpsertAutoIdThenUpdate 1 240.41s Logs
Commit f016d4b - 2 flaky tests
Test Retries Total Time Details
UsageTest::testVectorsDBStats 1 10.10s Logs
DatabaseServerTest::testUpdateURLColumn 1 240.39s Logs
Commit 4de9ec7 - 5 flaky tests
Test Retries Total Time Details
DocumentsDBConsoleClientTest::testTimeout 1 120.07s Logs
DocumentsDBCustomServerTest::testTimeout 1 120.70s Logs
LegacyCustomClientTest::testCreateIndexes 1 240.92s Logs
LegacyTransactionsConsoleClientTest::testDeleteDocumentDuringTransaction 1 240.45s Logs
UsageTest::testVectorsDBStats 1 10.12s Logs

Note: Flaky test results are tracked for the last 5 commits

Copy link
Copy Markdown
Contributor

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

🧹 Nitpick comments (4)
app/views/install/installer/templates/steps/step-5.phtml (1)

55-62: Consider adding confirmation for the "Reset Everything" button.

The hard reset button could have destructive consequences. Consider whether a confirmation dialog should be added before executing the hard reset to prevent accidental data loss.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/views/install/installer/templates/steps/step-5.phtml` around lines 55 -
62, The "Reset Everything" button (data attribute data-install-hard-reset) can
trigger destructive actions without confirmation; modify the click handler that
listens for [data-install-hard-reset] to present a confirmation dialog/modal
before proceeding (e.g., browser confirm or site modal) and only call the
existing hard-reset routine if the user explicitly confirms (optionally require
typing a keyword or clicking a secondary confirm button); ensure the cancel path
aborts without side effects and that any analytics/logging still reflect whether
the action was confirmed.
src/Appwrite/Platform/Tasks/Install.php (1)

752-753: Shell commands for system info are safe but may fail silently on non-Linux systems.

The commands nproc and reading from /proc/meminfo are Linux-specific. On other platforms (macOS during local dev, Windows WSL edge cases), these will return empty/null values, which is handled gracefully with the ?: null fallback and (int) round(...) conversion.

Consider adding a brief inline comment noting this is Linux-specific for maintainability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Appwrite/Platform/Tasks/Install.php` around lines 752 - 753, The code in
Install.php that sets the 'cpus' and 'ram' values using shell_exec('nproc') and
reading /proc/meminfo is Linux-specific and may return empty on non-Linux
systems; add a brief inline comment immediately above the 'cpus' and 'ram'
assignments (the lines that call nproc and grep MemTotal) stating that these
commands are Linux-only and may return null/zero on other platforms (e.g.,
macOS/Windows), so the fallback behavior is intentional for portability and
debugging.
app/views/install/installer/js/modules/state.js (1)

141-149: Consider mirroring localStorage fallback back to sessionStorage for consistency.

getInstallLock() mirrors the localStorage value back to sessionStorage when falling back (line 71), but getStoredInstallId() does not. This creates an inconsistency where the install lock will be synced but the install ID won't be if sessionStorage was cleared.

♻️ Suggested fix for consistency
     const getStoredInstallId = () => {
         try {
             const val = sessionStorage.getItem(INSTALL_ID_KEY);
             if (val) return val;
         } catch (error) {}
         try {
-            return localStorage.getItem(INSTALL_ID_LOCAL_KEY);
+            const val = localStorage.getItem(INSTALL_ID_LOCAL_KEY);
+            if (val) {
+                sessionStorage.setItem(INSTALL_ID_KEY, val);
+                return val;
+            }
         } catch (error) {}
         return null;
     };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/views/install/installer/js/modules/state.js` around lines 141 - 149,
getStoredInstallId currently reads INSTALL_ID_KEY from sessionStorage and falls
back to INSTALL_ID_LOCAL_KEY in localStorage but does not mirror the
localStorage value back into sessionStorage like getInstallLock does; update
getStoredInstallId so that when it successfully retrieves the install id from
localStorage (INSTALL_ID_LOCAL_KEY) it also writes that value into
sessionStorage under INSTALL_ID_KEY, handling possible storage exceptions
(try/catch) the same way as existing reads to maintain consistency with
getInstallLock.
src/Appwrite/Platform/Installer/Http/Installer/Complete.php (1)

111-123: IPv6 edge case handling could be clearer.

When $domain contains more than 2 colons (e.g., a bare IPv6 address like 2001:db8::1), the method returns the entire string lowercased rather than extracting the IP portion. This works for buildCookieDomain() since filter_var(..., FILTER_VALIDATE_IP) will correctly identify it as an IP and return an empty cookie domain, but the method name extractHostname is slightly misleading in this case.

Consider adding a brief inline comment to clarify the intent:

📝 Suggested clarification
         $parts = explode(':', $domain);
-        return count($parts) <= 2 ? strtolower($parts[0]) : strtolower($domain);
+        // More than 2 colons indicates bare IPv6; return as-is for FILTER_VALIDATE_IP
+        return count($parts) <= 2 ? strtolower($parts[0]) : strtolower($domain);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Appwrite/Platform/Installer/Http/Installer/Complete.php` around lines 111
- 123, Update the extractHostname(string $domain) function to clarify IPv6
handling: add a concise inline comment near the colon-splitting logic (the
branch that returns strtolower($domain) when count($parts) > 2) explaining that
bare IPv6 addresses contain multiple colons and we intentionally return the full
lowercased string so downstream checks (e.g., buildCookieDomain() using
filter_var(..., FILTER_VALIDATE_IP)) can detect it as an IP; also add a short
comment by the '[' bracket handling to note it covers scoped IPv6 literal
syntax. Keep comments minimal and focused, referencing extractHostname and
buildCookieDomain for clarity.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/views/install/compose.phtml`:
- Around line 16-19: Restore consistency by re-adding 'postgresql' to the
$allowedDbServices array so $dbService isn't silently converted to 'mongodb'
(update the $allowedDbServices declaration and the $dbService fallback logic in
compose.phtml); alternatively, if PostgreSQL support should be removed, remove
it from the WhiteList validators in Install.php and Tasks/Install.php and from
the default/labels in context.js and ui.js so the UI, API and template remain
consistent—ensure the unique symbols to change are $allowedDbServices and
$dbService in the compose template, the WhiteList validators in Install.php and
Tasks/Install.php, and the defaults/label formatter in context.js and ui.js.

---

Nitpick comments:
In `@app/views/install/installer/js/modules/state.js`:
- Around line 141-149: getStoredInstallId currently reads INSTALL_ID_KEY from
sessionStorage and falls back to INSTALL_ID_LOCAL_KEY in localStorage but does
not mirror the localStorage value back into sessionStorage like getInstallLock
does; update getStoredInstallId so that when it successfully retrieves the
install id from localStorage (INSTALL_ID_LOCAL_KEY) it also writes that value
into sessionStorage under INSTALL_ID_KEY, handling possible storage exceptions
(try/catch) the same way as existing reads to maintain consistency with
getInstallLock.

In `@app/views/install/installer/templates/steps/step-5.phtml`:
- Around line 55-62: The "Reset Everything" button (data attribute
data-install-hard-reset) can trigger destructive actions without confirmation;
modify the click handler that listens for [data-install-hard-reset] to present a
confirmation dialog/modal before proceeding (e.g., browser confirm or site
modal) and only call the existing hard-reset routine if the user explicitly
confirms (optionally require typing a keyword or clicking a secondary confirm
button); ensure the cancel path aborts without side effects and that any
analytics/logging still reflect whether the action was confirmed.

In `@src/Appwrite/Platform/Installer/Http/Installer/Complete.php`:
- Around line 111-123: Update the extractHostname(string $domain) function to
clarify IPv6 handling: add a concise inline comment near the colon-splitting
logic (the branch that returns strtolower($domain) when count($parts) > 2)
explaining that bare IPv6 addresses contain multiple colons and we intentionally
return the full lowercased string so downstream checks (e.g.,
buildCookieDomain() using filter_var(..., FILTER_VALIDATE_IP)) can detect it as
an IP; also add a short comment by the '[' bracket handling to note it covers
scoped IPv6 literal syntax. Keep comments minimal and focused, referencing
extractHostname and buildCookieDomain for clarity.

In `@src/Appwrite/Platform/Tasks/Install.php`:
- Around line 752-753: The code in Install.php that sets the 'cpus' and 'ram'
values using shell_exec('nproc') and reading /proc/meminfo is Linux-specific and
may return empty on non-Linux systems; add a brief inline comment immediately
above the 'cpus' and 'ram' assignments (the lines that call nproc and grep
MemTotal) stating that these commands are Linux-only and may return null/zero on
other platforms (e.g., macOS/Windows), so the fallback behavior is intentional
for portability and debugging.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 980f3387-45d7-4389-9f4d-3773051ec2bb

📥 Commits

Reviewing files that changed from the base of the PR and between d4f7d51 and 7668487.

📒 Files selected for processing (16)
  • app/views/install/compose.phtml
  • app/views/install/installer/css/styles.css
  • app/views/install/installer/js/modules/progress.js
  • app/views/install/installer/js/modules/state.js
  • app/views/install/installer/js/modules/ui.js
  • app/views/install/installer/js/steps.js
  • app/views/install/installer/templates/steps/step-5.phtml
  • docker-compose.yml
  • src/Appwrite/Platform/Installer/Http/Installer/Complete.php
  • src/Appwrite/Platform/Installer/Http/Installer/Install.php
  • src/Appwrite/Platform/Installer/Http/Installer/Reset.php
  • src/Appwrite/Platform/Installer/Http/Installer/Status.php
  • src/Appwrite/Platform/Installer/Runtime/State.php
  • src/Appwrite/Platform/Installer/Services/Http.php
  • src/Appwrite/Platform/Tasks/Install.php
  • tests/unit/Platform/Modules/Installer/ModuleTest.php

@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 24, 2026

✨ Benchmark results

  • Requests per second: 1,632
  • Requests with 200 status code: 293,899
  • P99 latency: 0.105372497

⚡ Benchmark Comparison

Metric This PR Latest version
RPS 1,632 1,164
200 293,899 209,632
P99 0.105372497 0.191268274

Copy link
Copy Markdown
Contributor

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

Caution

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

⚠️ Outside diff range comments (2)
src/Appwrite/Platform/Tasks/Install.php (2)

661-676: ⚠️ Potential issue | 🟠 Major

Do not substitute the session ID into userId.

Lines 674-676 intentionally leave $userId unset on the tolerated rerun/restricted path. Line 691 then falls back to $session['id'], which is a session resource, not an account resource, so the completion payload now carries an invalid user ID in exactly the scenario this change adds.

Possible fix
-                    'userId' => $userId ?? $session['id'],
+                    'userId' => $userId,

If the client still needs the account ID here, resolve it explicitly after the session is created instead of reusing the session ID.

Also applies to: 681-695

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Appwrite/Platform/Tasks/Install.php` around lines 661 - 676, The current
change incorrectly allows a session ID to be reused as an account ID (the
$userId variable) when makeApiCall('/v1/account', ...) fails with tolerated
errors; instead of falling back to $session['id'], leave $userId null and, after
creating the session (where $session is produced), explicitly resolve the real
account ID via the appropriate API call (e.g., lookup account by session or call
'/v1/account' GET) and assign that result to $userId; do not substitute
$session['id'] for $userId and update code paths using $userId (including the
completion payload) to use the resolved account ID.

739-757: ⚠️ Potential issue | 🟠 Major

Move host-metadata probes inside error handling boundary.

Lines 756–757 call shell_exec() before the try/catch block (line 761). On PHP 8.3+ with shell_exec in disable_functions, this raises a fatal error that prevents payload construction, breaking the installation even though this method is explicitly designed to be non-blocking (per line 767). Move the shell_exec() calls inside the try block or guard them with function_exists('shell_exec') and null fallbacks to preserve the non-blocking contract.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Appwrite/Platform/Tasks/Install.php` around lines 739 - 757, The payload
construction currently calls shell_exec() for 'cpus' and 'ram' (inside
Install::... around the $payload/data array) before the try/catch boundary,
which can fatal on PHP 8.3+ when shell_exec is disabled; move those probes into
the try block (the existing try/catch that sends the payload) or wrap each
shell_exec call with a guard like function_exists('shell_exec') and provide
null-safe fallbacks so failures do not abort payload creation—update references
to the 'cpus' and 'ram' fields in the $payload/data array (and any related
hostIp resolution using gethostbyname) to be computed safely inside the
error-handling boundary.
🧹 Nitpick comments (2)
src/Appwrite/Platform/Tasks/Install.php (2)

982-983: Document the new callback and return contracts.

The added ?callable $progress parameter and the shaped array returned from execWithContainerProgress() are not self-describing. Add PHPDoc for the callback signature and return keys so callers do not have to infer the contract from implementation details. As per coding guidelines, "Include comprehensive PHPDoc comments".

Also applies to: 1053-1094

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Appwrite/Platform/Tasks/Install.php` around lines 982 - 983, Add
comprehensive PHPDoc to runDockerCompose to document the new ?callable $progress
parameter and the shaped array returned by execWithContainerProgress(): specify
the callable signature (e.g., function(string $container, string $status, int
$completed, int $total): void or whatever fields you expect) and describe exact
return array keys and value types produced by execWithContainerProgress() (e.g.,
['container' => string, 'status' => string, 'progress' => int, ...]). Update the
PHPDoc block above the runDockerCompose method and also add/adjust PHPDoc on
execWithContainerProgress() (and any related helper methods around lines
~1053-1094) so callers can rely on the parameter and return contracts without
reading implementation details.

1043-1050: countComposeServices() is counting container_name, not services.

This total stays correct only while every service emits exactly one container_name: line. If a service ever omits that field, the progress counters will drift or stay at 0/N. Counting the actual service entries would make this helper match its name and keep the installer counters stable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Appwrite/Platform/Tasks/Install.php` around lines 1043 - 1050,
countComposeServices() currently counts "container_name:" occurrences, which
breaks when services omit that field; change it to parse the compose YAML and
return the number of top-level service entries instead. In the
countComposeServices(string $composeFile) function, load and parse the compose
file (e.g., using yaml_parse_file or Symfony\Component\Yaml\Yaml::parseFile) and
if parsing succeeds, return count($parsed['services']) (with checks that
'services' exists and is an array); on any failure keep returning 0. This
ensures the function counts actual service keys rather than container_name
lines.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/Appwrite/Platform/Tasks/Install.php`:
- Around line 661-676: The current change incorrectly allows a session ID to be
reused as an account ID (the $userId variable) when makeApiCall('/v1/account',
...) fails with tolerated errors; instead of falling back to $session['id'],
leave $userId null and, after creating the session (where $session is produced),
explicitly resolve the real account ID via the appropriate API call (e.g.,
lookup account by session or call '/v1/account' GET) and assign that result to
$userId; do not substitute $session['id'] for $userId and update code paths
using $userId (including the completion payload) to use the resolved account ID.
- Around line 739-757: The payload construction currently calls shell_exec() for
'cpus' and 'ram' (inside Install::... around the $payload/data array) before the
try/catch boundary, which can fatal on PHP 8.3+ when shell_exec is disabled;
move those probes into the try block (the existing try/catch that sends the
payload) or wrap each shell_exec call with a guard like
function_exists('shell_exec') and provide null-safe fallbacks so failures do not
abort payload creation—update references to the 'cpus' and 'ram' fields in the
$payload/data array (and any related hostIp resolution using gethostbyname) to
be computed safely inside the error-handling boundary.

---

Nitpick comments:
In `@src/Appwrite/Platform/Tasks/Install.php`:
- Around line 982-983: Add comprehensive PHPDoc to runDockerCompose to document
the new ?callable $progress parameter and the shaped array returned by
execWithContainerProgress(): specify the callable signature (e.g.,
function(string $container, string $status, int $completed, int $total): void or
whatever fields you expect) and describe exact return array keys and value types
produced by execWithContainerProgress() (e.g., ['container' => string, 'status'
=> string, 'progress' => int, ...]). Update the PHPDoc block above the
runDockerCompose method and also add/adjust PHPDoc on
execWithContainerProgress() (and any related helper methods around lines
~1053-1094) so callers can rely on the parameter and return contracts without
reading implementation details.
- Around line 1043-1050: countComposeServices() currently counts
"container_name:" occurrences, which breaks when services omit that field;
change it to parse the compose YAML and return the number of top-level service
entries instead. In the countComposeServices(string $composeFile) function, load
and parse the compose file (e.g., using yaml_parse_file or
Symfony\Component\Yaml\Yaml::parseFile) and if parsing succeeds, return
count($parsed['services']) (with checks that 'services' exists and is an array);
on any failure keep returning 0. This ensures the function counts actual service
keys rather than container_name lines.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7f794092-0def-4efa-a484-e3f681798350

📥 Commits

Reviewing files that changed from the base of the PR and between 7668487 and 5ca30d3.

📒 Files selected for processing (1)
  • src/Appwrite/Platform/Tasks/Install.php

@blacksmith-sh

This comment has been minimized.

Copy link
Copy Markdown
Contributor

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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/views/install/installer/js/modules/progress.js`:
- Around line 996-1021: The catch block in performReset currently swallows
errors; update it to capture the exception (e) and log it for diagnostics and
user feedback: inside the catch of performReset, call console.error with a clear
message including installId and hard plus the error object, and also invoke
showToast? (if available) with status: 'error' and a short description from
e.message (fallback to a generic message) so failures during soft resets are
reported; keep the subsequent cleanup/redirect behavior intact.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: bcd89d22-5624-44cd-9adf-b1ee3ae22f04

📥 Commits

Reviewing files that changed from the base of the PR and between 5ca30d3 and d0978d8.

📒 Files selected for processing (6)
  • app/views/install/compose.phtml
  • app/views/install/installer/css/styles.css
  • app/views/install/installer/js/modules/progress.js
  • app/views/install/installer/templates/steps/step-5.phtml
  • docker-compose.yml
  • src/Appwrite/Platform/Tasks/Install.php
✅ Files skipped from review due to trivial changes (1)
  • app/views/install/installer/css/styles.css
🚧 Files skipped from review as they are similar to previous changes (4)
  • docker-compose.yml
  • app/views/install/compose.phtml
  • app/views/install/installer/templates/steps/step-5.phtml
  • src/Appwrite/Platform/Tasks/Install.php

Copy link
Copy Markdown
Contributor

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

Caution

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

⚠️ Outside diff range comments (1)
src/Appwrite/Platform/Tasks/Install.php (1)

206-210: ⚠️ Potential issue | 🟠 Major

Don't reject the database you just auto-detected.

Lines 173-203 can now coerce $database to the adapter from the existing install, but this guard still validates it against the default ['mongodb', 'mariadb']. Existing PostgreSQL installs will still exit here, so the upgrade path stays blocked.

💡 Suggested fix
         $installerConfig = $this->readInstallerConfig();
         $enabledDatabases = $installerConfig['enabledDatabases'] ?? ['mongodb', 'mariadb'];
+        if (isset($existingDatabase) && $existingDatabase !== null) {
+            $enabledDatabases[] = $existingDatabase;
+            $enabledDatabases = array_values(array_unique($enabledDatabases));
+        }
         if (!in_array($database, $enabledDatabases, true)) {
             Console::error("Database '{$database}' is not available. Available options: " . implode(', ', $enabledDatabases));
             Console::exit(1);
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Appwrite/Platform/Tasks/Install.php` around lines 206 - 210, The guard
that rejects $database uses a hard-coded default list instead of honoring the
actual installer configuration: read the current installer config via
readInstallerConfig(), derive $enabledDatabases from $installerConfig (not the
literal ['mongodb','mariadb']) and then validate $database against that list so
auto-detected adapters (e.g. 'postgresql') are allowed; update the check in
Install.php that references $enabledDatabases and ensure the variable comes from
$installerConfig['enabledDatabases'] ?? [] (or similar) before calling in_array
on $database, then keep the existing Console::error/Console::exit behavior if
validation truly fails.
♻️ Duplicate comments (1)
app/views/install/installer/js/modules/progress.js (1)

1010-1034: ⚠️ Potential issue | 🟠 Major

Don't clear client state after a failed reset.

src/Appwrite/Platform/Installer/Http/Installer/Reset.php Lines 36-65 can still return a non-2xx on CSRF failure, and thrown fetch errors also land in the empty catch here. Both cases fall through to clearInstallLock()/clearInstallId()/cleanupInstallFlow()/redirect, so the browser behaves as if the reset succeeded while the server-side lock/progress can still exist.

💡 Suggested fix
         try {
             const res = await fetch('/install/reset', {
                 method: 'POST',
                 headers: withCsrfHeader({ 'Content-Type': 'application/json' }),
                 body: JSON.stringify({ installId: installId || '', hard })
             });
-            if (hard && !res.ok) {
+            if (!res.ok) {
                 const data = await res.json().catch(() => ({}));
                 showToast?.({
                     status: 'error',
                     title: 'Reset failed',
-                    description: data?.message || 'Could not stop containers. Try running "docker compose down -v" manually.',
+                    description: data?.message || (hard
+                        ? 'Could not stop containers. Try running "docker compose down -v" manually.'
+                        : 'Could not reset installer state. Refresh the page and try again.'),
                     dismissible: true
                 });
                 return;
             }
-        } catch (e) {}
+        } catch (error) {
+            console.error('Reset request failed:', { installId, hard, error });
+            showToast?.({
+                status: 'error',
+                title: 'Reset failed',
+                description: error?.message || 'Could not reach the installer server.',
+                dismissible: true
+            });
+            return;
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/views/install/installer/js/modules/progress.js` around lines 1010 - 1034,
performReset currently clears client state and redirects even when the server
returned a non-2xx or the fetch threw, causing the UI to assume success; update
performReset to only call clearInstallLock(), clearInstallId(),
cleanupInstallFlow(), and set window.location.href after a confirmed successful
response (res.ok). Concretely: in the try block check if (!res.ok) for all cases
(not only when hard) — parse error JSON (or fallback {}) and call showToast with
the error message and return; in the catch block show an error toast describing
the fetch/CSRF failure and return; only when the request succeeded (res.ok)
proceed to run clearInstallLock, clearInstallId, cleanupInstallFlow, and
redirect. Ensure you reference the performReset function and the
clearInstallLock/clearInstallId/cleanupInstallFlow/window.location.href calls
when making the change.
🧹 Nitpick comments (1)
src/Appwrite/Platform/Tasks/Install.php (1)

1069-1120: Add PHPDoc for the new compose-progress helpers.

countComposeServices() and execWithContainerProgress() now hide non-obvious parsing and return-value contracts, but the helpers are undocumented. A short docblock here would make this path much easier to maintain.

As per coding guidelines, "Include comprehensive PHPDoc comments".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Appwrite/Platform/Tasks/Install.php` around lines 1069 - 1120, Add
comprehensive PHPDoc blocks to both countComposeServices() and
execWithContainerProgress(): for countComposeServices(string $composeFile): int,
document that it reads the compose file path, returns 0 on unreadable file or on
preg_match_all failure, and that it counts lines matching the
/^\s*container_name:/m pattern; for execWithContainerProgress(string
$commandLine, int $totalServices, callable $progress, bool $isUpgrade): array,
document the meaning and types of parameters (commandLine run via proc_open,
totalServices used to cap progress, progress callable signature and expected
constants InstallerServer::STEP_DOCKER_CONTAINERS and STATUS_IN_PROGRESS),
describe returned array keys ('output' => array of stdout lines, 'exit' =>
process exit code), note that proc_open is used with stderr redirected, that
progress exceptions are swallowed, and reference the timeout behavior via
procCloseWithTimeout and self::PROC_CLOSE_TIMEOUT_SECONDS.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/Appwrite/Platform/Tasks/Install.php`:
- Around line 621-630: The currentStep variable is set after calling
waitForApiReady, so if waitForApiReady reports progress on STEP_ACCOUNT_SETUP or
times out the error is attributed to STEP_DOCKER_CONTAINERS; move the assignment
of $currentStep = InstallerServer::STEP_ACCOUNT_SETUP to before calling
waitForApiReady when !$isUpgrade so the progress/exception context matches;
update the block around waitForApiReady and createInitialAdminAccount
(references: waitForApiReady, $currentStep, InstallerServer::STEP_ACCOUNT_SETUP,
createInitialAdminAccount) so currentStep is set for fresh installs prior to the
readiness wait.
- Around line 1086-1118: The current fgets($pipes[1]) loop blocks indefinitely
if the child process stalls, preventing procCloseWithTimeout() from enforcing
the timeout; modify the read loop to set the stdout pipe non-blocking (call
stream_set_blocking($pipes[1], false)) and replace the blocking fgets loop with
a poll that uses stream_select() (or a loop that periodically calls
proc_get_status($process)) to check for available data and process liveness so
you can read available lines, update $output and call $progress
(InstallerServer::STEP_DOCKER_CONTAINERS) while still allowing
procCloseWithTimeout($process, self::PROC_CLOSE_TIMEOUT_SECONDS) to apply;
ensure you still fclose($pipes[1]) and preserve the existing logic that trims
lines and increments $started when checking for 'Container' +
'Started'/'Running'.

---

Outside diff comments:
In `@src/Appwrite/Platform/Tasks/Install.php`:
- Around line 206-210: The guard that rejects $database uses a hard-coded
default list instead of honoring the actual installer configuration: read the
current installer config via readInstallerConfig(), derive $enabledDatabases
from $installerConfig (not the literal ['mongodb','mariadb']) and then validate
$database against that list so auto-detected adapters (e.g. 'postgresql') are
allowed; update the check in Install.php that references $enabledDatabases and
ensure the variable comes from $installerConfig['enabledDatabases'] ?? [] (or
similar) before calling in_array on $database, then keep the existing
Console::error/Console::exit behavior if validation truly fails.

---

Duplicate comments:
In `@app/views/install/installer/js/modules/progress.js`:
- Around line 1010-1034: performReset currently clears client state and
redirects even when the server returned a non-2xx or the fetch threw, causing
the UI to assume success; update performReset to only call clearInstallLock(),
clearInstallId(), cleanupInstallFlow(), and set window.location.href after a
confirmed successful response (res.ok). Concretely: in the try block check if
(!res.ok) for all cases (not only when hard) — parse error JSON (or fallback {})
and call showToast with the error message and return; in the catch block show an
error toast describing the fetch/CSRF failure and return; only when the request
succeeded (res.ok) proceed to run clearInstallLock, clearInstallId,
cleanupInstallFlow, and redirect. Ensure you reference the performReset function
and the clearInstallLock/clearInstallId/cleanupInstallFlow/window.location.href
calls when making the change.

---

Nitpick comments:
In `@src/Appwrite/Platform/Tasks/Install.php`:
- Around line 1069-1120: Add comprehensive PHPDoc blocks to both
countComposeServices() and execWithContainerProgress(): for
countComposeServices(string $composeFile): int, document that it reads the
compose file path, returns 0 on unreadable file or on preg_match_all failure,
and that it counts lines matching the /^\s*container_name:/m pattern; for
execWithContainerProgress(string $commandLine, int $totalServices, callable
$progress, bool $isUpgrade): array, document the meaning and types of parameters
(commandLine run via proc_open, totalServices used to cap progress, progress
callable signature and expected constants
InstallerServer::STEP_DOCKER_CONTAINERS and STATUS_IN_PROGRESS), describe
returned array keys ('output' => array of stdout lines, 'exit' => process exit
code), note that proc_open is used with stderr redirected, that progress
exceptions are swallowed, and reference the timeout behavior via
procCloseWithTimeout and self::PROC_CLOSE_TIMEOUT_SECONDS.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: dec9bbec-736e-49fd-8836-746f804eb8aa

📥 Commits

Reviewing files that changed from the base of the PR and between d0978d8 and f016d4b.

📒 Files selected for processing (3)
  • app/views/install/installer/css/styles.css
  • app/views/install/installer/js/modules/progress.js
  • src/Appwrite/Platform/Tasks/Install.php
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/views/install/installer/css/styles.css

@blacksmith-sh

This comment has been minimized.

abnegate and others added 4 commits March 25, 2026 00:56
- Restore postgresql in compose.phtml allowedDbServices for consistency
  with WhiteList validators, JS defaults, and compose template sections
- Log errors in performReset catch block instead of swallowing silently
- Move $currentStep assignment before waitForApiReady so timeout errors
  are attributed to the correct step
- Replace blocking fgets loop in execWithContainerProgress with
  non-blocking stream_select polling to prevent unbounded hangs

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@abnegate abnegate merged commit a92ad3a into 1.9.x Mar 24, 2026
76 of 78 checks passed
@abnegate abnegate deleted the fix-installer-state branch March 24, 2026 12:47
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