From f709e23065a7d64805829b10703ba835173103f8 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 7 Apr 2026 09:49:03 +0530 Subject: [PATCH 1/3] feat: add minimal GitLab adapter with CI/CD setup - Add GitLab CE service to docker-compose with OAuth2 bootstrap - Add GitLab adapter with createRepository, getRepository, deleteRepository - Add GitLabTest with 4 passing tests, remaining skipped for follow-up PRs - Fix Adapter.php body decoding bug (strpos ?: 0 -> strlen) for adapters returning plain 'application/json' without charset suffix - All unimplemented methods throw Exception('Not implemented') --- docker-compose.yml | 71 +++++++- src/VCS/Adapter.php | 2 +- src/VCS/Adapter/Git/GitLab.php | 292 +++++++++++++++++++++++++++++++ tests/VCS/Adapter/GitLabTest.php | 224 ++++++++++++++++++++++++ 4 files changed, 587 insertions(+), 2 deletions(-) create mode 100644 src/VCS/Adapter/Git/GitLab.php create mode 100644 tests/VCS/Adapter/GitLabTest.php diff --git a/docker-compose.yml b/docker-compose.yml index 75912bc5..ec803f03 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,6 +9,7 @@ services: - gitea-data:/data:ro - forgejo-data:/forgejo-data:ro - gogs-data:/gogs-data:ro + - gitlab-data:/gitlab-data:ro environment: - TESTS_GITHUB_PRIVATE_KEY - TESTS_GITHUB_APP_IDENTIFIER @@ -17,6 +18,7 @@ services: - TESTS_GITEA_REQUEST_CATCHER_URL=http://request-catcher:5000 - TESTS_FORGEJO_URL=http://forgejo:3000 - TESTS_GOGS_URL=http://gogs:3000 + - TESTS_GITLAB_URL=http://gitlab:80 depends_on: gitea: condition: service_healthy @@ -30,6 +32,10 @@ services: condition: service_healthy gogs-bootstrap: condition: service_completed_successfully + gitlab: + condition: service_healthy + gitlab-bootstrap: + condition: service_completed_successfully request-catcher: condition: service_started @@ -172,7 +178,70 @@ services: echo $$TOKEN > /data/gogs/token.txt fi + gitlab: + image: gitlab/gitlab-ce:18.10.1-ce.0 + environment: + - GITLAB_ROOT_PASSWORD=${GITLAB_ROOT_PASSWORD:-;asiweml@562} + - GITLAB_ROOT_EMAIL=${GITLAB_ROOT_EMAIL:-utopia@example.com} + - GITLAB_OMNIBUS_CONFIG=gitlab_rails['initial_root_password'] = ENV['GITLAB_ROOT_PASSWORD']; gitlab_rails['gitlab_signup_enabled'] = false; + volumes: + - gitlab-data:/var/opt/gitlab + ports: + - "3003:80" + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost/-/health"] + interval: 30s + timeout: 10s + retries: 20 + start_period: 300s + + gitlab-bootstrap: + image: alpine/curl:8.12.1 + volumes: + - gitlab-data:/gitlab-data + depends_on: + gitlab: + condition: service_healthy + environment: + - GITLAB_ROOT_PASSWORD=${GITLAB_ROOT_PASSWORD:-;asiweml@562} + - GITLAB_OMNIBUS_CONFIG=gitlab_rails['initial_root_password'] = ENV['GITLAB_ROOT_PASSWORD']; gitlab_rails['gitlab_signup_enabled'] = false; gitlab_rails['allow_local_requests_from_web_hooks_and_services'] = true; + entrypoint: /bin/sh + command: + - -c + - | + if [ -f /gitlab-data/token.txt ]; then exit 0; fi + + apk add --no-cache perl + + echo "Waiting for GitLab to be ready..." + sleep 10 + + # Use OAuth2 password grant to get access token + OAUTH_RESPONSE=$$(curl -s -X POST http://gitlab:80/oauth/token \ + -d "grant_type=password&username=root&password=$$GITLAB_ROOT_PASSWORD&scope=api") + echo "OAuth response: $$OAUTH_RESPONSE" + + OAUTH_TOKEN=$$(echo "$$OAUTH_RESPONSE" | grep -o '"access_token":"[^"]*"' | cut -d'"' -f4) + echo "OAuth token: $$OAUTH_TOKEN" + + if [ -z "$$OAUTH_TOKEN" ]; then echo "Failed to get OAuth token"; exit 1; fi + + # Create PAT using OAuth token + PAT_RESPONSE=$$(curl -s -X POST http://gitlab:80/api/v4/users/1/personal_access_tokens \ + -H "Authorization: Bearer $$OAUTH_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"name":"bootstrap","scopes":["api","read_user","read_repository","write_repository"],"expires_at":"2027-01-01"}') + echo "PAT response: $$PAT_RESPONSE" + + TOKEN=$$(echo "$$PAT_RESPONSE" | grep -o '"token":"[^"]*"' | cut -d'"' -f4) + echo "Token: $$TOKEN" + + if [ -z "$$TOKEN" ]; then echo "Failed to get token"; exit 1; fi + mkdir -p /gitlab-data + echo $$TOKEN > /gitlab-data/token.txt + volumes: gitea-data: forgejo-data: - gogs-data: \ No newline at end of file + gogs-data: + gitlab-data: \ No newline at end of file diff --git a/src/VCS/Adapter.php b/src/VCS/Adapter.php index 823fbe89..96ae8437 100644 --- a/src/VCS/Adapter.php +++ b/src/VCS/Adapter.php @@ -389,7 +389,7 @@ protected function call(string $method, string $path = '', array $headers = [], $responseStatus = \curl_getinfo($ch, CURLINFO_HTTP_CODE); if ($decode) { - $length = strpos($responseType, ';') ?: 0; + $length = strpos($responseType, ';') ?: strlen($responseType); switch (substr($responseType, 0, $length)) { case 'application/json': $json = \json_decode($responseBody, true); diff --git a/src/VCS/Adapter/Git/GitLab.php b/src/VCS/Adapter/Git/GitLab.php new file mode 100644 index 00000000..575e2d6d --- /dev/null +++ b/src/VCS/Adapter/Git/GitLab.php @@ -0,0 +1,292 @@ + + */ + protected $headers = ['content-type' => 'application/json']; + + public function __construct(Cache $cache) + { + $this->cache = $cache; + } + + public function setEndpoint(string $endpoint): void + { + $this->gitlabUrl = rtrim($endpoint, '/'); + $this->endpoint = $this->gitlabUrl . '/api/v4'; + } + + public function getName(): string + { + return 'gitlab'; + } + + public function initializeVariables(string $installationId, string $privateKey, ?string $appId = null, ?string $accessToken = null, ?string $refreshToken = null): void + { + if (!empty($accessToken)) { + $this->accessToken = $accessToken; + return; + } + throw new Exception("accessToken is required for this adapter."); + } + + protected function generateAccessToken(string $privateKey, string $appId): void + { + return; + } + + /** + * Create a new group/organization + * Returns "id:path" format so both numeric ID and path are available + */ + public function createOrganization(string $orgName): string + { + $url = "/groups"; + + $response = $this->call(self::METHOD_POST, $url, ['PRIVATE-TOKEN' => $this->accessToken], [ + 'name' => $orgName, + 'path' => $orgName, + 'visibility' => 'public', + ]); + + $responseBody = $response['body'] ?? []; + + return ($responseBody['id'] ?? '') . ':' . ($responseBody['path'] ?? ''); + } + + /** + * Extract owner path from "id:path" format + */ + private function getOwnerPath(string $owner): string + { + if (strstr($owner, ':') !== false) { + return substr($owner, strpos($owner, ':') + 1); + } + return $owner; + } + + /** + * Extract namespace ID from "id:path" format + */ + private function getNamespaceId(string $owner): string + { + if (strstr($owner, ':') !== false) { + return substr($owner, 0, strpos($owner, ':')); + } + return $owner; + } + + public function createRepository(string $owner, string $repositoryName, bool $private): array + { + $namespaceId = $this->getNamespaceId($owner); + + $url = "/projects"; + + $response = $this->call(self::METHOD_POST, $url, ['PRIVATE-TOKEN' => $this->accessToken], [ + 'name' => $repositoryName, + 'path' => $repositoryName, + 'namespace_id' => $namespaceId, + 'visibility' => $private ? 'private' : 'public', + ]); + + $body = $response['body'] ?? []; + return is_array($body) ? $body : []; + } + + public function deleteRepository(string $owner, string $repositoryName): bool + { + $ownerPath = $this->getOwnerPath($owner); + $projectPath = urlencode("{$ownerPath}/{$repositoryName}"); + $url = "/projects/{$projectPath}"; + + $response = $this->call(self::METHOD_DELETE, $url, ['PRIVATE-TOKEN' => $this->accessToken]); + + $responseHeaders = $response['headers'] ?? []; + $responseHeadersStatusCode = $responseHeaders['status-code'] ?? 0; + if ($responseHeadersStatusCode >= 400) { + throw new Exception("Deleting repository {$repositoryName} failed with status code {$responseHeadersStatusCode}"); + } + + return true; + } + + public function getRepository(string $owner, string $repositoryName): array + { + $ownerPath = $this->getOwnerPath($owner); + $projectPath = urlencode("{$ownerPath}/{$repositoryName}"); + $url = "/projects/{$projectPath}"; + + $response = $this->call(self::METHOD_GET, $url, ['PRIVATE-TOKEN' => $this->accessToken]); + + $responseHeaders = $response['headers'] ?? []; + $responseHeadersStatusCode = $responseHeaders['status-code'] ?? 0; + if ($responseHeadersStatusCode >= 400) { + throw new RepositoryNotFound("Repository not found"); + } + + return $response['body'] ?? []; + } + + + public function hasAccessToAllRepositories(): bool + { + return true; + } + + public function getInstallationRepository(string $repositoryName): array + { + throw new Exception("getInstallationRepository is not applicable for this adapter"); + } + + public function searchRepositories(string $owner, int $page, int $per_page, string $search = ''): array + { + throw new Exception("Not implemented"); + } + + public function getRepositoryName(string $repositoryId): string + { + throw new Exception("Not implemented"); + } + + public function getRepositoryTree(string $owner, string $repositoryName, string $branch, bool $recursive = false): array + { + throw new Exception("Not implemented"); + } + + public function getRepositoryContent(string $owner, string $repositoryName, string $path, string $ref = ''): array + { + throw new Exception("Not implemented"); + } + + public function listRepositoryContents(string $owner, string $repositoryName, string $path = '', string $ref = ''): array + { + throw new Exception("Not implemented"); + } + + public function listRepositoryLanguages(string $owner, string $repositoryName): array + { + throw new Exception("Not implemented"); + } + + public function createFile(string $owner, string $repositoryName, string $filepath, string $content, string $message = 'Add file', string $branch = ''): array + { + throw new Exception("Not implemented"); + } + + public function createBranch(string $owner, string $repositoryName, string $newBranchName, string $oldBranchName): array + { + throw new Exception("Not implemented"); + } + + public function createPullRequest(string $owner, string $repositoryName, string $title, string $head, string $base, string $body = ''): array + { + throw new Exception("Not implemented"); + } + + public function createWebhook(string $owner, string $repositoryName, string $url, string $secret, array $events = ['push', 'pull_request']): int + { + throw new Exception("Not implemented"); + } + + public function createComment(string $owner, string $repositoryName, int $pullRequestNumber, string $comment): string + { + throw new Exception("Not implemented"); + } + + public function getComment(string $owner, string $repositoryName, string $commentId): string + { + throw new Exception("Not implemented"); + } + + public function updateComment(string $owner, string $repositoryName, int $commentId, string $comment): string + { + throw new Exception("Not implemented"); + } + + public function getUser(string $username): array + { + throw new Exception("Not implemented"); + } + + public function getOwnerName(string $installationId, ?int $repositoryId = null): string + { + throw new Exception("Not implemented"); + } + + public function getPullRequest(string $owner, string $repositoryName, int $pullRequestNumber): array + { + throw new Exception("Not implemented"); + } + + public function getPullRequestFiles(string $owner, string $repositoryName, int $pullRequestNumber): array + { + throw new Exception("Not implemented"); + } + + public function getPullRequestFromBranch(string $owner, string $repositoryName, string $branch): array + { + throw new Exception("Not implemented"); + } + + public function listBranches(string $owner, string $repositoryName): array + { + throw new Exception("Not implemented"); + } + + public function getCommit(string $owner, string $repositoryName, string $commitHash): array + { + throw new Exception("Not implemented"); + } + + public function getLatestCommit(string $owner, string $repositoryName, string $branch): array + { + throw new Exception("Not implemented"); + } + + public function updateCommitStatus(string $repositoryName, string $commitHash, string $owner, string $state, string $description = '', string $target_url = '', string $context = ''): void + { + throw new Exception("Not implemented"); + } + + public function generateCloneCommand(string $owner, string $repositoryName, string $version, string $versionType, string $directory, string $rootDirectory): string + { + throw new Exception("Not implemented"); + } + + public function getEvent(string $event, string $payload): array + { + throw new Exception("Not implemented"); + } + + public function validateWebhookEvent(string $payload, string $signature, string $signatureKey): bool + { + throw new Exception("Not implemented"); + } + + public function createTag(string $owner, string $repositoryName, string $tagName, string $target, string $message = ''): array + { + throw new Exception("Not implemented"); + } + + public function getCommitStatuses(string $owner, string $repositoryName, string $commitHash): array + { + throw new Exception("Not implemented"); + } +} diff --git a/tests/VCS/Adapter/GitLabTest.php b/tests/VCS/Adapter/GitLabTest.php new file mode 100644 index 00000000..f89cb696 --- /dev/null +++ b/tests/VCS/Adapter/GitLabTest.php @@ -0,0 +1,224 @@ +setupGitLab(); + } + + if (empty(static::$accessToken)) { + $this->markTestSkipped('GitLab access token not available'); + return; + } + + $adapter = new GitLab(new Cache(new None())); + $gitlabUrl = System::getEnv('TESTS_GITLAB_URL', 'http://gitlab:80'); + + $adapter->initializeVariables( + installationId: '', + privateKey: '', + appId: '', + accessToken: static::$accessToken, + refreshToken: '' + ); + $adapter->setEndpoint($gitlabUrl); + + if (empty(static::$owner)) { + $orgName = 'test-org-' . \uniqid(); + static::$owner = $adapter->createOrganization($orgName); + } + + $this->vcsAdapter = $adapter; + } + + protected function setupGitLab(): void + { + $tokenFile = '/gitlab-data/token.txt'; + + if (file_exists($tokenFile)) { + $contents = file_get_contents($tokenFile); + if ($contents !== false) { + static::$accessToken = trim($contents); + } + } + } + + + public function testCreateRepository(): void + { + $repositoryName = 'test-create-repository-' . \uniqid(); + + $result = $this->vcsAdapter->createRepository(static::$owner, $repositoryName, false); + + try { + $this->assertIsArray($result); + $this->assertArrayHasKey('name', $result); + $this->assertSame($repositoryName, $result['name']); + $this->assertFalse($result['visibility'] === 'private'); + } finally { + $this->vcsAdapter->deleteRepository(static::$owner, $repositoryName); + } + } + + public function testGetRepository(): void + { + $repositoryName = 'test-get-repository-' . \uniqid(); + $this->vcsAdapter->createRepository(static::$owner, $repositoryName, false); + + try { + $result = $this->vcsAdapter->getRepository(static::$owner, $repositoryName); + + $this->assertIsArray($result); + $this->assertSame($repositoryName, $result['name']); + } finally { + $this->vcsAdapter->deleteRepository(static::$owner, $repositoryName); + } + } + + public function testDeleteRepository(): void + { + $repositoryName = 'test-delete-repository-' . \uniqid(); + $this->vcsAdapter->createRepository(static::$owner, $repositoryName, false); + + $result = $this->vcsAdapter->deleteRepository(static::$owner, $repositoryName); + + $this->assertTrue($result); + } + + public function testGetDeletedRepositoryFails(): void + { + $repositoryName = 'non-existing-repository-' . \uniqid(); + + $this->expectException(\Exception::class); + $this->vcsAdapter->getRepository(static::$owner, $repositoryName); + } + + // --- Override Base tests that use GitHub-hardcoded data --- + + public function testGetPullRequestFromBranch(): void + { + $this->markTestSkipped('Not implemented for GitLab yet'); + } + + public function testGetOwnerName(): void + { + $this->markTestSkipped('Not implemented for GitLab yet'); + } + + public function testSearchRepositories(): void + { + $this->markTestSkipped('Not implemented for GitLab yet'); + } + + public function testCreateComment(): void + { + $this->markTestSkipped('Not implemented for GitLab yet'); + } + + // --- Skip abstract methods not yet implemented --- + + public function testUpdateComment(): void + { + $this->markTestSkipped('Not implemented for GitLab yet'); + } + + public function testGenerateCloneCommand(): void + { + $this->markTestSkipped('Not implemented for GitLab yet'); + } + + public function testGenerateCloneCommandWithCommitHash(): void + { + $this->markTestSkipped('Not implemented for GitLab yet'); + } + + public function testGenerateCloneCommandWithTag(): void + { + $this->markTestSkipped('Not implemented for GitLab yet'); + } + + public function testGenerateCloneCommandWithInvalidRepository(): void + { + $this->markTestSkipped('Not implemented for GitLab yet'); + } + + public function testWebhookPushEvent(): void + { + $this->markTestSkipped('Not implemented for GitLab yet'); + } + + public function testWebhookPullRequestEvent(): void + { + $this->markTestSkipped('Not implemented for GitLab yet'); + } + + public function testGetEventPush(): void + { + $this->markTestSkipped('Not implemented for GitLab yet'); + } + + public function testGetRepositoryName(): void + { + $this->markTestSkipped('Not implemented for GitLab yet'); + } + + public function testGetComment(): void + { + $this->markTestSkipped('Not implemented for GitLab yet'); + } + + public function testGetPullRequest(): void + { + $this->markTestSkipped('Not implemented for GitLab yet'); + } + + public function testGetPullRequestFiles(): void + { + $this->markTestSkipped('Not implemented for GitLab yet'); + } + + public function testGetPullRequestWithInvalidNumber(): void + { + $this->markTestSkipped('Not implemented for GitLab yet'); + } + + public function testGetRepositoryTree(): void + { + $this->markTestSkipped('Not implemented for GitLab yet'); + } + + public function testListBranches(): void + { + $this->markTestSkipped('Not implemented for GitLab yet'); + } + + public function testListRepositoryLanguages(): void + { + $this->markTestSkipped('Not implemented for GitLab yet'); + } + + public function testListRepositoryContents(): void + { + $this->markTestSkipped('Not implemented for GitLab yet'); + } +} From eb4ae25ca5054762a04078b45ced1a973dde69f5 Mon Sep 17 00:00:00 2001 From: CA Prasad Zawar Date: Tue, 7 Apr 2026 11:43:54 +0530 Subject: [PATCH 2/3] fix: address Greptile P1 and P2 feedback --- docker-compose.yml | 8 +++++--- src/VCS/Adapter/Git/GitLab.php | 5 +++++ tests/VCS/Adapter/GitLabTest.php | 4 ---- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index ec803f03..702fe5fd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -204,7 +204,6 @@ services: condition: service_healthy environment: - GITLAB_ROOT_PASSWORD=${GITLAB_ROOT_PASSWORD:-;asiweml@562} - - GITLAB_OMNIBUS_CONFIG=gitlab_rails['initial_root_password'] = ENV['GITLAB_ROOT_PASSWORD']; gitlab_rails['gitlab_signup_enabled'] = false; gitlab_rails['allow_local_requests_from_web_hooks_and_services'] = true; entrypoint: /bin/sh command: - -c @@ -218,7 +217,10 @@ services: # Use OAuth2 password grant to get access token OAUTH_RESPONSE=$$(curl -s -X POST http://gitlab:80/oauth/token \ - -d "grant_type=password&username=root&password=$$GITLAB_ROOT_PASSWORD&scope=api") + --data-urlencode "grant_type=password" \ + --data-urlencode "username=root" \ + --data-urlencode "password=$$GITLAB_ROOT_PASSWORD" \ + --data-urlencode "scope=api") echo "OAuth response: $$OAUTH_RESPONSE" OAUTH_TOKEN=$$(echo "$$OAUTH_RESPONSE" | grep -o '"access_token":"[^"]*"' | cut -d'"' -f4) @@ -230,7 +232,7 @@ services: PAT_RESPONSE=$$(curl -s -X POST http://gitlab:80/api/v4/users/1/personal_access_tokens \ -H "Authorization: Bearer $$OAUTH_TOKEN" \ -H "Content-Type: application/json" \ - -d '{"name":"bootstrap","scopes":["api","read_user","read_repository","write_repository"],"expires_at":"2027-01-01"}') + -d '{"name":"bootstrap","scopes":["api","read_user","read_repository","write_repository"]}') echo "PAT response: $$PAT_RESPONSE" TOKEN=$$(echo "$$PAT_RESPONSE" | grep -o '"token":"[^"]*"' | cut -d'"' -f4) diff --git a/src/VCS/Adapter/Git/GitLab.php b/src/VCS/Adapter/Git/GitLab.php index 575e2d6d..fe3a0739 100644 --- a/src/VCS/Adapter/Git/GitLab.php +++ b/src/VCS/Adapter/Git/GitLab.php @@ -107,6 +107,11 @@ public function createRepository(string $owner, string $repositoryName, bool $pr ]); $body = $response['body'] ?? []; + $responseHeaders = $response['headers'] ?? []; + $statusCode = $responseHeaders['status-code'] ?? 0; + if ($statusCode >= 400) { + throw new Exception("Creating repository {$repositoryName} failed with status code {$statusCode}"); + } return is_array($body) ? $body : []; } diff --git a/tests/VCS/Adapter/GitLabTest.php b/tests/VCS/Adapter/GitLabTest.php index f89cb696..606e964c 100644 --- a/tests/VCS/Adapter/GitLabTest.php +++ b/tests/VCS/Adapter/GitLabTest.php @@ -113,8 +113,6 @@ public function testGetDeletedRepositoryFails(): void $this->vcsAdapter->getRepository(static::$owner, $repositoryName); } - // --- Override Base tests that use GitHub-hardcoded data --- - public function testGetPullRequestFromBranch(): void { $this->markTestSkipped('Not implemented for GitLab yet'); @@ -135,8 +133,6 @@ public function testCreateComment(): void $this->markTestSkipped('Not implemented for GitLab yet'); } - // --- Skip abstract methods not yet implemented --- - public function testUpdateComment(): void { $this->markTestSkipped('Not implemented for GitLab yet'); From f120a9475e2bd42c89446f8e0ac8bdafc95251ce Mon Sep 17 00:00:00 2001 From: CA Prasad Zawar Date: Tue, 7 Apr 2026 12:24:30 +0530 Subject: [PATCH 3/3] fix: suggestions2 --- src/VCS/Adapter/Git/GitLab.php | 10 ++++++++-- tests/VCS/Adapter/GitLabTest.php | 1 - 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/VCS/Adapter/Git/GitLab.php b/src/VCS/Adapter/Git/GitLab.php index fe3a0739..4128b313 100644 --- a/src/VCS/Adapter/Git/GitLab.php +++ b/src/VCS/Adapter/Git/GitLab.php @@ -67,6 +67,11 @@ public function createOrganization(string $orgName): string ]); $responseBody = $response['body'] ?? []; + $responseHeaders = $response['headers'] ?? []; + $statusCode = $responseHeaders['status-code'] ?? 0; + if ($statusCode >= 400) { + throw new Exception("Creating organization {$orgName} failed with status code {$statusCode}"); + } return ($responseBody['id'] ?? '') . ':' . ($responseBody['path'] ?? ''); } @@ -87,8 +92,9 @@ private function getOwnerPath(string $owner): string */ private function getNamespaceId(string $owner): string { - if (strstr($owner, ':') !== false) { - return substr($owner, 0, strpos($owner, ':')); + $pos = strpos($owner, ':'); + if ($pos !== false) { + return substr($owner, 0, $pos); } return $owner; } diff --git a/tests/VCS/Adapter/GitLabTest.php b/tests/VCS/Adapter/GitLabTest.php index 606e964c..5a7d7c17 100644 --- a/tests/VCS/Adapter/GitLabTest.php +++ b/tests/VCS/Adapter/GitLabTest.php @@ -28,7 +28,6 @@ public function setUp(): void if (empty(static::$accessToken)) { $this->markTestSkipped('GitLab access token not available'); - return; } $adapter = new GitLab(new Cache(new None()));