appwrite/app/controllers/api/vcs.php

703 lines
38 KiB
PHP
Raw Normal View History

<?php
use Appwrite\Event\Build;
use Appwrite\Extend\Exception;
2025-01-17 04:31:39 +00:00
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
2024-03-06 17:34:21 +00:00
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Appwrite\Vcs\Comment;
use Utopia\Console;
2024-03-06 17:34:21 +00:00
use Utopia\Database\Database;
use Utopia\Database\Document;
2025-02-24 15:22:23 +00:00
use Utopia\Database\Exception\Duplicate;
2023-08-06 08:51:53 +00:00
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Http\Http;
2024-04-01 11:08:46 +00:00
use Utopia\System\System;
2024-10-08 07:54:40 +00:00
use Utopia\Validator\Text;
2024-03-06 17:34:21 +00:00
use Utopia\VCS\Adapter\Git\GitHub;
2023-10-13 13:20:58 +00:00
use Utopia\VCS\Exception\RepositoryNotFound;
$createGitDeployments = function (GitHub $github, string $providerInstallationId, array $repositories, string $providerBranch, string $providerBranchUrl, string $providerRepositoryName, string $providerRepositoryUrl, string $providerRepositoryOwner, string $providerCommitHash, string $providerCommitAuthor, string $providerCommitAuthorUrl, string $providerCommitMessage, string $providerCommitUrl, string $providerPullRequestId, bool $external, Database $dbForPlatform, Authorization $authorization, Build $queueForBuilds, callable $getProjectDB, Request $request, array $platform) {
2024-10-08 07:54:40 +00:00
$errors = [];
foreach ($repositories as $repository) {
try {
$resourceType = $repository->getAttribute('resourceType');
2023-07-28 08:27:16 +00:00
if ($resourceType !== "function" && $resourceType !== "site") {
2024-03-10 20:53:57 +00:00
continue;
}
2023-07-28 08:27:16 +00:00
$projectId = $repository->getAttribute('projectId');
$project = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $projectId));
2024-03-10 20:53:57 +00:00
$dbForProject = $getProjectDB($project);
$resourceCollection = $resourceType === "function" ? 'functions' : 'sites';
$resourceId = $repository->getAttribute('resourceId');
$resource = $authorization->skip(fn () => $dbForProject->getDocument($resourceCollection, $resourceId));
$resourceInternalId = $resource->getSequence();
2024-03-10 20:53:57 +00:00
$deploymentId = ID::unique();
$repositoryId = $repository->getId();
$repositoryInternalId = $repository->getSequence();
$providerRepositoryId = $repository->getAttribute('providerRepositoryId');
$installationId = $repository->getAttribute('installationId');
$installationInternalId = $repository->getAttribute('installationInternalId');
$productionBranch = $resource->getAttribute('providerBranch');
2024-03-10 20:53:57 +00:00
$activate = false;
if ($providerBranch == $productionBranch && $external === false) {
$activate = true;
}
2023-08-09 09:52:34 +00:00
2024-03-10 20:53:57 +00:00
$owner = $github->getOwnerName($providerInstallationId) ?? '';
try {
$repositoryName = $github->getRepositoryName($providerRepositoryId) ?? '';
if (empty($repositoryName)) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
2024-03-10 20:53:57 +00:00
} catch (RepositoryNotFound $e) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
2023-08-09 09:52:34 +00:00
2024-03-10 20:53:57 +00:00
if (empty($repositoryName)) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
$isAuthorized = !$external;
if (!$isAuthorized && !empty($providerPullRequestId)) {
if (\in_array($providerPullRequestId, $repository->getAttribute('providerPullRequestIds', []))) {
2024-03-10 20:53:57 +00:00
$isAuthorized = true;
}
2024-03-10 20:53:57 +00:00
}
$commentStatus = $isAuthorized ? 'waiting' : 'failed';
2025-06-18 05:36:35 +00:00
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
2025-12-11 18:36:11 +00:00
$hostname = $platform['consoleHostname'] ?? '';
2023-08-19 12:51:27 +00:00
2025-06-13 10:20:53 +00:00
$authorizeUrl = $protocol . '://' . $hostname . "/console/git/authorize-contributor?projectId={$projectId}&installationId={$installationId}&repositoryId={$repositoryId}&providerPullRequestId={$providerPullRequestId}";
2023-07-28 08:27:16 +00:00
2024-03-10 20:53:57 +00:00
$action = $isAuthorized ? ['type' => 'logs'] : ['type' => 'authorize', 'url' => $authorizeUrl];
$latestCommentId = '';
if (!empty($providerPullRequestId) && $resource->getAttribute('providerSilentMode', false) === false) {
$latestComment = $authorization->skip(fn () => $dbForPlatform->findOne('vcsComments', [
2024-03-10 20:53:57 +00:00
Query::equal('providerRepositoryId', [$providerRepositoryId]),
Query::equal('providerPullRequestId', [$providerPullRequestId]),
Query::orderDesc('$createdAt'),
]));
if (!$latestComment->isEmpty()) {
2024-03-10 20:53:57 +00:00
$latestCommentId = $latestComment->getAttribute('providerCommentId', '');
2024-10-28 14:20:36 +00:00
$retries = 0;
$lockAcquired = false;
while ($retries < 9) {
$retries++;
try {
$dbForPlatform->createDocument('vcsCommentLocks', new Document([
'$id' => $latestCommentId
]));
$lockAcquired = true;
break;
} catch (\Throwable $err) {
if ($retries >= 9) {
2025-09-03 13:15:43 +00:00
Console::warning("Error creating vcs comment lock for " . $latestCommentId . ": " . $err->getMessage());
}
\sleep(1);
}
}
if ($lockAcquired) {
// Wrap in try/finally to ensure lock file gets deleted
try {
2025-12-07 20:29:45 +00:00
$comment = new Comment($platform);
$comment->parseComment($github->getComment($owner, $repositoryName, $latestCommentId));
$comment->addBuild($project, $resource, $resourceType, $commentStatus, $deploymentId, $action, '');
2024-03-10 20:53:57 +00:00
$latestCommentId = \strval($github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment()));
} finally {
$authorization->skip(fn () => $dbForPlatform->deleteDocument('vcsCommentLocks', $latestCommentId));
}
}
2024-03-10 20:53:57 +00:00
} else {
2025-12-07 20:29:45 +00:00
$comment = new Comment($platform);
$comment->addBuild($project, $resource, $resourceType, $commentStatus, $deploymentId, $action, '');
2024-03-10 20:53:57 +00:00
$latestCommentId = \strval($github->createComment($owner, $repositoryName, $providerPullRequestId, $comment->generateComment()));
if (!empty($latestCommentId)) {
$teamId = $project->getAttribute('teamId', '');
$latestComment = $authorization->skip(fn () => $dbForPlatform->createDocument('vcsComments', new Document([
2024-03-10 20:53:57 +00:00
'$id' => ID::unique(),
'$permissions' => [
Permission::read(Role::team(ID::custom($teamId))),
Permission::update(Role::team(ID::custom($teamId), 'owner')),
Permission::update(Role::team(ID::custom($teamId), 'developer')),
Permission::delete(Role::team(ID::custom($teamId), 'owner')),
Permission::delete(Role::team(ID::custom($teamId), 'developer')),
],
'installationInternalId' => $installationInternalId,
'installationId' => $installationId,
2025-05-26 05:42:11 +00:00
'projectInternalId' => $project->getSequence(),
2024-03-10 20:53:57 +00:00
'projectId' => $project->getId(),
'providerRepositoryId' => $providerRepositoryId,
'providerBranch' => $providerBranch,
'providerPullRequestId' => $providerPullRequestId,
'providerCommentId' => $latestCommentId
])));
2023-07-28 08:27:16 +00:00
}
}
2024-03-10 20:53:57 +00:00
} elseif (!empty($providerBranch)) {
$latestComments = $authorization->skip(fn () => $dbForPlatform->find('vcsComments', [
2024-03-10 20:53:57 +00:00
Query::equal('providerRepositoryId', [$providerRepositoryId]),
Query::equal('providerBranch', [$providerBranch]),
Query::orderDesc('$createdAt'),
]));
2023-07-28 08:27:16 +00:00
2024-03-10 20:53:57 +00:00
foreach ($latestComments as $comment) {
$latestCommentId = $comment->getAttribute('providerCommentId', '');
$retries = 0;
$lockAcquired = false;
while ($retries < 9) {
$retries++;
try {
$dbForPlatform->createDocument('vcsCommentLocks', new Document([
'$id' => $latestCommentId
]));
$lockAcquired = true;
break;
} catch (\Throwable $err) {
if ($retries >= 9) {
2025-09-03 13:15:43 +00:00
Console::warning("Error creating vcs comment lock for " . $latestCommentId . ": " . $err->getMessage());
}
\sleep(1);
}
}
if ($lockAcquired) {
// Wrap in try/finally to ensure lock file gets deleted
try {
2025-12-07 20:29:45 +00:00
$comment = new Comment($platform);
$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 {
$authorization->skip(fn () => $dbForPlatform->deleteDocument('vcsCommentLocks', $latestCommentId));
}
}
2023-08-19 12:51:27 +00:00
}
2024-03-10 20:53:57 +00:00
}
2023-07-28 08:27:16 +00:00
2024-03-10 20:53:57 +00:00
if (!$isAuthorized) {
$resourceName = $resource->getAttribute('name');
2024-03-10 20:53:57 +00:00
$projectName = $project->getAttribute('name');
$name = "{$resourceName} ({$projectName})";
2024-03-10 20:53:57 +00:00
$message = 'Authorization required for external contributor.';
$providerRepositoryId = $repository->getAttribute('providerRepositoryId');
2024-03-10 20:53:57 +00:00
try {
$repositoryName = $github->getRepositoryName($providerRepositoryId) ?? '';
if (empty($repositoryName)) {
2023-10-27 14:08:33 +00:00
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
2024-03-10 20:53:57 +00:00
} catch (RepositoryNotFound $e) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
2023-10-27 14:08:33 +00:00
}
2024-03-10 20:53:57 +00:00
$owner = $github->getOwnerName($providerInstallationId);
$github->updateCommitStatus($repositoryName, $providerCommitHash, $owner, 'failure', $message, $authorizeUrl, $name);
continue;
}
2023-07-28 08:27:16 +00:00
2024-03-10 20:53:57 +00:00
if ($external) {
$pullRequestResponse = $github->getPullRequest($owner, $repositoryName, $providerPullRequestId);
$providerRepositoryName = $pullRequestResponse['head']['repo']['owner']['login'];
$providerRepositoryOwner = $pullRequestResponse['head']['repo']['name'];
}
2023-08-11 16:52:13 +00:00
2025-03-07 19:14:11 +00:00
$commands = [];
if (!empty($resource->getAttribute('installCommand', ''))) {
$commands[] = $resource->getAttribute('installCommand', '');
}
2025-03-09 15:15:08 +00:00
if (!empty($resource->getAttribute('buildCommand', ''))) {
$commands[] = $resource->getAttribute('buildCommand', '');
}
2025-03-07 19:14:11 +00:00
if (!empty($resource->getAttribute('commands', ''))) {
$commands[] = $resource->getAttribute('commands', '');
}
$deployment = $authorization->skip(fn () => $dbForProject->createDocument('deployments', new Document([
2024-03-10 20:53:57 +00:00
'$id' => $deploymentId,
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'resourceId' => $resourceId,
'resourceInternalId' => $resourceInternalId,
'resourceType' => $resourceCollection,
'entrypoint' => $resource->getAttribute('entrypoint', ''),
2025-03-07 19:14:11 +00:00
'buildCommands' => \implode(' && ', $commands),
'startCommand' => $resource->getAttribute('startCommand', ''),
2025-03-07 19:14:11 +00:00
'buildOutput' => $resource->getAttribute('outputDirectory', ''),
'adapter' => $resource->getAttribute('adapter', ''),
'fallbackFile' => $resource->getAttribute('fallbackFile', ''),
2024-03-10 20:53:57 +00:00
'type' => 'vcs',
'installationId' => $installationId,
'installationInternalId' => $installationInternalId,
'providerRepositoryId' => $providerRepositoryId,
'repositoryId' => $repositoryId,
'repositoryInternalId' => $repositoryInternalId,
'providerBranchUrl' => $providerBranchUrl,
'providerRepositoryName' => $providerRepositoryName,
'providerRepositoryOwner' => $providerRepositoryOwner,
'providerRepositoryUrl' => $providerRepositoryUrl,
'providerCommitHash' => $providerCommitHash,
'providerCommitAuthorUrl' => $providerCommitAuthorUrl,
'providerCommitAuthor' => $providerCommitAuthor,
2025-03-05 08:10:15 +00:00
'providerCommitMessage' => mb_strimwidth($providerCommitMessage, 0, 255, '...'),
2024-03-10 20:53:57 +00:00
'providerCommitUrl' => $providerCommitUrl,
'providerCommentId' => \strval($latestCommentId),
'providerBranch' => $providerBranch,
'activate' => $activate,
])));
2024-03-10 20:53:57 +00:00
$resource = $resource
->setAttribute('latestDeploymentId', $deployment->getId())
->setAttribute('latestDeploymentInternalId', $deployment->getSequence())
->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt())
->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', ''));
$authorization->skip(fn () => $dbForProject->updateDocument($resource->getCollection(), $resource->getId(), $resource));
if ($resource->getCollection() === 'sites') {
$projectId = $project->getId();
2025-02-24 15:22:23 +00:00
// Deployment preview
2026-01-30 20:30:00 +00:00
$sitesDomain = $platform['sitesDomain'];
2025-02-20 13:22:30 +00:00
$domain = ID::unique() . "." . $sitesDomain;
$ruleId = md5($domain);
2025-08-28 13:47:07 +00:00
$previewRuleId = $ruleId;
$authorization->skip(
2025-02-04 09:51:33 +00:00
fn () => $dbForPlatform->createDocument('rules', new Document([
'$id' => $ruleId,
'projectId' => $project->getId(),
'projectInternalId' => $project->getSequence(),
'domain' => $domain,
2025-02-23 20:34:14 +00:00
'type' => 'deployment',
'trigger' => 'deployment',
'deploymentId' => $deployment->getId(),
'deploymentInternalId' => $deployment->getSequence(),
'deploymentResourceType' => 'site',
'deploymentResourceId' => $resourceId,
'deploymentResourceInternalId' => $resourceInternalId,
'deploymentVcsProviderBranch' => $providerBranch,
'status' => 'verified',
'certificateId' => '',
2025-02-25 09:47:47 +00:00
'search' => implode(' ', [$ruleId, $domain]),
2025-03-17 10:41:02 +00:00
'owner' => 'Appwrite',
'region' => $project->getAttribute('region')
]))
);
2025-02-24 15:22:23 +00:00
// VCS branch preview
if (!empty($providerBranch)) {
2025-05-28 07:20:47 +00:00
$branchPrefix = substr($providerBranch, 0, 16);
if (strlen($providerBranch) > 16) {
$remainingChars = substr($providerBranch, 16);
$branchPrefix .= '-' . substr(hash('sha256', $remainingChars), 0, 7);
2025-05-28 07:20:47 +00:00
}
$resourceProjectHash = substr(hash('sha256', $resource->getId() . $project->getId()), 0, 7);
2025-05-28 07:25:50 +00:00
$domain = "branch-{$branchPrefix}-{$resourceProjectHash}.{$sitesDomain}";
2025-02-24 15:22:23 +00:00
$ruleId = md5($domain);
try {
$authorization->skip(
2025-02-24 15:22:23 +00:00
fn () => $dbForPlatform->createDocument('rules', new Document([
'$id' => $ruleId,
'projectId' => $project->getId(),
'projectInternalId' => $project->getSequence(),
2025-02-24 15:22:23 +00:00
'domain' => $domain,
'type' => 'deployment',
'trigger' => 'deployment',
'deploymentId' => $deployment->getId(),
'deploymentInternalId' => $deployment->getSequence(),
'deploymentResourceType' => 'site',
'deploymentResourceId' => $resourceId,
'deploymentResourceInternalId' => $resourceInternalId,
'deploymentVcsProviderBranch' => $providerBranch,
2025-02-24 15:22:23 +00:00
'status' => 'verified',
'certificateId' => '',
2025-02-25 09:47:47 +00:00
'search' => implode(' ', [$ruleId, $domain]),
2025-03-17 10:41:02 +00:00
'owner' => 'Appwrite',
'region' => $project->getAttribute('region')
2025-02-24 15:22:23 +00:00
]))
);
} catch (Duplicate $err) {
// Ignore, rule already exists; will be updated by builds worker
}
}
2024-03-10 20:53:57 +00:00
2025-02-24 15:22:23 +00:00
// VCS commit preview
if (!empty($providerCommitHash)) {
2025-05-28 08:40:12 +00:00
$domain = "commit-" . substr($providerCommitHash, 0, 16) . ".{$sitesDomain}";
2025-02-24 15:22:23 +00:00
$ruleId = md5($domain);
try {
$authorization->skip(
2025-02-24 15:22:23 +00:00
fn () => $dbForPlatform->createDocument('rules', new Document([
'$id' => $ruleId,
'projectId' => $project->getId(),
'projectInternalId' => $project->getSequence(),
2025-02-24 15:22:23 +00:00
'domain' => $domain,
'type' => 'deployment',
'trigger' => 'deployment',
'deploymentId' => $deployment->getId(),
'deploymentInternalId' => $deployment->getSequence(),
'deploymentResourceType' => 'site',
'deploymentResourceId' => $resourceId,
'deploymentResourceInternalId' => $resourceInternalId,
'deploymentVcsProviderBranch' => $providerBranch,
2025-02-24 15:22:23 +00:00
'status' => 'verified',
'certificateId' => '',
2025-02-25 09:47:47 +00:00
'search' => implode(' ', [$ruleId, $domain]),
2025-03-17 10:41:02 +00:00
'owner' => 'Appwrite',
'region' => $project->getAttribute('region')
2025-02-24 15:22:23 +00:00
]))
);
} catch (Duplicate $err) {
// Ignore, rule already exists; will be updated by builds worker
}
}
}
2024-03-10 20:53:57 +00:00
if ($resource->getCollection() === 'sites' && !empty($latestCommentId) && !empty($previewRuleId)) {
$retries = 0;
$lockAcquired = false;
while ($retries < 9) {
$retries++;
try {
$dbForPlatform->createDocument('vcsCommentLocks', new Document([
'$id' => $latestCommentId
]));
$lockAcquired = true;
break;
} catch (\Throwable $err) {
if ($retries >= 9) {
2025-09-03 13:15:43 +00:00
Console::warning("Error creating vcs comment lock for " . $latestCommentId . ": " . $err->getMessage());
}
\sleep(1);
}
}
if ($lockAcquired) {
// Wrap in try/finally to ensure lock file gets deleted
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', '')) : '';
if (!empty($previewUrl)) {
2025-12-07 20:29:45 +00:00
$comment = new Comment($platform);
$comment->parseComment($github->getComment($owner, $repositoryName, $latestCommentId));
$comment->addBuild($project, $resource, $resourceType, $commentStatus, $deploymentId, $action, $previewUrl);
$github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment());
}
} finally {
$authorization->skip(fn () => $dbForPlatform->deleteDocument('vcsCommentLocks', $latestCommentId));
}
}
}
if (!empty($providerCommitHash) && $resource->getAttribute('providerSilentMode', false) === false) {
$resourceName = $resource->getAttribute('name');
2024-03-10 20:53:57 +00:00
$projectName = $project->getAttribute('name');
2025-06-12 06:13:11 +00:00
$region = $project->getAttribute('region', 'default');
$name = "{$resourceName} ({$projectName})";
2024-03-10 20:53:57 +00:00
$message = 'Starting...';
2023-07-28 08:27:16 +00:00
$providerRepositoryId = $repository->getAttribute('providerRepositoryId');
2024-03-10 20:53:57 +00:00
try {
$repositoryName = $github->getRepositoryName($providerRepositoryId) ?? '';
if (empty($repositoryName)) {
2023-10-27 14:08:33 +00:00
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
2024-03-10 20:53:57 +00:00
} catch (RepositoryNotFound $e) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
2024-03-10 20:53:57 +00:00
$owner = $github->getOwnerName($providerInstallationId);
2023-07-28 08:27:16 +00:00
2025-06-13 10:20:53 +00:00
$providerTargetUrl = $protocol . '://' . $hostname . "/console/project-$region-$projectId/$resourceCollection/$resourceType-$resourceId";
2024-03-10 20:53:57 +00:00
$github->updateCommitStatus($repositoryName, $providerCommitHash, $owner, 'pending', $message, $providerTargetUrl, $name);
}
2024-03-10 20:53:57 +00:00
$queueForBuilds
->setType(BUILD_TYPE_DEPLOYMENT)
->setResource($resource)
2024-03-10 20:53:57 +00:00
->setDeployment($deployment)
->setProject($project); // set the project because it won't be set for git deployments
2023-07-28 08:27:16 +00:00
$queueForBuilds->trigger(); // must trigger here so that we create a build for each function/site
2024-03-10 20:53:57 +00:00
//TODO: Add event?
} catch (Throwable $e) {
$errors[] = $e->getMessage();
2023-07-28 08:27:16 +00:00
}
}
$queueForBuilds->reset(); // prevent shutdown hook from triggering again
if (!empty($errors)) {
throw new Exception(Exception::GENERAL_UNKNOWN, \implode("\n", $errors));
}
2023-07-28 08:27:16 +00:00
};
2026-02-04 05:30:22 +00:00
Http::post('/v1/vcs/github/events')
2024-10-08 07:54:40 +00:00
->desc('Create event')
->groups(['api', 'vcs'])
2023-08-30 18:44:33 +00:00
->label('scope', 'public')
2023-05-26 08:44:08 +00:00
->inject('gitHub')
->inject('request')
->inject('response')
->inject('dbForPlatform')
->inject('authorization')
2023-05-23 04:37:25 +00:00
->inject('getProjectDB')
->inject('queueForBuilds')
2025-12-07 20:29:45 +00:00
->inject('platform')
->action(
function (GitHub $github, Request $request, Response $response, Database $dbForPlatform, Authorization $authorization, callable $getProjectDB, Build $queueForBuilds, array $platform) use ($createGitDeployments) {
$payload = $request->getRawPayload();
2023-09-06 17:04:03 +00:00
$signatureRemote = $request->getHeader('x-hub-signature-256', '');
2024-04-01 11:02:47 +00:00
$signatureLocal = System::getEnv('_APP_VCS_GITHUB_WEBHOOK_SECRET', '');
2023-06-15 10:37:28 +00:00
2023-09-06 17:04:03 +00:00
$valid = empty($signatureRemote) ? true : $github->validateWebhookEvent($payload, $signatureRemote, $signatureLocal);
2023-06-15 10:38:03 +00:00
if (!$valid) {
throw new Exception(Exception::GENERAL_ACCESS_FORBIDDEN, "Invalid webhook payload signature. Please make sure the webhook secret has same value in your GitHub app and in the _APP_VCS_GITHUB_WEBHOOK_SECRET environment variable");
2023-06-15 10:37:28 +00:00
}
$event = $request->getHeader('x-github-event', '');
2024-04-01 11:02:47 +00:00
$privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY');
$githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID');
2023-07-24 10:11:30 +00:00
$parsedPayload = $github->getEvent($event, $payload);
if ($event == $github::EVENT_PUSH) {
$providerBranchCreated = $parsedPayload["branchCreated"] ?? false;
$providerBranchDeleted = $parsedPayload["branchDeleted"] ?? false;
$providerBranch = $parsedPayload["branch"] ?? '';
2023-08-09 17:35:23 +00:00
$providerBranchUrl = $parsedPayload["branchUrl"] ?? '';
$providerRepositoryId = $parsedPayload["repositoryId"] ?? '';
$providerRepositoryName = $parsedPayload["repositoryName"] ?? '';
$providerInstallationId = $parsedPayload["installationId"] ?? '';
$providerRepositoryUrl = $parsedPayload["repositoryUrl"] ?? '';
$providerCommitHash = $parsedPayload["commitHash"] ?? '';
$providerRepositoryOwner = $parsedPayload["owner"] ?? '';
2025-07-23 04:23:09 +00:00
$providerCommitAuthorName = $parsedPayload["headCommitAuthorName"] ?? '';
$providerCommitAuthorEmail = $parsedPayload["headCommitAuthorEmail"] ?? '';
2023-08-09 17:35:23 +00:00
$providerCommitAuthorUrl = $parsedPayload["authorUrl"] ?? '';
$providerCommitMessage = $parsedPayload["headCommitMessage"] ?? '';
$providerCommitUrl = $parsedPayload["headCommitUrl"] ?? '';
2023-08-09 17:35:23 +00:00
$github->initializeVariables($providerInstallationId, $privateKey, $githubAppId);
//find resourceId from relevant resources table
$repositories = $authorization->skip(fn () => $dbForPlatform->find('repositories', [
Query::equal('providerRepositoryId', [$providerRepositoryId]),
Query::limit(100),
]));
// create new deployment only on push (not committed by us) and not when branch is created or deleted
if ($providerCommitAuthorEmail !== APP_VCS_GITHUB_EMAIL && !$providerBranchCreated && !$providerBranchDeleted) {
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthorName, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, '', false, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $request, $platform);
2023-08-09 15:53:58 +00:00
}
} elseif ($event == $github::EVENT_INSTALLATION) {
if ($parsedPayload["action"] == "deleted") {
// TODO: Use worker for this job instead (update function/site as well)
$providerInstallationId = $parsedPayload["installationId"];
$installations = $dbForPlatform->find('installations', [
Query::equal('providerInstallationId', [$providerInstallationId]),
Query::limit(1000)
]);
foreach ($installations as $installation) {
$repositories = $authorization->skip(fn () => $dbForPlatform->find('repositories', [
2025-05-26 05:42:11 +00:00
Query::equal('installationInternalId', [$installation->getSequence()]),
Query::limit(1000)
]));
foreach ($repositories as $repository) {
$authorization->skip(fn () => $dbForPlatform->deleteDocument('repositories', $repository->getId()));
}
$authorization->skip(fn () => $dbForPlatform->deleteDocument('installations', $installation->getId()));
}
}
} elseif ($event == $github::EVENT_PULL_REQUEST) {
2023-06-28 08:48:10 +00:00
if ($parsedPayload["action"] == "opened" || $parsedPayload["action"] == "reopened" || $parsedPayload["action"] == "synchronize") {
$providerBranch = $parsedPayload["branch"] ?? '';
2023-08-09 17:35:23 +00:00
$providerBranchUrl = $parsedPayload["branchUrl"] ?? '';
$providerRepositoryId = $parsedPayload["repositoryId"] ?? '';
$providerRepositoryName = $parsedPayload["repositoryName"] ?? '';
$providerInstallationId = $parsedPayload["installationId"] ?? '';
$providerRepositoryUrl = $parsedPayload["repositoryUrl"] ?? '';
$providerPullRequestId = $parsedPayload["pullRequestNumber"] ?? '';
$providerCommitHash = $parsedPayload["commitHash"] ?? '';
$providerRepositoryOwner = $parsedPayload["owner"] ?? '';
$external = $parsedPayload["external"] ?? true;
$providerCommitUrl = $parsedPayload["headCommitUrl"] ?? '';
2023-08-09 17:35:23 +00:00
$providerCommitAuthorUrl = $parsedPayload["authorUrl"] ?? '';
2023-06-28 11:31:35 +00:00
// Ignore sync for non-external. We handle it in push webhook
2023-07-01 06:46:21 +00:00
if (!$external && $parsedPayload["action"] == "synchronize") {
2023-06-28 11:31:35 +00:00
return $response->json($parsedPayload);
}
2023-08-09 17:35:23 +00:00
$github->initializeVariables($providerInstallationId, $privateKey, $githubAppId);
$commitDetails = $github->getCommit($providerRepositoryOwner, $providerRepositoryName, $providerCommitHash);
$providerCommitAuthor = $commitDetails["commitAuthor"] ?? '';
$providerCommitMessage = $commitDetails["commitMessage"] ?? '';
$repositories = $authorization->skip(fn () => $dbForPlatform->find('repositories', [
Query::equal('providerRepositoryId', [$providerRepositoryId]),
Query::orderDesc('$createdAt')
]));
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, $external, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $request, $platform);
2023-06-28 08:48:10 +00:00
} elseif ($parsedPayload["action"] == "closed") {
// Allowed external contributions cleanup
$providerRepositoryId = $parsedPayload["repositoryId"] ?? '';
$providerPullRequestId = $parsedPayload["pullRequestNumber"] ?? '';
$external = $parsedPayload["external"] ?? true;
2023-06-28 08:48:10 +00:00
if ($external) {
$repositories = $authorization->skip(fn () => $dbForPlatform->find('repositories', [
Query::equal('providerRepositoryId', [$providerRepositoryId]),
2023-06-28 08:48:10 +00:00
Query::orderDesc('$createdAt')
]));
2023-06-28 08:48:10 +00:00
foreach ($repositories as $repository) {
$providerPullRequestIds = $repository->getAttribute('providerPullRequestIds', []);
2023-06-28 08:48:10 +00:00
if (\in_array($providerPullRequestId, $providerPullRequestIds)) {
$providerPullRequestIds = \array_diff($providerPullRequestIds, [$providerPullRequestId]);
$repository = $repository->setAttribute('providerPullRequestIds', $providerPullRequestIds);
$repository = $authorization->skip(fn () => $dbForPlatform->updateDocument('repositories', $repository->getId(), $repository));
2023-06-28 08:48:10 +00:00
}
}
}
}
}
$response->json($parsedPayload);
}
);
2026-02-04 05:30:22 +00:00
Http::patch('/v1/vcs/github/installations/:installationId/repositories/:repositoryId')
->desc('Update external deployment (authorize)')
2023-06-28 08:48:10 +00:00
->groups(['api', 'vcs'])
2023-08-30 18:44:33 +00:00
->label('scope', 'vcs.write')
2025-01-17 04:31:39 +00:00
->label('sdk', new Method(
namespace: 'vcs',
2025-03-31 05:48:17 +00:00
group: 'repositories',
2025-01-17 04:31:39 +00:00
name: 'updateExternalDeployments',
description: '/docs/references/vcs/update-external-deployments.md',
2025-01-17 04:31:39 +00:00
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_NOCONTENT,
model: Response::MODEL_NONE,
)
]
))
2023-06-28 08:48:10 +00:00
->param('installationId', '', new Text(256), 'Installation Id')
->param('repositoryId', '', new Text(256), 'VCS Repository Id')
->param('providerPullRequestId', '', new Text(256), 'GitHub Pull Request Id')
2023-06-28 08:48:10 +00:00
->inject('gitHub')
->inject('response')
->inject('project')
->inject('dbForPlatform')
->inject('authorization')
2023-06-28 08:48:10 +00:00
->inject('getProjectDB')
->inject('queueForBuilds')
2025-12-07 20:29:45 +00:00
->inject('platform')
->action(function (string $installationId, string $repositoryId, string $providerPullRequestId, GitHub $github, Request $request, Response $response, Document $project, Database $dbForPlatform, Authorization $authorization, callable $getProjectDB, Build $queueForBuilds, array $platform) use ($createGitDeployments) {
$installation = $dbForPlatform->getDocument('installations', $installationId);
2023-06-28 08:48:10 +00:00
if ($installation->isEmpty()) {
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
}
$repository = $authorization->skip(fn () => $dbForPlatform->findOne('repositories', [
2025-12-01 14:29:07 +00:00
Query::equal('$id', [$repositoryId]),
2025-05-26 05:42:11 +00:00
Query::equal('projectInternalId', [$project->getSequence()])
]));
2023-06-28 08:48:10 +00:00
if ($repository->isEmpty()) {
2023-07-31 06:47:47 +00:00
throw new Exception(Exception::REPOSITORY_NOT_FOUND);
2023-06-28 08:48:10 +00:00
}
if (\in_array($providerPullRequestId, $repository->getAttribute('providerPullRequestIds', []))) {
2023-07-31 06:47:47 +00:00
throw new Exception(Exception::PROVIDER_CONTRIBUTION_CONFLICT);
2023-06-28 08:48:10 +00:00
}
$providerPullRequestIds = \array_unique(\array_merge($repository->getAttribute('providerPullRequestIds', []), [$providerPullRequestId]));
$repository = $repository->setAttribute('providerPullRequestIds', $providerPullRequestIds);
2023-06-28 08:48:10 +00:00
// TODO: Delete from array when PR is closed
$repository = $authorization->skip(fn () => $dbForPlatform->updateDocument('repositories', $repository->getId(), $repository));
2023-06-28 08:48:10 +00:00
2024-04-01 11:02:47 +00:00
$privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY');
$githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID');
$providerInstallationId = $installation->getAttribute('providerInstallationId');
2023-08-09 17:35:23 +00:00
$github->initializeVariables($providerInstallationId, $privateKey, $githubAppId);
2023-06-28 08:48:10 +00:00
$repositories = [$repository];
$providerRepositoryId = $repository->getAttribute('providerRepositoryId');
2023-06-28 08:48:10 +00:00
$owner = $github->getOwnerName($providerInstallationId);
2023-10-27 14:08:33 +00:00
try {
$repositoryName = $github->getRepositoryName($providerRepositoryId) ?? '';
if (empty($repositoryName)) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
} catch (RepositoryNotFound $e) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
$pullRequestResponse = $github->getPullRequest($owner, $repositoryName, $providerPullRequestId);
2023-06-28 08:48:10 +00:00
$providerBranch = \explode(':', $pullRequestResponse['head']['label'])[1] ?? '';
$providerCommitHash = $pullRequestResponse['head']['sha'] ?? '';
2025-12-07 20:29:45 +00:00
$providerBranchUrl = $pullRequestResponse['head']['repo']['html_url'] ?? '';
$providerRepositoryName = $pullRequestResponse['head']['repo']['name'] ?? '';
$providerRepositoryUrl = $pullRequestResponse['head']['repo']['html_url'] ?? '';
$providerRepositoryOwner = $pullRequestResponse['head']['repo']['owner']['login'] ?? '';
$providerCommitAuthor = $pullRequestResponse['head']['user']['login'] ?? '';
$providerCommitAuthorUrl = $pullRequestResponse['head']['user']['html_url'] ?? '';
$providerCommitMessage = $pullRequestResponse['title'] ?? '';
$providerCommitUrl = $pullRequestResponse['html_url'] ?? '';
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, '', '', '', '', $providerCommitHash, '', '', '', '', $providerPullRequestId, true, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $request, $platform);
2023-06-28 08:48:10 +00:00
$response->noContent();
});