127 lines
4.6 KiB
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}";
|
|
}
|
|
}
|