From f59bb9d8cbbaccc6bbbb18f5d2974bea6cd90980 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 10 Apr 2026 15:31:48 +0200 Subject: [PATCH 1/6] Add branch list test --- tests/VCS/Adapter/GiteaTest.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/VCS/Adapter/GiteaTest.php b/tests/VCS/Adapter/GiteaTest.php index 861b9a9b..684ccf4e 100644 --- a/tests/VCS/Adapter/GiteaTest.php +++ b/tests/VCS/Adapter/GiteaTest.php @@ -1443,6 +1443,21 @@ public function testListBranches(): void } } + public function testListBranchesEmptyRepo(): void + { + $repositoryName = 'test-list-branches-empty-' . \uniqid(); + $this->vcsAdapter->createRepository(static::$owner, $repositoryName, false); + + try { + $branches = $this->vcsAdapter->listBranches(static::$owner, $repositoryName); + + $this->assertIsArray($branches); + $this->assertEmpty($branches); + } finally { + $this->vcsAdapter->deleteRepository(static::$owner, $repositoryName); + } + } + public function testCreateTag(): void { $repositoryName = 'test-create-tag-' . \uniqid(); From 54532989e31a880fc3a00eb770df5ba5076d49d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 10 Apr 2026 15:41:52 +0200 Subject: [PATCH 2/6] Fix gitea empty branch list --- src/VCS/Adapter/Git/Gitea.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/VCS/Adapter/Git/Gitea.php b/src/VCS/Adapter/Git/Gitea.php index bc6544ef..fbfd48ba 100644 --- a/src/VCS/Adapter/Git/Gitea.php +++ b/src/VCS/Adapter/Git/Gitea.php @@ -724,7 +724,8 @@ public function listBranches(string $owner, string $repositoryName): array for ($currentPage = 1; $currentPage <= $maxPages; $currentPage++) { $url = "/repos/{$owner}/{$repositoryName}/branches?page={$currentPage}&limit={$perPage}"; - $response = $this->call(self::METHOD_GET, $url, ['Authorization' => "token $this->accessToken"]); + // We decode ourselves later, because there is edge-case when Gitea returns empty body instead of empty array + $response = $this->call(self::METHOD_GET, $url, ['Authorization' => "token $this->accessToken"], decode: false); $responseHeaders = $response['headers'] ?? []; $responseHeadersStatusCode = $responseHeaders['status-code'] ?? 0; @@ -740,7 +741,7 @@ public function listBranches(string $owner, string $repositoryName): array break; } - $responseBody = $response['body'] ?? []; + $responseBody = \json_decode($response['body'] ?? '', true) ?? []; if (!is_array($responseBody)) { break; From 69e360302611f22fc6fee53d47b2b66624b81707 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 10 Apr 2026 15:49:17 +0200 Subject: [PATCH 3/6] Improve tests coverage --- tests/VCS/Adapter/GitHubTest.php | 5 +++++ tests/VCS/Adapter/GiteaTest.php | 15 --------------- tests/VCS/Base.php | 15 +++++++++++++++ 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/tests/VCS/Adapter/GitHubTest.php b/tests/VCS/Adapter/GitHubTest.php index eec05722..d1fc4627 100644 --- a/tests/VCS/Adapter/GitHubTest.php +++ b/tests/VCS/Adapter/GitHubTest.php @@ -474,4 +474,9 @@ public function testGetLatestCommit(): void $this->assertSame('https://avatars.githubusercontent.com/in/287220?v=4', $commitDetails['commitAuthorAvatar']); $this->assertSame('https://github.com/apps/appwritedemoapp', $commitDetails['commitAuthorUrl']); } + + public function testListBranchesEmptyRepo(): void + { + $this->markTestSkipped('GitHub creates a default branch on repository creation'); + } } diff --git a/tests/VCS/Adapter/GiteaTest.php b/tests/VCS/Adapter/GiteaTest.php index 684ccf4e..861b9a9b 100644 --- a/tests/VCS/Adapter/GiteaTest.php +++ b/tests/VCS/Adapter/GiteaTest.php @@ -1443,21 +1443,6 @@ public function testListBranches(): void } } - public function testListBranchesEmptyRepo(): void - { - $repositoryName = 'test-list-branches-empty-' . \uniqid(); - $this->vcsAdapter->createRepository(static::$owner, $repositoryName, false); - - try { - $branches = $this->vcsAdapter->listBranches(static::$owner, $repositoryName); - - $this->assertIsArray($branches); - $this->assertEmpty($branches); - } finally { - $this->vcsAdapter->deleteRepository(static::$owner, $repositoryName); - } - } - public function testCreateTag(): void { $repositoryName = 'test-create-tag-' . \uniqid(); diff --git a/tests/VCS/Base.php b/tests/VCS/Base.php index 0542fcef..b0541700 100644 --- a/tests/VCS/Base.php +++ b/tests/VCS/Base.php @@ -123,6 +123,21 @@ public function testListBranches(): void $this->assertNotEmpty($branches); } + public function testListBranchesEmptyRepo(): void + { + $repositoryName = 'test-list-branches-empty-' . \uniqid(); + $this->vcsAdapter->createRepository('test-kh', $repositoryName, false); + + try { + $branches = $this->vcsAdapter->listBranches('test-kh', $repositoryName); + + $this->assertIsArray($branches); + $this->assertEmpty($branches); + } finally { + $this->vcsAdapter->deleteRepository('test-kh', $repositoryName); + } + } + public function testListRepositoryLanguages(): void { $languages = $this->vcsAdapter->listRepositoryLanguages('vermakhushboo', 'basic-js-crud'); From facfafed136aad1998d86ede406efa9aa7402d71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 10 Apr 2026 15:52:25 +0200 Subject: [PATCH 4/6] Improve tests CI/CD splitting --- .github/workflows/tests-external.yml | 13 +++++--- .github/workflows/tests.yml | 13 +++++--- CONTRIBUTING.md | 24 +++++++++++++++ docker-compose.yml | 46 +++++++++++++++++++++++----- phpunit.xml | 18 +++++++++-- 5 files changed, 95 insertions(+), 19 deletions(-) diff --git a/.github/workflows/tests-external.yml b/.github/workflows/tests-external.yml index 288b43aa..f236a303 100644 --- a/.github/workflows/tests-external.yml +++ b/.github/workflows/tests-external.yml @@ -9,11 +9,16 @@ permissions: jobs: tests: - name: Tests (External) + name: ${{ matrix.adapter }} runs-on: ubuntu-latest if: github.event.label.name == 'test' + strategy: + fail-fast: false + matrix: + adapter: [gitea, forgejo, github, gitlab, gogs] + steps: - name: Check out the repo uses: actions/checkout@v4 @@ -26,15 +31,15 @@ jobs: TESTS_GITHUB_APP_IDENTIFIER: ${{ secrets.TESTS_GITHUB_APP_IDENTIFIER }} TESTS_GITHUB_INSTALLATION_ID: ${{ secrets.TESTS_GITHUB_INSTALLATION_ID }} run: | - docker compose up -d + docker compose --profile ${{ matrix.adapter }} up -d sleep 15 - name: Doctor run: | - docker compose logs + docker compose --profile ${{ matrix.adapter }} logs docker ps docker network ls - name: Run Tests run: | - docker compose exec -T tests vendor/bin/phpunit --configuration phpunit.xml tests + docker compose exec -T tests vendor/bin/phpunit --configuration phpunit.xml --testsuite ${{ matrix.adapter }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a44030ff..6ec9ecee 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -8,10 +8,15 @@ permissions: jobs: tests: - name: Tests + name: ${{ matrix.adapter }} runs-on: ubuntu-latest if: github.event.pull_request.head.repo.full_name == github.repository + strategy: + fail-fast: false + matrix: + adapter: [gitea, forgejo, github, gitlab, gogs] + steps: - name: Check out the repo uses: actions/checkout@v4 @@ -22,15 +27,15 @@ jobs: TESTS_GITHUB_APP_IDENTIFIER: ${{ secrets.TESTS_GITHUB_APP_IDENTIFIER }} TESTS_GITHUB_INSTALLATION_ID: ${{ secrets.TESTS_GITHUB_INSTALLATION_ID }} run: | - docker compose up -d + docker compose --profile ${{ matrix.adapter }} up -d sleep 15 - name: Doctor run: | - docker compose logs + docker compose --profile ${{ matrix.adapter }} logs docker ps docker network ls - name: Run Tests run: | - docker compose exec -T tests vendor/bin/phpunit --configuration phpunit.xml tests + docker compose exec -T tests vendor/bin/phpunit --configuration phpunit.xml --testsuite ${{ matrix.adapter }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 05f288b6..6509d457 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -76,6 +76,30 @@ This will allow the Utopia-php community to have sufficient discussion about the This is also important for the Utopia-php lead developers to be able to give technical input and different emphasis regarding the feature design and architecture. Some bigger features might need to go through our [RFC process](https://github.com/appwrite/rfc). +## Running Tests + +Tests are split by adapter. Each adapter has its own Docker Compose profile and PHPUnit test suite. + +To start the stack and run tests for a specific adapter: + +```bash +docker compose --profile up -d +sleep 15 +docker compose exec -T tests vendor/bin/phpunit --configuration phpunit.xml --testsuite +``` + +Where `` is one of: `gitea`, `forgejo`, `github`, `gitlab`, `gogs`. + +For example, to run Gitea tests: + +```bash +docker compose --profile gitea up -d +sleep 15 +docker compose exec -T tests vendor/bin/phpunit --configuration phpunit.xml --testsuite gitea +``` + +The `github` adapter does not require any local services — only the GitHub secrets (`TESTS_GITHUB_PRIVATE_KEY`, `TESTS_GITHUB_APP_IDENTIFIER`, `TESTS_GITHUB_INSTALLATION_ID`) as environment variables. + ## Adding A New Adapter You can follow our [Adding new VCS Adapter](docs/add-new-vcs-adapter.md) tutorial to add a new VCS adapter like GitLab, Bitbucket etc. in this library. diff --git a/docker-compose.yml b/docker-compose.yml index 702fe5fd..c5d511ac 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,23 +21,32 @@ services: - TESTS_GITLAB_URL=http://gitlab:80 depends_on: gitea: - condition: service_healthy + condition: service_started + required: false gitea-bootstrap: condition: service_completed_successfully + required: false forgejo: - condition: service_healthy + condition: service_started + required: false forgejo-bootstrap: condition: service_completed_successfully + required: false gogs: - condition: service_healthy + condition: service_started + required: false gogs-bootstrap: condition: service_completed_successfully + required: false gitlab: - condition: service_healthy + condition: service_started + required: false gitlab-bootstrap: condition: service_completed_successfully - request-catcher: + required: false + request-catcher: condition: service_started + required: false gitea: image: gitea/gitea:1.21.5 @@ -46,7 +55,7 @@ services: - USER_GID=1000 - GITEA__database__DB_TYPE=sqlite3 - GITEA__security__INSTALL_LOCK=true - - GITEA__webhook__ALLOWED_HOST_LIST=* + - GITEA__webhook__ALLOWED_HOST_LIST=* - GITEA__webhook__SKIP_TLS_VERIFY=true - GITEA__webhook__DELIVER_TIMEOUT=10 - GITEA__server__LOCAL_ROOT_URL=http://gitea:3000/ @@ -60,6 +69,8 @@ services: timeout: 5s retries: 10 start_period: 10s + profiles: + - gitea gitea-bootstrap: image: gitea/gitea:1.21.5 @@ -81,10 +92,17 @@ services: echo $$TOKEN > /data/gitea/token.txt; fi " - request-catcher: + profiles: + - gitea + + request-catcher: image: appwrite/requestcatcher:1.1.0 ports: - "5000:5000" + profiles: + - gitea + - forgejo + - gogs forgejo: image: codeberg.org/forgejo/forgejo:9 @@ -105,6 +123,8 @@ services: timeout: 5s retries: 10 start_period: 10s + profiles: + - forgejo forgejo-bootstrap: image: codeberg.org/forgejo/forgejo:9 @@ -126,6 +146,8 @@ services: echo $$TOKEN > /data/gitea/token.txt; fi " + profiles: + - forgejo gogs: image: gogs/gogs:0.14 @@ -140,6 +162,8 @@ services: timeout: 5s retries: 10 start_period: 15s + profiles: + - gogs gogs-bootstrap: image: gogs/gogs:0.14 @@ -177,6 +201,8 @@ services: mkdir -p /data/gogs echo $$TOKEN > /data/gogs/token.txt fi + profiles: + - gogs gitlab: image: gitlab/gitlab-ce:18.10.1-ce.0 @@ -194,6 +220,8 @@ services: timeout: 10s retries: 20 start_period: 300s + profiles: + - gitlab gitlab-bootstrap: image: alpine/curl:8.12.1 @@ -241,9 +269,11 @@ services: if [ -z "$$TOKEN" ]; then echo "Failed to get token"; exit 1; fi mkdir -p /gitlab-data echo $$TOKEN > /gitlab-data/token.txt + profiles: + - gitlab volumes: gitea-data: forgejo-data: gogs-data: - gitlab-data: \ No newline at end of file + gitlab-data: diff --git a/phpunit.xml b/phpunit.xml index ec39a7ed..5de40462 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -9,8 +9,20 @@ stopOnFailure="false" > - - ./tests/ + + ./tests/VCS/Adapter/GiteaTest.php + + + ./tests/VCS/Adapter/ForgejoTest.php + + + ./tests/VCS/Adapter/GitHubTest.php + + + ./tests/VCS/Adapter/GitLabTest.php + + + ./tests/VCS/Adapter/GogsTest.php - \ No newline at end of file + From 48786fe5e6d7c76bcafdb78360b6247c4f55979b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 10 Apr 2026 16:03:05 +0200 Subject: [PATCH 5/6] Add more tests for Appwrite usecases --- src/VCS/Adapter/Git/GitLab.php | 8 ++++++-- tests/VCS/Adapter/GitHubTest.php | 2 ++ tests/VCS/Adapter/GitLabTest.php | 4 ++++ tests/VCS/Adapter/GiteaTest.php | 7 +++++++ tests/VCS/Base.php | 5 +++++ 5 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/VCS/Adapter/Git/GitLab.php b/src/VCS/Adapter/Git/GitLab.php index 844b5c1f..c926fc22 100644 --- a/src/VCS/Adapter/Git/GitLab.php +++ b/src/VCS/Adapter/Git/GitLab.php @@ -118,7 +118,9 @@ public function createRepository(string $owner, string $repositoryName, bool $pr if ($statusCode >= 400) { throw new Exception("Creating repository {$repositoryName} failed with status code {$statusCode}"); } - return is_array($body) ? $body : []; + $result = is_array($body) ? $body : []; + $result['pushed_at'] = $result['last_activity_at'] ?? ''; + return $result; } public function deleteRepository(string $owner, string $repositoryName): bool @@ -152,7 +154,9 @@ public function getRepository(string $owner, string $repositoryName): array throw new RepositoryNotFound("Repository not found"); } - return $response['body'] ?? []; + $result = $response['body'] ?? []; + $result['pushed_at'] = $result['last_activity_at'] ?? ''; + return $result; } diff --git a/tests/VCS/Adapter/GitHubTest.php b/tests/VCS/Adapter/GitHubTest.php index d1fc4627..6c157454 100644 --- a/tests/VCS/Adapter/GitHubTest.php +++ b/tests/VCS/Adapter/GitHubTest.php @@ -201,6 +201,8 @@ public function testGetRepository(): void $repo = $this->vcsAdapter->getRepository($owner, $repositoryName); $this->assertIsArray($repo); $this->assertSame($repositoryName, $repo['name']); + $this->assertArrayHasKey('pushed_at', $repo); + $this->assertNotFalse(\strtotime($repo['pushed_at'])); } public function testGetRepositoryName(): void diff --git a/tests/VCS/Adapter/GitLabTest.php b/tests/VCS/Adapter/GitLabTest.php index 5a7d7c17..b2849379 100644 --- a/tests/VCS/Adapter/GitLabTest.php +++ b/tests/VCS/Adapter/GitLabTest.php @@ -74,6 +74,8 @@ public function testCreateRepository(): void $this->assertArrayHasKey('name', $result); $this->assertSame($repositoryName, $result['name']); $this->assertFalse($result['visibility'] === 'private'); + $this->assertArrayHasKey('pushed_at', $result); + $this->assertNotFalse(\strtotime($result['pushed_at'])); } finally { $this->vcsAdapter->deleteRepository(static::$owner, $repositoryName); } @@ -89,6 +91,8 @@ public function testGetRepository(): void $this->assertIsArray($result); $this->assertSame($repositoryName, $result['name']); + $this->assertArrayHasKey('pushed_at', $result); + $this->assertNotFalse(\strtotime($result['pushed_at'])); } finally { $this->vcsAdapter->deleteRepository(static::$owner, $repositoryName); } diff --git a/tests/VCS/Adapter/GiteaTest.php b/tests/VCS/Adapter/GiteaTest.php index 861b9a9b..0247fa31 100644 --- a/tests/VCS/Adapter/GiteaTest.php +++ b/tests/VCS/Adapter/GiteaTest.php @@ -74,6 +74,8 @@ public function testCreateRepository(): void $this->assertArrayHasKey('owner', $result); $this->assertSame($owner, $result['owner']['login']); $this->assertFalse($result['private']); + $this->assertArrayHasKey('pushed_at', $result); + $this->assertNotFalse(\strtotime($result['pushed_at'])); $this->assertTrue($this->vcsAdapter->deleteRepository(static::$owner, $repositoryName)); } @@ -238,6 +240,8 @@ public function testGetRepository(): void $this->assertIsArray($result); $this->assertSame($repositoryName, $result['name']); $this->assertSame(static::$owner, $result['owner']['login']); + $this->assertArrayHasKey('pushed_at', $result); + $this->assertNotFalse(\strtotime($result['pushed_at'])); $this->assertTrue($this->vcsAdapter->deleteRepository(static::$owner, $repositoryName)); } @@ -1111,6 +1115,9 @@ public function testSearchRepositories(): void $repoNames = array_column($result['items'], 'name'); $this->assertContains($repo1Name, $repoNames); $this->assertContains($repo2Name, $repoNames); + + $this->assertArrayHasKey('pushed_at', $result['items'][0]); + $this->assertNotFalse(\strtotime($result['items'][0]['pushed_at'])); } finally { $this->vcsAdapter->deleteRepository(static::$owner, $repo1Name); $this->vcsAdapter->deleteRepository(static::$owner, $repo2Name); diff --git a/tests/VCS/Base.php b/tests/VCS/Base.php index b0541700..a09f4317 100644 --- a/tests/VCS/Base.php +++ b/tests/VCS/Base.php @@ -108,6 +108,9 @@ public function testSearchRepositories(): void ['items' => $repos, 'total' => $total] = $this->vcsAdapter->searchRepositories('test-kh', 1, 2); $this->assertCount(2, $repos); $this->assertSame(6, $total); + + $this->assertArrayHasKey('pushed_at', $repos[0]); + $this->assertNotFalse(\strtotime($repos[0]['pushed_at'])); } public function testCreateComment(): void @@ -197,6 +200,8 @@ public function testCreateRepository(): void $repository = $this->vcsAdapter->createRepository('test-kh', 'new-repo', true); $this->assertIsArray($repository); $this->assertSame('test-kh/new-repo', $repository['full_name']); + $this->assertArrayHasKey('pushed_at', $repository); + $this->assertNotFalse(\strtotime($repository['pushed_at'])); } /** From ff7feacff8cd3e23de77dd360ffb931d984b8c03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 10 Apr 2026 16:43:29 +0200 Subject: [PATCH 6/6] Fix root directory clone bug --- src/VCS/Adapter/Git/Gitea.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/VCS/Adapter/Git/Gitea.php b/src/VCS/Adapter/Git/Gitea.php index fbfd48ba..b05b8ff1 100644 --- a/src/VCS/Adapter/Git/Gitea.php +++ b/src/VCS/Adapter/Git/Gitea.php @@ -897,6 +897,10 @@ public function updateCommitStatus(string $repositoryName, string $commitHash, s */ public function generateCloneCommand(string $owner, string $repositoryName, string $version, string $versionType, string $directory, string $rootDirectory): string { + if (empty($rootDirectory)) { + $rootDirectory = '*'; + } + $cloneUrl = "{$this->giteaUrl}/{$owner}/{$repositoryName}"; if (!empty($this->accessToken)) { $cloneUrl = str_replace('://', "://{$owner}:{$this->accessToken}@", $this->giteaUrl) . "/{$owner}/{$repositoryName}";