Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update self-update to support an optional version constraint #154

Draft
wants to merge 2 commits into
base: 0.4.x
Choose a base branch
from
Draft
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
44 changes: 24 additions & 20 deletions src/Updater/Plugin/Command.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use my127\Console\Application\Application;
use my127\Console\Application\Plugin\Plugin;
use my127\Console\Usage\Input;
use my127\Workspace\Application as BaseApplication;
use my127\Workspace\Updater\Exception\NoUpdateAvailableException;
use my127\Workspace\Updater\Exception\NoVersionDeterminedException;
Expand All @@ -23,30 +24,33 @@ public function setup(Application $application): void
{
$application->section('self-update')
->description('Updates the current version of workspace.')
->action($this->action());
->usage('self-update [<version-constraint>]')
->action(fn (Input $input) => $this->action($input));
}

private function action()
private function action(Input $input)
{
return function () {
$pharPath = \Phar::running(false);
if (empty($pharPath)) {
echo 'This command can only be executed from within the ws utility.' . PHP_EOL;
exit(1);
}
$pharPath = \Phar::running(false);
if (empty($pharPath)) {
echo 'This command can only be executed from within the ws utility.' . PHP_EOL;
exit(1);
}

try {
$this->updater->update(BaseApplication::getVersion(), $pharPath);
} catch (NoUpdateAvailableException $e) {
echo sprintf('You are already running the latest version of workspace: %s', $e->getCurrentVersion()) . PHP_EOL;
exit(1);
} catch (NoVersionDeterminedException $e) {
echo 'Unable to determine your current workspace version. You are likely not using a tagged released.' . PHP_EOL;
exit(1);
} catch (\RuntimeException $e) {
echo sprintf('%s. Aborting self-update', $e->getMessage()) . PHP_EOL;
exit(1);
try {
if ($input->getArgument('version-constraint')) {
$this->updater->update(BaseApplication::getVersion(), $input->getArgument('version-constraint'), $pharPath);
} else {
$this->updater->updateLatest(BaseApplication::getVersion(), $pharPath);
}
};
} catch (NoUpdateAvailableException $e) {
echo sprintf('You are already running the latest version of workspace: %s', $e->getCurrentVersion()) . PHP_EOL;
exit(1);
} catch (NoVersionDeterminedException $e) {
echo 'Unable to determine your current workspace version. You are likely not using a tagged released.' . PHP_EOL;
exit(1);
} catch (\RuntimeException $e) {
echo sprintf('%s. Aborting self-update', $e->getMessage()) . PHP_EOL;
exit(1);
}
}
}
106 changes: 92 additions & 14 deletions src/Updater/Updater.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace my127\Workspace\Updater;

use Composer\Semver\Semver;
use Composer\Semver\VersionParser;
use my127\Workspace\Updater\Exception\NoUpdateAvailableException;
use my127\Workspace\Updater\Exception\NoVersionDeterminedException;

Expand All @@ -11,6 +13,21 @@ class Updater
public const CODE_NO_RELEASES = 101;
public const CODE_ERR_FETCHING_NEXT_RELEASE = 102;

public const STABILITY_STABLE = 0;
public const STABILITY_RC = 5;
public const STABILITY_BETA = 10;
public const STABILITY_ALPHA = 15;
public const STABILITY_DEV = 20;

/** @var array<string, self::STABILITY_*> */
public static $stabilities = [
'stable' => self::STABILITY_STABLE,
'RC' => self::STABILITY_RC,
'beta' => self::STABILITY_BETA,
'alpha' => self::STABILITY_ALPHA,
'dev' => self::STABILITY_DEV,
];

/** @var string */
private $apiUrl;

Expand All @@ -23,24 +40,37 @@ public function __construct(string $apiUrl, ?Output $output = null)
$this->output = $output ?: new StdOutput();
}

public function update(string $currentVersion, string $targetPath)
public function updateLatest(string $currentVersion, string $targetPath)
{
if (empty($currentVersion)) {
throw new NoVersionDeterminedException();
}

$latest = $this->getLatestRelease();
if (!$latest->isMoreRecentThan($currentVersion)) {
throw new NoUpdateAvailableException($currentVersion);
}
$this->doUpdate($currentVersion, $latest, $targetPath);
}

public function update(string $currentVersion, string $targetConstraint, string $targetPath)
{
$latest = $this->getLatestReleaseByConstraint($targetConstraint);
if ($latest->getVersion() == $currentVersion) {
throw new NoUpdateAvailableException($currentVersion);
}
$this->doUpdate($currentVersion, $latest, $targetPath);
}

private function doUpdate(string $currentVersion, Release $release, string $targetPath)
{
if (empty($currentVersion)) {
throw new NoVersionDeterminedException();
}

$temp = tempnam(sys_get_temp_dir(), 'workspace-update-') . '.phar';

try {
$this->output->infof('Downloading new version (%s) from %s', $latest->getVersion(), $latest->getUrl());
$releaseData = @file_get_contents($latest->getUrl());
$this->output->infof('Downloading new version (%s) from %s', $release->getVersion(), $release->getUrl());
$releaseData = @file_get_contents($release->getUrl());
if ($releaseData === false) {
throw new \RuntimeException(sprintf('Unable to download latest release at %s', $latest->getUrl()), self::CODE_ERR_FETCHING_NEXT_RELEASE);
throw new \RuntimeException(sprintf('Unable to download latest release at %s', $release->getUrl()), self::CODE_ERR_FETCHING_NEXT_RELEASE);
}

$this->output->infof('Writing to %s', $temp);
Expand All @@ -67,22 +97,70 @@ public function update(string $currentVersion, string $targetPath)
private function getLatestRelease(): Release
{
try {
$releases = file_get_contents($this->apiUrl, false, $this->createStreamContext());
$release = file_get_contents($this->apiUrl . '/latest', false, $this->createStreamContext());
} catch (\Throwable $e) {
throw new \RuntimeException('Error fetching latest release from GitHub.', self::CODE_ERR_FETCHING_RELEASES);
}

$latest = json_decode($release);

return new Release($latest->assets[0]->browser_download_url, $latest->tag_name);
}

private function getLatestReleaseByConstraint(string $targetConstraint): Release
{
try {
$releasesRaw = file_get_contents($this->apiUrl, false, $this->createStreamContext());
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the api's paginated as well, default 30 releases per page

} catch (\Throwable $e) {
throw new \RuntimeException('Error fetching releases from GitHub.', self::CODE_ERR_FETCHING_RELEASES);
throw new \RuntimeException('Error fetching latest release from GitHub.', self::CODE_ERR_FETCHING_RELEASES);
}

$releases = json_decode($releases);
$versionParser = new VersionParser();

if (count($releases) === 0) {
throw new \RuntimeException('No releases present in the GitHub API response.', self::CODE_NO_RELEASES);
$parts = explode('@', $targetConstraint);
$constraint = $parts[0];
if (count($parts) > 1) {
$minStability = VersionParser::normalizeStability($parts[1]);
} else {
$minStability = $versionParser->parseStability($constraint);
}

$latest = $releases[0];
$releases = json_decode($releasesRaw);
$sortedVersions = Semver::rsort(array_map(fn ($release) => $release->tag_name, $releases));
$filteredVersions = Semver::satisfiedBy($sortedVersions, $constraint);
$filteredStabilityVersions = $this->filterVersionsByMinStability($versionParser, $filteredVersions, $minStability);
$filteredReleases = $this->filterReleasesByVersions($releases, $filteredStabilityVersions);

if (count($filteredReleases) == 0) {
throw new \RuntimeException(sprintf('No releases match the version constraint "%s".', $targetConstraint), self::CODE_ERR_FETCHING_RELEASES);
}

$latest = $filteredReleases[0];

return new Release($latest->assets[0]->browser_download_url, $latest->tag_name);
}

/**
* @param string[] $versions
*
* @return string[]
*/
private function filterVersionsByMinStability(VersionParser $versionParser, array $versions, string $minStability): array
{
return array_filter($versions, fn ($version) => self::$stabilities[$versionParser->parseStability($version)] <= self::$stabilities[$minStability]);
}

/**
* @param object[] $releases
* @param string[] $versions
*
* @return object[]
*/
private function filterReleasesByVersions(array $releases, array $versions): array
{
return array_values(array_filter($releases, fn ($release) => in_array($release->tag_name, $versions)));
}

/**
* @return resource
*/
Expand Down
Loading