CluPilotCloud/app/Services/Deployment/Release.php

127 lines
4.6 KiB
PHP

<?php
namespace App\Services\Deployment;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\File;
use Throwable;
/**
* What is actually running here.
*
* Read from the manifest the deployment writes when every step has SUCCEEDED —
* not from the checkout, and not from live git. The checkout moves before the
* migrations run; git tells you what the files say, not whether the deployment
* finished. An operator asking "which version is this?" during a failed update
* needs the answer "still the old one", and only a manifest written at the very
* end can give it.
*
* Deliberately not cached in config either: `optimize` runs BEFORE the manifest
* is written, so a cached copy would be one deployment behind forever.
*/
final readonly class Release
{
private function __construct(
/** The release number from VERSION, e.g. "1.0.0". */
public string $version,
/** The exact commit deployed, or null if nothing has deployed yet. */
public ?string $commit,
/** What it was deployed from: "refs/tags/v1.0.0" or "main". */
public ?string $source,
/** "release" — pinned to a tag — or "branch", following a line. */
public string $mode,
public ?Carbon $deployedAt,
/** True when the deployed commit is exactly the one VERSION is tagged at. */
public bool $isTaggedRelease,
) {}
public static function current(): self
{
$manifest = self::manifest();
// The version that DEPLOYED, not the one the checkout now claims. An
// update moves the checkout before it migrates, so a run that fails in
// between leaves a newer VERSION on disk than is actually serving —
// and pairing that number with the old commit is worse than either
// alone. Config is the fallback only until something has deployed.
$version = isset($manifest['version']) && $manifest['version'] !== ''
? (string) $manifest['version']
: (string) config('app.version', '0.0.0');
return new self(
version: $version,
commit: isset($manifest['commit']) ? (string) $manifest['commit'] : null,
source: isset($manifest['source']) ? (string) $manifest['source'] : null,
mode: (string) ($manifest['mode'] ?? 'branch'),
deployedAt: self::timestamp($manifest['deployed_at'] ?? null),
// Only a deployment pinned to the tag is that release. Every commit
// after v1.0.0 still carries VERSION=1.0.0 and is not 1.0.0.
isTaggedRelease: isset($manifest['source'])
&& $manifest['source'] === "refs/tags/v{$version}",
);
}
/** @return array<string, mixed> */
private static function manifest(): array
{
$path = storage_path('app/deployment.json');
if (! File::exists($path)) {
return [];
}
return (array) (json_decode((string) File::get($path), true) ?: []);
}
/**
* A timestamp that cannot take the console down.
*
* The manifest is written by a shell script during a deployment, and every
* other field here is already treated as possibly corrupt. An unparseable
* date throwing out of a layout partial would 500 every admin page — over a
* line of small print in the sidebar.
*/
private static function timestamp(mixed $value): ?Carbon
{
if (! is_string($value) || $value === '') {
return null;
}
try {
return Carbon::parse($value);
} catch (Throwable) {
return null;
}
}
/** The short commit, for a footer that has no room for forty characters. */
public function shortCommit(): ?string
{
return $this->commit !== null ? substr($this->commit, 0, 7) : null;
}
/**
* One line, honest about what it is.
*
* "1.0.0 (abc1234)" only for a deployment pinned to that tag. Anything else
* is somewhere after the release and says so — reporting a main deployment
* as a clean 1.0.0 is how a bug report ends up filed against the wrong code.
*/
public function label(): string
{
$commit = $this->shortCommit();
if ($this->isTaggedRelease) {
return $commit !== null ? "{$this->version} ({$commit})" : $this->version;
}
$line = $this->source !== null && $this->source !== ''
? ' · '.$this->source
: '';
return $commit !== null
? "{$this->version}-dev ({$commit}){$line}"
: "{$this->version}-dev{$line}";
}
}