Skip to content

Conversation

vermakhushboo
Copy link
Member

What does this PR do?

Update previewUrl even when builds worker hasn't picked up the job.

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?

@vermakhushboo vermakhushboo requested a review from Meldiron August 27, 2025 14:39
Copy link
Contributor

coderabbitai bot commented Aug 27, 2025

📝 Walkthrough

Walkthrough

Adds distributed locking in app/controllers/api/vcs.php for GitHub VCS comment updates by creating vcsCommentLocks documents keyed by latestCommentId with up to 9 acquisition attempts and 1s backoff; locks are released in finally blocks. Applies this lock pattern to both single- and multi-comment update flows. Introduces previewRuleId when creating site deployment rules and, when latestCommentId and previewRuleId exist, acquires the lock, fetches the preview rule, constructs a preview URL (respecting _APP_OPTIONS_FORCE_HTTPS), appends it to parsed comment builds, and updates the GitHub comment. Adds use Utopia\CLI\Console and logs a warning if lock creation fails. No public interfaces changed.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20–30 minutes

Suggested reviewers

  • Meldiron
  • abnegate
  • christyjacob4

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 PHPStan (2.1.17)

Note: Using configuration file /phpstan.neon.
Invalid entry in excludePaths:
Path "/app/sdks" is neither a directory, nor a file path, nor a fnmatch pattern.

If the excluded path can sometimes exist, append (?)
to its config entry to mark it as optional. Example:

parameters:
excludePaths:
analyseAndScan:
- app/sdks (?)

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat-update-preview-url-in-vcs-controller

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

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

Support

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

CodeRabbit Commands (Invoked using PR/Issue comments)

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

Other keywords and placeholders

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

Status, Documentation and Community

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

Copy link

github-actions bot commented Aug 27, 2025

Security Scan Results for PR

Docker Image Scan Results

Package Version Vulnerability Severity
golang.org/x/crypto v0.31.0 CVE-2025-22869 HIGH
golang.org/x/oauth2 v0.24.0 CVE-2025-22868 HIGH
stdlib 1.22.10 CVE-2025-47907 HIGH

Source Code Scan Results

🎉 No vulnerabilities found!

Copy link
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

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

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

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between f75d2d6 and 5eb52d3.

📒 Files selected for processing (1)
  • app/controllers/api/vcs.php (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
app/controllers/api/vcs.php (2)
src/Appwrite/Event/Database.php (2)
  • getCollection (137-140)
  • getDocument (159-162)
src/Appwrite/Vcs/Comment.php (4)
  • Comment (10-226)
  • parseComment (214-225)
  • addBuild (31-48)
  • generateComment (50-199)
🔇 Additional comments (1)
app/controllers/api/vcs.php (1)

274-274: Confirm intended preview rule source (deployment vs branch/commit).

Right now previewRuleId points to the deployment preview rule. Is that the URL we want to surface in PR comments over the branch/commit preview rules when those exist? If branch/commit previews are preferred, we should derive previewRuleId accordingly.

Copy link

github-actions bot commented Aug 27, 2025

✨ Benchmark results

  • Requests per second: 1,234
  • Requests with 200 status code: 222,096
  • P99 latency: 0.157228257

⚡ Benchmark Comparison

Metric This PR Latest version
RPS 1,234 959
200 222,096 172,639
P99 0.157228257 0.206894792

Copy link
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 (2)
src/Appwrite/Platform/Modules/Functions/Workers/Builds.php (2)

1560-1580: Preview URL derivation: add guard for empty _APP_DOMAIN_SITES, simplify rule check, keep logic minimal

  • Guard missing _APP_DOMAIN_SITES to avoid computing invalid domains.
  • empty($branchRule) is redundant for Documents; isEmpty() suffices.

Apply this diff:

-                            $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', '');
+                            $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', '');
+                            if ($sitesDomain === '') {
+                                return '';
+                            }
@@
-                                $branchRule = Authorization::skip(fn () => $dbForPlatform->getDocument('rules', $ruleId));
-
-                                if (!empty($branchRule) && !$branchRule->isEmpty()) {
+                                $branchRule = Authorization::skip(fn () => $dbForPlatform->getDocument('rules', $ruleId));
+                                if (!$branchRule->isEmpty()) {
                                     return "{$protocol}://" . $branchRule->getAttribute('domain', '');
                                 }

1564-1571: Normalize branch to a DNS-safe label and de-duplicate algorithm across code paths

Branch names can contain chars not valid in host labels (e.g., /, _, uppercase). Recommend:

  • Centralize the “branch prefix + 7-char hash” algorithm into a helper to prevent drift with the rule-creation code in this file and in app/controllers/api/vcs.php.
  • Normalize to lowercase and replace disallowed chars with -, collapsing repeats.

Example helper to add to this class (and use for both rule creation and lookup):

private function deriveBranchPrefix(string $branch): string
{
    $prefix = substr($branch, 0, 16);
    if (strlen($branch) > 16) {
        $remaining = substr($branch, 16);
        $prefix .= '-' . substr(hash('sha256', $remaining), 0, 7);
    }
    // Normalize to DNS-safe label
    $label = strtolower(preg_replace('/[^a-z0-9-]/', '-', $prefix));
    $label = trim(preg_replace('/-+/', '-', $label), '-');
    return $label;
}

Then here:

-                                $branchPrefix = substr($providerBranch, 0, 16);
-                                if (strlen($providerBranch) > 16) {
-                                    $remainingChars = substr($providerBranch, 16);
-                                    $branchPrefix .= '-' . substr(hash('sha256', $remainingChars), 0, 7);
-                                }
+                                $branchPrefix = $this->deriveBranchPrefix($providerBranch);

Note: apply the same helper where the preview rule is created to avoid mismatched IDs/domains.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

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

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between a737328 and f3204d2.

📒 Files selected for processing (1)
  • src/Appwrite/Platform/Modules/Functions/Workers/Builds.php (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/Appwrite/Platform/Modules/Functions/Workers/Builds.php (1)
src/Appwrite/Event/Database.php (1)
  • getDocument (159-162)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (19)
  • GitHub Check: E2E Service Test (Avatars)
  • GitHub Check: E2E Service Test (Tokens)
  • GitHub Check: E2E Service Test (FunctionsSchedule)
  • GitHub Check: E2E Service Test (Account)
  • GitHub Check: E2E Service Test (VCS)
  • GitHub Check: E2E Service Test (Storage)
  • GitHub Check: E2E Service Test (Migrations)
  • GitHub Check: E2E Service Test (Health)
  • GitHub Check: E2E Service Test (GraphQL)
  • GitHub Check: E2E Service Test (Sites)
  • GitHub Check: E2E Service Test (Databases/Legacy)
  • GitHub Check: E2E Service Test (Functions)
  • GitHub Check: E2E Service Test (Realtime)
  • GitHub Check: E2E Service Test (Site Screenshots)
  • GitHub Check: Unit Test
  • GitHub Check: E2E Service Test (Dev Keys)
  • GitHub Check: E2E General Test
  • GitHub Check: Benchmark
  • GitHub Check: scan
🔇 Additional comments (1)
src/Appwrite/Platform/Modules/Functions/Workers/Builds.php (1)

1558-1583: LGTM on adding previewUrl into the GitHub comment flow

Using the comment lock + updating comment with addBuild(..., $previewUrl) is a solid integration. With the small nits above, this is good to ship.

Comment on lines 1560 to 1580
'sites' => (function () use ($deployment, $project, $dbForPlatform, $protocol, $resource) {
$providerBranch = $deployment->getAttribute('providerBranch', '');
if (!empty($providerBranch)) {
$sitesDomain = System::getEnv('_APP_DOMAIN_SITES', '');
$branchPrefix = substr($providerBranch, 0, 16);
if (strlen($providerBranch) > 16) {
$remainingChars = substr($providerBranch, 16);
$branchPrefix .= '-' . substr(hash('sha256', $remainingChars), 0, 7);
}
$resourceProjectHash = substr(hash('sha256', $resource->getId() . $project->getId()), 0, 7);
$domain = "branch-{$branchPrefix}-{$resourceProjectHash}.{$sitesDomain}";
$ruleId = md5($domain);

$branchRule = Authorization::skip(fn () => $dbForPlatform->getDocument('rules', $ruleId));

if (!empty($branchRule) && !$branchRule->isEmpty()) {
return "{$protocol}://" . $branchRule->getAttribute('domain', '');
}
}
return '';
})(),
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Verify consistency of preview domain algorithm across codebase

Ensure the exact same derivation is used everywhere (rule creation, lookup here, and in VCS controller), or preview links may intermittently fail.

Run from repo root:

Expected:

  • All derivations use the same normalization and hashing.
  • No occurrences of ->foreach(; only ->forEach(.

🏁 Script executed:

#!/bin/bash
# Find all occurrences of branch-based preview domain derivation and ruleId usage
rg -nP -C3 '(branch-|_APP_DOMAIN_SITES|deploymentVcsProviderBranch|md5\(\$domain\)|deriveBranchPrefix\()' --type=php

# Sanity-check any accidental use of `$dbForPlatform->foreach` (case typo) vs `forEach`
rg -nP '\->foreach\(' --type=php -C2

Length of output: 61073


Replace all incorrect ->foreach( calls with ->forEach(
Search the codebase for ->foreach( and update each to the correct camel-case method.

🤖 Prompt for AI Agents
In src/Appwrite/Platform/Modules/Functions/Workers/Builds.php around lines 1560
to 1580, the review requests replacing incorrect method calls named "->foreach("
with the correct camel-case "->forEach("; search the repository for any
occurrences of "->foreach(" and update them to "->forEach(" (preserving the same
arguments and whitespace), run unit/static checks to ensure no other
casing-sensitive calls remain, and commit the changes.

Copy link
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

♻️ Duplicate comments (1)
app/controllers/api/vcs.php (1)

366-382: Fix rule existence check to avoid posting an invalid "https://" preview URL.

Document is truthy even when empty. Guard with isEmpty() and the domain value before composing previewUrl; also tweak the catch comment.

-            if ($resource->getCollection() === 'sites' && !empty($latestCommentId) && !empty($previewRuleId)) {
+            if ($resource->getCollection() === 'sites' && !empty($latestCommentId) && !empty($previewRuleId)) {
                 try {
-                    $rule = Authorization::skip(fn () => $dbForPlatform->getDocument('rules', $previewRuleId));
-
-                    $protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
-                    $previewUrl = !empty($rule) ? ("{$protocol}://" . $rule->getAttribute('domain', '')) : '';
+                    $rule = Authorization::skip(fn () => $dbForPlatform->getDocument('rules', $previewRuleId));
+                    $protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
+                    $domain = !$rule->isEmpty() ? $rule->getAttribute('domain', '') : '';
+                    $previewUrl = $domain !== '' ? "{$protocol}://{$domain}" : '';
 
-                    if (!empty($previewUrl)) {
+                    if ($previewUrl !== '') {
                         $comment = new Comment();
                         $comment->parseComment($github->getComment($owner, $repositoryName, $latestCommentId));
                         $comment->addBuild($project, $resource, $resourceType, $commentStatus, $deploymentId, $action, $previewUrl);
                         $github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment());
                     }
                 } catch (\Throwable $th) {
-                    // Ignore, rule already exists; will be updated by builds worker
+                    // Ignore; preview URL will be updated by the builds worker
                 }
             }
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

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

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between f3204d2 and 2f49722.

📒 Files selected for processing (1)
  • app/controllers/api/vcs.php (2 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: E2E Service Test (Site Screenshots)
  • GitHub Check: Unit Test
  • GitHub Check: Benchmark
  • GitHub Check: scan
🔇 Additional comments (1)
app/controllers/api/vcs.php (1)

373-378: Comment::addBuild is already idempotent—entries are keyed by projectId_resourceId and use direct assignment, so repeated calls overwrite instead of appending duplicates.

Comment on lines +274 to 275
$previewRuleId = $ruleId;
Authorization::skip(
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Prefer branch preview domain for previewRuleId to keep URL stable across deployments.

Initialize previewRuleId, then set it to the branch rule when available; fall back to commit rule, then to the ephemeral deployment rule.

-                $previewRuleId = $ruleId;
+                $previewRuleId = ''; // will prefer branch rule below, then commit, else fallback to this deployment rule

Add the following in the Branch Preview block (after computing $ruleId = md5($domain); and also inside the Duplicate catch):

// after: $ruleId = md5($domain);
$previewRuleId = $ruleId;

Add the following in the Commit Preview block (after computing $ruleId = md5($domain); and also inside its Duplicate catch):

// Prefer branch; only use commit if branch wasn't set
if (empty($previewRuleId)) {
    $previewRuleId = $ruleId;
}

🏁 Script executed:

#!/bin/bash
rg -n "previewRuleId" -C3 app/controllers/api/vcs.php
rg -n "md5" -C3 app/controllers/api/vcs.php

Length of output: 2337


🏁 Script executed:

#!/bin/bash
rg -nP --type=php 'branchPrefix' -C3 app/controllers/api/vcs.php
rg -nP --type=php 'VCS commit preview' -C3 app/controllers/api/vcs.php
rg -nP --type=php 'catch' -C5 app/controllers/api/vcs.php

Length of output: 10635


Prefer branch or commit preview rule before falling back to the ephemeral deployment rule

Refactor the $previewRuleId assignment so that branch previews override commit previews, which in turn override the initial deployment preview:

At the top of the Deployment preview block (around line 274), capture the fallback and reset for later:

-                $previewRuleId = $ruleId;
+                $deploymentRuleId = $ruleId;
+                $previewRuleId = ''; // will be set below: branch > commit > deployment fallback

In the Branch Preview section (after \$ruleId = md5($domain); and inside its catch (Duplicate $err)):

$previewRuleId = $ruleId;

In the Commit Preview section (after \$ruleId = md5($domain); and inside its catch (Duplicate $err)):

if (empty($previewRuleId)) {
    $previewRuleId = $ruleId;
}

Immediately before constructing the preview URL (just before the if ($resource->getCollection() === 'sites'…) at line 366):

if (empty($previewRuleId)) {
    $previewRuleId = $deploymentRuleId;
}

This guarantees the preview URL always uses the branch rule if present, otherwise the commit rule, and finally the deployment rule.

🤖 Prompt for AI Agents
In app/controllers/api/vcs.php around lines 274-275, the preview rule selection
currently defaults to the deployment rule too early; refactor so branch preview
overrides commit preview which overrides deployment: at the top of the
Deployment preview block save the deployment rule id into a variable for
fallback, in the Branch Preview catch (after $ruleId = md5($domain);) assign
$previewRuleId = $ruleId to ensure branch wins, in the Commit Preview catch
(after $ruleId = md5($domain);) set $previewRuleId = $ruleId only if
$previewRuleId is empty so commits only set when no branch exists, and
immediately before building the preview URL set $previewRuleId =
$deploymentRuleId if it is still empty so deployment is the final fallback.

Copy link
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

♻️ Duplicate comments (1)
app/controllers/api/vcs.php (1)

415-451: Guard preview URL construction: avoid posting "https://" without a domain

!empty($rule) is always true for Document; check isEmpty() and ensure domain exists before composing.

Apply:

-                try {
-                    $rule = Authorization::skip(fn () => $dbForPlatform->getDocument('rules', $previewRuleId));
-
-                    $protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
-                    $previewUrl = !empty($rule) ? ("{$protocol}://" . $rule->getAttribute('domain', '')) : '';
+                try {
+                    $rule = Authorization::skip(fn () => $dbForPlatform->getDocument('rules', $previewRuleId));
+
+                    $protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
+                    $domain = $rule->isEmpty() ? '' : $rule->getAttribute('domain', '');
+                    $previewUrl = $domain !== '' ? "{$protocol}://{$domain}" : '';
🧹 Nitpick comments (2)
app/controllers/api/vcs.php (2)

323-323: Choose a stable preview URL: prefer branch rule, then commit, then deployment fallback

Right now previewRuleId is set to the ephemeral deployment rule; per discussion, branch URLs are preferable (Netlify/Vercel-style), with commit as secondary and deployment as fallback.

Apply at deployment preview creation:

-                $previewRuleId = $ruleId;
+                $deploymentRuleId = $ruleId; // fallback only
+                $previewRuleId = ''; // will prefer branch > commit > deployment

Then:

  • In the Branch Preview block, after computing $ruleId: set $previewRuleId = $ruleId;
  • In the Commit Preview block, set if (empty($previewRuleId)) { $previewRuleId = $ruleId; }
  • Just before using $previewRuleId (see Line 415 block), fallback: if (empty($previewRuleId)) { $previewRuleId = $deploymentRuleId; }

415-451: Ensure previewRuleId fallback is applied before fetching the rule

If branch/commit didn’t set previewRuleId, fall back to the deploymentRuleId before getDocument.

Apply:

-                try {
+                try {
+                    if (empty($previewRuleId) && !empty($deploymentRuleId)) {
+                        $previewRuleId = $deploymentRuleId;
+                    }
                     $rule = Authorization::skip(fn () => $dbForPlatform->getDocument('rules', $previewRuleId));
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

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

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 2f49722 and b32e1ad.

📒 Files selected for processing (1)
  • app/controllers/api/vcs.php (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
app/controllers/api/vcs.php (2)
src/Appwrite/Vcs/Comment.php (4)
  • Comment (10-226)
  • parseComment (214-225)
  • addBuild (31-48)
  • generateComment (50-199)
src/Appwrite/Platform/Modules/Proxy/Http/Rules/API/Create.php (1)
  • action (72-199)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Benchmark
  • GitHub Check: Setup & Build Appwrite Image
  • GitHub Check: scan

Comment on lines 138 to 166
$retries = 0;

while (true) {
$retries++;

try {
$dbForPlatform->createDocument('vcsCommentLocks', new Document([
'$id' => $latestCommentId
]));
break;
} catch (\Throwable $err) {
if ($retries >= 9) {
throw $err;
}

\sleep(1);
}
}

// Wrap in try/finally to ensure lock file gets deleted
try {
$comment = new Comment();
$comment->parseComment($github->getComment($owner, $repositoryName, $latestCommentId));
$comment->addBuild($project, $resource, $resourceType, $commentStatus, $deploymentId, $action, '');

$latestCommentId = \strval($github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment()));
$latestCommentId = \strval($github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment()));
} finally {
$dbForPlatform->deleteDocument('vcsCommentLocks', $latestCommentId);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix lock-release bug: deleting the wrong lock ID after updateComment mutates latestCommentId

updateComment may return a different ID. You overwrite latestCommentId, then attempt to delete the lock with the new value, leaking the original lock and blocking future updates.

Apply:

-                    // Wrap in try/finally to ensure lock file gets deleted
+                    // Wrap in try/finally to ensure lock doc gets deleted
                     try {
+                        $lockedCommentId = $latestCommentId; // keep original for lock cleanup
                         $comment = new Comment();
                         $comment->parseComment($github->getComment($owner, $repositoryName, $latestCommentId));
                         $comment->addBuild($project, $resource, $resourceType, $commentStatus, $deploymentId, $action, '');
 
-                        $latestCommentId = \strval($github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment()));
+                        $github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment());
                     } finally {
-                        $dbForPlatform->deleteDocument('vcsCommentLocks', $latestCommentId);
+                        try { $dbForPlatform->deleteDocument('vcsCommentLocks', $lockedCommentId); } catch (\Throwable $e) { /* no-op */ }
                     }

Optional: guard against empty IDs to avoid creating a lock with an empty key.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$retries = 0;
while (true) {
$retries++;
try {
$dbForPlatform->createDocument('vcsCommentLocks', new Document([
'$id' => $latestCommentId
]));
break;
} catch (\Throwable $err) {
if ($retries >= 9) {
throw $err;
}
\sleep(1);
}
}
// Wrap in try/finally to ensure lock file gets deleted
try {
$comment = new Comment();
$comment->parseComment($github->getComment($owner, $repositoryName, $latestCommentId));
$comment->addBuild($project, $resource, $resourceType, $commentStatus, $deploymentId, $action, '');
$latestCommentId = \strval($github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment()));
$latestCommentId = \strval($github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment()));
} finally {
$dbForPlatform->deleteDocument('vcsCommentLocks', $latestCommentId);
}
// Wrap in try/finally to ensure lock doc gets deleted
try {
$lockedCommentId = $latestCommentId; // keep original for lock cleanup
$comment = new Comment();
$comment->parseComment($github->getComment($owner, $repositoryName, $latestCommentId));
$comment->addBuild($project, $resource, $resourceType, $commentStatus, $deploymentId, $action, '');
// Perform the update but don’t overwrite $latestCommentId here
$github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment());
} finally {
// Clean up the original lock, ignore any delete errors
try {
$dbForPlatform->deleteDocument('vcsCommentLocks', $lockedCommentId);
} catch (\Throwable $e) {
/* no-op */
}
}
🤖 Prompt for AI Agents
In app/controllers/api/vcs.php around lines 138 to 166, the code overwrites
$latestCommentId with the return from updateComment and then deletes a lock
using the new ID, leaking the original lock; preserve the original lock id in a
separate variable before calling updateComment (e.g. $lockId =
$latestCommentId), use that $lockId when creating and deleting the
'vcsCommentLocks' document inside the retry/finally blocks, and only overwrite
$latestCommentId with the updated value after the lock is deleted; additionally,
validate/guard against empty IDs when creating the lock (do not attempt to
create a lock if $lockId is empty, and handle that as an error).

Comment on lines 205 to 233
$retries = 0;

while (true) {
$retries++;

try {
$dbForPlatform->createDocument('vcsCommentLocks', new Document([
'$id' => $latestCommentId
]));
break;
} catch (\Throwable $err) {
if ($retries >= 9) {
throw $err;
}

\sleep(1);
}
}

// Wrap in try/finally to ensure lock file gets deleted
try {
$comment = new Comment();
$comment->parseComment($github->getComment($owner, $repositoryName, $latestCommentId));
$comment->addBuild($project, $resource, $resourceType, $commentStatus, $deploymentId, $action, '');

$latestCommentId = \strval($github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment()));
} finally {
$dbForPlatform->deleteDocument('vcsCommentLocks', $latestCommentId);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Same lock-release bug in branch-comments loop

The same mutation of latestCommentId risks leaking the lock per iteration.

Apply:

-                    // Wrap in try/finally to ensure lock file gets deleted
+                    // Wrap in try/finally to ensure lock doc gets deleted
                     try {
+                        $lockedCommentId = $latestCommentId;
                         $comment = new Comment();
                         $comment->parseComment($github->getComment($owner, $repositoryName, $latestCommentId));
                         $comment->addBuild($project, $resource, $resourceType, $commentStatus, $deploymentId, $action, '');
 
-                        $latestCommentId = \strval($github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment()));
+                        $github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment());
                     } finally {
-                        $dbForPlatform->deleteDocument('vcsCommentLocks', $latestCommentId);
+                        try { $dbForPlatform->deleteDocument('vcsCommentLocks', $lockedCommentId); } catch (\Throwable $e) { /* no-op */ }
                     }

Minor: consider small jitter in the retry loop to reduce thundering herd.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$retries = 0;
while (true) {
$retries++;
try {
$dbForPlatform->createDocument('vcsCommentLocks', new Document([
'$id' => $latestCommentId
]));
break;
} catch (\Throwable $err) {
if ($retries >= 9) {
throw $err;
}
\sleep(1);
}
}
// Wrap in try/finally to ensure lock file gets deleted
try {
$comment = new Comment();
$comment->parseComment($github->getComment($owner, $repositoryName, $latestCommentId));
$comment->addBuild($project, $resource, $resourceType, $commentStatus, $deploymentId, $action, '');
$latestCommentId = \strval($github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment()));
} finally {
$dbForPlatform->deleteDocument('vcsCommentLocks', $latestCommentId);
}
// Wrap in try/finally to ensure lock doc gets deleted
try {
$lockedCommentId = $latestCommentId;
$comment = new Comment();
$comment->parseComment($github->getComment($owner, $repositoryName, $latestCommentId));
$comment->addBuild($project, $resource, $resourceType, $commentStatus, $deploymentId, $action, '');
$github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment());
} finally {
try {
$dbForPlatform->deleteDocument('vcsCommentLocks', $lockedCommentId);
} catch (\Throwable $e) {
/* no-op */
}
}

Copy link
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: 0

♻️ Duplicate comments (4)
app/controllers/api/vcs.php (4)

332-334: Prefer branch (then commit) preview domain; fall back to deployment rule.

Use stable branch preview URLs when available, else commit, else the ephemeral deployment rule. This keeps the PR comment’s preview link consistent across deployments.

Apply:

-                $previewRuleId = $ruleId;
+                $deploymentRuleId = $ruleId;          // fallback
+                $previewRuleId = '';                  // will prefer branch > commit > deployment
@@
                 // VCS branch preview
                 if (!empty($providerBranch)) {
@@
-                    try {
+                    try {
                         Authorization::skip(
                             fn () => $dbForPlatform->createDocument('rules', new Document([
@@
                             ]))
                         );
+                        $previewRuleId = $ruleId; // prefer branch domain
                     } catch (Duplicate $err) {
-                        // Ignore, rule already exists; will be updated by builds worker
+                        $previewRuleId = $ruleId; // prefer branch domain
                     }
                 }
@@
                 // VCS commit preview
                 if (!empty($providerCommitHash)) {
@@
-                    try {
+                    try {
                         Authorization::skip(
                             fn () => $dbForPlatform->createDocument('rules', new Document([
@@
                             ]))
                         );
+                        if (empty($previewRuleId)) { $previewRuleId = $ruleId; }
                     } catch (Duplicate $err) {
-                        // Ignore, rule already exists; will be updated by builds worker
+                        if (empty($previewRuleId)) { $previewRuleId = $ruleId; }
                     }
                 }
@@
-            if ($resource->getCollection() === 'sites' && !empty($latestCommentId) && !empty($previewRuleId)) {
+            // final fallback to deployment rule
+            if (empty($previewRuleId ?? '')) { $previewRuleId = $deploymentRuleId; }
+            if ($resource->getCollection() === 'sites' && !empty($latestCommentId) && !empty($previewRuleId)) {

Also applies to: 355-390, 392-421, 423-424


451-455: Guard against empty rule/domain to avoid posting an invalid “https://”.

!empty($rule) is always truthy for a Document. Use isEmpty() and verify domain is non-empty before composing the URL.

Apply:

-                        $protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
-                        $previewUrl = !empty($rule) ? ("{$protocol}://" . $rule->getAttribute('domain', '')) : '';
+                        $protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
+                        $domain = !$rule->isEmpty() ? $rule->getAttribute('domain', '') : '';
+                        $previewUrl = $domain !== '' ? "{$protocol}://{$domain}" : '';

139-171: Fix lock-release bug and avoid mutating the lock key.

updateComment may return a different ID. You overwrite $latestCommentId and then try to delete the lock using the new value, leaking the original lock and blocking future updates. Keep a stable $lockId, don’t reassign $latestCommentId, and always delete using $lockId with a safe try/catch.

Apply:

-                    $retries = 0;
+                    $retries = 0;
+                    $lockId = $latestCommentId;
                     $lockAcquired = false;
@@
-                            $dbForPlatform->createDocument('vcsCommentLocks', new Document([
-                                '$id' => $latestCommentId
+                            $dbForPlatform->createDocument('vcsCommentLocks', new Document([
+                                '$id' => $lockId
                             ]));
@@
-                            $comment = new Comment();
+                            $comment = new Comment();
                             $comment->parseComment($github->getComment($owner, $repositoryName, $latestCommentId));
                             $comment->addBuild($project, $resource, $resourceType, $commentStatus, $deploymentId, $action, '');
-
-                            $latestCommentId = \strval($github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment()));
+                            // Do not overwrite $latestCommentId; use a stable lock key.
+                            $github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment());
                         } finally {
-                            $dbForPlatform->deleteDocument('vcsCommentLocks', $latestCommentId);
+                            try { if ($lockId !== '') { $dbForPlatform->deleteDocument('vcsCommentLocks', $lockId); } } catch (\Throwable $e) { /* no-op */ }
                         }

210-242: Same lock key mutation/leak in branch-comments loop.

Mirror the fix: preserve a $lockId, don’t reassign $latestCommentId, and delete using $lockId.

Apply:

-                    $retries = 0;
+                    $retries = 0;
+                    $lockId = $latestCommentId;
                     $lockAcquired = false;
@@
-                            $dbForPlatform->createDocument('vcsCommentLocks', new Document([
-                                '$id' => $latestCommentId
+                            $dbForPlatform->createDocument('vcsCommentLocks', new Document([
+                                '$id' => $lockId
                             ]));
@@
-                            $comment = new Comment();
+                            $comment = new Comment();
                             $comment->parseComment($github->getComment($owner, $repositoryName, $latestCommentId));
                             $comment->addBuild($project, $resource, $resourceType, $commentStatus, $deploymentId, $action, '');
-
-                            $latestCommentId = \strval($github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment()));
+                            $github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment());
                         } finally {
-                            $dbForPlatform->deleteDocument('vcsCommentLocks', $latestCommentId);
+                            try { if ($lockId !== '') { $dbForPlatform->deleteDocument('vcsCommentLocks', $lockId); } } catch (\Throwable $e) { /* no-op */ }
                         }
🧹 Nitpick comments (1)
app/controllers/api/vcs.php (1)

428-444: Reduce thundering herd and request blocking in lock backoff.

Current backoff sleeps for 1s up to 9s per request. Add small jitter to spread contention; consider a shorter base delay.

Example:

-                            \sleep(1);
+                            \usleep(\random_int(200_000, 800_000)); // 200–800ms jitter
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

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

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between df1b4c9 and 3aa163a.

📒 Files selected for processing (1)
  • app/controllers/api/vcs.php (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
app/controllers/api/vcs.php (1)
src/Appwrite/Vcs/Comment.php (4)
  • Comment (10-226)
  • parseComment (214-225)
  • addBuild (31-48)
  • generateComment (50-199)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Benchmark
  • GitHub Check: scan
🔇 Additional comments (1)
app/controllers/api/vcs.php (1)

18-18: LGTM: Console logger import is appropriate.

@Meldiron Meldiron merged commit b09af87 into 1.8.x Sep 4, 2025
41 checks passed
@stnguyen90 stnguyen90 deleted the feat-update-preview-url-in-vcs-controller branch October 1, 2025 23:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants