Add gitea support

This commit is contained in:
Matej Bačo
2026-04-10 17:01:12 +02:00
parent c6dd7de216
commit ebadbad847
32 changed files with 1414 additions and 249 deletions
+6
View File
@@ -133,6 +133,12 @@ _APP_VCS_GITHUB_APP_ID=
_APP_VCS_GITHUB_CLIENT_ID=
_APP_VCS_GITHUB_CLIENT_SECRET=
_APP_VCS_GITHUB_WEBHOOK_SECRET=
_APP_VCS_GITEA_ENDPOINT=http://gitea:3000
_APP_VCS_GITEA_BROWSER_ENDPOINT=http://localhost:9510
_APP_VCS_GITEA_CLIENT_ID=appwrite-gitea-client-id
_APP_VCS_GITEA_CLIENT_SECRET=appwrite-gitea-client-secret
_APP_VCS_GITEA_WEBHOOK_SECRET=gitea-webhook-secret
_APP_VCS_WEBHOOK_URL=http://appwrite:80
_APP_MIGRATIONS_FIREBASE_CLIENT_ID=
_APP_MIGRATIONS_FIREBASE_CLIENT_SECRET=
_APP_ASSISTANT_OPENAI_API_KEY=
+2 -2
View File
@@ -1546,7 +1546,7 @@ $platformCollections = [
'$id' => ID::custom('personalAccessToken'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 256,
'size' => 2048,
'signed' => true,
'required' => false,
'default' => null,
@@ -1568,7 +1568,7 @@ $platformCollections = [
'$id' => ID::custom('personalRefreshToken'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 256,
'size' => 2048,
'signed' => true,
'required' => false,
'default' => null,
+3
View File
@@ -101,6 +101,9 @@ const APP_SDK_PLATFORM_STATIC = 'static';
const APP_VCS_GITHUB_USERNAME = 'Appwrite';
const APP_VCS_GITHUB_EMAIL = 'team@appwrite.io';
const APP_VCS_GITHUB_URL = 'https://github.com/TeamAppwrite';
const APP_VCS_COMMIT_USERNAME = 'Appwrite';
const APP_VCS_COMMIT_EMAIL = 'team@appwrite.io';
const APP_VCS_PROVIDERS = ['github', 'gitea'];
const APP_BRANDED_EMAIL_BASE_TEMPLATE = 'email-base-styled';
/**
+1 -1
View File
@@ -84,7 +84,7 @@
"utopia-php/storage": "1.0.*",
"utopia-php/system": "0.10.*",
"utopia-php/telemetry": "0.2.*",
"utopia-php/vcs": "3.*",
"utopia-php/vcs": "dev-fix-gitea-branch-list as 3.99.99",
"utopia-php/websocket": "1.0.*",
"matomo/device-detector": "6.4.*",
"dragonmantank/cron-expression": "3.4.*",
Generated
+36 -27
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "4fb974e9843f6104e40396e7cad4a833",
"content-hash": "cf36a6ac7c5041c9ec493333e6e05c52",
"packages": [
{
"name": "adhocore/jwt",
@@ -1996,16 +1996,16 @@
},
{
"name": "phpseclib/phpseclib",
"version": "3.0.50",
"version": "3.0.51",
"source": {
"type": "git",
"url": "https://github.com/phpseclib/phpseclib.git",
"reference": "aa6ad8321ed103dc3624fb600a25b66ebf78ec7b"
"reference": "d59c94077f9c9915abb51ddb52ce85188ece1748"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/aa6ad8321ed103dc3624fb600a25b66ebf78ec7b",
"reference": "aa6ad8321ed103dc3624fb600a25b66ebf78ec7b",
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/d59c94077f9c9915abb51ddb52ce85188ece1748",
"reference": "d59c94077f9c9915abb51ddb52ce85188ece1748",
"shasum": ""
},
"require": {
@@ -2086,7 +2086,7 @@
],
"support": {
"issues": "https://github.com/phpseclib/phpseclib/issues",
"source": "https://github.com/phpseclib/phpseclib/tree/3.0.50"
"source": "https://github.com/phpseclib/phpseclib/tree/3.0.51"
},
"funding": [
{
@@ -2102,7 +2102,7 @@
"type": "tidelift"
}
],
"time": "2026-03-19T02:57:58+00:00"
"time": "2026-04-10T01:33:53+00:00"
},
{
"name": "psr/clock",
@@ -3850,16 +3850,16 @@
},
{
"name": "utopia-php/database",
"version": "5.3.19",
"version": "5.3.20",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/database.git",
"reference": "72ee1614c37e37c7fdd9d4dc87f1f7cdfa1ca691"
"reference": "fad8e6b93c4d08cc611e41a828df3bbe0d9cfa24"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/database/zipball/72ee1614c37e37c7fdd9d4dc87f1f7cdfa1ca691",
"reference": "72ee1614c37e37c7fdd9d4dc87f1f7cdfa1ca691",
"url": "https://api.github.com/repos/utopia-php/database/zipball/fad8e6b93c4d08cc611e41a828df3bbe0d9cfa24",
"reference": "fad8e6b93c4d08cc611e41a828df3bbe0d9cfa24",
"shasum": ""
},
"require": {
@@ -3903,9 +3903,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/database/issues",
"source": "https://github.com/utopia-php/database/tree/5.3.19"
"source": "https://github.com/utopia-php/database/tree/5.3.20"
},
"time": "2026-03-31T15:52:08+00:00"
"time": "2026-04-10T08:27:41+00:00"
},
{
"name": "utopia-php/detector",
@@ -5225,16 +5225,16 @@
},
{
"name": "utopia-php/vcs",
"version": "3.1.0",
"version": "dev-fix-gitea-branch-list",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/vcs.git",
"reference": "03b76ad5fd01bc50f809915bca6ff0745ea913af"
"reference": "ff7feacff8cd3e23de77dd360ffb931d984b8c03"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/vcs/zipball/03b76ad5fd01bc50f809915bca6ff0745ea913af",
"reference": "03b76ad5fd01bc50f809915bca6ff0745ea913af",
"url": "https://api.github.com/repos/utopia-php/vcs/zipball/ff7feacff8cd3e23de77dd360ffb931d984b8c03",
"reference": "ff7feacff8cd3e23de77dd360ffb931d984b8c03",
"shasum": ""
},
"require": {
@@ -5268,9 +5268,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/vcs/issues",
"source": "https://github.com/utopia-php/vcs/tree/3.1.0"
"source": "https://github.com/utopia-php/vcs/tree/fix-gitea-branch-list"
},
"time": "2026-03-24T08:49:14+00:00"
"time": "2026-04-10T14:43:29+00:00"
},
{
"name": "utopia-php/websocket",
@@ -5448,16 +5448,16 @@
"packages-dev": [
{
"name": "appwrite/sdk-generator",
"version": "1.17.7",
"version": "1.17.8",
"source": {
"type": "git",
"url": "https://github.com/appwrite/sdk-generator.git",
"reference": "291471d04c3f0e7b9fcc46668a6255a4c0f2947e"
"reference": "b7109e13cec89ed56ad80111973b12646e4eb5f7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/291471d04c3f0e7b9fcc46668a6255a4c0f2947e",
"reference": "291471d04c3f0e7b9fcc46668a6255a4c0f2947e",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/b7109e13cec89ed56ad80111973b12646e4eb5f7",
"reference": "b7109e13cec89ed56ad80111973b12646e4eb5f7",
"shasum": ""
},
"require": {
@@ -5493,9 +5493,9 @@
"description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms",
"support": {
"issues": "https://github.com/appwrite/sdk-generator/issues",
"source": "https://github.com/appwrite/sdk-generator/tree/1.17.7"
"source": "https://github.com/appwrite/sdk-generator/tree/1.17.8"
},
"time": "2026-04-08T08:51:05+00:00"
"time": "2026-04-09T05:08:21+00:00"
},
{
"name": "brianium/paratest",
@@ -8426,9 +8426,18 @@
"time": "2024-11-07T12:36:22+00:00"
}
],
"aliases": [],
"aliases": [
{
"package": "utopia-php/vcs",
"version": "dev-fix-gitea-branch-list",
"alias": "3.99.99",
"alias_normalized": "3.99.99.0"
}
],
"minimum-stability": "dev",
"stability-flags": {},
"stability-flags": {
"utopia-php/vcs": 20
},
"prefer-stable": true,
"prefer-lowest": false,
"platform": {
+82 -1
View File
@@ -232,6 +232,12 @@ services:
- _APP_VCS_GITHUB_WEBHOOK_SECRET
- _APP_VCS_GITHUB_CLIENT_SECRET
- _APP_VCS_GITHUB_CLIENT_ID
- _APP_VCS_GITEA_ENDPOINT
- _APP_VCS_GITEA_BROWSER_ENDPOINT
- _APP_VCS_GITEA_CLIENT_ID
- _APP_VCS_GITEA_CLIENT_SECRET
- _APP_VCS_GITEA_WEBHOOK_SECRET
- _APP_VCS_WEBHOOK_URL
- _APP_MIGRATIONS_FIREBASE_CLIENT_ID
- _APP_MIGRATIONS_FIREBASE_CLIENT_SECRET
- _APP_ASSISTANT_OPENAI_API_KEY
@@ -546,6 +552,11 @@ services:
- _APP_VCS_GITHUB_APP_NAME
- _APP_VCS_GITHUB_PRIVATE_KEY
- _APP_VCS_GITHUB_APP_ID
- _APP_VCS_GITEA_ENDPOINT
- _APP_VCS_GITEA_BROWSER_ENDPOINT
- _APP_VCS_GITEA_CLIENT_ID
- _APP_VCS_GITEA_CLIENT_SECRET
- _APP_VCS_GITEA_WEBHOOK_SECRET
- _APP_FUNCTIONS_TIMEOUT
- _APP_SITES_TIMEOUT
- _APP_COMPUTE_BUILD_TIMEOUT
@@ -1447,6 +1458,75 @@ services:
networks:
- appwrite
gitea:
image: gitea/gitea:1.21.5
container_name: appwrite-gitea
<<: *x-logging
ports:
- "9510:3000"
volumes:
- appwrite-gitea:/data
environment:
- USER_UID=1000
- USER_GID=1000
- GITEA__database__DB_TYPE=sqlite3
- GITEA__security__INSTALL_LOCK=true
- GITEA__webhook__ALLOWED_HOST_LIST=*
- GITEA__webhook__SKIP_TLS_VERIFY=true
- GITEA__webhook__DELIVER_TIMEOUT=10
- GITEA__server__ROOT_URL=http://gitea:3000/
- GITEA__server__LOCAL_ROOT_URL=http://gitea:3000/
networks:
- appwrite
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/api/healthz"]
interval: 10s
timeout: 5s
retries: 10
start_period: 10s
gitea-setup:
image: gitea/gitea:1.21.5
container_name: appwrite-gitea-setup
<<: *x-logging
depends_on:
gitea:
condition: service_healthy
volumes:
- appwrite-gitea:/data
environment:
- GITEA_ADMIN_USERNAME=appwrite
- GITEA_ADMIN_PASSWORD=password
- GITEA_ADMIN_EMAIL=admin@appwrite.local
- GITEA_OAUTH2_CLIENT_ID=${_APP_VCS_GITEA_CLIENT_ID:-appwrite-gitea-client-id}
- GITEA_OAUTH2_CLIENT_SECRET=${_APP_VCS_GITEA_CLIENT_SECRET:-appwrite-gitea-client-secret}
- GITEA_OAUTH2_REDIRECT_URIS=["http://localhost/v1/vcs/gitea/callback","http://localhost:9501/v1/vcs/gitea/callback","http://localhost:80/v1/vcs/gitea/callback"]
entrypoint: >
sh -c "
echo 'Setting up Gitea...' &&
su git -c 'gitea admin user create --username $$GITEA_ADMIN_USERNAME --password $$GITEA_ADMIN_PASSWORD --email $$GITEA_ADMIN_EMAIL --admin --must-change-password=false' 2>/dev/null || true &&
TOKEN=$$(su git -c 'gitea admin user generate-access-token --username $$GITEA_ADMIN_USERNAME --token-name appwrite-token --scopes all --raw' 2>/dev/null) &&
echo $$TOKEN > /data/gitea/token.txt &&
echo 'Creating OAuth2 application...' &&
apk add --no-cache apache2-utils sqlite > /dev/null 2>&1 &&
SECRET_HASH=$$(htpasswd -nbBC 10 '' \"$$GITEA_OAUTH2_CLIENT_SECRET\" | cut -d: -f2) &&
USER_ID=$$(sqlite3 /data/gitea/gitea.db \"SELECT id FROM user WHERE lower_name='$$GITEA_ADMIN_USERNAME' LIMIT 1;\") &&
TIMESTAMP=$$(date +%s) &&
sqlite3 /data/gitea/gitea.db \"DELETE FROM oauth2_application WHERE name='Appwrite';\" &&
sqlite3 /data/gitea/gitea.db \"INSERT INTO oauth2_application (uid, name, client_id, client_secret, confidential_client, redirect_uris, created_unix, updated_unix) VALUES ($$USER_ID, 'Appwrite', '$$GITEA_OAUTH2_CLIENT_ID', '$$SECRET_HASH', 1, '$$GITEA_OAUTH2_REDIRECT_URIS', $$TIMESTAMP, $$TIMESTAMP);\" &&
echo '' &&
echo '========================================' &&
echo 'Gitea setup complete!' &&
echo '========================================' &&
echo \"Gitea URL: http://localhost:9510\" &&
echo \"Admin user: $$GITEA_ADMIN_USERNAME\" &&
echo \"Admin password: $$GITEA_ADMIN_PASSWORD\" &&
echo \"OAuth2 Client ID: $$GITEA_OAUTH2_CLIENT_ID\" &&
echo '========================================'
"
networks:
- appwrite
# Dev tools (adminer, redis-insight, mongo-express, graphql-explorer)
# are defined in docker-compose.override.yml
@@ -1477,4 +1557,5 @@ volumes:
appwrite-sites:
appwrite-builds:
appwrite-config:
appwrite-models:
appwrite-models:
appwrite-gitea:
+213
View File
@@ -0,0 +1,213 @@
<?php
namespace Appwrite\Auth\OAuth2;
use Appwrite\Auth\OAuth2;
class Gitea extends OAuth2
{
/**
* @var string
*/
protected string $endpoint;
/**
* @var array
*/
protected array $user = [];
/**
* @var array
*/
protected array $tokens = [];
/**
* @var array
*/
protected array $scopes = [];
/**
* @param string $appId
* @param string $appSecret
* @param string $callback
* @param array $state
* @param array $scopes
* @param string $endpoint Base URL of the Gitea instance (e.g. https://gitea.example.com)
*/
public function __construct(string $appId, string $appSecret, string $callback, array $state = [], array $scopes = [], string $endpoint = '')
{
$this->endpoint = rtrim($endpoint, '/');
parent::__construct($appId, $appSecret, $callback, $state, $scopes);
}
/**
* @return string
*/
public function getName(): string
{
return 'gitea';
}
/**
* @return string
*/
public function getLoginURL(): string
{
return $this->endpoint . '/login/oauth/authorize?' . \http_build_query([
'client_id' => $this->appID,
'redirect_uri' => $this->callback,
'response_type' => 'code',
'state' => \json_encode($this->state),
]);
}
/**
* @param string $code
*
* @return array
*/
protected function getTokens(string $code): array
{
if (empty($this->tokens)) {
$response = $this->request(
'POST',
$this->endpoint . '/login/oauth/access_token',
['Content-Type: application/x-www-form-urlencoded', 'Accept: application/json'],
\http_build_query([
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
'redirect_uri' => $this->callback,
'grant_type' => 'authorization_code',
'code' => $code,
])
);
$this->tokens = \json_decode($response, true) ?? [];
}
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken): array
{
$response = $this->request(
'POST',
$this->endpoint . '/login/oauth/access_token',
['Content-Type: application/x-www-form-urlencoded', 'Accept: application/json'],
\http_build_query([
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
'grant_type' => 'refresh_token',
'refresh_token' => $refreshToken,
])
);
$this->tokens = \json_decode($response, true) ?? [];
if (empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
return $this->tokens;
}
/**
* @param string $accessToken
*
* @return string
*/
public function getUserID(string $accessToken): string
{
$user = $this->getUser($accessToken);
return \strval($user['id'] ?? '');
}
/**
* @param string $accessToken
*
* @return string
*/
public function getUserEmail(string $accessToken): string
{
$user = $this->getUser($accessToken);
return $user['email'] ?? '';
}
/**
* @param string $accessToken
*
* @return bool
*/
public function isEmailVerified(string $accessToken): bool
{
return true;
}
/**
* @param string $accessToken
*
* @return string
*/
public function getUserName(string $accessToken): string
{
$user = $this->getUser($accessToken);
return $user['full_name'] ?? $user['login'] ?? '';
}
/**
* @param string $accessToken
*
* @return string
*/
public function getUserSlug(string $accessToken): string
{
$user = $this->getUser($accessToken);
return $user['login'] ?? '';
}
/**
* @param string $accessToken
*
* @return array
*/
protected function getUser(string $accessToken): array
{
if (empty($this->user)) {
$response = $this->request('GET', $this->endpoint . '/api/v1/user', ['Authorization: token ' . \urlencode($accessToken)]);
$this->user = \json_decode($response, true) ?? [];
}
return $this->user;
}
/**
* @param string $accessToken
* @param string $repositoryName
* @param bool $private
*
* @return array
*/
public function createRepository(string $accessToken, string $repositoryName, bool $private): array
{
$response = $this->request(
'POST',
$this->endpoint . '/api/v1/user/repos',
['Authorization: token ' . \urlencode($accessToken), 'Content-Type: application/json'],
\json_encode([
'name' => $repositoryName,
'private' => $private,
])
);
return \json_decode($response, true) ?? [];
}
}
+25 -33
View File
@@ -17,9 +17,11 @@ use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Appwrite\Vcs\VcsFactory;
use Utopia\Cache\Cache;
use Utopia\Http\Adapter\Swoole\Request;
use Utopia\System\System;
use Utopia\VCS\Adapter\Git\GitHub;
use Utopia\VCS\Adapter\Git;
use Utopia\VCS\Exception\RepositoryNotFound;
class Base extends Action
@@ -57,18 +59,16 @@ class Base extends Action
return $allowedSpecifications[0] ?? APP_COMPUTE_SPECIFICATION_DEFAULT;
}
public function redeployVcsFunction(Request $request, Document $function, Document $project, Document $installation, Database $dbForProject, Build $queueForBuilds, Document $template, GitHub $github, bool $activate, string $referenceType = 'branch', string $reference = ''): Document
public function redeployVcsFunction(Request $request, Document $function, Document $project, Document $installation, Database $dbForProject, Build $queueForBuilds, Document $template, Git $vcs, bool $activate, string $referenceType = 'branch', string $reference = ''): Document
{
$deploymentId = ID::unique();
$entrypoint = $function->getAttribute('entrypoint', '');
$provider = $installation->getAttribute('provider', 'github');
$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);
$providerRepositoryId = $function->getAttribute('providerRepositoryId', '');
$owner = VcsFactory::getOwnerName($vcs, $provider, $providerInstallationId, $providerRepositoryId);
try {
$repositoryName = $github->getRepositoryName($providerRepositoryId) ?? '';
$repositoryName = $vcs->getRepositoryName($providerRepositoryId) ?? '';
if (empty($repositoryName)) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
@@ -77,33 +77,30 @@ class Base extends Action
}
$commitDetails = [];
$branchUrl = "";
$providerBranch = "";
$branchUrl = '';
$providerBranch = '';
// TODO: Support tag in future
if ($referenceType === 'branch') {
$providerBranch = empty($reference) ? $function->getAttribute('providerBranch', 'main') : $reference;
$branchUrl = "https://github.com/$owner/$repositoryName/tree/$providerBranch";
$branchUrl = VcsFactory::getBranchUrl($provider, $owner, $repositoryName, $providerBranch);
try {
$commitDetails = $github->getLatestCommit($owner, $repositoryName, $providerBranch);
$commitDetails = $vcs->getLatestCommit($owner, $repositoryName, $providerBranch);
} catch (\Throwable $error) {
// Ignore; deployment can continue
}
} elseif ($referenceType === 'commit') {
try {
$commitDetails = $github->getCommit($owner, $repositoryName, $reference);
$commitDetails = $vcs->getCommit($owner, $repositoryName, $reference);
} catch (\Throwable $error) {
// Ignore; deployment can continue
}
} else {
// Fallback till we have tag support here
// Goal is to set providerBranch, so build worker knows what to clone as base
// Without this, clone command would be cloning empty branch, and failing
$providerBranch = $function->getAttribute('providerBranch', 'main');
$branchUrl = "https://github.com/$owner/$repositoryName/tree/$providerBranch";
$branchUrl = VcsFactory::getBranchUrl($provider, $owner, $repositoryName, $providerBranch);
}
$repositoryUrl = "https://github.com/$owner/$repositoryName";
$repositoryUrl = VcsFactory::getRepoUrl($provider, $owner, $repositoryName);
$deployment = $dbForProject->createDocument('deployments', new Document([
'$id' => $deploymentId,
@@ -159,17 +156,15 @@ class Base extends Action
return $deployment;
}
public function redeployVcsSite(Request $request, Document $site, Document $project, Document $installation, Database $dbForProject, Database $dbForPlatform, Build $queueForBuilds, Document $template, GitHub $github, bool $activate, Authorization $authorization, array $platform, string $referenceType = 'branch', string $reference = ''): Document
public function redeployVcsSite(Request $request, Document $site, Document $project, Document $installation, Database $dbForProject, Database $dbForPlatform, Build $queueForBuilds, Document $template, Git $vcs, bool $activate, Authorization $authorization, array $platform, string $referenceType = 'branch', string $reference = ''): Document
{
$deploymentId = ID::unique();
$provider = $installation->getAttribute('provider', 'github');
$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);
$providerRepositoryId = $site->getAttribute('providerRepositoryId', '');
$owner = VcsFactory::getOwnerName($vcs, $provider, $providerInstallationId, $providerRepositoryId);
try {
$repositoryName = $github->getRepositoryName($providerRepositoryId) ?? '';
$repositoryName = $vcs->getRepositoryName($providerRepositoryId) ?? '';
if (empty($repositoryName)) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
@@ -178,33 +173,30 @@ class Base extends Action
}
$commitDetails = [];
$branchUrl = "";
$providerBranch = "";
$branchUrl = '';
$providerBranch = '';
// TODO: Support tag in future
if ($referenceType === 'branch') {
$providerBranch = empty($reference) ? $site->getAttribute('providerBranch', 'main') : $reference;
$branchUrl = "https://github.com/$owner/$repositoryName/tree/$providerBranch";
$branchUrl = VcsFactory::getBranchUrl($provider, $owner, $repositoryName, $providerBranch);
try {
$commitDetails = $github->getLatestCommit($owner, $repositoryName, $providerBranch);
$commitDetails = $vcs->getLatestCommit($owner, $repositoryName, $providerBranch);
} catch (\Throwable $error) {
// Ignore; deployment can continue
}
} elseif ($referenceType === 'commit') {
try {
$commitDetails = $github->getCommit($owner, $repositoryName, $reference);
$commitDetails = $vcs->getCommit($owner, $repositoryName, $reference);
} catch (\Throwable $error) {
// Ignore; deployment can continue
}
} else {
// Fallback till we have tag support here
// Goal is to set providerBranch, so build worker knows what to clone as base
// Without this, clone command would be cloning empty branch, and failing
$providerBranch = $site->getAttribute('providerBranch', 'main');
$branchUrl = "https://github.com/$owner/$repositoryName/tree/$providerBranch";
$branchUrl = VcsFactory::getBranchUrl($provider, $owner, $repositoryName, $providerBranch);
}
$repositoryUrl = "https://github.com/$owner/$repositoryName";
$repositoryUrl = VcsFactory::getRepoUrl($provider, $owner, $repositoryName);
$commands = [];
if (!empty($site->getAttribute('installCommand', ''))) {
@@ -7,6 +7,7 @@ use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Appwrite\Vcs\VcsFactory;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Domains\Domain;
@@ -66,11 +67,7 @@ class Get extends Action
$isDomainEnabled = $isAAAAValid || $isAValid || $isCNAMEValid;
$isVcsEnabled = !empty(System::getEnv('_APP_VCS_GITHUB_APP_NAME', ''))
&& !empty(System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY', ''))
&& !empty(System::getEnv('_APP_VCS_GITHUB_APP_ID', ''))
&& !empty(System::getEnv('_APP_VCS_GITHUB_CLIENT_ID', ''))
&& !empty(System::getEnv('_APP_VCS_GITHUB_CLIENT_SECRET', ''));
$isVcsEnabled = !empty(VcsFactory::getConfiguredProviders());
$isAssistantEnabled = !empty(System::getEnv('_APP_ASSISTANT_OPENAI_API_KEY', ''));
@@ -86,6 +83,7 @@ class Get extends Action
'_APP_COMPUTE_SIZE_LIMIT' => +System::getEnv('_APP_COMPUTE_SIZE_LIMIT'),
'_APP_USAGE_STATS' => System::getEnv('_APP_USAGE_STATS'),
'_APP_VCS_ENABLED' => $isVcsEnabled,
'_APP_VCS_PROVIDERS' => VcsFactory::getConfiguredProviders(),
'_APP_DOMAIN_ENABLED' => $isDomainEnabled,
'_APP_ASSISTANT_ENABLED' => $isAssistantEnabled,
'_APP_DOMAIN_SITES' => $platform['sitesDomain'],
@@ -20,10 +20,11 @@ use Utopia\Database\Validator\UID;
use Utopia\Http\Adapter\Swoole\Request;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Appwrite\Vcs\VcsFactory;
use Utopia\Cache\Cache;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
use Utopia\VCS\Adapter\Git\GitHub;
class Create extends Base
{
@@ -77,7 +78,7 @@ class Create extends Base
->inject('queueForEvents')
->inject('project')
->inject('queueForBuilds')
->inject('gitHub')
->inject('cache')
->inject('authorization')
->callback($this->action(...));
}
@@ -97,7 +98,7 @@ class Create extends Base
Event $queueForEvents,
Document $project,
Build $queueForBuilds,
GitHub $github,
Cache $cache,
Authorization $authorization
) {
$function = $dbForProject->getDocument('functions', $functionId);
@@ -106,8 +107,8 @@ class Create extends Base
throw new Exception(Exception::FUNCTION_NOT_FOUND);
}
// Templates are always from GitHub
$branchUrl = "https://github.com/$owner/$repository/blob/$reference";
$repositoryUrl = "https://github.com/$owner/$repository";
$template = new Document([
@@ -120,6 +121,8 @@ class Create extends Base
if (!empty($function->getAttribute('providerRepositoryId'))) {
$installation = $dbForPlatform->getDocument('installations', $function->getAttribute('installationId'));
$provider = $installation->getAttribute('provider', 'github');
$vcs = VcsFactory::getInitializedAdapter($provider, $installation, $cache);
$deployment = $this->redeployVcsFunction(
request: $request,
@@ -129,7 +132,7 @@ class Create extends Base
dbForProject: $dbForProject,
queueForBuilds: $queueForBuilds,
template: $template,
github: $github,
vcs: $vcs,
activate: $activate,
referenceType: $type,
reference: $reference
@@ -16,10 +16,11 @@ use Utopia\Database\Validator\UID;
use Utopia\Http\Adapter\Swoole\Request;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Appwrite\Vcs\VcsFactory;
use Utopia\Cache\Cache;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
use Utopia\VCS\Adapter\Git\GitHub;
class Create extends Base
{
@@ -71,7 +72,7 @@ class Create extends Base
->inject('project')
->inject('queueForEvents')
->inject('queueForBuilds')
->inject('gitHub')
->inject('cache')
->callback($this->action(...));
}
@@ -87,7 +88,7 @@ class Create extends Base
Document $project,
Event $queueForEvents,
Build $queueForBuilds,
GitHub $github,
Cache $cache,
) {
$function = $dbForProject->getDocument('functions', $functionId);
@@ -98,6 +99,8 @@ class Create extends Base
$template = new Document();
$installation = $dbForPlatform->getDocument('installations', $function->getAttribute('installationId'));
$provider = $installation->getAttribute('provider', 'github');
$vcs = VcsFactory::getInitializedAdapter($provider, $installation, $cache);
$deployment = $this->redeployVcsFunction(
request: $request,
@@ -107,7 +110,7 @@ class Create extends Base
dbForProject: $dbForProject,
queueForBuilds: $queueForBuilds,
template: $template,
github: $github,
vcs: $vcs,
activate: $activate,
reference: $reference,
referenceType: $type
@@ -37,8 +37,9 @@ use Utopia\Validator\ArrayList;
use Utopia\Validator\Boolean;
use Utopia\Validator\Range;
use Utopia\Validator\Text;
use Appwrite\Vcs\VcsFactory;
use Utopia\Cache\Cache;
use Utopia\Validator\WhiteList;
use Utopia\VCS\Adapter\Git\GitHub;
class Create extends Base
{
@@ -121,7 +122,7 @@ class Create extends Base
->inject('queueForFunctions')
->inject('dbForPlatform')
->inject('request')
->inject('gitHub')
->inject('cache')
->inject('authorization')
->inject('platform')
->callback($this->action(...));
@@ -163,7 +164,7 @@ class Create extends Base
Func $queueForFunctions,
Database $dbForPlatform,
Request $request,
GitHub $github,
Cache $cache,
Authorization $authorization,
array $platform
) {
@@ -287,6 +288,12 @@ class Create extends Base
'providerPullRequestIds' => []
]));
$provider = $installation->getAttribute('provider', 'github');
$vcs = VcsFactory::getInitializedAdapter($provider, $installation, $cache);
$owner = VcsFactory::getOwnerName($vcs, $provider, $installation->getAttribute('providerInstallationId', ''), $providerRepositoryId);
$repositoryName = $vcs->getRepositoryName($providerRepositoryId);
VcsFactory::createRepositoryWebhook($vcs, $provider, $owner, $repositoryName);
$function->setAttribute('repositoryId', $repository->getId());
$function->setAttribute('repositoryInternalId', $repository->getSequence());
}
@@ -320,6 +327,8 @@ class Create extends Base
$template = new Document();
$installation = $dbForPlatform->getDocument('installations', $function->getAttribute('installationId'));
$provider = $installation->getAttribute('provider', 'github');
$vcs = VcsFactory::getInitializedAdapter($provider, $installation, $cache);
$deployment = $this->redeployVcsFunction(
request: $request,
function: $function,
@@ -328,7 +337,7 @@ class Create extends Base
dbForProject: $dbForProject,
queueForBuilds: $queueForBuilds,
template: $template,
github: $github,
vcs: $vcs,
activate: true,
reference: $providerBranch,
referenceType: 'branch'
@@ -33,7 +33,8 @@ use Utopia\Validator\Nullable;
use Utopia\Validator\Range;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
use Utopia\VCS\Adapter\Git\GitHub;
use Appwrite\Vcs\VcsFactory;
use Utopia\Cache\Cache;
class Update extends Base
{
@@ -107,7 +108,7 @@ class Update extends Base
->inject('queueForEvents')
->inject('queueForBuilds')
->inject('dbForPlatform')
->inject('gitHub')
->inject('cache')
->inject('executor')
->inject('authorization')
->callback($this->action(...));
@@ -141,7 +142,7 @@ class Update extends Base
Event $queueForEvents,
Build $queueForBuilds,
Database $dbForPlatform,
GitHub $github,
Cache $cache,
Executor $executor,
Authorization $authorization
) {
@@ -219,6 +220,12 @@ class Update extends Base
'providerPullRequestIds' => [],
]));
$provider = $installation->getAttribute('provider', 'github');
$vcs = VcsFactory::getInitializedAdapter($provider, $installation, $cache);
$owner = VcsFactory::getOwnerName($vcs, $provider, $installation->getAttribute('providerInstallationId', ''), $providerRepositoryId);
$repositoryName = $vcs->getRepositoryName($providerRepositoryId);
VcsFactory::createRepositoryWebhook($vcs, $provider, $owner, $repositoryName);
$repositoryId = $repository->getId();
$repositoryInternalId = $repository->getSequence();
}
@@ -285,7 +292,9 @@ class Update extends Base
// Redeploy logic
if (!$isConnected && !empty($providerRepositoryId)) {
$this->redeployVcsFunction($request, $function, $project, $installation, $dbForProject, $queueForBuilds, new Document(), $github, true);
$provider = $installation->getAttribute('provider', 'github');
$vcs = VcsFactory::getInitializedAdapter($provider, $installation, $cache);
$this->redeployVcsFunction($request, $function, $project, $installation, $dbForProject, $queueForBuilds, new Document(), $vcs, true);
}
// Inform scheduler if function is still active
@@ -36,8 +36,9 @@ use Utopia\Platform\Action;
use Utopia\Queue\Message;
use Utopia\Storage\Device;
use Utopia\Storage\Device\Local;
use Appwrite\Vcs\VcsFactory;
use Utopia\System\System;
use Utopia\VCS\Adapter\Git\GitHub;
use Utopia\VCS\Adapter;
class Builds extends Action
{
@@ -121,7 +122,6 @@ class Builds extends Action
case BUILD_TYPE_DEPLOYMENT:
case BUILD_TYPE_RETRY:
Console::info('Creating build for deployment: ' . $deployment->getId());
$github = new GitHub($cache);
$this->buildDeployment(
$deviceForFunctions,
$deviceForSites,
@@ -135,7 +135,7 @@ class Builds extends Action
$publisherForUsage,
$dbForPlatform,
$dbForProject,
$github,
$cache,
$project,
$resource,
$deployment,
@@ -170,7 +170,7 @@ class Builds extends Action
UsagePublisher $publisherForUsage,
Database $dbForPlatform,
Database $dbForProject,
GitHub $github,
Cache $cache,
Document $project,
Document $resource,
Document $deployment,
@@ -270,13 +270,15 @@ class Builds extends Action
$owner = '';
$repositoryName = '';
Console::log('[DEBUG build] deploymentId=' . $deploymentId . ' installationId=' . $installationId . ' providerRepositoryId=' . $providerRepositoryId . ' isVcsEnabled=' . ($isVcsEnabled ? 'true' : 'false') . ' source=' . $source);
$provider = 'github';
$vcs = null;
if ($isVcsEnabled) {
$installation = $dbForPlatform->getDocument('installations', $installationId);
$providerInstallationId = $installation->getAttribute('providerInstallationId');
$privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY');
$githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID');
$provider = $installation->getAttribute('provider', 'github');
$github->initializeVariables($providerInstallationId, $privateKey, $githubAppId);
$vcs = VcsFactory::getInitializedAdapter($provider, $installation, $cache);
}
try {
@@ -299,7 +301,9 @@ class Builds extends Action
// Clone template repo
$tmpTemplateDirectory = '/tmp/builds/' . $deploymentId . '-template';
$gitCloneCommandForTemplate = $github->generateCloneCommand($templateOwnerName, $templateRepositoryName, $templateReferenceValue, $templateReferenceType, $tmpTemplateDirectory, $templateRootDirectory);
// Templates are public GitHub repos — no authentication needed
$templateGitHub = VcsFactory::getAdapter('github', $cache);
$gitCloneCommandForTemplate = $templateGitHub->generateCloneCommand($templateOwnerName, $templateRepositoryName, $templateReferenceValue, $templateReferenceType, $tmpTemplateDirectory, $templateRootDirectory);
$exit = Console::execute($gitCloneCommandForTemplate, '', $stdout, $stderr);
@@ -357,8 +361,9 @@ class Builds extends Action
$rootDirectory = \ltrim($rootDirectory, '.');
$rootDirectory = \ltrim($rootDirectory, '/');
$owner = $github->getOwnerName($providerInstallationId);
$repositoryName = $github->getRepositoryName($providerRepositoryId);
$providerInstallationId = $installation->getAttribute('providerInstallationId', '');
$owner = VcsFactory::getOwnerName($vcs, $provider, $providerInstallationId, $providerRepositoryId);
$repositoryName = $vcs->getRepositoryName($providerRepositoryId);
$cloneOwner = $deployment->getAttribute('providerRepositoryOwner', $owner);
$cloneRepository = $deployment->getAttribute('providerRepositoryName', $repositoryName);
@@ -367,13 +372,15 @@ class Builds extends Action
$commitHash = $deployment->getAttribute('providerCommitHash', '');
$cloneVersion = $branchName;
$cloneType = GitHub::CLONE_TYPE_BRANCH;
$cloneType = Adapter::CLONE_TYPE_BRANCH;
if (! empty($commitHash)) {
$cloneVersion = $commitHash;
$cloneType = GitHub::CLONE_TYPE_COMMIT;
$cloneType = Adapter::CLONE_TYPE_COMMIT;
}
$gitCloneCommand = $github->generateCloneCommand($cloneOwner, $cloneRepository, $cloneVersion, $cloneType, $tmpDirectory, $rootDirectory);
Console::log('[DEBUG build-vcs] provider=' . $provider . ' cloneOwner=' . $cloneOwner . ' cloneRepo=' . $cloneRepository . ' cloneVersion=' . $cloneVersion . ' cloneType=' . $cloneType . ' branch=' . $branchName . ' commitHash=' . $commitHash);
$gitCloneCommand = $vcs->generateCloneCommand($cloneOwner, $cloneRepository, $cloneVersion, $cloneType, $tmpDirectory, $rootDirectory);
$stdout = '';
$stderr = '';
@@ -387,12 +394,20 @@ class Builds extends Action
$exit = Console::execute($gitCloneCommand, '', $stdout, $stderr);
Console::log('[DEBUG build-vcs] clone exit=' . $exit . ' stdout=' . $stdout . ' stderr=' . $stderr);
if ($exit !== 0) {
throw new \Exception('Unable to clone code repository: ' . $stderr);
}
Console::log('Git repository cloned');
// List files to verify clone content
$lsOut = '';
$lsErr = '';
Console::execute('ls -la ' . \escapeshellarg($tmpDirectory), '', $lsOut, $lsErr);
Console::log('[DEBUG build-vcs] files in tmpDirectory: ' . $lsOut);
// Local refactoring for function folder with spaces
if (str_contains($rootDirectory, ' ')) {
$rootDirectoryWithoutSpaces = str_replace(' ', '', $rootDirectory);
@@ -421,7 +436,9 @@ class Builds extends Action
// Clone template repo
$tmpTemplateDirectory = '/tmp/builds/' . $deploymentId . '/template';
$gitCloneCommandForTemplate = $github->generateCloneCommand($templateOwnerName, $templateRepositoryName, $templateReferenceValue, $templateReferenceType, $tmpTemplateDirectory, $templateRootDirectory);
// Templates are public GitHub repos — no authentication needed
$templateGitHub = VcsFactory::getAdapter('github', $cache);
$gitCloneCommandForTemplate = $templateGitHub->generateCloneCommand($templateOwnerName, $templateRepositoryName, $templateReferenceValue, $templateReferenceType, $tmpTemplateDirectory, $templateRootDirectory);
$exit = Console::execute($gitCloneCommandForTemplate, '', $stdout, $stderr);
if ($exit !== 0) {
@@ -435,8 +452,8 @@ class Builds extends Action
// Merge template into user repo
Console::execute('rsync -av --exclude \'.git\' ' . \escapeshellarg($tmpTemplateDirectory . '/' . $templateRootDirectory . '/') . ' ' . \escapeshellarg($tmpDirectory . '/' . $rootDirectory), '', $stdout, $stderr);
// Commit and push
$exit = Console::execute('git config --global user.email ' . \escapeshellarg(APP_VCS_GITHUB_EMAIL) . ' && git config --global user.name ' . \escapeshellarg(APP_VCS_GITHUB_USERNAME) . ' && cd ' . \escapeshellarg($tmpDirectory) . ' && git checkout -b ' . \escapeshellarg($branchName) . ' && git add . && git commit -m "Create ' . \escapeshellarg($resource->getAttribute('name', '')) . ' function" && git push origin ' . \escapeshellarg($branchName), '', $stdout, $stderr);
// Disable sparse-checkout so template files can be staged, then commit and push
$exit = Console::execute('git config --global user.email ' . \escapeshellarg(APP_VCS_COMMIT_EMAIL) . ' && git config --global user.name ' . \escapeshellarg(APP_VCS_COMMIT_USERNAME) . ' && cd ' . \escapeshellarg($tmpDirectory) . ' && git sparse-checkout disable && git checkout -b ' . \escapeshellarg($branchName) . ' && git add . && git commit -m "Create ' . \escapeshellarg($resource->getAttribute('name', '')) . ' function" && git push origin ' . \escapeshellarg($branchName), '', $stdout, $stderr);
if ($exit !== 0) {
throw new \Exception('Unable to push code repository: ' . $stderr);
@@ -451,10 +468,10 @@ class Builds extends Action
$providerCommitHash = \trim($stdout);
$deployment->setAttribute('providerCommitHash', $providerCommitHash);
$deployment->setAttribute('providerCommitAuthorUrl', APP_VCS_GITHUB_URL);
$deployment->setAttribute('providerCommitAuthor', APP_VCS_GITHUB_USERNAME);
$deployment->setAttribute('providerCommitAuthorUrl', VcsFactory::getProviderUrl($provider));
$deployment->setAttribute('providerCommitAuthor', APP_VCS_COMMIT_USERNAME);
$deployment->setAttribute('providerCommitMessage', "Create '" . $resource->getAttribute('name', '') . "' function");
$deployment->setAttribute('providerCommitUrl', "https://github.com/$cloneOwner/$cloneRepository/commit/$providerCommitHash");
$deployment->setAttribute('providerCommitUrl', VcsFactory::getCommitUrl($provider, $cloneOwner, $cloneRepository, $providerCommitHash));
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), new Document([
'providerCommitHash' => $deployment->getAttribute('providerCommitHash'),
'providerCommitAuthorUrl' => $deployment->getAttribute('providerCommitAuthorUrl'),
@@ -521,7 +538,7 @@ class Builds extends Action
Console::log('Git source uploaded');
$this->runGitAction('processing', $github, $providerCommitHash, $owner, $repositoryName, $project, $resource, $deployment->getId(), $dbForProject, $dbForPlatform, $queueForRealtime, $platform);
$this->runGitAction('processing', $vcs, $providerCommitHash, $owner, $repositoryName, $project, $resource, $deployment->getId(), $dbForProject, $dbForPlatform, $queueForRealtime, $platform);
}
Console::log('Status marked as building');
@@ -541,7 +558,7 @@ class Builds extends Action
->trigger();
if ($isVcsEnabled) {
$this->runGitAction('building', $github, $providerCommitHash, $owner, $repositoryName, $project, $resource, $deployment->getId(), $dbForProject, $dbForPlatform, $queueForRealtime, $platform);
$this->runGitAction('building', $vcs, $providerCommitHash, $owner, $repositoryName, $project, $resource, $deployment->getId(), $dbForProject, $dbForPlatform, $queueForRealtime, $platform);
}
$deploymentModel = new Deployment();
@@ -931,7 +948,7 @@ class Builds extends Action
}
if ($isVcsEnabled) {
$this->runGitAction('ready', $github, $providerCommitHash, $owner, $repositoryName, $project, $resource, $deployment->getId(), $dbForProject, $dbForPlatform, $queueForRealtime, $platform);
$this->runGitAction('ready', $vcs, $providerCommitHash, $owner, $repositoryName, $project, $resource, $deployment->getId(), $dbForProject, $dbForPlatform, $queueForRealtime, $platform);
}
/** Set auto deploy */
@@ -1183,7 +1200,7 @@ class Builds extends Action
->trigger();
if ($isVcsEnabled) {
$this->runGitAction('failed', $github, $providerCommitHash, $owner, $repositoryName, $project, $resource, $deployment->getId(), $dbForProject, $dbForPlatform, $queueForRealtime, $platform);
$this->runGitAction('failed', $vcs, $providerCommitHash, $owner, $repositoryName, $project, $resource, $deployment->getId(), $dbForProject, $dbForPlatform, $queueForRealtime, $platform);
}
} finally {
$queueForRealtime
@@ -1354,7 +1371,7 @@ class Builds extends Action
*/
protected function runGitAction(
string $status,
GitHub $github,
?Adapter\Git $vcs,
string $providerCommitHash,
string $owner,
string $repositoryName,
@@ -1369,7 +1386,7 @@ class Builds extends Action
$deployment = new Document();
try {
if ($resource->getAttribute('providerSilentMode', false) === true) {
if ($resource->getAttribute('providerSilentMode', false) === true || $vcs === null) {
return;
}
@@ -1408,7 +1425,7 @@ class Builds extends Action
default => throw new \Exception('Invalid resource type')
};
$github->updateCommitStatus($repositoryName, $providerCommitHash, $owner, $state, $message, $providerTargetUrl, $name);
$vcs->updateCommitStatus($repositoryName, $providerCommitHash, $owner, $state, $message, $providerTargetUrl, $name);
}
if (! empty($commentId)) {
@@ -1453,9 +1470,9 @@ class Builds extends Action
};
$comment = new Comment($platform);
$comment->parseComment($github->getComment($owner, $repositoryName, $commentId));
$comment->parseComment($vcs->getComment($owner, $repositoryName, $commentId));
$comment->addBuild($project, $resource, $resourceType, $status, $deployment->getId(), ['type' => 'logs'], $previewUrl);
$github->updateComment($owner, $repositoryName, $commentId, $comment->generateComment());
$vcs->updateComment($owner, $repositoryName, $commentId, $comment->generateComment());
} finally {
$dbForPlatform->deleteDocument('vcsCommentLocks', $commentId);
}
@@ -21,10 +21,11 @@ use Utopia\Http\Adapter\Swoole\Request;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\System\System;
use Appwrite\Vcs\VcsFactory;
use Utopia\Cache\Cache;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
use Utopia\VCS\Adapter\Git\GitHub;
class Create extends Base
{
@@ -78,7 +79,7 @@ class Create extends Base
->inject('project')
->inject('queueForEvents')
->inject('queueForBuilds')
->inject('gitHub')
->inject('cache')
->inject('authorization')
->inject('platform')
->callback($this->action(...));
@@ -99,7 +100,7 @@ class Create extends Base
Document $project,
Event $queueForEvents,
Build $queueForBuilds,
GitHub $github,
Cache $cache,
Authorization $authorization,
array $platform
) {
@@ -109,6 +110,7 @@ class Create extends Base
throw new Exception(Exception::SITE_NOT_FOUND);
}
// Templates are always from GitHub
$branchUrl = "https://github.com/$owner/$repository/blob/$reference";
$repositoryUrl = "https://github.com/$owner/$repository";
@@ -122,6 +124,8 @@ class Create extends Base
if (!empty($site->getAttribute('providerRepositoryId'))) {
$installation = $dbForPlatform->getDocument('installations', $site->getAttribute('installationId'));
$provider = $installation->getAttribute('provider', 'github');
$vcs = VcsFactory::getInitializedAdapter($provider, $installation, $cache);
$deployment = $this->redeployVcsSite(
request: $request,
@@ -132,7 +136,7 @@ class Create extends Base
dbForPlatform: $dbForPlatform,
queueForBuilds: $queueForBuilds,
template: $template,
github: $github,
vcs: $vcs,
activate: $activate,
authorization: $authorization,
platform: $platform
@@ -17,10 +17,11 @@ use Utopia\Database\Validator\UID;
use Utopia\Http\Adapter\Swoole\Request;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Appwrite\Vcs\VcsFactory;
use Utopia\Cache\Cache;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
use Utopia\VCS\Adapter\Git\GitHub;
class Create extends Base
{
@@ -72,7 +73,7 @@ class Create extends Base
->inject('project')
->inject('queueForEvents')
->inject('queueForBuilds')
->inject('gitHub')
->inject('cache')
->inject('authorization')
->inject('platform')
->callback($this->action(...));
@@ -90,7 +91,7 @@ class Create extends Base
Document $project,
Event $queueForEvents,
Build $queueForBuilds,
GitHub $github,
Cache $cache,
Authorization $authorization,
array $platform
) {
@@ -103,6 +104,8 @@ class Create extends Base
$template = new Document();
$installation = $dbForPlatform->getDocument('installations', $site->getAttribute('installationId'));
$provider = $installation->getAttribute('provider', 'github');
$vcs = VcsFactory::getInitializedAdapter($provider, $installation, $cache);
$deployment = $this->redeployVcsSite(
request: $request,
@@ -113,7 +116,7 @@ class Create extends Base
dbForPlatform: $dbForPlatform,
queueForBuilds: $queueForBuilds,
template: $template,
github: $github,
vcs: $vcs,
activate: $activate,
authorization: $authorization,
reference: $reference,
@@ -19,6 +19,8 @@ use Utopia\Database\Helpers\ID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\System\System;
use Appwrite\Vcs\VcsFactory;
use Utopia\Cache\Cache;
use Utopia\Validator\Boolean;
use Utopia\Validator\Range;
use Utopia\Validator\Text;
@@ -96,6 +98,7 @@ class Create extends Base
->inject('project')
->inject('queueForEvents')
->inject('dbForPlatform')
->inject('cache')
->callback($this->action(...));
}
@@ -125,7 +128,8 @@ class Create extends Base
Database $dbForProject,
Document $project,
Event $queueForEvents,
Database $dbForPlatform
Database $dbForPlatform,
Cache $cache
) {
if (!empty($adapter)) {
$configFramework = Config::getParam('frameworks')[$framework] ?? [];
@@ -201,6 +205,13 @@ class Create extends Base
'providerPullRequestIds' => []
]);
$repository = $dbForPlatform->createDocument('repositories', $repository);
$provider = $installation->getAttribute('provider', 'github');
$vcs = VcsFactory::getInitializedAdapter($provider, $installation, $cache);
$owner = VcsFactory::getOwnerName($vcs, $provider, $installation->getAttribute('providerInstallationId', ''), $providerRepositoryId);
$repoName = $vcs->getRepositoryName($providerRepositoryId);
VcsFactory::createRepositoryWebhook($vcs, $provider, $owner, $repoName);
$site->setAttribute('repositoryId', $repository->getId());
$site->setAttribute('repositoryInternalId', $repository->getSequence());
@@ -26,7 +26,8 @@ use Utopia\Validator\Boolean;
use Utopia\Validator\Range;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
use Utopia\VCS\Adapter\Git\GitHub;
use Appwrite\Vcs\VcsFactory;
use Utopia\Cache\Cache;
class Update extends Base
{
@@ -101,7 +102,7 @@ class Update extends Base
->inject('queueForEvents')
->inject('queueForBuilds')
->inject('dbForPlatform')
->inject('gitHub')
->inject('cache')
->inject('executor')
->callback($this->action(...));
}
@@ -135,7 +136,7 @@ class Update extends Base
Event $queueForEvents,
Build $queueForBuilds,
Database $dbForPlatform,
GitHub $github,
Cache $cache,
Executor $executor
) {
if (!empty($adapter)) {
@@ -215,6 +216,13 @@ class Update extends Base
'providerPullRequestIds' => []
]);
$repository = $dbForPlatform->createDocument('repositories', $repository);
$provider = $installation->getAttribute('provider', 'github');
$vcs = VcsFactory::getInitializedAdapter($provider, $installation, $cache);
$owner = VcsFactory::getOwnerName($vcs, $provider, $installation->getAttribute('providerInstallationId', ''), $providerRepositoryId);
$repoName = $vcs->getRepositoryName($providerRepositoryId);
VcsFactory::createRepositoryWebhook($vcs, $provider, $owner, $repoName);
$repositoryId = $repository->getId();
$repositoryInternalId = $repository->getSequence();
}
@@ -283,7 +291,9 @@ class Update extends Base
// Redeploy logic
if (!$isConnected && !empty($providerRepositoryId)) {
$this->redeployVcsFunction($request, $site, $project, $installation, $dbForProject, $queueForBuilds, new Document(), $github, true);
$provider = $installation->getAttribute('provider', 'github');
$vcs = VcsFactory::getInitializedAdapter($provider, $installation, $cache);
$this->redeployVcsFunction($request, $site, $project, $installation, $dbForProject, $queueForBuilds, new Document(), $vcs, true);
}
$queueForEvents->setParam('siteId', $site->getId());
@@ -10,14 +10,14 @@ use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Appwrite\Vcs\VcsFactory;
use Utopia\Cache\Cache;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Platform\Scope\HTTP;
use Utopia\System\System;
use Utopia\Validator\Text;
use Utopia\VCS\Adapter\Git\GitHub;
use Utopia\VCS\Exception\RepositoryNotFound;
class Update extends Action
@@ -34,7 +34,8 @@ class Update extends Action
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/vcs/github/installations/:installationId/repositories/:repositoryId')
->setHttpPath('/v1/vcs/installations/:installationId/repositories/:repositoryId')
->httpAlias('/v1/vcs/github/installations/:installationId/repositories/:repositoryId')
->desc('Update external deployment (authorize)')
->groups(['api', 'vcs'])
->label('scope', 'vcs.write')
@@ -53,8 +54,8 @@ class Update extends Action
))
->param('installationId', '', new Text(256), 'Installation Id')
->param('repositoryId', '', new Text(256), 'VCS Repository Id')
->param('providerPullRequestId', '', new Text(256), 'GitHub Pull Request Id')
->inject('gitHub')
->param('providerPullRequestId', '', new Text(256), 'Pull Request Id')
->inject('cache')
->inject('response')
->inject('project')
->inject('dbForPlatform')
@@ -69,7 +70,7 @@ class Update extends Action
string $installationId,
string $repositoryId,
string $providerPullRequestId,
GitHub $github,
Cache $cache,
Response $response,
Document $project,
Database $dbForPlatform,
@@ -101,36 +102,35 @@ class Update extends Action
$repository = $authorization->skip(fn () => $dbForPlatform->updateDocument('repositories', $repository->getId(), new Document(['providerPullRequestIds' => $providerPullRequestIds])));
$privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY');
$githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID');
$provider = $installation->getAttribute('provider', 'github');
$providerInstallationId = $installation->getAttribute('providerInstallationId');
$github->initializeVariables($providerInstallationId, $privateKey, $githubAppId);
$vcs = VcsFactory::getInitializedAdapter($provider, $installation, $cache);
$repositories = [$repository];
$providerRepositoryId = $repository->getAttribute('providerRepositoryId');
try {
$providerRepositoryName = $github->getRepositoryName($providerRepositoryId);
$providerRepositoryName = $vcs->getRepositoryName($providerRepositoryId);
} catch (RepositoryNotFound $e) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
$owner = $github->getOwnerName($providerInstallationId);
$pullRequestResponse = $github->getPullRequest($owner, $providerRepositoryName, $providerPullRequestId);
$owner = VcsFactory::getOwnerName($vcs, $provider, $providerInstallationId, $providerRepositoryId);
$pullRequestResponse = $vcs->getPullRequest($owner, $providerRepositoryName, $providerPullRequestId);
$providerRepositoryUrl = $pullRequestResponse['head']['repo']['html_url'] ?? '';
$providerRepositoryOwner = $pullRequestResponse['head']['repo']['owner']['login'] ?? '';
$providerBranch = \explode(':', $pullRequestResponse['head']['label'])[1] ?? '';
$providerBranch = \explode(':', $pullRequestResponse['head']['label'] ?? '')[1] ?? ($pullRequestResponse['head']['ref'] ?? '');
$providerBranchUrl = "$providerRepositoryUrl/tree/$providerBranch";
$providerCommitHash = $pullRequestResponse['head']['sha'] ?? '';
$commitDetails = $github->getCommit($providerRepositoryOwner, $providerRepositoryName, $providerCommitHash);
$providerCommitMessage = $commitDetails["commitMessage"] ?? '';
$providerCommitUrl = $commitDetails["commitUrl"] ?? '';
$providerCommitAuthor = $commitDetails["commitAuthor"] ?? '';
$providerCommitAuthorUrl = $commitDetails["commitAuthorUrl"] ?? '';
$commitDetails = $vcs->getCommit($providerRepositoryOwner, $providerRepositoryName, $providerCommitHash);
$providerCommitMessage = $commitDetails['commitMessage'] ?? '';
$providerCommitUrl = $commitDetails['commitUrl'] ?? '';
$providerCommitAuthor = $commitDetails['commitAuthor'] ?? '';
$providerCommitAuthorUrl = $commitDetails['commitAuthorUrl'] ?? '';
$this->createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, true, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $platform);
$this->createGitDeployments($vcs, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, true, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $platform);
$response->noContent();
}
@@ -20,13 +20,13 @@ use Utopia\Database\Validator\Authorization;
use Utopia\DSN\DSN;
use Utopia\Span\Span;
use Utopia\System\System;
use Utopia\VCS\Adapter\Git\GitHub;
use Utopia\VCS\Adapter\Git;
use Utopia\VCS\Exception\RepositoryNotFound;
trait Deployment
{
protected function createGitDeployments(
GitHub $github,
Git $github,
string $providerInstallationId,
array $repositories,
string $providerBranch,
@@ -107,7 +107,7 @@ trait Deployment
$activate = true;
}
$owner = $github->getOwnerName($providerInstallationId) ?? '';
$owner = $providerRepositoryOwner;
try {
$repositoryName = $github->getRepositoryName($providerRepositoryId);
} catch (RepositoryNotFound $e) {
@@ -290,7 +290,7 @@ trait Deployment
} catch (RepositoryNotFound $e) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
$owner = $github->getOwnerName($providerInstallationId);
$owner = $providerRepositoryOwner;
$github->updateCommitStatus($repositoryName, $providerCommitHash, $owner, 'pending', $message, $authorizeUrl, $name);
continue;
}
@@ -520,7 +520,7 @@ trait Deployment
} catch (RepositoryNotFound $e) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
$owner = $github->getOwnerName($providerInstallationId);
$owner = $providerRepositoryOwner;
$providerTargetUrl = $protocol . '://' . $hostname . "/console/project-$region-$projectId/$resourceCollection/$resourceType-$resourceId";
$github->updateCommitStatus($repositoryName, $providerCommitHash, $owner, 'pending', $message, $providerTargetUrl, $name);
@@ -163,7 +163,7 @@ class Create extends Action
]));
// Create new deployment only on push (not committed by us) and not when branch is deleted
if ($providerCommitAuthorEmail !== APP_VCS_GITHUB_EMAIL && !$providerBranchDeleted) {
if ($providerCommitAuthorEmail !== APP_VCS_COMMIT_EMAIL && !$providerBranchDeleted) {
$this->createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthorName, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, '', false, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $platform);
}
}
@@ -0,0 +1,97 @@
<?php
namespace Appwrite\Platform\Modules\VCS\Http\Gitea\Authorize;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\MethodType;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Appwrite\Vcs\VcsFactory;
use Utopia\Database\Document;
use Utopia\Platform\Scope\HTTP;
use Utopia\System\System;
class Get extends Action
{
use HTTP;
public static function getName()
{
return 'getVCSGiteaAuthorize';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/vcs/gitea/authorize')
->desc('Create Gitea installation')
->groups(['api', 'vcs'])
->label('scope', 'vcs.read')
->label('error', APP_VIEWS_DIR . '/general/error.phtml')
->label('sdk', new Method(
namespace: 'vcs',
group: 'installations',
name: 'createGiteaInstallation',
description: '/docs/references/vcs/create-gitea-installation.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_MOVED_PERMANENTLY,
model: Response::MODEL_NONE,
)
],
contentType: ContentType::HTML,
type: MethodType::WEBAUTH,
hide: true,
))
->param('success', '', fn ($redirectValidator) => $redirectValidator, 'URL to redirect back to console after a successful installation attempt.', true, ['redirectValidator'])
->param('failure', '', fn ($redirectValidator) => $redirectValidator, 'URL to redirect back to console after a failed installation attempt.', true, ['redirectValidator'])
->inject('response')
->inject('project')
->inject('platform')
->callback($this->action(...));
}
public function action(
string $success,
string $failure,
Response $response,
Document $project,
array $platform
) {
$endpoint = VcsFactory::getEndpoint('gitea');
$browserEndpoint = VcsFactory::getBrowserEndpoint('gitea');
$clientId = VcsFactory::getClientId('gitea');
if (empty($endpoint) || empty($clientId)) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Gitea VCS is not configured. Please configure _APP_VCS_GITEA_ENDPOINT and _APP_VCS_GITEA_CLIENT_ID environment variables.');
}
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$hostname = $platform['consoleHostname'] ?? '';
$state = \json_encode([
'projectId' => $project->getId(),
'success' => $success,
'failure' => $failure,
]);
$url = rtrim($browserEndpoint, '/') . '/login/oauth/authorize?' . \http_build_query([
'client_id' => $clientId,
'redirect_uri' => $protocol . '://' . $hostname . '/v1/vcs/gitea/callback',
'response_type' => 'code',
'scope' => 'read:user write:repository read:organization',
'state' => $state,
]);
$response
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
->addHeader('Pragma', 'no-cache')
->redirect($url);
}
}
@@ -0,0 +1,180 @@
<?php
namespace Appwrite\Platform\Modules\VCS\Http\Gitea\Callback;
use Appwrite\Auth\OAuth2\Gitea as OAuth2Gitea;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Permission as AppwritePermission;
use Appwrite\Utopia\Response;
use Appwrite\Vcs\VcsFactory;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Query;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\System\System;
use Utopia\Validator\Text;
class Get extends Action
{
use HTTP;
use AppwritePermission;
public static function getName()
{
return 'getVCSGiteaCallback';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/vcs/gitea/callback')
->desc('Get installation and authorization from Gitea')
->groups(['api', 'vcs'])
->label('scope', 'public')
->label('error', APP_VIEWS_DIR . '/general/error.phtml')
->param('code', '', new Text(2048, 0), 'OAuth2 code.', true)
->param('state', '', new Text(2048), 'State containing project info.', true)
->inject('project')
->inject('response')
->inject('dbForPlatform')
->inject('platform')
->callback($this->action(...));
}
public function action(
string $code,
string $state,
Document $project,
Response $response,
Database $dbForPlatform,
array $platform
) {
if (empty($state)) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Missing state parameter.');
}
$state = \json_decode($state, true);
$redirectFailure = $state['failure'] ?? '';
$projectId = $state['projectId'] ?? '';
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
$error = 'Project with the ID from state could not be found.';
if (!empty($redirectFailure)) {
$separator = \str_contains($redirectFailure, '?') ? '&' : ':';
$response
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
->addHeader('Pragma', 'no-cache')
->redirect($redirectFailure . $separator . \http_build_query(['error' => $error]));
return;
}
throw new Exception(Exception::PROJECT_NOT_FOUND, $error);
}
$region = $project->getAttribute('region', 'default');
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$hostname = $platform['consoleHostname'] ?? '';
$defaultState = [
'success' => $protocol . '://' . $hostname . "/console/project-$region-$projectId/settings/git-installations",
'failure' => $protocol . '://' . $hostname . "/console/project-$region-$projectId/settings/git-installations",
];
$state = \array_merge($defaultState, $state ?? []);
$redirectSuccess = $state['success'] ?? '';
$redirectFailure = $state['failure'] ?? '';
if (empty($code)) {
$error = 'OAuth2 authorization code is missing.';
if (!empty($redirectFailure)) {
$separator = \str_contains($redirectFailure, '?') ? '&' : ':';
$response
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
->addHeader('Pragma', 'no-cache')
->redirect($redirectFailure . $separator . \http_build_query(['error' => $error]));
return;
}
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, $error);
}
$endpoint = VcsFactory::getEndpoint('gitea');
$clientId = VcsFactory::getClientId('gitea');
$clientSecret = VcsFactory::getClientSecret('gitea');
$oauth2 = new OAuth2Gitea($clientId, $clientSecret, '', [], [], $endpoint);
$accessToken = $oauth2->getAccessToken($code) ?? '';
$refreshToken = $oauth2->getRefreshToken($code) ?? '';
$accessTokenExpiry = DateTime::addSeconds(new \DateTime(), \intval($oauth2->getAccessTokenExpiry($code)));
$owner = $oauth2->getUserSlug($accessToken) ?? '';
if (empty($owner)) {
$error = 'Failed to get user information from Gitea.';
if (!empty($redirectFailure)) {
$separator = \str_contains($redirectFailure, '?') ? '&' : ':';
$response
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
->addHeader('Pragma', 'no-cache')
->redirect($redirectFailure . $separator . \http_build_query(['error' => $error]));
return;
}
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, $error);
}
$projectInternalId = $project->getSequence();
// Use the OAuth2 user ID as the providerInstallationId for Gitea
$providerInstallationId = $oauth2->getUserID($accessToken);
$installation = $dbForPlatform->findOne('installations', [
Query::equal('providerInstallationId', [$providerInstallationId]),
Query::equal('projectInternalId', [$projectInternalId]),
Query::equal('provider', ['gitea']),
]);
if ($installation->isEmpty()) {
$teamId = $project->getAttribute('teamId', '');
$installation = new Document([
'$id' => ID::unique(),
'$permissions' => $this->getPermissions($teamId, $projectId),
'providerInstallationId' => $providerInstallationId,
'projectId' => $projectId,
'projectInternalId' => $projectInternalId,
'provider' => 'gitea',
'organization' => $owner,
'personal' => true,
'personalRefreshToken' => $refreshToken,
'personalAccessToken' => $accessToken,
'personalAccessTokenExpiry' => $accessTokenExpiry,
]);
$installation = $dbForPlatform->createDocument('installations', $installation);
} else {
$installation = $dbForPlatform->updateDocument('installations', $installation->getId(), new Document([
'organization' => $owner,
'personal' => true,
'personalRefreshToken' => $refreshToken,
'personalAccessToken' => $accessToken,
'personalAccessTokenExpiry' => $accessTokenExpiry,
]));
}
$response
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
->addHeader('Pragma', 'no-cache')
->redirect($redirectSuccess);
}
}
@@ -0,0 +1,223 @@
<?php
namespace Appwrite\Platform\Modules\VCS\Http\Gitea\Events;
use Appwrite\Event\Build;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Action;
use Appwrite\Platform\Modules\VCS\Http\GitHub\Deployment;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Appwrite\Vcs\VcsFactory;
use Utopia\Cache\Cache;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Platform\Scope\HTTP;
use Utopia\Span\Span;
use Utopia\VCS\Adapter\Git\Gitea;
class Create extends Action
{
use HTTP;
use Deployment;
public static function getName()
{
return 'createVCSGiteaEvent';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/vcs/gitea/events')
->desc('Create Gitea event')
->groups(['api', 'vcs'])
->label('scope', 'public')
->inject('cache')
->inject('request')
->inject('response')
->inject('dbForPlatform')
->inject('authorization')
->inject('getProjectDB')
->inject('queueForBuilds')
->inject('platform')
->callback($this->action(...));
}
public function action(
Cache $cache,
Request $request,
Response $response,
Database $dbForPlatform,
Authorization $authorization,
callable $getProjectDB,
Build $queueForBuilds,
array $platform
) {
$event = $request->getHeader('x-gitea-event', '');
Span::add('vcs.gitea.event.name', $event);
$payload = $request->getRawPayload();
$signature = $request->getHeader('x-gitea-signature', '');
$secretKey = VcsFactory::getWebhookSecret('gitea');
$gitea = VcsFactory::getAdapter('gitea', $cache);
$valid = empty($secretKey) ? true : $gitea->validateWebhookEvent($payload, $signature, $secretKey);
Span::add('vcs.gitea.event.signature.valid', $valid);
if (!$valid) {
throw new Exception(Exception::GENERAL_ACCESS_FORBIDDEN, 'Invalid webhook payload signature.');
}
$parsedPayload = $gitea->getEvent($event, $payload);
match ($event) {
'push' => $this->handlePushEvent($parsedPayload, $gitea, $cache, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $platform),
'pull_request' => $this->handlePullRequestEvent($parsedPayload, $gitea, $cache, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $platform),
default => null,
};
$response->json($parsedPayload);
}
private function handlePushEvent(
array $parsedPayload,
Gitea $gitea,
Cache $cache,
Database $dbForPlatform,
Authorization $authorization,
Build $queueForBuilds,
callable $getProjectDB,
array $platform,
) {
$providerBranchDeleted = $parsedPayload['branchDeleted'] ?? false;
$providerBranch = $parsedPayload['branch'] ?? '';
$providerBranchUrl = $parsedPayload['branchUrl'] ?? '';
$providerRepositoryId = $parsedPayload['repositoryId'] ?? '';
$providerRepositoryName = $parsedPayload['repositoryName'] ?? '';
$providerRepositoryUrl = $parsedPayload['repositoryUrl'] ?? '';
$providerCommitHash = $parsedPayload['commitHash'] ?? '';
$providerRepositoryOwner = $parsedPayload['owner'] ?? '';
$providerCommitAuthorName = $parsedPayload['headCommitAuthorName'] ?? '';
$providerCommitAuthorEmail = $parsedPayload['headCommitAuthorEmail'] ?? '';
$providerCommitAuthorUrl = $parsedPayload['authorUrl'] ?? '';
$providerCommitMessage = $parsedPayload['headCommitMessage'] ?? '';
$providerCommitUrl = $parsedPayload['headCommitUrl'] ?? '';
Span::add('vcs.gitea.event.repo.id', $providerRepositoryId);
Span::add('vcs.gitea.event.repo.name', $providerRepositoryName);
Span::add('vcs.gitea.event.branch', $providerBranch);
// Find the installation by looking up the repository's organization and provider
$installation = $this->findInstallation($providerRepositoryOwner, $dbForPlatform, $authorization);
if ($installation === null) {
return;
}
$vcs = VcsFactory::getInitializedAdapter('gitea', $installation, $cache);
$repositories = $authorization->skip(fn () => $dbForPlatform->find('repositories', [
Query::equal('providerRepositoryId', [$providerRepositoryId]),
Query::limit(100),
]));
if ($providerCommitAuthorEmail !== APP_VCS_COMMIT_EMAIL && !$providerBranchDeleted) {
$this->createGitDeployments($vcs, '', $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthorName, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, '', false, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $platform);
}
}
private function handlePullRequestEvent(
array $parsedPayload,
Gitea $gitea,
Cache $cache,
Database $dbForPlatform,
Authorization $authorization,
Build $queueForBuilds,
callable $getProjectDB,
array $platform,
) {
$action = $parsedPayload['action'] ?? '';
if ($action === 'opened' || $action === 'reopened' || $action === 'synchronize' || $action === 'synchronized') {
$providerBranch = $parsedPayload['branch'] ?? '';
$providerBranchUrl = $parsedPayload['branchUrl'] ?? '';
$providerRepositoryId = $parsedPayload['repositoryId'] ?? '';
$providerRepositoryName = $parsedPayload['repositoryName'] ?? '';
$providerRepositoryUrl = $parsedPayload['repositoryUrl'] ?? '';
$providerPullRequestId = $parsedPayload['pullRequestNumber'] ?? '';
$providerCommitHash = $parsedPayload['commitHash'] ?? '';
$providerRepositoryOwner = $parsedPayload['owner'] ?? '';
$external = $parsedPayload['external'] ?? true;
$providerCommitUrl = $parsedPayload['headCommitUrl'] ?? '';
$providerCommitAuthorUrl = $parsedPayload['authorUrl'] ?? '';
Span::add('vcs.gitea.event.repo.id', $providerRepositoryId);
Span::add('vcs.gitea.event.repo.name', $providerRepositoryName);
Span::add('vcs.gitea.event.branch', $providerBranch);
// Ignore sync for non-external
if (!$external && ($action === 'synchronize' || $action === 'synchronized')) {
return;
}
$installation = $this->findInstallation($providerRepositoryOwner, $dbForPlatform, $authorization);
if ($installation === null) {
return;
}
$vcs = VcsFactory::getInitializedAdapter('gitea', $installation, $cache);
$commitDetails = $vcs->getCommit($providerRepositoryOwner, $providerRepositoryName, $providerCommitHash);
$providerCommitAuthor = $commitDetails['commitAuthor'] ?? '';
$providerCommitMessage = $commitDetails['commitMessage'] ?? '';
$repositories = $authorization->skip(fn () => $dbForPlatform->find('repositories', [
Query::equal('providerRepositoryId', [$providerRepositoryId]),
Query::orderDesc('$createdAt')
]));
$this->createGitDeployments($vcs, '', $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, $external, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $platform);
} elseif ($action === 'closed') {
$providerRepositoryId = $parsedPayload['repositoryId'] ?? '';
$providerPullRequestId = $parsedPayload['pullRequestNumber'] ?? '';
$external = $parsedPayload['external'] ?? true;
if ($external) {
$repositories = $authorization->skip(fn () => $dbForPlatform->find('repositories', [
Query::equal('providerRepositoryId', [$providerRepositoryId]),
Query::orderDesc('$createdAt')
]));
foreach ($repositories as $repository) {
$providerPullRequestIds = $repository->getAttribute('providerPullRequestIds', []);
if (\in_array($providerPullRequestId, $providerPullRequestIds)) {
$providerPullRequestIds = \array_diff($providerPullRequestIds, [$providerPullRequestId]);
$authorization->skip(fn () => $dbForPlatform->updateDocument('repositories', $repository->getId(), new Document(['providerPullRequestIds' => $providerPullRequestIds])));
}
}
}
}
}
/**
* Find an installation by organization name for Gitea provider.
*/
private function findInstallation(string $organization, Database $dbForPlatform, Authorization $authorization): ?Document
{
$installation = $authorization->skip(fn () => $dbForPlatform->findOne('installations', [
Query::equal('organization', [$organization]),
Query::equal('provider', ['gitea']),
]));
if ($installation->isEmpty()) {
return null;
}
return $installation;
}
}
@@ -8,12 +8,12 @@ use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Appwrite\Vcs\VcsFactory;
use Utopia\Cache\Cache;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Platform\Scope\HTTP;
use Utopia\System\System;
use Utopia\Validator\Text;
use Utopia\VCS\Adapter\Git\GitHub;
use Utopia\VCS\Exception\RepositoryNotFound;
class XList extends Action
@@ -29,7 +29,8 @@ class XList extends Action
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId/branches')
->setHttpPath('/v1/vcs/installations/:installationId/providerRepositories/:providerRepositoryId/branches')
->httpAlias('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId/branches')
->desc('List repository branches')
->groups(['api', 'vcs'])
->label('scope', 'vcs.read')
@@ -49,7 +50,7 @@ class XList extends Action
))
->param('installationId', '', new Text(256), 'Installation Id')
->param('providerRepositoryId', '', new Text(256), 'Repository Id')
->inject('gitHub')
->inject('cache')
->inject('response')
->inject('dbForPlatform')
->callback($this->action(...));
@@ -58,7 +59,7 @@ class XList extends Action
public function action(
string $installationId,
string $providerRepositoryId,
GitHub $github,
Cache $cache,
Response $response,
Database $dbForPlatform
) {
@@ -68,14 +69,15 @@ class XList extends Action
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
}
$provider = $installation->getAttribute('provider', 'github');
$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);
$accessToken = $installation->getAttribute('personalAccessToken', '');
$vcs = VcsFactory::getInitializedAdapter($provider, $installation, $cache);
$owner = VcsFactory::getOwnerName($vcs, $provider, $providerInstallationId, $providerRepositoryId) ?? '';
$owner = $github->getOwnerName($providerInstallationId) ?? '';
try {
$repositoryName = $github->getRepositoryName($providerRepositoryId) ?? '';
$repositoryName = $vcs->getRepositoryName($providerRepositoryId) ?? '';
if (empty($repositoryName)) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
@@ -83,7 +85,7 @@ class XList extends Action
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
$branches = $github->listBranches($owner, $repositoryName) ?? [];
$branches = $vcs->listBranches($owner, $repositoryName) ?? [];
$response->dynamic(new Document([
'branches' => \array_map(function ($branch) {
@@ -8,12 +8,14 @@ use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Appwrite\Vcs\VcsFactory;
use Utopia\Cache\Cache;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Platform\Scope\HTTP;
use Utopia\System\System;
use Utopia\Validator\Text;
use Utopia\VCS\Adapter\Git\GitHub;
use Utopia\VCS\Adapter\Git\Gitea;
use Utopia\VCS\Exception\RepositoryNotFound;
class Get extends Action
@@ -29,7 +31,8 @@ class Get extends Action
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId/contents')
->setHttpPath('/v1/vcs/installations/:installationId/providerRepositories/:providerRepositoryId/contents')
->httpAlias('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId/contents')
->desc('Get files and directories of a VCS repository')
->groups(['api', 'vcs'])
->label('scope', 'vcs.read')
@@ -51,7 +54,7 @@ class Get extends Action
->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('cache')
->inject('response')
->inject('dbForPlatform')
->callback($this->action(...));
@@ -62,7 +65,7 @@ class Get extends Action
string $providerRepositoryId,
string $providerRootDirectory,
string $providerReference,
GitHub $github,
Cache $cache,
Response $response,
Database $dbForPlatform
) {
@@ -72,14 +75,13 @@ class Get extends Action
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
}
$provider = $installation->getAttribute('provider', 'github');
$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);
$vcs = VcsFactory::getInitializedAdapter($provider, $installation, $cache);
$owner = $github->getOwnerName($providerInstallationId);
$owner = VcsFactory::getOwnerName($vcs, $provider, $providerInstallationId, $providerRepositoryId);
try {
$repositoryName = $github->getRepositoryName($providerRepositoryId) ?? '';
$repositoryName = $vcs->getRepositoryName($providerRepositoryId) ?? '';
if (empty($repositoryName)) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
@@ -87,12 +89,13 @@ class Get extends Action
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
$contents = $github->listRepositoryContents($owner, $repositoryName, $providerRootDirectory, $providerReference);
$contents = $vcs->listRepositoryContents($owner, $repositoryName, $providerRootDirectory, $providerReference);
$contentsDir = ($vcs instanceof GitHub) ? GitHub::CONTENTS_DIRECTORY : Gitea::CONTENTS_DIRECTORY;
$vcsContents = [];
foreach ($contents as $content) {
$isDirectory = false;
if ($content['type'] === GitHub::CONTENTS_DIRECTORY) {
if ($content['type'] === $contentsDir) {
$isDirectory = true;
}
@@ -2,6 +2,7 @@
namespace Appwrite\Platform\Modules\VCS\Http\Installations\Repositories;
use Appwrite\Auth\OAuth2\Gitea as OAuth2Gitea;
use Appwrite\Auth\OAuth2\Github as OAuth2Github;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Action;
@@ -9,15 +10,15 @@ use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Appwrite\Vcs\VcsFactory;
use Utopia\Cache\Cache;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Query;
use Utopia\Platform\Scope\HTTP;
use Utopia\System\System;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
use Utopia\VCS\Adapter\Git\GitHub;
class Create extends Action
{
@@ -32,7 +33,8 @@ class Create extends Action
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/vcs/github/installations/:installationId/providerRepositories')
->setHttpPath('/v1/vcs/installations/:installationId/providerRepositories')
->httpAlias('/v1/vcs/github/installations/:installationId/providerRepositories')
->desc('Create repository')
->groups(['api', 'vcs'])
->label('scope', 'vcs.write')
@@ -53,7 +55,7 @@ class Create extends Action
->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('cache')
->inject('user')
->inject('response')
->inject('dbForPlatform')
@@ -64,7 +66,7 @@ class Create extends Action
string $installationId,
string $name,
bool $private,
GitHub $github,
Cache $cache,
Document $user,
Response $response,
Database $dbForPlatform
@@ -75,28 +77,38 @@ class Create extends Action
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
}
$provider = $installation->getAttribute('provider', 'github');
if ($installation->getAttribute('personal', false) === true) {
$oauth2 = new OAuth2Github(System::getEnv('_APP_VCS_GITHUB_CLIENT_ID', ''), System::getEnv('_APP_VCS_GITHUB_CLIENT_SECRET', ''), "");
$clientId = VcsFactory::getClientId($provider);
$clientSecret = VcsFactory::getClientSecret($provider);
$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);
}
if ($provider === 'github') {
$oauth2 = new OAuth2Github($clientId, $clientSecret, '');
$accessToken = $accessToken ?? $identity->getAttribute('providerAccessToken');
$refreshToken = $refreshToken ?? $identity->getAttribute('providerRefreshToken');
$accessTokenExpiry = $accessTokenExpiry ?? $identity->getAttribute('providerAccessTokenExpiry');
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');
}
} else {
$endpoint = VcsFactory::getEndpoint($provider);
$oauth2 = new OAuth2Gitea($clientId, $clientSecret, '', [], [], $endpoint);
}
$isExpired = new \DateTime($accessTokenExpiry) < new \DateTime('now');
$isExpired = !empty($accessTokenExpiry) && new \DateTime($accessTokenExpiry) < new \DateTime('now');
if ($isExpired) {
$oauth2->refreshTokens($refreshToken);
@@ -106,13 +118,13 @@ class Create extends Action
$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.");
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('')));
->setAttribute('personalAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int) $oauth2->getAccessTokenExpiry('')));
$dbForPlatform->updateDocument('installations', $installation->getId(), new Document([
'personalAccessToken' => $installation->getAttribute('personalAccessToken'),
@@ -124,19 +136,17 @@ class Create extends Action
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());
throw new Exception(Exception::GENERAL_PROVIDER_FAILURE, "Provider failed to process the request: " . $exception->getMessage());
}
} else {
$vcs = VcsFactory::getInitializedAdapter($provider, $installation, $cache);
$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);
$owner = VcsFactory::getOwnerName($vcs, $provider, $providerInstallationId);
try {
$repository = $github->createRepository($owner, $name, $private);
$repository = $vcs->createRepository($owner, $name, $private);
} catch (Exception $exception) {
throw new Exception(Exception::GENERAL_PROVIDER_FAILURE, "GitHub failed to process the request: " . $exception->getMessage());
throw new Exception(Exception::GENERAL_PROVIDER_FAILURE, "Provider failed to process the request: " . $exception->getMessage());
}
}
@@ -46,11 +46,11 @@ use Utopia\Detector\Detector\Framework;
use Utopia\Detector\Detector\Packager;
use Utopia\Detector\Detector\Runtime;
use Utopia\Detector\Detector\Strategy;
use Appwrite\Vcs\VcsFactory;
use Utopia\Cache\Cache;
use Utopia\Platform\Scope\HTTP;
use Utopia\System\System;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
use Utopia\VCS\Adapter\Git\GitHub;
use Utopia\VCS\Exception\FileNotFound;
use Utopia\VCS\Exception\RepositoryNotFound;
@@ -67,8 +67,10 @@ class Create extends Action
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/vcs/github/installations/:installationId/detections')
->setHttpPath('/v1/vcs/installations/:installationId/detections')
->httpAlias('/v1/vcs/github/installations/:installationId/detections')
->httpAlias('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId/detection')
->httpAlias('/v1/vcs/installations/:installationId/providerRepositories/:providerRepositoryId/detection')
->desc('Create repository detection')
->groups(['api', 'vcs'])
->label('scope', 'vcs.write')
@@ -93,7 +95,7 @@ class Create extends Action
->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('cache')
->inject('response')
->inject('dbForPlatform')
->callback($this->action(...));
@@ -104,7 +106,7 @@ class Create extends Action
string $providerRepositoryId,
string $type,
string $providerRootDirectory,
GitHub $github,
Cache $cache,
Response $response,
Database $dbForPlatform
) {
@@ -114,14 +116,13 @@ class Create extends Action
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
}
$provider = $installation->getAttribute('provider', 'github');
$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);
$vcs = VcsFactory::getInitializedAdapter($provider, $installation, $cache);
$owner = $github->getOwnerName($providerInstallationId);
$owner = VcsFactory::getOwnerName($vcs, $provider, $providerInstallationId, $providerRepositoryId);
try {
$repositoryName = $github->getRepositoryName($providerRepositoryId) ?? '';
$repositoryName = $vcs->getRepositoryName($providerRepositoryId) ?? '';
if (empty($repositoryName)) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
@@ -129,9 +130,9 @@ class Create extends Action
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
$files = $github->listRepositoryContents($owner, $repositoryName, $providerRootDirectory);
$files = $vcs->listRepositoryContents($owner, $repositoryName, $providerRootDirectory);
$files = \array_column($files, 'name');
$languages = $github->listRepositoryLanguages($owner, $repositoryName);
$languages = $vcs->listRepositoryLanguages($owner, $repositoryName);
$detector = new Packager();
foreach ($files as $file) {
@@ -148,7 +149,7 @@ class Create extends Action
if ($type === 'framework') {
$packages = '';
try {
$contentResponse = $github->getRepositoryContent($owner, $repositoryName, \rtrim($providerRootDirectory, '/') . '/package.json');
$contentResponse = $vcs->getRepositoryContent($owner, $repositoryName, \rtrim($providerRootDirectory, '/') . '/package.json');
$packages = $contentResponse['content'] ?? '';
} catch (FileNotFound $e) {
// Continue detection without package.json
@@ -280,7 +281,7 @@ class Create extends Action
$wg->add();
go(function () use ($github, $owner, $repositoryName, $providerRootDirectory, $file, $wg, &$envs) {
try {
$contentResponse = $github->getRepositoryContent($owner, $repositoryName, \rtrim($providerRootDirectory, '/') . '/' . $file);
$contentResponse = $vcs->getRepositoryContent($owner, $repositoryName, \rtrim($providerRootDirectory, '/') . '/' . $file);
$envFile = $contentResponse['content'] ?? '';
$configAdapter = new ConfigDotenv();
@@ -8,10 +8,11 @@ use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Appwrite\Vcs\VcsFactory;
use Utopia\Cache\Cache;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Platform\Scope\HTTP;
use Utopia\System\System;
use Utopia\Validator\Text;
use Utopia\VCS\Adapter\Git\GitHub;
use Utopia\VCS\Exception\RepositoryNotFound;
@@ -29,7 +30,8 @@ class Get extends Action
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId')
->setHttpPath('/v1/vcs/installations/:installationId/providerRepositories/:providerRepositoryId')
->httpAlias('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId')
->desc('Get repository')
->groups(['api', 'vcs'])
->label('scope', 'vcs.read')
@@ -49,7 +51,7 @@ class Get extends Action
))
->param('installationId', '', new Text(256), 'Installation Id')
->param('providerRepositoryId', '', new Text(256), 'Repository Id')
->inject('gitHub')
->inject('cache')
->inject('response')
->inject('dbForPlatform')
->callback($this->action(...));
@@ -58,7 +60,7 @@ class Get extends Action
public function action(
string $installationId,
string $providerRepositoryId,
GitHub $github,
Cache $cache,
Response $response,
Database $dbForPlatform
) {
@@ -68,14 +70,13 @@ class Get extends Action
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
}
$provider = $installation->getAttribute('provider', 'github');
$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);
$vcs = VcsFactory::getInitializedAdapter($provider, $installation, $cache);
$owner = $github->getOwnerName($providerInstallationId) ?? '';
$owner = VcsFactory::getOwnerName($vcs, $provider, $providerInstallationId, $providerRepositoryId) ?? '';
try {
$repositoryName = $github->getRepositoryName($providerRepositoryId) ?? '';
$repositoryName = $vcs->getRepositoryName($providerRepositoryId) ?? '';
if (empty($repositoryName)) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
@@ -83,12 +84,12 @@ class Get extends Action
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
$repository = $github->getRepository($owner, $repositoryName);
$repository = $vcs->getRepository($owner, $repositoryName);
$authorized = $github->hasAccessToAllRepositories();
if (!$authorized) {
$authorized = $vcs->hasAccessToAllRepositories();
if (!$authorized && $vcs instanceof GitHub) {
try {
$installationRepository = $github->getInstallationRepository($repositoryName);
$installationRepository = $vcs->getInstallationRepository($repositoryName);
if (!empty($installationRepository)) {
$authorized = true;
}
@@ -100,8 +101,8 @@ class Get extends Action
$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'] ?? '';
$repository['provider'] = $provider;
$repository['defaultBranch'] = $repository['default_branch'] ?? '';
$repository['authorized'] = $authorized;
$repository['providerInstallationId'] = $providerInstallationId;
@@ -50,11 +50,11 @@ use Utopia\Detector\Detector\Framework;
use Utopia\Detector\Detector\Packager;
use Utopia\Detector\Detector\Runtime;
use Utopia\Detector\Detector\Strategy;
use Appwrite\Vcs\VcsFactory;
use Utopia\Cache\Cache;
use Utopia\Platform\Scope\HTTP;
use Utopia\System\System;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
use Utopia\VCS\Adapter\Git\GitHub;
use Utopia\VCS\Exception\FileNotFound;
use function Swoole\Coroutine\batch;
@@ -72,7 +72,8 @@ class XList extends Action
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/vcs/github/installations/:installationId/providerRepositories')
->setHttpPath('/v1/vcs/installations/:installationId/providerRepositories')
->httpAlias('/v1/vcs/github/installations/:installationId/providerRepositories')
->desc('List repositories')
->groups(['api', 'vcs'])
->label('scope', 'vcs.read')
@@ -97,7 +98,7 @@ class XList extends Action
->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('cache')
->inject('response')
->inject('dbForPlatform')
->callback($this->action(...));
@@ -108,7 +109,7 @@ class XList extends Action
string $type,
string $search,
array $queries,
GitHub $github,
Cache $cache,
Response $response,
Database $dbForPlatform
) {
@@ -122,10 +123,9 @@ class XList extends Action
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
}
$provider = $installation->getAttribute('provider', 'github');
$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);
$vcs = VcsFactory::getInitializedAdapter($provider, $installation, $cache);
$queries = Query::parseQueries($queries);
$limitQuery = current(array_filter($queries, fn ($query) => $query->getMethod() === Query::TYPE_LIMIT));
@@ -139,8 +139,8 @@ class XList extends Action
}
$page = ($offset / $limit) + 1;
$owner = $github->getOwnerName($providerInstallationId);
['items' => $repos, 'total' => $total] = $github->searchRepositories($owner, $page, $limit, $search);
$owner = $installation->getAttribute('organization', '');
['items' => $repos, 'total' => $total] = $vcs->searchRepositories($owner, $page, $limit, $search);
$repos = \array_map(function ($repo) use ($installation) {
$repo['id'] = \strval($repo['id'] ?? '');
@@ -152,9 +152,9 @@ class XList extends Action
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'], '');
$repos = batch(\array_map(function ($repo) use ($type, $vcs) {
return function () use ($repo, $type, $vcs) {
$files = $vcs->listRepositoryContents($repo['organization'], $repo['name'], '');
$files = \array_column($files, 'name');
$detector = new Packager();
@@ -172,7 +172,7 @@ class XList extends Action
if ($type === 'framework') {
$packages = '';
try {
$contentResponse = $github->getRepositoryContent($repo['organization'], $repo['name'], 'package.json');
$contentResponse = $vcs->getRepositoryContent($repo['organization'], $repo['name'], 'package.json');
$packages = $contentResponse['content'] ?? '';
} catch (FileNotFound $e) {
// Continue detection without package.json
@@ -214,7 +214,7 @@ class XList extends Action
}
$repo['framework'] = $framework;
} else {
$languages = $github->listRepositoryLanguages($repo['organization'], $repo['name']);
$languages = $vcs->listRepositoryLanguages($repo['organization'], $repo['name']);
$strategies = [
new Strategy(Strategy::FILEMATCH),
@@ -275,9 +275,9 @@ class XList extends Action
}
$wg->add();
go(function () use ($github, $repo, $file, $wg, &$envs) {
go(function () use ($vcs, $repo, $file, $wg, &$envs) {
try {
$contentResponse = $github->getRepositoryContent($repo['organization'], $repo['name'], $file);
$contentResponse = $vcs->getRepositoryContent($repo['organization'], $repo['name'], $file);
$envFile = $contentResponse['content'] ?? '';
$configAdapter = new ConfigDotenv();
@@ -2,6 +2,9 @@
namespace Appwrite\Platform\Modules\VCS\Services;
use Appwrite\Platform\Modules\VCS\Http\Gitea\Authorize\Get as GetGiteaAuthorize;
use Appwrite\Platform\Modules\VCS\Http\Gitea\Callback\Get as GetGiteaCallback;
use Appwrite\Platform\Modules\VCS\Http\Gitea\Events\Create as CreateGiteaEvent;
use Appwrite\Platform\Modules\VCS\Http\GitHub\Authorize\External\Update as UpdateExternalDeployment;
use Appwrite\Platform\Modules\VCS\Http\GitHub\Authorize\Get as GetGitHubAuthorize;
use Appwrite\Platform\Modules\VCS\Http\GitHub\Callback\Get as GetGitHubCallback;
@@ -26,6 +29,12 @@ class Http extends Service
// GitHub Authorization & Callback
$this->addAction(GetGitHubAuthorize::getName(), new GetGitHubAuthorize());
$this->addAction(GetGitHubCallback::getName(), new GetGitHubCallback());
// Gitea Authorization & Callback
$this->addAction(GetGiteaAuthorize::getName(), new GetGiteaAuthorize());
$this->addAction(GetGiteaCallback::getName(), new GetGiteaCallback());
// External Deployments
$this->addAction(UpdateExternalDeployment::getName(), new UpdateExternalDeployment());
// Installations
@@ -41,7 +50,8 @@ class Http extends Service
$this->addAction(GetRepositoryContents::getName(), new GetRepositoryContents());
$this->addAction(CreateRepositoryDetections::getName(), new CreateRepositoryDetections());
// Events
// Events (per-provider webhook handlers)
$this->addAction(CreateGitHubEvent::getName(), new CreateGitHubEvent());
$this->addAction(CreateGiteaEvent::getName(), new CreateGiteaEvent());
}
}
+267
View File
@@ -0,0 +1,267 @@
<?php
namespace Appwrite\Vcs;
use Appwrite\Extend\Exception;
use Utopia\Cache\Cache;
use Utopia\Database\Document;
use Utopia\System\System;
use Utopia\VCS\Adapter\Git;
use Utopia\VCS\Adapter\Git\Gitea;
use Utopia\VCS\Adapter\Git\GitHub;
class VcsFactory
{
/**
* Create a VCS adapter instance for the given provider.
*/
public static function getAdapter(string $provider, Cache $cache): Git
{
return match ($provider) {
'github' => new GitHub($cache),
'gitea' => self::createGiteaBasedAdapter($provider, $cache),
default => throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, "Unsupported VCS provider: {$provider}"),
};
}
/**
* Initialize a VCS adapter with credentials from the installation document and environment.
*/
public static function initializeAdapter(Git $adapter, string $provider, Document $installation): void
{
$providerInstallationId = $installation->getAttribute('providerInstallationId', '');
match ($provider) {
'github' => $adapter->initializeVariables(
$providerInstallationId,
System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY', ''),
System::getEnv('_APP_VCS_GITHUB_APP_ID', ''),
),
'gitea' => $adapter->initializeVariables(
'',
'',
null,
$installation->getAttribute('personalAccessToken', ''),
$installation->getAttribute('personalRefreshToken', ''),
),
default => throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, "Unsupported VCS provider: {$provider}"),
};
}
/**
* Create and initialize a VCS adapter in one call.
*/
public static function getInitializedAdapter(string $provider, Document $installation, Cache $cache): Git
{
$adapter = self::getAdapter($provider, $cache);
self::initializeAdapter($adapter, $provider, $installation);
return $adapter;
}
/**
* Get the browser-facing base URL for a VCS provider.
* Used for constructing user-visible URLs (repo, branch, commit links).
*/
public static function getProviderUrl(string $provider): string
{
return match ($provider) {
'github' => 'https://github.com',
'gitea' => rtrim(self::getBrowserEndpoint($provider), '/'),
default => '',
};
}
/**
* Build a branch URL for the given provider.
*/
public static function getBranchUrl(string $provider, string $owner, string $repo, string $branch): string
{
$base = self::getProviderUrl($provider);
if (empty($base)) {
return '';
}
return match ($provider) {
'github' => "{$base}/{$owner}/{$repo}/tree/{$branch}",
'gitea' => "{$base}/{$owner}/{$repo}/src/branch/{$branch}",
default => '',
};
}
/**
* Build a repository URL for the given provider.
*/
public static function getRepoUrl(string $provider, string $owner, string $repo): string
{
$base = self::getProviderUrl($provider);
if (empty($base)) {
return '';
}
return "{$base}/{$owner}/{$repo}";
}
/**
* Build a commit URL for the given provider.
*/
public static function getCommitUrl(string $provider, string $owner, string $repo, string $hash): string
{
$base = self::getProviderUrl($provider);
if (empty($base)) {
return '';
}
return match ($provider) {
'github' => "{$base}/{$owner}/{$repo}/commit/{$hash}",
'gitea' => "{$base}/{$owner}/{$repo}/commit/{$hash}",
default => '',
};
}
/**
* Get the webhook secret env var for a provider.
*/
public static function getWebhookSecret(string $provider): string
{
return match ($provider) {
'github' => System::getEnv('_APP_VCS_GITHUB_WEBHOOK_SECRET', ''),
'gitea' => System::getEnv('_APP_VCS_GITEA_WEBHOOK_SECRET', ''),
default => '',
};
}
/**
* Get the OAuth2 client ID env var for a provider.
*/
public static function getClientId(string $provider): string
{
return match ($provider) {
'github' => System::getEnv('_APP_VCS_GITHUB_CLIENT_ID', ''),
'gitea' => System::getEnv('_APP_VCS_GITEA_CLIENT_ID', ''),
default => '',
};
}
/**
* Get the OAuth2 client secret env var for a provider.
*/
public static function getClientSecret(string $provider): string
{
return match ($provider) {
'github' => System::getEnv('_APP_VCS_GITHUB_CLIENT_SECRET', ''),
'gitea' => System::getEnv('_APP_VCS_GITEA_CLIENT_SECRET', ''),
default => '',
};
}
/**
* Get the endpoint URL for a self-hosted provider.
*/
public static function getEndpoint(string $provider): string
{
return match ($provider) {
'gitea' => System::getEnv('_APP_VCS_GITEA_ENDPOINT', ''),
default => '',
};
}
/**
* Get the browser-facing endpoint URL for a self-hosted provider.
*
* In Docker, the internal endpoint (e.g. http://gitea:3000) differs from
* the URL the browser can reach (e.g. http://localhost:9510). This returns
* the browser-facing URL, falling back to the internal endpoint if not set.
*/
public static function getBrowserEndpoint(string $provider): string
{
return match ($provider) {
'gitea' => System::getEnv('_APP_VCS_GITEA_BROWSER_ENDPOINT', System::getEnv('_APP_VCS_GITEA_ENDPOINT', '')),
default => '',
};
}
/**
* Get the owner name from a VCS adapter, handling provider differences.
*
* GitHub uses installationId, Gitea-based providers require repositoryId.
*/
public static function getOwnerName(Git $adapter, string $provider, string $providerInstallationId, ?string $providerRepositoryId = null): string
{
return match ($provider) {
'github' => $adapter->getOwnerName($providerInstallationId),
'gitea' => $adapter->getOwnerName('', !empty($providerRepositoryId) ? intval($providerRepositoryId) : null),
default => '',
};
}
/**
* Check if a provider is configured via environment variables.
*/
public static function isProviderConfigured(string $provider): bool
{
return match ($provider) {
'github' => !empty(System::getEnv('_APP_VCS_GITHUB_APP_NAME', ''))
&& !empty(System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY', ''))
&& !empty(System::getEnv('_APP_VCS_GITHUB_APP_ID', ''))
&& !empty(System::getEnv('_APP_VCS_GITHUB_CLIENT_ID', ''))
&& !empty(System::getEnv('_APP_VCS_GITHUB_CLIENT_SECRET', '')),
'gitea' => !empty(System::getEnv('_APP_VCS_GITEA_ENDPOINT', '')),
default => false,
};
}
/**
* Get all configured VCS providers.
*
* @return string[]
*/
public static function getConfiguredProviders(): array
{
$configured = [];
foreach (APP_VCS_PROVIDERS as $provider) {
if (self::isProviderConfigured($provider)) {
$configured[] = $provider;
}
}
return $configured;
}
/**
* Create a webhook on a repository for non-GitHub providers.
*
* GitHub manages webhooks via its App — no per-repo setup needed.
* Gitea (and similar) require explicit webhook creation on each repository.
*/
public static function createRepositoryWebhook(Git $adapter, string $provider, string $owner, string $repositoryName): void
{
if ($provider === 'github') {
return;
}
// Use the internal webhook URL if set (for Docker-to-Docker communication),
// otherwise fall back to the public domain.
$internalUrl = System::getEnv('_APP_VCS_WEBHOOK_URL', '');
if (empty($internalUrl)) {
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http://' : 'https://';
$internalUrl = $protocol . System::getEnv('_APP_DOMAIN', 'localhost');
}
$webhookUrl = rtrim($internalUrl, '/') . "/v1/vcs/{$provider}/events";
$webhookSecret = self::getWebhookSecret($provider);
$adapter->createWebhook($owner, $repositoryName, $webhookUrl, $webhookSecret, ['push', 'pull_request']);
}
/**
* Create a Gitea-based adapter with the correct endpoint.
*/
private static function createGiteaBasedAdapter(string $provider, Cache $cache): Gitea
{
$adapter = new Gitea($cache);
$endpoint = self::getEndpoint($provider);
if (!empty($endpoint)) {
$adapter->setEndpoint($endpoint);
}
return $adapter;
}
}