diff --git a/app/controllers/api/vcs.php b/app/controllers/api/vcs.php index 373f413304..6571032a62 100644 --- a/app/controllers/api/vcs.php +++ b/app/controllers/api/vcs.php @@ -11,11 +11,7 @@ use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; use Appwrite\Vcs\Comment; -use Swoole\Coroutine\WaitGroup; use Utopia\CLI\Console; -use Utopia\Config\Adapters\Dotenv as ConfigDotenv; -use Utopia\Config\Config; -use Utopia\Config\Exceptions\Parse; use Utopia\Database\Database; use Utopia\Database\DateTime; use Utopia\Database\Document; @@ -25,52 +21,12 @@ use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; -use Utopia\Database\Validator\Queries; -use Utopia\Database\Validator\Query\Limit; -use Utopia\Database\Validator\Query\Offset; -use Utopia\Detector\Detection\Framework\Analog; -use Utopia\Detector\Detection\Framework\Angular; -use Utopia\Detector\Detection\Framework\Astro; -use Utopia\Detector\Detection\Framework\Flutter; -use Utopia\Detector\Detection\Framework\Lynx; -use Utopia\Detector\Detection\Framework\NextJs; -use Utopia\Detector\Detection\Framework\Nuxt; -use Utopia\Detector\Detection\Framework\React; -use Utopia\Detector\Detection\Framework\ReactNative; -use Utopia\Detector\Detection\Framework\Remix; -use Utopia\Detector\Detection\Framework\Svelte; -use Utopia\Detector\Detection\Framework\SvelteKit; -use Utopia\Detector\Detection\Framework\TanStackStart; -use Utopia\Detector\Detection\Framework\Vue; -use Utopia\Detector\Detection\Packager\NPM; -use Utopia\Detector\Detection\Packager\PNPM; -use Utopia\Detector\Detection\Packager\Yarn; -use Utopia\Detector\Detection\Runtime\Bun; -use Utopia\Detector\Detection\Runtime\CPP; -use Utopia\Detector\Detection\Runtime\Dart; -use Utopia\Detector\Detection\Runtime\Deno; -use Utopia\Detector\Detection\Runtime\Dotnet; -use Utopia\Detector\Detection\Runtime\Java; -use Utopia\Detector\Detection\Runtime\Node; -use Utopia\Detector\Detection\Runtime\PHP; -use Utopia\Detector\Detection\Runtime\Python; -use Utopia\Detector\Detection\Runtime\Ruby; -use Utopia\Detector\Detection\Runtime\Swift; -use Utopia\Detector\Detector\Framework; -use Utopia\Detector\Detector\Packager; -use Utopia\Detector\Detector\Runtime; -use Utopia\Detector\Detector\Strategy; use Utopia\Http; use Utopia\System\System; -use Utopia\Validator\Boolean; use Utopia\Validator\Text; -use Utopia\Validator\WhiteList; use Utopia\VCS\Adapter\Git\GitHub; -use Utopia\VCS\Exception\FileNotFound; use Utopia\VCS\Exception\RepositoryNotFound; -use function Swoole\Coroutine\batch; - $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) { $errors = []; foreach ($repositories as $repository) { @@ -705,765 +661,6 @@ Http::get('/v1/vcs/github/callback') ->redirect($redirectSuccess); }); -Http::get('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId/contents') - ->desc('Get files and directories of a VCS repository') - ->groups(['api', 'vcs']) - ->label('scope', 'vcs.read') - ->label('sdk', new Method( - namespace: 'vcs', - group: 'repositories', - name: 'getRepositoryContents', - description: '/docs/references/vcs/get-repository-contents.md', - auth: [AuthType::ADMIN], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_VCS_CONTENT_LIST, - ) - ] - )) - ->param('installationId', '', new Text(256), 'Installation Id') - ->param('providerRepositoryId', '', new Text(256), 'Repository Id') - ->param('providerRootDirectory', '', new Text(256, 0), 'Path to get contents of nested directory', true) - ->param('providerReference', '', new Text(256, 0), 'Git reference (branch, tag, commit) to get contents from', true) - ->inject('gitHub') - ->inject('response') - ->inject('project') - ->inject('dbForPlatform') - ->action(function (string $installationId, string $providerRepositoryId, string $providerRootDirectory, string $providerReference, GitHub $github, Response $response, Document $project, Database $dbForPlatform) { - $installation = $dbForPlatform->getDocument('installations', $installationId); - - if ($installation->isEmpty()) { - throw new Exception(Exception::INSTALLATION_NOT_FOUND); - } - - $providerInstallationId = $installation->getAttribute('providerInstallationId'); - $privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY'); - $githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID'); - $github->initializeVariables($providerInstallationId, $privateKey, $githubAppId); - - $owner = $github->getOwnerName($providerInstallationId); - 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); - } - - $contents = $github->listRepositoryContents($owner, $repositoryName, $providerRootDirectory, $providerReference); - - $vcsContents = []; - foreach ($contents as $content) { - $isDirectory = false; - if ($content['type'] === GitHub::CONTENTS_DIRECTORY) { - $isDirectory = true; - } - - $vcsContents[] = new Document([ - 'isDirectory' => $isDirectory, - 'name' => $content['name'] ?? '', - 'size' => $content['size'] ?? 0 - ]); - } - - $response->dynamic(new Document([ - 'contents' => $vcsContents - ]), Response::MODEL_VCS_CONTENT_LIST); - }); - -Http::post('/v1/vcs/github/installations/:installationId/detections') - ->alias('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId/detection') - ->desc('Create repository detection') - ->groups(['api', 'vcs']) - ->label('scope', 'vcs.write') - ->label('sdk', new Method( - namespace: 'vcs', - group: 'repositories', - name: 'createRepositoryDetection', - description: '/docs/references/vcs/create-repository-detection.md', - auth: [AuthType::ADMIN], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_DETECTION_RUNTIME, - ), - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_DETECTION_FRAMEWORK, - ) - ] - )) - ->param('installationId', '', new Text(256), 'Installation Id') - ->param('providerRepositoryId', '', new Text(256), 'Repository Id') - ->param('type', '', new WhiteList(['runtime', 'framework']), 'Detector type. Must be one of the following: runtime, framework') - ->param('providerRootDirectory', '', new Text(256, 0), 'Path to Root Directory', true) - ->inject('gitHub') - ->inject('response') - ->inject('dbForPlatform') - ->action(function (string $installationId, string $providerRepositoryId, string $type, string $providerRootDirectory, GitHub $github, Response $response, Database $dbForPlatform) { - $installation = $dbForPlatform->getDocument('installations', $installationId); - - if ($installation->isEmpty()) { - throw new Exception(Exception::INSTALLATION_NOT_FOUND); - } - - $providerInstallationId = $installation->getAttribute('providerInstallationId'); - $privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY'); - $githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID'); - $github->initializeVariables($providerInstallationId, $privateKey, $githubAppId); - - $owner = $github->getOwnerName($providerInstallationId); - 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); - } - - $files = $github->listRepositoryContents($owner, $repositoryName, $providerRootDirectory); - $files = \array_column($files, 'name'); - $languages = $github->listRepositoryLanguages($owner, $repositoryName); - - $detector = new Packager(); - foreach ($files as $file) { - $detector->addInput($file); - } - $detector - ->addOption(new Yarn()) - ->addOption(new PNPM()) - ->addOption(new NPM()); - $detection = $detector->detect(); - - $packager = !\is_null($detection) ? $detection->getName() : 'npm'; - - if ($type === 'framework') { - $packages = ''; - try { - $contentResponse = $github->getRepositoryContent($owner, $repositoryName, \rtrim($providerRootDirectory, '/') . '/package.json'); - $packages = $contentResponse['content'] ?? ''; - } catch (FileNotFound $e) { - // Continue detection without package.json - } - - $output = new Document([ - 'framework' => '', - 'installCommand' => '', - 'buildCommand' => '', - 'outputDirectory' => '', - ]); - - $detector = new Framework($packager); - $detector->addInput($packages, Framework::INPUT_PACKAGES); - foreach ($files as $file) { - $detector->addInput($file, Framework::INPUT_FILE); - } - - $detector - ->addOption(new Analog()) - ->addOption(new Angular()) - ->addOption(new Astro()) - ->addOption(new Flutter()) - ->addOption(new Lynx()) - ->addOption(new NextJs()) - ->addOption(new Nuxt()) - ->addOption(new React()) - ->addOption(new ReactNative()) - ->addOption(new Remix()) - ->addOption(new Svelte()) - ->addOption(new SvelteKit()) - ->addOption(new TanStackStart()) - ->addOption(new Vue()); - - $framework = $detector->detect(); - - if (!\is_null($framework)) { - $output->setAttribute('installCommand', $framework->getInstallCommand()); - $output->setAttribute('buildCommand', $framework->getBuildCommand()); - $output->setAttribute('outputDirectory', $framework->getOutputDirectory()); - $framework = $framework->getName(); - } else { - $framework = 'other'; - $output->setAttribute('installCommand', ''); - $output->setAttribute('buildCommand', ''); - $output->setAttribute('outputDirectory', ''); - } - - $frameworks = Config::getParam('frameworks'); - if (!\in_array($framework, \array_keys($frameworks), true)) { - $framework = 'other'; - } - $output->setAttribute('framework', $framework); - } else { - $output = new Document([ - 'runtime' => '', - 'commands' => '', - 'entrypoint' => '', - ]); - - $strategies = [ - new Strategy(Strategy::FILEMATCH), - new Strategy(Strategy::LANGUAGES), - new Strategy(Strategy::EXTENSION), - ]; - - foreach ($strategies as $strategy) { - $detector = new Runtime($strategy, $packager); - - if ($strategy === Strategy::LANGUAGES) { - foreach ($languages as $language) { - $detector->addInput($language); - } - } else { - foreach ($files as $file) { - $detector->addInput($file); - } - } - - $detector - ->addOption(new Node()) - ->addOption(new Bun()) - ->addOption(new Deno()) - ->addOption(new PHP()) - ->addOption(new Python()) - ->addOption(new Dart()) - ->addOption(new Swift()) - ->addOption(new Ruby()) - ->addOption(new Java()) - ->addOption(new CPP()) - ->addOption(new Dotnet()); - - $runtime = $detector->detect(); - - if (!\is_null($runtime)) { - $output->setAttribute('commands', $runtime->getCommands()); - $output->setAttribute('entrypoint', $runtime->getEntrypoint()); - $runtime = $runtime->getName(); - break; - } - } - - if (!empty($runtime)) { - $runtimes = Config::getParam('runtimes'); - $runtimeWithVersion = ''; - foreach ($runtimes as $runtimeKey => $runtimeConfig) { - if ($runtimeConfig['key'] === $runtime) { - $runtimeWithVersion = $runtimeKey; - } - } - - if (empty($runtimeWithVersion)) { - throw new Exception(Exception::FUNCTION_RUNTIME_NOT_DETECTED); - } - - $output->setAttribute('runtime', $runtimeWithVersion); - } else { - throw new Exception(Exception::FUNCTION_RUNTIME_NOT_DETECTED); - } - } - - $wg = new WaitGroup(); - $envs = []; - foreach ($files as $file) { - if (!(\str_starts_with($file, '.env'))) { - continue; - } - - $wg->add(); - go(function () use ($github, $owner, $repositoryName, $providerRootDirectory, $file, $wg, &$envs) { - try { - $contentResponse = $github->getRepositoryContent($owner, $repositoryName, \rtrim($providerRootDirectory, '/') . '/' . $file); - $envFile = $contentResponse['content'] ?? ''; - - $configAdapter = new ConfigDotenv(); - try { - $envObject = $configAdapter->parse($envFile); - foreach ($envObject as $envName => $envValue) { - $envs[$envName] = $envValue; - } - } catch (Parse $err) { - // Silence error, so rest of endpoint can return - } - } finally { - $wg->done(); - } - }); - } - $wg->wait(); - - $variables = []; - foreach ($envs as $key => $value) { - $variables[] = [ - 'name' => $key, - 'value' => $value, - ]; - } - - $output->setAttribute('variables', $variables); - - $response->dynamic($output, $type === 'framework' ? Response::MODEL_DETECTION_FRAMEWORK : Response::MODEL_DETECTION_RUNTIME); - }); - -Http::get('/v1/vcs/github/installations/:installationId/providerRepositories') - ->desc('List repositories') - ->groups(['api', 'vcs']) - ->label('scope', 'vcs.read') - ->label('sdk', new Method( - namespace: 'vcs', - group: 'repositories', - name: 'listRepositories', - description: '/docs/references/vcs/list-repositories.md', - auth: [AuthType::ADMIN], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_PROVIDER_REPOSITORY_RUNTIME_LIST, - ), - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_PROVIDER_REPOSITORY_FRAMEWORK_LIST, - ) - ] - )) - ->param('installationId', '', new Text(256), 'Installation Id') - ->param('type', '', new WhiteList(['runtime', 'framework']), 'Detector type. Must be one of the following: runtime, framework') - ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true) - ->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true) - ->inject('gitHub') - ->inject('response') - ->inject('dbForPlatform') - ->action(function (string $installationId, string $type, string $search, array $queries, GitHub $github, Response $response, Database $dbForPlatform) { - if (empty($search)) { - $search = ""; - } - - $installation = $dbForPlatform->getDocument('installations', $installationId); - - if ($installation->isEmpty()) { - throw new Exception(Exception::INSTALLATION_NOT_FOUND); - } - - $providerInstallationId = $installation->getAttribute('providerInstallationId'); - $privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY'); - $githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID'); - $github->initializeVariables($providerInstallationId, $privateKey, $githubAppId); - - $queries = Query::parseQueries($queries); - $limitQuery = current(array_filter($queries, fn ($query) => $query->getMethod() === Query::TYPE_LIMIT)); - $offsetQuery = current(array_filter($queries, fn ($query) => $query->getMethod() === Query::TYPE_OFFSET)); - - $limit = !empty($limitQuery) ? $limitQuery->getValue() : 4; - $offset = !empty($offsetQuery) ? $offsetQuery->getValue() : 0; - - if ($offset % $limit !== 0) { - throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'offset must be a multiple of the limit'); - } - - $page = ($offset / $limit) + 1; - $owner = $github->getOwnerName($providerInstallationId); - ['items' => $repos, 'total' => $total] = $github->searchRepositories($owner, $page, $limit, $search); - - $repos = \array_map(function ($repo) use ($installation) { - $repo['id'] = \strval($repo['id'] ?? ''); - $repo['pushedAt'] = $repo['pushed_at'] ?? null; - $repo['provider'] = $installation->getAttribute('provider', '') ?? ''; - $repo['organization'] = $installation->getAttribute('organization', '') ?? ''; - return $repo; - }, $repos); - - $repos = batch(\array_map(function ($repo) use ($type, $github) { - return function () use ($repo, $type, $github) { - $files = $github->listRepositoryContents($repo['organization'], $repo['name'], ''); - $files = \array_column($files, 'name'); - - $detector = new Packager(); - foreach ($files as $file) { - $detector->addInput($file); - } - $detector - ->addOption(new Yarn()) - ->addOption(new PNPM()) - ->addOption(new NPM()); - $detection = $detector->detect(); - - $packager = !\is_null($detection) ? $detection->getName() : 'npm'; - - if ($type === 'framework') { - $packages = ''; - try { - $contentResponse = $github->getRepositoryContent($repo['organization'], $repo['name'], 'package.json'); - $packages = $contentResponse['content'] ?? ''; - } catch (FileNotFound $e) { - // Continue detection without package.json - } - - $frameworkDetector = new Framework($packager); - $frameworkDetector->addInput($packages, Framework::INPUT_PACKAGES); - foreach ($files as $file) { - $frameworkDetector->addInput($file, Framework::INPUT_FILE); - } - - $frameworkDetector - ->addOption(new Analog()) - ->addOption(new Angular()) - ->addOption(new Astro()) - ->addOption(new Flutter()) - ->addOption(new Lynx()) - ->addOption(new NextJs()) - ->addOption(new Nuxt()) - ->addOption(new React()) - ->addOption(new ReactNative()) - ->addOption(new Remix()) - ->addOption(new Svelte()) - ->addOption(new SvelteKit()) - ->addOption(new TanStackStart()) - ->addOption(new Vue()); - - $detectedFramework = $frameworkDetector->detect(); - - if (!\is_null($detectedFramework)) { - $framework = $detectedFramework->getName(); - } else { - $framework = 'other'; - } - - $frameworks = Config::getParam('frameworks'); - if (!\in_array($framework, \array_keys($frameworks), true)) { - $framework = 'other'; - } - $repo['framework'] = $framework; - } else { - $languages = $github->listRepositoryLanguages($repo['organization'], $repo['name']); - - $strategies = [ - new Strategy(Strategy::FILEMATCH), - new Strategy(Strategy::LANGUAGES), - new Strategy(Strategy::EXTENSION), - ]; - - foreach ($strategies as $strategy) { - $detector = new Runtime($strategy, $packager); - if ($strategy === Strategy::LANGUAGES) { - foreach ($languages as $language) { - $detector->addInput($language); - } - } else { - foreach ($files as $file) { - $detector->addInput($file); - } - } - $detector - ->addOption(new Node()) - ->addOption(new Bun()) - ->addOption(new Deno()) - ->addOption(new PHP()) - ->addOption(new Python()) - ->addOption(new Dart()) - ->addOption(new Swift()) - ->addOption(new Ruby()) - ->addOption(new Java()) - ->addOption(new CPP()) - ->addOption(new Dotnet()); - - $runtime = $detector->detect(); - - if (!\is_null($runtime)) { - $runtime = $runtime->getName(); - break; - } - } - - if (!empty($runtime)) { - $runtimes = Config::getParam('runtimes'); - $runtimeWithVersion = ''; - foreach ($runtimes as $runtimeKey => $runtimeConfig) { - if ($runtimeConfig['key'] === $runtime) { - $runtimeWithVersion = $runtimeKey; - } - } - - $repo['runtime'] = $runtimeWithVersion ?? ''; - } - } - - $wg = new WaitGroup(); - $envs = []; - foreach ($files as $file) { - if (!(\str_starts_with($file, '.env'))) { - continue; - } - - $wg->add(); - go(function () use ($github, $repo, $file, $wg, &$envs) { - try { - $contentResponse = $github->getRepositoryContent($repo['organization'], $repo['name'], $file); - $envFile = $contentResponse['content'] ?? ''; - - $configAdapter = new ConfigDotenv(); - try { - $envObject = $configAdapter->parse($envFile); - foreach ($envObject as $envName => $envValue) { - $envs[$envName] = $envValue; - } - } catch (Parse) { - // Silence error, so rest of endpoint can return - } - } finally { - $wg->done(); - } - }); - } - $wg->wait(); - - $repo['variables'] = []; - foreach ($envs as $key => $value) { - $repo['variables'][] = [ - 'name' => $key, - 'value' => $value, - ]; - } - - return $repo; - }; - }, $repos)); - - $repos = \array_map(function ($repo) { - return new Document($repo); - }, $repos); - - $response->dynamic(new Document([ - $type === 'framework' ? 'frameworkProviderRepositories' : 'runtimeProviderRepositories' => $repos, - 'total' => $total, - ]), ($type === 'framework') ? Response::MODEL_PROVIDER_REPOSITORY_FRAMEWORK_LIST : Response::MODEL_PROVIDER_REPOSITORY_RUNTIME_LIST); - }); - -Http::post('/v1/vcs/github/installations/:installationId/providerRepositories') - ->desc('Create repository') - ->groups(['api', 'vcs']) - ->label('scope', 'vcs.write') - ->label('sdk', new Method( - namespace: 'vcs', - group: 'repositories', - name: 'createRepository', - description: '/docs/references/vcs/create-repository.md', - auth: [AuthType::ADMIN], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_PROVIDER_REPOSITORY, - ) - ] - )) - ->param('installationId', '', new Text(256), 'Installation Id') - ->param('name', '', new Text(256), 'Repository name (slug)') - ->param('private', '', new Boolean(false), 'Mark repository public or private') - ->inject('gitHub') - ->inject('user') - ->inject('response') - ->inject('project') - ->inject('dbForPlatform') - ->action(function (string $installationId, string $name, bool $private, GitHub $github, Document $user, Response $response, Document $project, Database $dbForPlatform) { - $installation = $dbForPlatform->getDocument('installations', $installationId); - - if ($installation->isEmpty()) { - throw new Exception(Exception::INSTALLATION_NOT_FOUND); - } - - if ($installation->getAttribute('personal', false) === true) { - $oauth2 = new OAuth2Github(System::getEnv('_APP_VCS_GITHUB_CLIENT_ID', ''), System::getEnv('_APP_VCS_GITHUB_CLIENT_SECRET', ''), ""); - - $accessToken = $installation->getAttribute('personalAccessToken'); - $refreshToken = $installation->getAttribute('personalRefreshToken'); - $accessTokenExpiry = $installation->getAttribute('personalAccessTokenExpiry'); - - if (empty($accessToken) || empty($refreshToken) || empty($accessTokenExpiry)) { - $identity = $dbForPlatform->findOne('identities', [ - Query::equal('provider', ['github']), - Query::equal('userInternalId', [$user->getSequence()]), - ]); - if ($identity->isEmpty()) { - throw new Exception(Exception::USER_IDENTITY_NOT_FOUND); - } - - $accessToken = $accessToken ?? $identity->getAttribute('providerAccessToken'); - $refreshToken = $refreshToken ?? $identity->getAttribute('providerRefreshToken'); - $accessTokenExpiry = $accessTokenExpiry ?? $identity->getAttribute('providerAccessTokenExpiry'); - } - - $isExpired = new \DateTime($accessTokenExpiry) < new \DateTime('now'); - if ($isExpired) { - $oauth2->refreshTokens($refreshToken); - - $accessToken = $oauth2->getAccessToken(''); - $refreshToken = $oauth2->getRefreshToken(''); - - $verificationId = $oauth2->getUserID($accessToken); - - if (empty($verificationId)) { - throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, "Another request is currently refreshing OAuth token. Please try again."); - } - - $installation = $installation - ->setAttribute('personalAccessToken', $accessToken) - ->setAttribute('personalRefreshToken', $refreshToken) - ->setAttribute('personalAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int)$oauth2->getAccessTokenExpiry(''))); - - $dbForPlatform->updateDocument('installations', $installation->getId(), $installation); - } - - try { - $repository = $oauth2->createRepository($accessToken, $name, $private); - } catch (Exception $exception) { - throw new Exception(Exception::GENERAL_PROVIDER_FAILURE, "GitHub failed to process the request: " . $exception->getMessage()); - } - } else { - $providerInstallationId = $installation->getAttribute('providerInstallationId'); - $privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY'); - $githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID'); - $github->initializeVariables($providerInstallationId, $privateKey, $githubAppId); - $owner = $github->getOwnerName($providerInstallationId); - - try { - $repository = $github->createRepository($owner, $name, $private); - } catch (Exception $exception) { - throw new Exception(Exception::GENERAL_PROVIDER_FAILURE, "GitHub failed to process the request: " . $exception->getMessage()); - } - } - - if (isset($repository['errors'])) { - $message = $repository['message'] ?? 'Unknown error.'; - if (isset($repository['errors'][0])) { - $message .= ' ' . $repository['errors'][0]['message']; - } - throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Provider Error: ' . $message); - } - - if (isset($repository['message'])) { - throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Provider Error: ' . $repository['message']); - } - - $repository['id'] = \strval($repository['id']) ?? ''; - $repository['pushedAt'] = $repository['pushed_at'] ?? ''; - $repository['organization'] = $installation->getAttribute('organization', ''); - $repository['provider'] = $installation->getAttribute('provider', ''); - - $response->dynamic(new Document($repository), Response::MODEL_PROVIDER_REPOSITORY); - }); - -Http::get('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId') - ->desc('Get repository') - ->groups(['api', 'vcs']) - ->label('scope', 'vcs.read') - ->label('sdk', new Method( - namespace: 'vcs', - group: 'repositories', - name: 'getRepository', - description: '/docs/references/vcs/get-repository.md', - auth: [AuthType::ADMIN], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_PROVIDER_REPOSITORY, - ) - ] - )) - ->param('installationId', '', new Text(256), 'Installation Id') - ->param('providerRepositoryId', '', new Text(256), 'Repository Id') - ->inject('gitHub') - ->inject('response') - ->inject('project') - ->inject('dbForPlatform') - ->action(function (string $installationId, string $providerRepositoryId, GitHub $github, Response $response, Document $project, Database $dbForPlatform) { - $installation = $dbForPlatform->getDocument('installations', $installationId); - - if ($installation->isEmpty()) { - throw new Exception(Exception::INSTALLATION_NOT_FOUND); - } - - $providerInstallationId = $installation->getAttribute('providerInstallationId'); - $privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY'); - $githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID'); - $github->initializeVariables($providerInstallationId, $privateKey, $githubAppId); - - $owner = $github->getOwnerName($providerInstallationId) ?? ''; - 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); - } - - $repository = $github->getRepository($owner, $repositoryName); - - $repository['id'] = \strval($repository['id']) ?? ''; - $repository['pushedAt'] = $repository['pushed_at'] ?? ''; - $repository['organization'] = $installation->getAttribute('organization', ''); - $repository['provider'] = $installation->getAttribute('provider', ''); - $repository['defaultBranch'] = $repository['default_branch'] ?? ''; - - $response->dynamic(new Document($repository), Response::MODEL_PROVIDER_REPOSITORY); - }); - -Http::get('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId/branches') - ->desc('List repository branches') - ->groups(['api', 'vcs']) - ->label('scope', 'vcs.read') - ->label('sdk', new Method( - namespace: 'vcs', - group: 'repositories', - name: 'listRepositoryBranches', - description: '/docs/references/vcs/list-repository-branches.md', - auth: [AuthType::ADMIN], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_BRANCH_LIST, - ) - ] - )) - ->param('installationId', '', new Text(256), 'Installation Id') - ->param('providerRepositoryId', '', new Text(256), 'Repository Id') - ->inject('gitHub') - ->inject('response') - ->inject('project') - ->inject('dbForPlatform') - ->action(function (string $installationId, string $providerRepositoryId, GitHub $github, Response $response, Document $project, Database $dbForPlatform) { - $installation = $dbForPlatform->getDocument('installations', $installationId); - - if ($installation->isEmpty()) { - throw new Exception(Exception::INSTALLATION_NOT_FOUND); - } - - $providerInstallationId = $installation->getAttribute('providerInstallationId'); - $privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY'); - $githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID'); - $github->initializeVariables($providerInstallationId, $privateKey, $githubAppId); - - $owner = $github->getOwnerName($providerInstallationId) ?? ''; - 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); - } - - $branches = $github->listBranches($owner, $repositoryName) ?? []; - - $response->dynamic(new Document([ - 'branches' => \array_map(function ($branch) { - return new Document(['name' => $branch]); - }, $branches), - 'total' => \count($branches), - ]), Response::MODEL_BRANCH_LIST); - }); - Http::post('/v1/vcs/github/events') ->desc('Create event') ->groups(['api', 'vcs']) diff --git a/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Branches/XList.php b/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Branches/XList.php new file mode 100644 index 0000000000..4ed4241d25 --- /dev/null +++ b/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Branches/XList.php @@ -0,0 +1,95 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId/branches') + ->desc('List repository branches') + ->groups(['api', 'vcs']) + ->label('scope', 'vcs.read') + ->label('resourceType', RESOURCE_TYPE_VCS) + ->label('sdk', new Method( + namespace: 'vcs', + group: 'repositories', + name: 'listRepositoryBranches', + description: '/docs/references/vcs/list-repository-branches.md', + auth: [AuthType::ADMIN], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_OK, + model: Response::MODEL_BRANCH_LIST, + ) + ] + )) + ->param('installationId', '', new Text(256), 'Installation Id') + ->param('providerRepositoryId', '', new Text(256), 'Repository Id') + ->inject('gitHub') + ->inject('response') + ->inject('dbForPlatform') + ->callback($this->action(...)); + } + + public function action( + string $installationId, + string $providerRepositoryId, + GitHub $github, + Response $response, + Database $dbForPlatform + ) { + $installation = $dbForPlatform->getDocument('installations', $installationId); + + if ($installation->isEmpty()) { + throw new Exception(Exception::INSTALLATION_NOT_FOUND); + } + + $providerInstallationId = $installation->getAttribute('providerInstallationId'); + $privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY'); + $githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID'); + $github->initializeVariables($providerInstallationId, $privateKey, $githubAppId); + + $owner = $github->getOwnerName($providerInstallationId) ?? ''; + 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); + } + + $branches = $github->listBranches($owner, $repositoryName) ?? []; + + $response->dynamic(new Document([ + 'branches' => \array_map(function ($branch) { + return new Document(['name' => $branch]); + }, $branches), + 'total' => \count($branches), + ]), Response::MODEL_BRANCH_LIST); + } +} diff --git a/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Contents/Get.php b/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Contents/Get.php new file mode 100644 index 0000000000..a0dcec8590 --- /dev/null +++ b/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Contents/Get.php @@ -0,0 +1,110 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId/contents') + ->desc('Get files and directories of a VCS repository') + ->groups(['api', 'vcs']) + ->label('scope', 'vcs.read') + ->label('resourceType', RESOURCE_TYPE_VCS) + ->label('sdk', new Method( + namespace: 'vcs', + group: 'repositories', + name: 'getRepositoryContents', + description: '/docs/references/vcs/get-repository-contents.md', + auth: [AuthType::ADMIN], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_OK, + model: Response::MODEL_VCS_CONTENT_LIST, + ) + ] + )) + ->param('installationId', '', new Text(256), 'Installation Id') + ->param('providerRepositoryId', '', new Text(256), 'Repository Id') + ->param('providerRootDirectory', '', new Text(256, 0), 'Path to get contents of nested directory', true) + ->param('providerReference', '', new Text(256, 0), 'Git reference (branch, tag, commit) to get contents from', true) + ->inject('gitHub') + ->inject('response') + ->inject('dbForPlatform') + ->callback($this->action(...)); + } + + public function action( + string $installationId, + string $providerRepositoryId, + string $providerRootDirectory, + string $providerReference, + GitHub $github, + Response $response, + Database $dbForPlatform + ) { + $installation = $dbForPlatform->getDocument('installations', $installationId); + + if ($installation->isEmpty()) { + throw new Exception(Exception::INSTALLATION_NOT_FOUND); + } + + $providerInstallationId = $installation->getAttribute('providerInstallationId'); + $privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY'); + $githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID'); + $github->initializeVariables($providerInstallationId, $privateKey, $githubAppId); + + $owner = $github->getOwnerName($providerInstallationId); + 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); + } + + $contents = $github->listRepositoryContents($owner, $repositoryName, $providerRootDirectory, $providerReference); + + $vcsContents = []; + foreach ($contents as $content) { + $isDirectory = false; + if ($content['type'] === GitHub::CONTENTS_DIRECTORY) { + $isDirectory = true; + } + + $vcsContents[] = new Document([ + 'isDirectory' => $isDirectory, + 'name' => $content['name'] ?? '', + 'size' => $content['size'] ?? 0 + ]); + } + + $response->dynamic(new Document([ + 'contents' => $vcsContents + ]), Response::MODEL_VCS_CONTENT_LIST); + } +} diff --git a/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Create.php b/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Create.php new file mode 100644 index 0000000000..e357d1d5fa --- /dev/null +++ b/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Create.php @@ -0,0 +1,158 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) + ->setHttpPath('/v1/vcs/github/installations/:installationId/providerRepositories') + ->desc('Create repository') + ->groups(['api', 'vcs']) + ->label('scope', 'vcs.write') + ->label('resourceType', RESOURCE_TYPE_VCS) + ->label('sdk', new Method( + namespace: 'vcs', + group: 'repositories', + name: 'createRepository', + description: '/docs/references/vcs/create-repository.md', + auth: [AuthType::ADMIN], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_OK, + model: Response::MODEL_PROVIDER_REPOSITORY, + ) + ] + )) + ->param('installationId', '', new Text(256), 'Installation Id') + ->param('name', '', new Text(256), 'Repository name (slug)') + ->param('private', '', new Boolean(false), 'Mark repository public or private') + ->inject('gitHub') + ->inject('user') + ->inject('response') + ->inject('dbForPlatform') + ->callback($this->action(...)); + } + + public function action( + string $installationId, + string $name, + bool $private, + GitHub $github, + Document $user, + Response $response, + Database $dbForPlatform + ) { + $installation = $dbForPlatform->getDocument('installations', $installationId); + + if ($installation->isEmpty()) { + throw new Exception(Exception::INSTALLATION_NOT_FOUND); + } + + if ($installation->getAttribute('personal', false) === true) { + $oauth2 = new OAuth2Github(System::getEnv('_APP_VCS_GITHUB_CLIENT_ID', ''), System::getEnv('_APP_VCS_GITHUB_CLIENT_SECRET', ''), ""); + + $accessToken = $installation->getAttribute('personalAccessToken'); + $refreshToken = $installation->getAttribute('personalRefreshToken'); + $accessTokenExpiry = $installation->getAttribute('personalAccessTokenExpiry'); + + if (empty($accessToken) || empty($refreshToken) || empty($accessTokenExpiry)) { + $identity = $dbForPlatform->findOne('identities', [ + Query::equal('provider', ['github']), + Query::equal('userInternalId', [$user->getSequence()]), + ]); + if ($identity->isEmpty()) { + throw new Exception(Exception::USER_IDENTITY_NOT_FOUND); + } + + $accessToken = $accessToken ?? $identity->getAttribute('providerAccessToken'); + $refreshToken = $refreshToken ?? $identity->getAttribute('providerRefreshToken'); + $accessTokenExpiry = $accessTokenExpiry ?? $identity->getAttribute('providerAccessTokenExpiry'); + } + + $isExpired = new \DateTime($accessTokenExpiry) < new \DateTime('now'); + if ($isExpired) { + $oauth2->refreshTokens($refreshToken); + + $accessToken = $oauth2->getAccessToken(''); + $refreshToken = $oauth2->getRefreshToken(''); + + $verificationId = $oauth2->getUserID($accessToken); + + if (empty($verificationId)) { + throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, "Another request is currently refreshing OAuth token. Please try again."); + } + + $installation = $installation + ->setAttribute('personalAccessToken', $accessToken) + ->setAttribute('personalRefreshToken', $refreshToken) + ->setAttribute('personalAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int)$oauth2->getAccessTokenExpiry(''))); + + $dbForPlatform->updateDocument('installations', $installation->getId(), $installation); + } + + try { + $repository = $oauth2->createRepository($accessToken, $name, $private); + } catch (Exception $exception) { + throw new Exception(Exception::GENERAL_PROVIDER_FAILURE, "GitHub failed to process the request: " . $exception->getMessage()); + } + } else { + $providerInstallationId = $installation->getAttribute('providerInstallationId'); + $privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY'); + $githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID'); + $github->initializeVariables($providerInstallationId, $privateKey, $githubAppId); + $owner = $github->getOwnerName($providerInstallationId); + + try { + $repository = $github->createRepository($owner, $name, $private); + } catch (Exception $exception) { + throw new Exception(Exception::GENERAL_PROVIDER_FAILURE, "GitHub failed to process the request: " . $exception->getMessage()); + } + } + + if (isset($repository['errors'])) { + $message = $repository['message'] ?? 'Unknown error.'; + if (isset($repository['errors'][0])) { + $message .= ' ' . $repository['errors'][0]['message']; + } + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Provider Error: ' . $message); + } + + if (isset($repository['message'])) { + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Provider Error: ' . $repository['message']); + } + + $repository['id'] = \strval($repository['id']) ?? ''; + $repository['pushedAt'] = $repository['pushed_at'] ?? ''; + $repository['organization'] = $installation->getAttribute('organization', ''); + $repository['provider'] = $installation->getAttribute('provider', ''); + + $response->dynamic(new Document($repository), Response::MODEL_PROVIDER_REPOSITORY); + } +} diff --git a/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Detections/Create.php b/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Detections/Create.php new file mode 100644 index 0000000000..bada6d98bb --- /dev/null +++ b/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Detections/Create.php @@ -0,0 +1,315 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) + ->setHttpPath('/v1/vcs/github/installations/:installationId/detections') + ->httpAlias('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId/detection') + ->desc('Create repository detection') + ->groups(['api', 'vcs']) + ->label('scope', 'vcs.write') + ->label('resourceType', RESOURCE_TYPE_VCS) + ->label('sdk', new Method( + namespace: 'vcs', + group: 'repositories', + name: 'createRepositoryDetection', + description: '/docs/references/vcs/create-repository-detection.md', + auth: [AuthType::ADMIN], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_OK, + model: Response::MODEL_DETECTION_RUNTIME, + ), + new SDKResponse( + code: Response::STATUS_CODE_OK, + model: Response::MODEL_DETECTION_FRAMEWORK, + ) + ] + )) + ->param('installationId', '', new Text(256), 'Installation Id') + ->param('providerRepositoryId', '', new Text(256), 'Repository Id') + ->param('type', '', new WhiteList(['runtime', 'framework']), 'Detector type. Must be one of the following: runtime, framework') + ->param('providerRootDirectory', '', new Text(256, 0), 'Path to Root Directory', true) + ->inject('gitHub') + ->inject('response') + ->inject('dbForPlatform') + ->callback($this->action(...)); + } + + public function action( + string $installationId, + string $providerRepositoryId, + string $type, + string $providerRootDirectory, + GitHub $github, + Response $response, + Database $dbForPlatform + ) { + $installation = $dbForPlatform->getDocument('installations', $installationId); + + if ($installation->isEmpty()) { + throw new Exception(Exception::INSTALLATION_NOT_FOUND); + } + + $providerInstallationId = $installation->getAttribute('providerInstallationId'); + $privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY'); + $githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID'); + $github->initializeVariables($providerInstallationId, $privateKey, $githubAppId); + + $owner = $github->getOwnerName($providerInstallationId); + 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); + } + + $files = $github->listRepositoryContents($owner, $repositoryName, $providerRootDirectory); + $files = \array_column($files, 'name'); + $languages = $github->listRepositoryLanguages($owner, $repositoryName); + + $detector = new Packager(); + foreach ($files as $file) { + $detector->addInput($file); + } + $detector + ->addOption(new Yarn()) + ->addOption(new PNPM()) + ->addOption(new NPM()); + $detection = $detector->detect(); + + $packager = !\is_null($detection) ? $detection->getName() : 'npm'; + + if ($type === 'framework') { + $packages = ''; + try { + $contentResponse = $github->getRepositoryContent($owner, $repositoryName, \rtrim($providerRootDirectory, '/') . '/package.json'); + $packages = $contentResponse['content'] ?? ''; + } catch (FileNotFound $e) { + // Continue detection without package.json + } + + $output = new Document([ + 'framework' => '', + 'installCommand' => '', + 'buildCommand' => '', + 'outputDirectory' => '', + ]); + + $detector = new Framework($packager); + $detector->addInput($packages, Framework::INPUT_PACKAGES); + foreach ($files as $file) { + $detector->addInput($file, Framework::INPUT_FILE); + } + + $detector + ->addOption(new Analog()) + ->addOption(new Angular()) + ->addOption(new Astro()) + ->addOption(new Flutter()) + ->addOption(new Lynx()) + ->addOption(new NextJs()) + ->addOption(new Nuxt()) + ->addOption(new React()) + ->addOption(new ReactNative()) + ->addOption(new Remix()) + ->addOption(new Svelte()) + ->addOption(new SvelteKit()) + ->addOption(new TanStackStart()) + ->addOption(new Vue()); + + $framework = $detector->detect(); + + if (!\is_null($framework)) { + $output->setAttribute('installCommand', $framework->getInstallCommand()); + $output->setAttribute('buildCommand', $framework->getBuildCommand()); + $output->setAttribute('outputDirectory', $framework->getOutputDirectory()); + $framework = $framework->getName(); + } else { + $framework = 'other'; + $output->setAttribute('installCommand', ''); + $output->setAttribute('buildCommand', ''); + $output->setAttribute('outputDirectory', ''); + } + + $frameworks = Config::getParam('frameworks'); + if (!\in_array($framework, \array_keys($frameworks), true)) { + $framework = 'other'; + } + $output->setAttribute('framework', $framework); + } else { + $output = new Document([ + 'runtime' => '', + 'commands' => '', + 'entrypoint' => '', + ]); + + $strategies = [ + new Strategy(Strategy::FILEMATCH), + new Strategy(Strategy::LANGUAGES), + new Strategy(Strategy::EXTENSION), + ]; + + foreach ($strategies as $strategy) { + $detector = new Runtime($strategy, $packager); + + if ($strategy->getValue() === Strategy::LANGUAGES) { + foreach ($languages as $language) { + $detector->addInput($language); + } + } else { + foreach ($files as $file) { + $detector->addInput($file); + } + } + + $detector + ->addOption(new Node()) + ->addOption(new Bun()) + ->addOption(new Deno()) + ->addOption(new PHP()) + ->addOption(new Python()) + ->addOption(new Dart()) + ->addOption(new Swift()) + ->addOption(new Ruby()) + ->addOption(new Java()) + ->addOption(new CPP()) + ->addOption(new Dotnet()); + + $runtime = $detector->detect(); + + if (!\is_null($runtime)) { + $output->setAttribute('commands', $runtime->getCommands()); + $output->setAttribute('entrypoint', $runtime->getEntrypoint()); + $runtime = $runtime->getName(); + break; + } + } + + if (!empty($runtime)) { + $runtimes = Config::getParam('runtimes'); + $runtimeWithVersion = ''; + foreach ($runtimes as $runtimeKey => $runtimeConfig) { + if ($runtimeConfig['key'] === $runtime) { + $runtimeWithVersion = $runtimeKey; + } + } + + if (empty($runtimeWithVersion)) { + throw new Exception(Exception::FUNCTION_RUNTIME_NOT_DETECTED); + } + + $output->setAttribute('runtime', $runtimeWithVersion); + } else { + throw new Exception(Exception::FUNCTION_RUNTIME_NOT_DETECTED); + } + } + + $wg = new WaitGroup(); + $envs = []; + foreach ($files as $file) { + if (!(\str_starts_with($file, '.env'))) { + continue; + } + + $wg->add(); + go(function () use ($github, $owner, $repositoryName, $providerRootDirectory, $file, $wg, &$envs) { + try { + $contentResponse = $github->getRepositoryContent($owner, $repositoryName, \rtrim($providerRootDirectory, '/') . '/' . $file); + $envFile = $contentResponse['content'] ?? ''; + + $configAdapter = new ConfigDotenv(); + try { + $envObject = $configAdapter->parse($envFile); + foreach ($envObject as $envName => $envValue) { + $envs[$envName] = $envValue; + } + } catch (Parse $err) { + // Silence error, so rest of endpoint can return + } + } finally { + $wg->done(); + } + }); + } + $wg->wait(); + + $variables = []; + foreach ($envs as $key => $value) { + $variables[] = [ + 'name' => $key, + 'value' => $value, + ]; + } + + $output->setAttribute('variables', $variables); + + $response->dynamic($output, $type === 'framework' ? Response::MODEL_DETECTION_FRAMEWORK : Response::MODEL_DETECTION_RUNTIME); + } +} diff --git a/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Get.php b/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Get.php new file mode 100644 index 0000000000..4dee84752b --- /dev/null +++ b/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Get.php @@ -0,0 +1,96 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId') + ->desc('Get repository') + ->groups(['api', 'vcs']) + ->label('scope', 'vcs.read') + ->label('resourceType', RESOURCE_TYPE_VCS) + ->label('sdk', new Method( + namespace: 'vcs', + group: 'repositories', + name: 'getRepository', + description: '/docs/references/vcs/get-repository.md', + auth: [AuthType::ADMIN], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_OK, + model: Response::MODEL_PROVIDER_REPOSITORY, + ) + ] + )) + ->param('installationId', '', new Text(256), 'Installation Id') + ->param('providerRepositoryId', '', new Text(256), 'Repository Id') + ->inject('gitHub') + ->inject('response') + ->inject('dbForPlatform') + ->callback($this->action(...)); + } + + public function action( + string $installationId, + string $providerRepositoryId, + GitHub $github, + Response $response, + Database $dbForPlatform + ) { + $installation = $dbForPlatform->getDocument('installations', $installationId); + + if ($installation->isEmpty()) { + throw new Exception(Exception::INSTALLATION_NOT_FOUND); + } + + $providerInstallationId = $installation->getAttribute('providerInstallationId'); + $privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY'); + $githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID'); + $github->initializeVariables($providerInstallationId, $privateKey, $githubAppId); + + $owner = $github->getOwnerName($providerInstallationId) ?? ''; + 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); + } + + $repository = $github->getRepository($owner, $repositoryName); + + $repository['id'] = \strval($repository['id']) ?? ''; + $repository['pushedAt'] = $repository['pushed_at'] ?? ''; + $repository['organization'] = $installation->getAttribute('organization', ''); + $repository['provider'] = $installation->getAttribute('provider', ''); + $repository['defaultBranch'] = $repository['default_branch'] ?? ''; + + $response->dynamic(new Document($repository), Response::MODEL_PROVIDER_REPOSITORY); + } +} diff --git a/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/XList.php b/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/XList.php new file mode 100644 index 0000000000..9acc77e1f2 --- /dev/null +++ b/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/XList.php @@ -0,0 +1,319 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/vcs/github/installations/:installationId/providerRepositories') + ->desc('List repositories') + ->groups(['api', 'vcs']) + ->label('scope', 'vcs.read') + ->label('resourceType', RESOURCE_TYPE_VCS) + ->label('sdk', new Method( + namespace: 'vcs', + group: 'repositories', + name: 'listRepositories', + description: '/docs/references/vcs/list-repositories.md', + auth: [AuthType::ADMIN], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_OK, + model: Response::MODEL_PROVIDER_REPOSITORY_RUNTIME_LIST, + ), + new SDKResponse( + code: Response::STATUS_CODE_OK, + model: Response::MODEL_PROVIDER_REPOSITORY_FRAMEWORK_LIST, + ) + ] + )) + ->param('installationId', '', new Text(256), 'Installation Id') + ->param('type', '', new WhiteList(['runtime', 'framework']), 'Detector type. Must be one of the following: runtime, framework') + ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true) + ->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true) + ->inject('gitHub') + ->inject('response') + ->inject('dbForPlatform') + ->callback($this->action(...)); + } + + public function action( + string $installationId, + string $type, + string $search, + array $queries, + GitHub $github, + Response $response, + Database $dbForPlatform + ) { + if (empty($search)) { + $search = ""; + } + + $installation = $dbForPlatform->getDocument('installations', $installationId); + + if ($installation->isEmpty()) { + throw new Exception(Exception::INSTALLATION_NOT_FOUND); + } + + $providerInstallationId = $installation->getAttribute('providerInstallationId'); + $privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY'); + $githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID'); + $github->initializeVariables($providerInstallationId, $privateKey, $githubAppId); + + $queries = Query::parseQueries($queries); + $limitQuery = current(array_filter($queries, fn ($query) => $query->getMethod() === Query::TYPE_LIMIT)); + $offsetQuery = current(array_filter($queries, fn ($query) => $query->getMethod() === Query::TYPE_OFFSET)); + + $limit = !empty($limitQuery) ? $limitQuery->getValue() : 4; + $offset = !empty($offsetQuery) ? $offsetQuery->getValue() : 0; + + if ($offset % $limit !== 0) { + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'offset must be a multiple of the limit'); + } + + $page = ($offset / $limit) + 1; + $owner = $github->getOwnerName($providerInstallationId); + ['items' => $repos, 'total' => $total] = $github->searchRepositories($owner, $page, $limit, $search); + + $repos = \array_map(function ($repo) use ($installation) { + $repo['id'] = \strval($repo['id'] ?? ''); + $repo['pushedAt'] = $repo['pushed_at'] ?? null; + $repo['provider'] = $installation->getAttribute('provider', '') ?? ''; + $repo['organization'] = $installation->getAttribute('organization', '') ?? ''; + return $repo; + }, $repos); + + $repos = batch(\array_map(function ($repo) use ($type, $github) { + return function () use ($repo, $type, $github) { + $files = $github->listRepositoryContents($repo['organization'], $repo['name'], ''); + $files = \array_column($files, 'name'); + + $detector = new Packager(); + foreach ($files as $file) { + $detector->addInput($file); + } + $detector + ->addOption(new Yarn()) + ->addOption(new PNPM()) + ->addOption(new NPM()); + $detection = $detector->detect(); + + $packager = !\is_null($detection) ? $detection->getName() : 'npm'; + + if ($type === 'framework') { + $packages = ''; + try { + $contentResponse = $github->getRepositoryContent($repo['organization'], $repo['name'], 'package.json'); + $packages = $contentResponse['content'] ?? ''; + } catch (FileNotFound $e) { + // Continue detection without package.json + } + + $frameworkDetector = new Framework($packager); + $frameworkDetector->addInput($packages, Framework::INPUT_PACKAGES); + foreach ($files as $file) { + $frameworkDetector->addInput($file, Framework::INPUT_FILE); + } + + $frameworkDetector + ->addOption(new Analog()) + ->addOption(new Angular()) + ->addOption(new Astro()) + ->addOption(new Flutter()) + ->addOption(new Lynx()) + ->addOption(new NextJs()) + ->addOption(new Nuxt()) + ->addOption(new React()) + ->addOption(new ReactNative()) + ->addOption(new Remix()) + ->addOption(new Svelte()) + ->addOption(new SvelteKit()) + ->addOption(new TanStackStart()) + ->addOption(new Vue()); + + $detectedFramework = $frameworkDetector->detect(); + + if (!\is_null($detectedFramework)) { + $framework = $detectedFramework->getName(); + } else { + $framework = 'other'; + } + + $frameworks = Config::getParam('frameworks'); + if (!\in_array($framework, \array_keys($frameworks), true)) { + $framework = 'other'; + } + $repo['framework'] = $framework; + } else { + $languages = $github->listRepositoryLanguages($repo['organization'], $repo['name']); + + $strategies = [ + new Strategy(Strategy::FILEMATCH), + new Strategy(Strategy::LANGUAGES), + new Strategy(Strategy::EXTENSION), + ]; + + foreach ($strategies as $strategy) { + $detector = new Runtime($strategy, $packager); + if ($strategy->getValue() === Strategy::LANGUAGES) { + foreach ($languages as $language) { + $detector->addInput($language); + } + } else { + foreach ($files as $file) { + $detector->addInput($file); + } + } + $detector + ->addOption(new Node()) + ->addOption(new Bun()) + ->addOption(new Deno()) + ->addOption(new PHP()) + ->addOption(new Python()) + ->addOption(new Dart()) + ->addOption(new Swift()) + ->addOption(new Ruby()) + ->addOption(new Java()) + ->addOption(new CPP()) + ->addOption(new Dotnet()); + + $runtime = $detector->detect(); + + if (!\is_null($runtime)) { + $runtime = $runtime->getName(); + break; + } + } + + if (!empty($runtime)) { + $runtimes = Config::getParam('runtimes'); + $runtimeWithVersion = ''; + foreach ($runtimes as $runtimeKey => $runtimeConfig) { + if ($runtimeConfig['key'] === $runtime) { + $runtimeWithVersion = $runtimeKey; + } + } + + $repo['runtime'] = $runtimeWithVersion ?? ''; + } + } + + $wg = new WaitGroup(); + $envs = []; + foreach ($files as $file) { + if (!(\str_starts_with($file, '.env'))) { + continue; + } + + $wg->add(); + go(function () use ($github, $repo, $file, $wg, &$envs) { + try { + $contentResponse = $github->getRepositoryContent($repo['organization'], $repo['name'], $file); + $envFile = $contentResponse['content'] ?? ''; + + $configAdapter = new ConfigDotenv(); + try { + $envObject = $configAdapter->parse($envFile); + foreach ($envObject as $envName => $envValue) { + $envs[$envName] = $envValue; + } + } catch (Parse) { + // Silence error, so rest of endpoint can return + } + } finally { + $wg->done(); + } + }); + } + $wg->wait(); + + $repo['variables'] = []; + foreach ($envs as $key => $value) { + $repo['variables'][] = [ + 'name' => $key, + 'value' => $value, + ]; + } + + return $repo; + }; + }, $repos)); + + $repos = \array_map(function ($repo) { + return new Document($repo); + }, $repos); + + $response->dynamic(new Document([ + $type === 'framework' ? 'frameworkProviderRepositories' : 'runtimeProviderRepositories' => $repos, + 'total' => $total, + ]), ($type === 'framework') ? Response::MODEL_PROVIDER_REPOSITORY_FRAMEWORK_LIST : Response::MODEL_PROVIDER_REPOSITORY_RUNTIME_LIST); + } +} diff --git a/src/Appwrite/Platform/Modules/VCS/Services/Http.php b/src/Appwrite/Platform/Modules/VCS/Services/Http.php index 3630a5b32f..11e26be0d5 100644 --- a/src/Appwrite/Platform/Modules/VCS/Services/Http.php +++ b/src/Appwrite/Platform/Modules/VCS/Services/Http.php @@ -4,6 +4,12 @@ namespace Appwrite\Platform\Modules\VCS\Services; use Appwrite\Platform\Modules\VCS\Http\Installations\Delete as DeleteInstallation; use Appwrite\Platform\Modules\VCS\Http\Installations\Get as GetInstallation; +use Appwrite\Platform\Modules\VCS\Http\Installations\Repositories\Branches\XList as ListRepositoryBranches; +use Appwrite\Platform\Modules\VCS\Http\Installations\Repositories\Contents\Get as GetRepositoryContents; +use Appwrite\Platform\Modules\VCS\Http\Installations\Repositories\Create as CreateRepository; +use Appwrite\Platform\Modules\VCS\Http\Installations\Repositories\Detections\Create as CreateRepositoryDetections; +use Appwrite\Platform\Modules\VCS\Http\Installations\Repositories\Get as GetRepository; +use Appwrite\Platform\Modules\VCS\Http\Installations\Repositories\XList as ListRepositories; use Appwrite\Platform\Modules\VCS\Http\Installations\XList as ListInstallations; use Utopia\Platform\Service; @@ -17,5 +23,13 @@ class Http extends Service $this->addAction(GetInstallation::getName(), new GetInstallation()); $this->addAction(ListInstallations::getName(), new ListInstallations()); $this->addAction(DeleteInstallation::getName(), new DeleteInstallation()); + + // Repositories + $this->addAction(CreateRepository::getName(), new CreateRepository()); + $this->addAction(GetRepository::getName(), new GetRepository()); + $this->addAction(ListRepositories::getName(), new ListRepositories()); + $this->addAction(ListRepositoryBranches::getName(), new ListRepositoryBranches()); + $this->addAction(GetRepositoryContents::getName(), new GetRepositoryContents()); + $this->addAction(CreateRepositoryDetections::getName(), new CreateRepositoryDetections()); } }