Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ VCS Adapters:
| Adapter | Status |
|---------|---------|
| GitHub | ✅ |
| GitLab | |
| Bitbucket | |
| GitLab | |
| Bitbucket | |
| Azure DevOps | |

`✅ - supported, 🛠 - work in progress`
Expand Down
50 changes: 50 additions & 0 deletions src/VCS/Adapter/Git.php
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,56 @@ abstract public function createWebhook(string $owner, string $repositoryName, st
*/
abstract public function createTag(string $owner, string $repositoryName, string $tagName, string $target, string $message = ''): array;

/**
* Whether the provider lets this integration create repositories. Some
* partner APIs reserve creation for user principals.
*/
public function supportsRepositoryCreation(): bool
{
return true;
}

/**
* Whether the provider exposes an endpoint to delete a repository.
*/
public function supportsRepositoryDeletion(): bool
{
return true;
}

/**
* Whether the provider can hand out an archive download URL at all, so a
* consumer can arrange its own source packaging before calling
* getRepositoryPresignedUrl() just to catch it throwing.
*/
public function supportsRepositoryArchives(): bool
{
return true;
}

/**
* Whether images embedded in pull request comments can render on the
* provider. Providers without an image proxy (the way GitHub rewrites
* comment images through its camo CDN) cannot display images hosted on
* the consumer's own host - often private or plain-HTTP - so consumers
* should fall back to text there.
*/
public function supportsCommentImages(): bool
{
return true;
}

/**
Comment thread
Meldiron marked this conversation as resolved.
* Whether the provider can host repositories that anonymous clients are
* able to read. Providers that scope every repository to an
* authenticated audience have no public repositories, whatever a
* visibility flag may claim.
*/
public function supportsPublicRepositories(): bool
{
return true;
}

/**
* Headers a caller must send with getRepositoryPresignedUrl() to reach a
* private repository.
Expand Down
5 changes: 5 additions & 0 deletions tests/VCS/Adapter/BitbucketTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ protected function signWebhookPayload(string $payload, string $secret): string
return 'sha256=' . hash_hmac('sha256', $payload, $secret);
}

protected function anonymousCloneUrl(string $repositoryName): string
{
return 'https://bitbucket.org/' . $this->ownerPath() . '/' . $repositoryName . '.git';
}

protected function setupAdapter(): void
{
if (empty(static::$accessToken)) {
Expand Down
5 changes: 5 additions & 0 deletions tests/VCS/Adapter/ForgejoTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ protected function setupAdapter(): void
$this->vcsAdapter = $adapter;
}

protected function anonymousCloneUrl(string $repositoryName): string
{
return System::getEnv('TESTS_FORGEJO_URL', 'http://forgejo:3000') . '/' . $this->ownerPath() . '/' . $repositoryName . '.git';
}

protected function setupForgejo(): void
{
$tokenFile = '/forgejo-data/gitea/token.txt';
Expand Down
5 changes: 5 additions & 0 deletions tests/VCS/Adapter/GitHubTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ protected function signWebhookPayload(string $payload, string $secret): string
protected static string $eventHeader = 'x-github-event';
protected static string $signatureHeader = 'x-hub-signature-256';

protected function anonymousCloneUrl(string $repositoryName): string
{
return 'https://github.com/' . $this->ownerPath() . '/' . $repositoryName . '.git';
}

protected function setupAdapter(): void
{
$privateKey = str_replace('\\n', "\n", System::getEnv('TESTS_GITHUB_PRIVATE_KEY') ?? '');
Expand Down
5 changes: 5 additions & 0 deletions tests/VCS/Adapter/GitLabTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ protected function setupAdapter(): void
$this->vcsAdapter = $adapter;
}

protected function anonymousCloneUrl(string $repositoryName): string
{
return System::getEnv('TESTS_GITLAB_URL', 'http://gitlab:80') . '/' . $this->ownerPath() . '/' . $repositoryName . '.git';
}

/**
* GitLab owners are carried as "id:path", but it reports the path alone.
*/
Expand Down
5 changes: 5 additions & 0 deletions tests/VCS/Adapter/GiteaTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ protected function setupAdapter(): void
$this->vcsAdapter = $adapter;
}

protected function anonymousCloneUrl(string $repositoryName): string
{
return System::getEnv('TESTS_GITEA_URL', 'http://gitea:3000') . '/' . $this->ownerPath() . '/' . $repositoryName . '.git';
}

protected function setupGitea(): void
{
$tokenFile = '/data/gitea/token.txt';
Expand Down
5 changes: 5 additions & 0 deletions tests/VCS/Adapter/GogsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ protected function setupAdapter(): void
$this->vcsAdapter = $adapter;
}

protected function anonymousCloneUrl(string $repositoryName): string
{
return System::getEnv('TESTS_GOGS_URL', 'http://gogs:3000') . '/' . $this->ownerPath() . '/' . $repositoryName . '.git';
}

protected function setupGogs(): void
{
$tokenFile = '/gogs-data/gogs/token.txt';
Expand Down
54 changes: 54 additions & 0 deletions tests/VCS/Base.php
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,12 @@ abstract protected function pushPayload(string $branch, array $added = [], array
*/
abstract protected function pullRequestPayload(bool $external = false): string;

/**
* URL an anonymous git client would clone the repository from over HTTP,
* with no credentials embedded.
*/
abstract protected function anonymousCloneUrl(string $repositoryName): string;

protected function setUp(): void
{
$this->setupAdapter();
Expand Down Expand Up @@ -476,6 +482,54 @@ public function testCreatePrivateRepository(): void
}
}

/**
* Response an anonymous git client gets for the repository: the ref
* advertisement request `git clone` opens with, sent without credentials.
*
* @return array{0: int, 1: string} Status code and body
*/
private function fetchAnonymousRefAdvertisement(string $repositoryName): array
{
$client = new Client();
$response = $client->fetch(
url: $this->anonymousCloneUrl($repositoryName) . '/info/refs',
method: 'GET',
query: ['service' => 'git-upload-pack']
);

return [$response->getStatusCode(), $response->text()];
}

/**
* The visibility flag alone proves nothing about what reaches the
* outside; a public repository has to answer an anonymous git client.
* A private repository has to refuse the same request, or the public
* answer would say nothing beyond the server being up.
*/
public function testPublicRepositoryIsPubliclyAccessible(): void
{
$this->skipUnlessSupported($this->vcsAdapter->supportsPublicRepositories(), 'public repositories');

$publicRepository = 'test-public-access-' . \uniqid();
$privateRepository = 'test-private-access-' . \uniqid();

$this->vcsAdapter->createRepository(static::$owner, $publicRepository, false);
$this->vcsAdapter->createRepository(static::$owner, $privateRepository, true);
Comment thread
Meldiron marked this conversation as resolved.

try {
$this->assertEventually(function () use ($publicRepository) {
[$status, $body] = $this->fetchAnonymousRefAdvertisement($publicRepository);
$this->assertSame(200, $status, 'An anonymous git client cannot reach the public repository');
$this->assertStringContainsString('git-upload-pack', $body, 'The anonymous response is not a git ref advertisement');
});

[$status] = $this->fetchAnonymousRefAdvertisement($privateRepository);
$this->assertNotSame(200, $status, 'An anonymous git client can read the private repository');
} finally {
$this->discardRepositories($publicRepository, $privateRepository);
}
}

public function testGetRepository(): void
{
$repositoryName = 'test-get-repository-' . \uniqid();
Expand Down
Loading