Skip to content
Open
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
48 changes: 48 additions & 0 deletions .github/scripts/bin/downstream.php
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
#!/usr/bin/env php
<?php

declare(strict_types=1);

use DockerBase\Automation\Clock\System as Clock;
use DockerBase\Automation\Sleeper\System as Sleeper;
use DockerBase\Automation\WorkflowOutput;
use DockerBase\Command\Process;
use DockerBase\Downstream\Application;
use DockerBase\Downstream\Constants;
use DockerBase\Downstream\Dockerfile;
use DockerBase\Downstream\Orchestrator;
use DockerBase\Downstream\Repository\GitHub;

$root = dirname(__DIR__, 3);

require $root . '/vendor/autoload.php';

$repository = getenv('DOWNSTREAM_REPOSITORY')
?: throw new RuntimeException('DOWNSTREAM_REPOSITORY is required');
$branch = getenv('DOWNSTREAM_BRANCH')
?: throw new RuntimeException('DOWNSTREAM_BRANCH is required');
$version = getenv('GITHUB_API_VERSION')
?: throw new RuntimeException('GITHUB_API_VERSION is required');

$application = new Application(
new Orchestrator(
new GitHub(new Process($root), $repository, $version),
new Dockerfile(),
new Constants(),
new Clock(),
new Sleeper(),
$branch,
),
);
$output = (new WorkflowOutput(
$application->execute(array_slice($argv, 1)),
))->render();

if ($output !== '') {
$path = getenv('GITHUB_OUTPUT')
?: throw new RuntimeException('GITHUB_OUTPUT is required');
$written = file_put_contents($path, $output, FILE_APPEND | LOCK_EX);
if ($written !== strlen($output)) {
throw new RuntimeException('Unable to write workflow outputs');
}
}
113 changes: 113 additions & 0 deletions .github/scripts/src/Downstream/Application.php
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
<?php

declare(strict_types=1);

namespace DockerBase\Downstream;

use InvalidArgumentException;

final readonly class Application
{
public function __construct(
private Orchestrator $orchestrator,
) {
}

/**
* @param list<string> $arguments
*
* @return array<string, string>
*/
public function execute(array $arguments): array
{
if ($arguments === []) {
throw new InvalidArgumentException(
'A downstream operation is required',
);
}

[$operation, $values] = [array_shift($arguments), $arguments];

return match ([$operation, count($values)]) {
['recover', 1] => $this->recover($values[0]),
['propose', 1] => $this->propose($values[0]),
['wait', 1] => $this->wait($this->integer($values[0])),
['release', 2] => $this->release(
$this->integer($values[0]),
$values[1],
),
default => throw new InvalidArgumentException(
"Unknown downstream operation '{$operation}'",
),
};
}

/**
* @return array<string, string>
*/
private function recover(string $version): array
{
$release = $this->orchestrator->recover($version);
if ($release === null) {
return ['recovered' => 'false'];
}

return [
'recovered' => 'true',
'tag' => (string) $release,
];
}

/**
* @return array<string, string>
*/
private function propose(string $version): array
{
$pull = $this->orchestrator->propose($version);
if ($pull === null) {
return ['changed' => 'false'];
}

return [
'changed' => 'true',
'pull' => (string) $pull->number,
'head' => $pull->head,
'base' => $pull->base,
];
}

/**
* @return array<string, string>
*/
private function wait(int $pull): array
{
$this->orchestrator->wait($pull);

return ['checks' => 'success'];
}

/**
* @return array<string, string>
*/
private function release(int $pull, string $head): array
{
$release = $this->orchestrator->release($pull, $head);

return [
'tag' => (string) $release,
'application' => $release->application,
'sub' => (string) $release->sub,
];
}

private function integer(string $value): int
{
if (preg_match('/\A[1-9][0-9]*\z/', $value) !== 1) {
throw new InvalidArgumentException(
"Expected a positive integer, got '{$value}'",
);
}

return (int) $value;
}
}
20 changes: 20 additions & 0 deletions .github/scripts/src/Downstream/Bump.php
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace DockerBase\Downstream;

final readonly class Bump
{
public function __construct(
public string $current,
public string $selected,
public string $content,
) {
}

public function changed(): bool
{
return $this->current !== $this->selected;
}
}
59 changes: 59 additions & 0 deletions .github/scripts/src/Downstream/Checks.php
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
<?php

declare(strict_types=1);

namespace DockerBase\Downstream;

final readonly class Checks
{
private const array PASSING = ['SUCCESS', 'SKIPPED', 'NEUTRAL'];

/**
* @param list<array{name: string, status: string, conclusion: string}> $checks
* @param list<string> $required
*
* @return list<string>
*/
public static function pending(array $checks, array $required): array
{
$concluded = [];
foreach ($checks as $check) {
if ($check['status'] === 'COMPLETED') {
$concluded[$check['name']] = $check['conclusion'];
}
}

$pending = [];
foreach ($required as $context) {
if (! isset($concluded[$context])) {
$pending[] = $context;
}
}
sort($pending, SORT_STRING);

return $pending;
}

/**
* @param list<array{name: string, status: string, conclusion: string}> $checks
* @param list<string> $required
*
* @return list<string>
*/
public static function failed(array $checks, array $required): array
{
$wanted = array_flip($required);
$failed = [];
foreach ($checks as $check) {
if (
isset($wanted[$check['name']])
&& ! in_array($check['conclusion'], self::PASSING, true)
) {
$failed[] = "{$check['name']}={$check['conclusion']}";
}
}
sort($failed, SORT_STRING);

return $failed;
}
}
32 changes: 32 additions & 0 deletions .github/scripts/src/Downstream/Constants.php
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

namespace DockerBase\Downstream;

final readonly class Constants
{
public function application(string $content): string
{
$count = preg_match_all(
'/APP_VERSION_STABLE[ \t]*=[ \t]*\'([^\']+)\'/',
$content,
$matches,
);
if ($count !== 1) {
throw new Exception(
'Expected exactly one APP_VERSION_STABLE declaration, found '
. (int) $count,
);
}

$version = $matches[1][0];
if (! Version::isExact($version)) {
throw new Exception(
"APP_VERSION_STABLE must be MAJOR.MINOR.PATCH, got '{$version}'",
);
}

return $version;
}
}
53 changes: 53 additions & 0 deletions .github/scripts/src/Downstream/Dockerfile.php
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
<?php

declare(strict_types=1);

namespace DockerBase\Downstream;

final readonly class Dockerfile
{
public const string IMAGE = 'appwrite/base';

public function bump(string $content, string $selected): Bump
{
if (! Version::isExact($selected)) {
throw new Exception(
"Base version must be an exact MAJOR.MINOR.PATCH release, got '{$selected}'",
);
}

$image = preg_quote(self::IMAGE, '/');
$count = preg_match_all(
"/{$image}:(" . Version::PATTERN . ')/',
$content,
$matches,
);
if ($count === false || $count === 0) {
throw new Exception(
'No ' . self::IMAGE . ' reference found to bump',
);
}

$versions = array_values(array_unique($matches[1]));
if (count($versions) !== 1) {
sort($versions, SORT_STRING);

throw new Exception(
'Conflicting ' . self::IMAGE . ' versions: '
. implode(', ', $versions),
);
}

$current = $versions[0];
$updated = preg_replace(
"/{$image}:" . preg_quote($current, '/') . '/',
self::IMAGE . ":{$selected}",
$content,
);
if ($updated === null) {
throw new Exception('Unable to rewrite the base image reference');
}

return new Bump($current, $selected, $updated);
}
}
11 changes: 11 additions & 0 deletions .github/scripts/src/Downstream/Exception.php
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace DockerBase\Downstream;

use RuntimeException;

class Exception extends RuntimeException
{
}
Loading
Loading