From 717736e13ae7d5138cd37c9b8667bec636cc7e89 Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Thu, 24 Sep 2026 16:44:02 +0000 Subject: [PATCH] fix: use published stable release for default CLI install and upgrade --- docs/distribution.md | 51 ++++++++--------- scripts/install.ps1 | 36 ++---------- scripts/install.sh | 51 ++--------------- src/Commands/UpgradeCommand.php | 8 +-- src/Support/ReleaseCatalog.php | 31 ++++------ tests/Commands/UpgradeCommandTest.php | 61 ++++++++++---------- tests/ReleaseInstallerContractTest.php | 78 +++++++++----------------- tests/Support/ReleaseCatalogTest.php | 47 ++++++++++------ 8 files changed, 132 insertions(+), 231 deletions(-) diff --git a/docs/distribution.md b/docs/distribution.md index c017f96..775996a 100644 --- a/docs/distribution.md +++ b/docs/distribution.md @@ -24,8 +24,7 @@ The one-line installer is the recommended path on every supported platform. Both `install.sh` and `install.ps1` download the matching `SHA256SUMS` manifest, verify the binary's checksum before writing it into the install directory, and refuse to proceed when the checksum does not match. An unpinned install -resolves the current stable release from the public artifact compatibility -authority. +resolves GitHub's latest published stable release. On Unix, installation is ready only when an ordinary `dw` invocation resolves to the installed path. The installer reports the installed and active paths @@ -36,25 +35,23 @@ the result requires a targeted `dw` cache refresh in that shell. Set `DURABLE_WORKFLOW_INSTALL_OUTPUT=json` to emit the final result as `durable-workflow.cli.install.v1` for release qualification. -The default installer follows the qualified supported release. Maintainers can -explicitly require a prerelease channel during future preview programs: +The default installer follows the latest stable release. To install a +prerelease, supply its exact tag: ```bash -curl -fsSL https://durable-workflow.com/install.sh | VERSION=prerelease sh +curl -fsSL https://durable-workflow.com/install.sh | VERSION="$PRERELEASE_TAG" sh ``` -For reproducible automation that requires an exact version, read the CLI tag -from the qualified artifact authority, store it in `QUALIFIED_CLI_TAG`, and -pass that value to the same installer: +For reproducible automation, pin a published CLI release tag and pass it to +the same installer: ```bash -curl -fsSL https://durable-workflow.com/install.sh | VERSION="$QUALIFIED_CLI_TAG" sh +curl -fsSL https://durable-workflow.com/install.sh | VERSION="$CLI_TAG" sh ``` -The qualified artifact authority is published at -. -This keeps the selected release aligned with the supported cross-component -tuple without maintaining a release-candidate sequence number in this guide. +The installer verifies the matching release `SHA256SUMS`. It does not infer +cross-component compatibility from the release version; check the Server and +SDK compatibility documentation for your deployment. ## Provenance boundary @@ -169,12 +166,12 @@ For an ordinary unpinned `dw upgrade`, requests occur in this order: | Endpoint family | Purpose | When requested | |-----------------|---------|----------------| -| `GET https://durable-workflow.com/public-artifact-compatibility-evidence.json` | Resolve the qualified, supported CLI release from the public compatibility authority. | Always, including `--dry-run` and outcomes where the installed version is equal to or newer than the supported release. An explicit `--tag` skips this lookup. | +| `GET https://api.github.com/repos/durable-workflow/cli/releases/latest` | Resolve GitHub's latest published stable CLI release. | Always for an unpinned upgrade, including `--dry-run` and no-op outcomes. An explicit `--tag` skips this lookup. | | `GET https://github.com/durable-workflow/cli/releases/download//SHA256SUMS` | Retrieve the checksum manifest for the selected release. | Only when the command will install; skipped by `--dry-run`, `status=noop`, and `status=newer`. | | `GET https://github.com/durable-workflow/cli/releases/download//` | Download the selected standalone binary after obtaining its expected checksum. | Only when the command will install; skipped by `--dry-run`, `status=noop`, and `status=newer`. | -The last two requests are GitHub release downloads, not GitHub release API -requests. The client follows HTTPS redirects returned by GitHub for those +The last two requests are GitHub release downloads, not GitHub API requests. +The client follows HTTPS redirects returned by GitHub for those assets, so an egress allowlist must also permit GitHub's release-asset delivery destination. A dry run performs the authority lookup and reports the two release URLs it would use, but does not request either download. @@ -205,24 +202,24 @@ as the rest of the binary. ## Auto-update `dw upgrade` performs an explicit, user-invoked self-update for standalone -release binaries. Without a tag, it resolves the project's supported CLI -channel and compares that release with the running binary before downloading +release binaries. Without a tag, it resolves GitHub's latest stable CLI +release and compares it with the running binary before downloading anything: -| Installed version compared with the supported release | Result | +| Installed version compared with the latest stable release | Result | |--------------------------------------------------------|--------| -| Older | Downloads the supported release, verifies its SHA256, and atomically replaces the running binary. | +| Older | Downloads the latest stable release, verifies its SHA256, and atomically replaces the running binary. | | Equal | Makes no change and reports `status=noop`. With `--force`, re-downloads and reinstalls the same release. | | Newer | Makes no change and reports `status=newer`, including when `--force` is present. | -Use `--tag=` to select an exact release instead of the supported -channel. This is also the required path for an intentional downgrade. The +Use `--tag=` to select an exact release instead of latest stable. +This is also the required path for an intentional downgrade. The other options are: - `--dry-run` resolves and reports the action without downloading or replacing the binary. - `--force` re-downloads when the installed and selected versions are equal. - It does not allow an unpinned supported-channel lookup to downgrade a newer + It does not allow an unpinned latest-stable lookup to downgrade a newer installation. - `--output=json` emits the result for automation. @@ -233,18 +230,18 @@ in each invocation: ```console $ dw upgrade -Upgraded dw to +Upgraded dw to path: /home/user/.local/bin/dw $ dw upgrade -dw is already at +dw is already at $ dw upgrade --force -Upgraded dw to +Upgraded dw to path: /home/user/.local/bin/dw $ dw upgrade -dw is newer than the supported release ; no change was made +dw is newer than the latest stable release ; no change was made $ dw upgrade --tag="$OLDER_RELEASE" --dry-run Would downgrade -> diff --git a/scripts/install.ps1 b/scripts/install.ps1 index 1782da1..d74d503 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -4,11 +4,9 @@ # irm https://durable-workflow.com/install.ps1 | iex # # Environment variables: -# $env:VERSION Release tag, supported, prerelease, or stable (default: supported). +# $env:VERSION Release tag or stable/latest (default: stable). # $env:DURABLE_WORKFLOW_INSTALL_DIR Install directory (default: %USERPROFILE%\.durable-workflow\bin). # $env:DURABLE_WORKFLOW_RELEASE_BASE_URL Release base URL override for tests. -# $env:DURABLE_WORKFLOW_QUALIFIED_AUTHORITY_URL -# Qualified artifact authority override for tests. # $env:DURABLE_WORKFLOW_INSTALL_VERIFY_ATTESTATIONS # Set to 1 to verify GitHub artifact attestations with gh. @@ -26,35 +24,9 @@ $installDir = if ($env:DURABLE_WORKFLOW_INSTALL_DIR) { } else { Join-Path $env:USERPROFILE '.durable-workflow\bin' } -$version = if ($env:VERSION) { $env:VERSION } else { 'supported' } -$qualifiedAuthorityUrl = if ($env:DURABLE_WORKFLOW_QUALIFIED_AUTHORITY_URL) { - $env:DURABLE_WORKFLOW_QUALIFIED_AUTHORITY_URL -} else { - 'https://durable-workflow.com/public-artifact-compatibility-evidence.json' -} -$version = if ($version -eq 'supported' -or $version -eq 'prerelease') { - $requestedChannel = $version - Write-Host '==> Resolving the qualified CLI release' -ForegroundColor Green - $authority = Invoke-RestMethod -Uri $qualifiedAuthorityUrl -UseBasicParsing - if ( - $authority.schema -ne 'durable-workflow.docs.public-artifact-compatibility-evidence' -or - $authority.schema_version -ne 2 -or - $authority.outcome -ne 'pass' - ) { - throw "The qualified artifact authority at $qualifiedAuthorityUrl is not a passing schema-v2 document." - } - $resolvedVersion = [string] $authority.qualified_artifact_versions.cli - $resolvedVersion = $resolvedVersion -replace '^v', '' - if ($resolvedVersion -notmatch '^\d+\.\d+\.\d+(-(alpha|beta|rc)\.\d+)?$') { - throw "Could not resolve a qualified CLI release from $qualifiedAuthorityUrl." - } - if ($requestedChannel -eq 'prerelease' -and $resolvedVersion -notmatch '-(alpha|beta|rc)\.\d+$') { - throw "The qualified CLI release at $qualifiedAuthorityUrl is not a prerelease." - } - $resolvedVersion -} else { - $version -} +$version = if ($env:VERSION) { $env:VERSION } else { 'stable' } +if ($version -eq 'supported') { $version = 'stable' } +if ($version -eq 'prerelease') { throw 'Pass an explicit prerelease tag with VERSION=...' } $releaseVersion = if ($version.StartsWith('v')) { $version.Substring(1) } else { diff --git a/scripts/install.sh b/scripts/install.sh index 179b4cd..b1194d6 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -5,12 +5,10 @@ # curl -fsSL https://durable-workflow.com/install.sh | sh # # Environment variables: -# VERSION Release tag, supported, prerelease, or stable (default: supported). +# VERSION Release tag or stable/latest (default: stable). # DURABLE_WORKFLOW_INSTALL_DIR Install directory (default: ~/.local/bin). # DURABLE_WORKFLOW_BIN_NAME Executable name (default: dw). # DURABLE_WORKFLOW_RELEASE_BASE_URL Release base URL override for tests. -# DURABLE_WORKFLOW_QUALIFIED_AUTHORITY_URL -# Qualified artifact authority override for tests. # DURABLE_WORKFLOW_INSTALL_VERIFY_ATTESTATIONS # Set to 1 to verify GitHub artifact attestations with gh. # DURABLE_WORKFLOW_INSTALL_OUTPUT Result format: human (default) or json. @@ -20,10 +18,9 @@ set -eu REPO="durable-workflow/cli" BIN_NAME="${DURABLE_WORKFLOW_BIN_NAME:-dw}" INSTALL_DIR="${DURABLE_WORKFLOW_INSTALL_DIR:-$HOME/.local/bin}" -VERSION="${VERSION:-supported}" +VERSION="${VERSION:-stable}" RELEASE_BASE_URL="${DURABLE_WORKFLOW_RELEASE_BASE_URL:-https://github.com/${REPO}/releases}" RELEASE_BASE_URL="${RELEASE_BASE_URL%/}" -QUALIFIED_AUTHORITY_URL="${DURABLE_WORKFLOW_QUALIFIED_AUTHORITY_URL:-https://durable-workflow.com/public-artifact-compatibility-evidence.json}" VERIFY_ATTESTATIONS="${DURABLE_WORKFLOW_INSTALL_VERIFY_ATTESTATIONS:-0}" OUTPUT_MODE="${DURABLE_WORKFLOW_INSTALL_OUTPUT:-human}" @@ -64,46 +61,10 @@ fi asset="dw-${os}-${arch}" command -v curl >/dev/null 2>&1 || err "curl is required" -if [ "$VERSION" = "supported" ] || [ "$VERSION" = "prerelease" ]; then - requested_channel="$VERSION" - info "Resolving the qualified CLI release" - if ! VERSION=$(curl -fsSL --retry 3 "$QUALIFIED_AUTHORITY_URL" | tr '{},' '\n\n\n' | awk ' - /"schema"[[:space:]]*:[[:space:]]*"durable-workflow\.docs\.public-artifact-compatibility-evidence"/ && !schema_seen { - schema_seen=1 - } - /"schema_version"[[:space:]]*:[[:space:]]*2([[:space:]]|$)/ && !schema_version_seen { - schema_version_seen=1 - } - /"outcome"[[:space:]]*:/ && !outcome_seen { - outcome_seen=1 - if ($0 ~ /"outcome"[[:space:]]*:[[:space:]]*"pass"/) outcome_pass=1 - } - /"qualified_artifact_versions"[[:space:]]*:/ { - qualified_versions=1 - next - } - qualified_versions && /"cli"[[:space:]]*:/ { - version=$0 - sub(/^.*"cli"[[:space:]]*:[[:space:]]*"v?/, "", version) - sub(/".*$/, "", version) - qualified_versions=0 - } - END { - if (schema_seen && schema_version_seen && outcome_pass && version ~ /^[0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta|rc)\.[0-9]+)?$/) { - print version - exit 0 - } - exit 1 - } - '); then - err "could not resolve a passing qualified CLI release from $QUALIFIED_AUTHORITY_URL" - fi - if [ "$requested_channel" = "prerelease" ]; then - printf '%s\n' "$VERSION" \ - | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+-(alpha|beta|rc)\.[0-9]+$' \ - || err "qualified CLI release is not an alpha, beta, or rc version" - fi -fi +case "$VERSION" in + supported) VERSION=stable ;; + prerelease) err "pass an explicit prerelease tag with VERSION=..." ;; +esac if [ "$VERSION" = "latest" ] || [ "$VERSION" = "stable" ]; then url="${RELEASE_BASE_URL}/latest/download/${asset}" diff --git a/src/Commands/UpgradeCommand.php b/src/Commands/UpgradeCommand.php index 6087f6f..65ebd59 100644 --- a/src/Commands/UpgradeCommand.php +++ b/src/Commands/UpgradeCommand.php @@ -43,7 +43,7 @@ class UpgradeCommand extends Command protected function configure(): void { $this->setName('upgrade') - ->setDescription('Upgrade the standalone dw binary to the supported (or a pinned) release') + ->setDescription('Upgrade the standalone dw binary to the latest stable (or a pinned) release') ->setHelp(<<<'HELP' Replace the currently running `dw` binary with a newer release from `durable-workflow/cli` on GitHub. The command verifies the downloaded @@ -58,7 +58,7 @@ protected function configure(): void dw upgrade --dry-run dw upgrade --output=json HELP) - ->addOption('tag', null, InputOption::VALUE_REQUIRED, 'Explicit release tag to install, including an intentional downgrade (defaults to the supported release)') + ->addOption('tag', null, InputOption::VALUE_REQUIRED, 'Explicit release tag to install, including an intentional downgrade (defaults to latest stable)') ->addOption('dry-run', null, InputOption::VALUE_NONE, 'Resolve the target release without downloading or replacing') ->addOption('force', null, InputOption::VALUE_NONE, 'Re-download and replace even when the current and target versions match') ->addOption( @@ -182,7 +182,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int return $this->emit($output, $asJson, [ 'status' => 'newer', 'reason' => sprintf( - 'dw %s is newer than the supported release %s; no change was made', + 'dw %s is newer than the latest stable release %s; no change was made', $currentVersion, $targetVersion, ), @@ -295,7 +295,7 @@ private function renderHuman(OutputInterface $output, array $payload): void $output->writeln(sprintf('dw is already at %s', (string) ($payload['current_version'] ?? 'unknown'))); break; case 'newer': - $output->writeln(sprintf('%s', (string) ($payload['reason'] ?? 'The installed dw release is newer than the supported release; no change was made.'))); + $output->writeln(sprintf('%s', (string) ($payload['reason'] ?? 'The installed dw release is newer than the latest stable release; no change was made.'))); break; case 'dry-run': $operation = ($payload['direction'] ?? null) === 'downgrade' ? 'downgrade' : 'upgrade'; diff --git a/src/Support/ReleaseCatalog.php b/src/Support/ReleaseCatalog.php index 3731a78..eb06960 100644 --- a/src/Support/ReleaseCatalog.php +++ b/src/Support/ReleaseCatalog.php @@ -11,15 +11,14 @@ /** * Resolves release metadata for the standalone `dw` binary. * - * Default discovery follows the passing public artifact compatibility - * authority. Asset downloads and `SHA256SUMS` then use the exact qualified tag - * rather than GitHub's stable-only /releases/latest route. + * Default discovery follows GitHub's latest published stable release. + * Asset downloads and `SHA256SUMS` use that exact tag. */ final class ReleaseCatalog { public const DEFAULT_REPO = 'durable-workflow/cli'; - public const DEFAULT_AUTHORITY_URL = 'https://durable-workflow.com/public-artifact-compatibility-evidence.json'; + public const DEFAULT_AUTHORITY_URL = 'https://api.github.com/repos/durable-workflow/cli/releases/latest'; public function __construct( private readonly HttpClientInterface $http, @@ -35,7 +34,7 @@ public static function create( ?string $baseUrl = null, ?string $authorityUrl = null, ): self { - $authorityUrl ??= getenv('DURABLE_WORKFLOW_QUALIFIED_AUTHORITY_URL') ?: self::DEFAULT_AUTHORITY_URL; + $authorityUrl ??= getenv('DURABLE_WORKFLOW_RELEASE_API_URL') ?: self::DEFAULT_AUTHORITY_URL; return new self( http: $http ?? HttpClient::create([ @@ -53,7 +52,7 @@ public static function create( } /** - * Resolve the supported release tag from the qualified artifact authority. + * Resolve the latest published stable release tag. */ public function supportedTag(): string { @@ -63,31 +62,23 @@ public function supportedTag(): string } catch (HttpExceptionInterface $e) { throw new ReleaseCatalogException( message: sprintf( - 'could not fetch the qualified CLI release authority: %s', + 'could not fetch the latest stable CLI release: %s', $e->getMessage(), ), previous: $e, ); } - if ( - ($data['schema'] ?? null) !== 'durable-workflow.docs.public-artifact-compatibility-evidence' - || ($data['schema_version'] ?? null) !== 2 - || ($data['outcome'] ?? null) !== 'pass' - ) { - throw new ReleaseCatalogException('qualified CLI release authority must be a passing schema-v2 document'); - } - $tag = $data['qualified_artifact_versions']['cli'] ?? null; + $tag = $data['tag_name'] ?? null; if (! is_string($tag) || $tag === '') { - throw new ReleaseCatalogException('qualified CLI release authority must include a CLI version'); + throw new ReleaseCatalogException('latest stable CLI release must include a tag_name'); } $tag = ltrim($tag, 'v'); $stablePattern = '/^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$/D'; - $prereleasePattern = '/^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)'. - '-(alpha|beta|rc)\.(0|[1-9][0-9]*)$/D'; - if (preg_match($stablePattern, $tag) !== 1 && preg_match($prereleasePattern, $tag) !== 1) { - throw new ReleaseCatalogException('qualified CLI release must name a stable, alpha, beta, or rc version'); + if (($data['draft'] ?? null) !== false || ($data['prerelease'] ?? null) !== false + || preg_match($stablePattern, $tag) !== 1) { + throw new ReleaseCatalogException('latest CLI release must be a published stable version'); } return $tag; diff --git a/tests/Commands/UpgradeCommandTest.php b/tests/Commands/UpgradeCommandTest.php index ad76a07..6c90fca 100644 --- a/tests/Commands/UpgradeCommandTest.php +++ b/tests/Commands/UpgradeCommandTest.php @@ -108,7 +108,7 @@ public function test_noop_when_current_version_matches_supported_release(): void public function test_dry_run_reports_asset_url_without_downloading(): void { $command = $this->command( - catalog: $this->catalog(['latest-tag' => '2.0.0-rc.31']), + catalog: $this->catalog(['latest-tag' => '2.0.0']), detector: fn () => new InstallationTarget( kind: InstallationTarget::KIND_BINARY, path: '/home/user/.local/bin/dw', @@ -128,9 +128,9 @@ public function test_dry_run_reports_asset_url_without_downloading(): void $decoded = $this->decode($tester->getDisplay()); self::assertSame('dry-run', $decoded['status']); self::assertSame('upgrade', $decoded['direction']); - self::assertSame('2.0.0-rc.31', $decoded['target_version']); - self::assertStringContainsString('2.0.0-rc.31/dw-linux-x86_64', $decoded['asset_url']); - self::assertStringContainsString('2.0.0-rc.31/SHA256SUMS', $decoded['checksum_url']); + self::assertSame('2.0.0', $decoded['target_version']); + self::assertStringContainsString('2.0.0/dw-linux-x86_64', $decoded['asset_url']); + self::assertStringContainsString('2.0.0/SHA256SUMS', $decoded['checksum_url']); } /** @@ -147,11 +147,10 @@ static function (string $method, string $url) use (&$requests, $binary, $hash): $requests[] = [$method, $url]; return match ($url) { - 'https://durable-workflow.com/public-artifact-compatibility-evidence.json' => new MockResponse(json_encode([ - 'schema' => 'durable-workflow.docs.public-artifact-compatibility-evidence', - 'schema_version' => 2, - 'outcome' => 'pass', - 'qualified_artifact_versions' => ['cli' => '0.1.9'], + 'https://api.github.com/repos/durable-workflow/cli/releases/latest' => new MockResponse(json_encode([ + 'tag_name' => '0.1.9', + 'draft' => false, + 'prerelease' => false, ], JSON_THROW_ON_ERROR), [ 'http_code' => 200, 'response_headers' => ['Content-Type: application/json'], @@ -184,7 +183,7 @@ static function (string $method, string $url) use (&$requests, $binary, $hash): self::assertSame(Command::SUCCESS, $tester->execute(['--output' => 'json'])); self::assertSame('upgraded', $this->decode($tester->getDisplay())['status']); self::assertSame([ - ['GET', 'https://durable-workflow.com/public-artifact-compatibility-evidence.json'], + ['GET', 'https://api.github.com/repos/durable-workflow/cli/releases/latest'], ['GET', 'https://github.com/durable-workflow/cli/releases/download/0.1.9/SHA256SUMS'], ['GET', 'https://github.com/durable-workflow/cli/releases/download/0.1.9/dw-linux-x86_64'], ], $requests); @@ -197,15 +196,14 @@ public function test_unpinned_dry_run_requests_only_the_release_authority(): voi static function (string $method, string $url) use (&$requests): MockResponse { $requests[] = [$method, $url]; - if ($url !== 'https://durable-workflow.com/public-artifact-compatibility-evidence.json') { + if ($url !== 'https://api.github.com/repos/durable-workflow/cli/releases/latest') { throw new \AssertionError('unexpected dry-run request: '.$method.' '.$url); } return new MockResponse(json_encode([ - 'schema' => 'durable-workflow.docs.public-artifact-compatibility-evidence', - 'schema_version' => 2, - 'outcome' => 'pass', - 'qualified_artifact_versions' => ['cli' => '0.1.9'], + 'tag_name' => '0.1.9', + 'draft' => false, + 'prerelease' => false, ], JSON_THROW_ON_ERROR), [ 'http_code' => 200, 'response_headers' => ['Content-Type: application/json'], @@ -240,7 +238,7 @@ static function (string $method, string $url) use (&$requests): MockResponse { $decoded['asset_url'], ); self::assertSame([ - ['GET', 'https://durable-workflow.com/public-artifact-compatibility-evidence.json'], + ['GET', 'https://api.github.com/repos/durable-workflow/cli/releases/latest'], ], $requests); } @@ -271,9 +269,9 @@ public function test_dry_run_accepts_qualified_stable_transition(): void public function test_default_dry_run_reports_that_current_release_is_newer_than_supported_channel(): void { - putenv('DW_CLI_VERSION=2.0.0-rc.33'); + putenv('DW_CLI_VERSION=2.2.0'); $command = $this->command( - catalog: $this->catalog(['latest-tag' => '2.0.0-rc.12']), + catalog: $this->catalog(['latest-tag' => '2.1.1']), detector: fn () => new InstallationTarget( kind: InstallationTarget::KIND_BINARY, path: '/home/user/.local/bin/dw', @@ -292,17 +290,17 @@ public function test_default_dry_run_reports_that_current_release_is_newer_than_ self::assertSame(Command::SUCCESS, $exit); $decoded = $this->decode($tester->getDisplay()); self::assertSame('newer', $decoded['status']); - self::assertSame('2.0.0-rc.33', $decoded['current_version']); - self::assertSame('2.0.0-rc.12', $decoded['target_version']); - self::assertStringContainsString('newer than the supported release', $decoded['reason']); + self::assertSame('2.2.0', $decoded['current_version']); + self::assertSame('2.1.1', $decoded['target_version']); + self::assertStringContainsString('newer than the latest stable release', $decoded['reason']); self::assertArrayNotHasKey('asset_url', $decoded); } public function test_default_upgrade_does_not_replace_newer_current_release(): void { - putenv('DW_CLI_VERSION=2.0.0-rc.33'); + putenv('DW_CLI_VERSION=2.2.0'); $command = $this->command( - catalog: $this->catalog(['latest-tag' => '2.0.0-rc.12']), + catalog: $this->catalog(['latest-tag' => '2.1.1']), detector: fn () => new InstallationTarget( kind: InstallationTarget::KIND_BINARY, path: '/home/user/.local/bin/dw', @@ -321,9 +319,9 @@ public function test_default_upgrade_does_not_replace_newer_current_release(): v public function test_force_does_not_allow_an_unpinned_supported_channel_downgrade(): void { - putenv('DW_CLI_VERSION=2.0.0-rc.33'); + putenv('DW_CLI_VERSION=2.2.0'); $command = $this->command( - catalog: $this->catalog(['latest-tag' => '2.0.0-rc.12']), + catalog: $this->catalog(['latest-tag' => '2.1.1']), detector: fn () => new InstallationTarget( kind: InstallationTarget::KIND_BINARY, path: '/home/user/.local/bin/dw', @@ -345,9 +343,9 @@ public function test_force_does_not_allow_an_unpinned_supported_channel_downgrad public function test_human_dry_run_does_not_advertise_downgrade_as_upgrade(): void { - putenv('DW_CLI_VERSION=2.0.0-rc.33'); + putenv('DW_CLI_VERSION=2.2.0'); $command = $this->command( - catalog: $this->catalog(['latest-tag' => '2.0.0-rc.12']), + catalog: $this->catalog(['latest-tag' => '2.1.1']), detector: fn () => new InstallationTarget( kind: InstallationTarget::KIND_BINARY, path: '/home/user/.local/bin/dw', @@ -361,7 +359,7 @@ public function test_human_dry_run_does_not_advertise_downgrade_as_upgrade(): vo $exit = $tester->execute(['--dry-run' => true]); self::assertSame(Command::SUCCESS, $exit); - self::assertStringContainsString('2.0.0-rc.33 is newer than the supported release 2.0.0-rc.12', $tester->getDisplay()); + self::assertStringContainsString('2.2.0 is newer than the latest stable release 2.1.1', $tester->getDisplay()); self::assertStringNotContainsString('Would upgrade', $tester->getDisplay()); } @@ -733,10 +731,9 @@ private function catalog(array $opts = []): ReleaseCatalog if (array_key_exists('latest-tag', $opts) && $opts['latest-tag'] !== null) { $tag = $opts['latest-tag']; $responses[] = new MockResponse(json_encode([ - 'schema' => 'durable-workflow.docs.public-artifact-compatibility-evidence', - 'schema_version' => 2, - 'outcome' => 'pass', - 'qualified_artifact_versions' => ['cli' => $tag], + 'tag_name' => $tag, + 'draft' => false, + 'prerelease' => false, ], JSON_THROW_ON_ERROR), [ 'http_code' => 200, 'response_headers' => ['Content-Type: application/json'], diff --git a/tests/ReleaseInstallerContractTest.php b/tests/ReleaseInstallerContractTest.php index 3f1d761..363e522 100644 --- a/tests/ReleaseInstallerContractTest.php +++ b/tests/ReleaseInstallerContractTest.php @@ -9,23 +9,22 @@ final class ReleaseInstallerContractTest extends TestCase { - public function test_default_installers_resolve_the_qualified_supported_release(): void + public function test_default_installers_use_latest_stable_release(): void { $shell = self::readRepoFile('scripts/install.sh'); $powershell = self::readRepoFile('scripts/install.ps1'); - self::assertStringContainsString('VERSION="${VERSION:-supported}"', $shell); - self::assertStringContainsString('public-artifact-compatibility-evidence.json', $shell); - self::assertStringContainsString('/"qualified_artifact_versions"', $shell); - self::assertStringContainsString('/"cli"', $shell); - self::assertStringNotContainsString('api.github.com/repos/${REPO}/releases', $shell); - self::assertStringContainsString("else { 'supported' }", $powershell); - self::assertStringContainsString('public-artifact-compatibility-evidence.json', $powershell); - self::assertStringContainsString('$authority.qualified_artifact_versions.cli', $powershell); - self::assertStringNotContainsString('api.github.com/repos/$repo/releases', $powershell); + self::assertStringContainsString('VERSION="${VERSION:-stable}"', $shell); + self::assertStringContainsString('supported) VERSION=stable', $shell); + self::assertStringContainsString('/latest/download/${asset}', $shell); + self::assertStringNotContainsString('public-artifact-compatibility-evidence.json', $shell); + self::assertStringContainsString("else { 'stable' }", $powershell); + self::assertStringContainsString("if (\$version -eq 'supported')", $powershell); + self::assertStringContainsString('/latest/download/$asset', $powershell); + self::assertStringNotContainsString('public-artifact-compatibility-evidence.json', $powershell); } - public function test_shell_installer_resolves_and_installs_from_the_prerelease_channel(): void + public function test_shell_installer_uses_latest_stable_and_accepts_explicit_prerelease(): void { if (PHP_OS_FAMILY !== 'Linux') { self::markTestSkipped('The shell installer fixture exercises the Linux artifact path.'); @@ -40,13 +39,13 @@ public function test_shell_installer_resolves_and_installs_from_the_prerelease_c self::markTestSkipped('The shell installer fixture requires a supported Linux architecture.'); } - $fixtureRoot = sys_get_temp_dir().'/dw-prerelease-installer-'.bin2hex(random_bytes(8)); + $fixtureRoot = sys_get_temp_dir().'/dw-stable-installer-'.bin2hex(random_bytes(8)); $mockBin = $fixtureRoot.'/bin'; $installDir = $fixtureRoot.'/install'; + $binName = 'dw-installer-fixture'; $asset = "dw-linux-{$architecture}"; $assetPath = $fixtureRoot.'/'.$asset; $sumsPath = $fixtureRoot.'/SHA256SUMS'; - $authorityPath = $fixtureRoot.'/qualified-authority.json'; $curlLogPath = $fixtureRoot.'/curl.log'; self::assertTrue(mkdir($mockBin, 0o777, true)); @@ -55,22 +54,12 @@ public function test_shell_installer_resolves_and_installs_from_the_prerelease_c try { self::assertIsInt(file_put_contents( $assetPath, - "#!/usr/bin/env sh\nprintf '%s\\n' 'dw 2.0.0-rc.998'\n", + "#!/usr/bin/env sh\nprintf '%s\\n' 'dw 2.1.1'\n", )); self::assertIsInt(file_put_contents( $sumsPath, hash_file('sha256', $assetPath)." {$asset}\n", )); - self::assertIsInt(file_put_contents( - $authorityPath, - json_encode([ - 'schema' => 'durable-workflow.docs.public-artifact-compatibility-evidence', - 'schema_version' => 2, - 'outcome' => 'pass', - 'qualified_artifact_versions' => ['cli' => '2.0.0-rc.998'], - ], JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR), - )); - $mockCurl = <<<'SH' #!/usr/bin/env sh set -eu @@ -89,9 +78,8 @@ public function test_shell_installer_resolves_and_installs_from_the_prerelease_c printf '%s\n' "$url" >> "$MOCK_CURL_LOG" case "$url" in - "$MOCK_QUALIFIED_AUTHORITY_URL") source="$MOCK_QUALIFIED_AUTHORITY_FILE" ;; - "$MOCK_RELEASE_BASE_URL/download/2.0.0-rc.998/$MOCK_RELEASE_ASSET_NAME") source="$MOCK_RELEASE_ASSET_FILE" ;; - "$MOCK_RELEASE_BASE_URL/download/2.0.0-rc.998/SHA256SUMS") source="$MOCK_RELEASE_SUMS_FILE" ;; + "$MOCK_RELEASE_BASE_URL/latest/download/$MOCK_RELEASE_ASSET_NAME") source="$MOCK_RELEASE_ASSET_FILE" ;; + "$MOCK_RELEASE_BASE_URL/latest/download/SHA256SUMS") source="$MOCK_RELEASE_SUMS_FILE" ;; "$MOCK_RELEASE_BASE_URL/download/2.0.0-rc.999/$MOCK_RELEASE_ASSET_NAME") source="$MOCK_RELEASE_ASSET_FILE" ;; "$MOCK_RELEASE_BASE_URL/download/2.0.0-rc.999/SHA256SUMS") source="$MOCK_RELEASE_SUMS_FILE" ;; "$MOCK_RELEASE_BASE_URL/download/2.0.0/$MOCK_RELEASE_ASSET_NAME") source="$MOCK_RELEASE_ASSET_FILE" ;; @@ -109,15 +97,12 @@ public function test_shell_installer_resolves_and_installs_from_the_prerelease_c self::assertIsInt(file_put_contents($mockCurlPath, $mockCurl)); self::assertTrue(chmod($mockCurlPath, 0o755)); - $qualifiedAuthorityUrl = 'https://releases.invalid/qualified-authority.json'; $releaseBaseUrl = 'https://releases.invalid/releases'; $environment = [ 'PATH' => $installDir.PATH_SEPARATOR.$mockBin.PATH_SEPARATOR.(getenv('PATH') ?: ''), 'DURABLE_WORKFLOW_INSTALL_DIR' => $installDir, - 'DURABLE_WORKFLOW_QUALIFIED_AUTHORITY_URL' => $qualifiedAuthorityUrl, + 'DURABLE_WORKFLOW_BIN_NAME' => $binName, 'DURABLE_WORKFLOW_RELEASE_BASE_URL' => $releaseBaseUrl, - 'MOCK_QUALIFIED_AUTHORITY_URL' => $qualifiedAuthorityUrl, - 'MOCK_QUALIFIED_AUTHORITY_FILE' => $authorityPath, 'MOCK_RELEASE_BASE_URL' => $releaseBaseUrl, 'MOCK_RELEASE_ASSET_NAME' => $asset, 'MOCK_RELEASE_ASSET_FILE' => $assetPath, @@ -131,10 +116,9 @@ public function test_shell_installer_resolves_and_installs_from_the_prerelease_c ); $process->mustRun(); - self::assertTrue(is_executable($installDir.'/dw')); - self::assertStringContainsString('Resolving the qualified CLI release', $process->getOutput()); - self::assertStringContainsString('dw 2.0.0-rc.998', $process->getOutput()); - self::assertStringContainsString($qualifiedAuthorityUrl, (string) file_get_contents($curlLogPath)); + self::assertTrue(is_executable($installDir.'/'.$binName)); + self::assertStringContainsString('dw 2.1.1', $process->getOutput()); + self::assertStringContainsString("{$releaseBaseUrl}/latest/download/{$asset}", (string) file_get_contents($curlLogPath)); self::assertIsInt(file_put_contents( $assetPath, @@ -153,10 +137,9 @@ public function test_shell_installer_resolves_and_installs_from_the_prerelease_c ); $pinnedProcess->mustRun(); - self::assertStringNotContainsString('Resolving the qualified CLI release', $pinnedProcess->getOutput()); self::assertStringContainsString('dw 2.0.0-rc.999', $pinnedProcess->getOutput()); $pinnedCurlLog = (string) file_get_contents($curlLogPath); - self::assertStringNotContainsString($qualifiedAuthorityUrl, $pinnedCurlLog); + self::assertStringNotContainsString('/latest/download/', $pinnedCurlLog); self::assertStringContainsString( "{$releaseBaseUrl}/download/2.0.0-rc.999/{$asset}", $pinnedCurlLog, @@ -170,15 +153,6 @@ public function test_shell_installer_resolves_and_installs_from_the_prerelease_c $sumsPath, hash_file('sha256', $assetPath)." {$asset}\n", )); - self::assertIsInt(file_put_contents( - $authorityPath, - json_encode([ - 'schema' => 'durable-workflow.docs.public-artifact-compatibility-evidence', - 'schema_version' => 2, - 'outcome' => 'pass', - 'qualified_artifact_versions' => ['cli' => '2.0.0'], - ], JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR), - )); self::assertIsInt(file_put_contents($curlLogPath, '')); $stableProcess = new Process( @@ -190,7 +164,7 @@ public function test_shell_installer_resolves_and_installs_from_the_prerelease_c self::assertStringContainsString('dw 2.0.0', $stableProcess->getOutput()); self::assertStringContainsString( - "{$releaseBaseUrl}/download/2.0.0/{$asset}", + "{$releaseBaseUrl}/latest/download/{$asset}", (string) file_get_contents($curlLogPath), ); } finally { @@ -631,9 +605,8 @@ public function test_installers_verify_release_checksums_before_installing(): vo self::assertStringContainsString('DURABLE_WORKFLOW_INSTALL_VERIFY_ATTESTATIONS', $shellInstaller); self::assertStringContainsString('DURABLE_WORKFLOW_INSTALL_OUTPUT', $shellInstaller); self::assertStringContainsString('release_version="${VERSION#v}"', $shellInstaller); - self::assertStringContainsString('DURABLE_WORKFLOW_QUALIFIED_AUTHORITY_URL', $shellInstaller); - self::assertStringContainsString('public-artifact-compatibility-evidence', $shellInstaller); - self::assertStringContainsString('VERSION="${VERSION:-supported}"', $shellInstaller); + self::assertStringContainsString('VERSION="${VERSION:-stable}"', $shellInstaller); + self::assertStringContainsString('/latest/download/${asset}', $shellInstaller); self::assertStringContainsString('gh attestation verify "$tmp" --repo "$REPO"', $shellInstaller); self::assertStringContainsString('gh attestation verify "$sums" --repo "$REPO"', $shellInstaller); self::assertStringContainsString('mv "$tmp" "$INSTALL_DIR/$BIN_NAME"', $shellInstaller); @@ -647,9 +620,8 @@ public function test_installers_verify_release_checksums_before_installing(): vo self::assertStringContainsString('SHA256SUMS', $powershellInstaller); self::assertStringContainsString('Checksum verification failed', $powershellInstaller); self::assertStringContainsString('DURABLE_WORKFLOW_INSTALL_VERIFY_ATTESTATIONS', $powershellInstaller); - self::assertStringContainsString('DURABLE_WORKFLOW_QUALIFIED_AUTHORITY_URL', $powershellInstaller); - self::assertStringContainsString('durable-workflow.docs.public-artifact-compatibility-evidence', $powershellInstaller); - self::assertStringContainsString("else { 'supported' }", $powershellInstaller); + self::assertStringContainsString("else { 'stable' }", $powershellInstaller); + self::assertStringContainsString('/latest/download/$asset', $powershellInstaller); self::assertStringContainsString('$version.StartsWith(\'v\')', $powershellInstaller); self::assertStringContainsString('gh attestation verify $tmp --repo $repo', $powershellInstaller); self::assertStringContainsString('gh attestation verify $sums --repo $repo', $powershellInstaller); diff --git a/tests/Support/ReleaseCatalogTest.php b/tests/Support/ReleaseCatalogTest.php index f59517b..4fbddb9 100644 --- a/tests/Support/ReleaseCatalogTest.php +++ b/tests/Support/ReleaseCatalogTest.php @@ -12,14 +12,13 @@ class ReleaseCatalogTest extends TestCase { - public function test_supported_tag_resolves_qualified_prerelease(): void + public function test_supported_tag_resolves_latest_stable_release(): void { $client = new MockHttpClient([ new MockResponse(json_encode([ - 'schema' => 'durable-workflow.docs.public-artifact-compatibility-evidence', - 'schema_version' => 2, - 'outcome' => 'pass', - 'qualified_artifact_versions' => ['cli' => '2.0.0-rc.31'], + 'tag_name' => 'v2.1.1', + 'draft' => false, + 'prerelease' => false, ], JSON_THROW_ON_ERROR), [ 'http_code' => 200, 'response_headers' => ['Content-Type: application/json'], @@ -27,17 +26,16 @@ public function test_supported_tag_resolves_qualified_prerelease(): void ]); $catalog = new ReleaseCatalog($client, 'durable-workflow/cli'); - self::assertSame('2.0.0-rc.31', $catalog->supportedTag()); + self::assertSame('2.1.1', $catalog->supportedTag()); } - public function test_supported_tag_accepts_authorized_stable_transition(): void + public function test_supported_tag_accepts_unprefixed_version(): void { $client = new MockHttpClient([ new MockResponse(json_encode([ - 'schema' => 'durable-workflow.docs.public-artifact-compatibility-evidence', - 'schema_version' => 2, - 'outcome' => 'pass', - 'qualified_artifact_versions' => ['cli' => '2.0.0'], + 'tag_name' => '2.0.0', + 'draft' => false, + 'prerelease' => false, ], JSON_THROW_ON_ERROR), [ 'http_code' => 200, 'response_headers' => ['Content-Type: application/json'], @@ -48,14 +46,13 @@ public function test_supported_tag_accepts_authorized_stable_transition(): void self::assertSame('2.0.0', $catalog->supportedTag()); } - public function test_supported_tag_rejects_unqualified_version_shape(): void + public function test_supported_tag_rejects_prerelease(): void { $client = new MockHttpClient([ new MockResponse(json_encode([ - 'schema' => 'durable-workflow.docs.public-artifact-compatibility-evidence', - 'schema_version' => 2, - 'outcome' => 'pass', - 'qualified_artifact_versions' => ['cli' => '2.0.0-preview.1'], + 'tag_name' => '2.1.1-rc.1', + 'draft' => false, + 'prerelease' => true, ], JSON_THROW_ON_ERROR), [ 'http_code' => 200, 'response_headers' => ['Content-Type: application/json'], @@ -64,10 +61,24 @@ public function test_supported_tag_rejects_unqualified_version_shape(): void $catalog = new ReleaseCatalog($client, 'durable-workflow/cli'); $this->expectException(ReleaseCatalogException::class); - $this->expectExceptionMessage('must name a stable, alpha, beta, or rc version'); + $this->expectExceptionMessage('must be a published stable version'); $catalog->supportedTag(); } + public function test_supported_tag_requires_explicit_publication_flags(): void + { + $client = new MockHttpClient([ + new MockResponse('{"tag_name":"2.1.1"}', [ + 'http_code' => 200, + 'response_headers' => ['Content-Type: application/json'], + ]), + ]); + + $this->expectException(ReleaseCatalogException::class); + $this->expectExceptionMessage('must be a published stable version'); + (new ReleaseCatalog($client))->supportedTag(); + } + public function test_supported_tag_throws_when_authority_is_unavailable(): void { $client = new MockHttpClient([ @@ -76,7 +87,7 @@ public function test_supported_tag_throws_when_authority_is_unavailable(): void $catalog = new ReleaseCatalog($client, 'durable-workflow/cli'); $this->expectException(ReleaseCatalogException::class); - $this->expectExceptionMessage('could not fetch the qualified CLI release authority'); + $this->expectExceptionMessage('could not fetch the latest stable CLI release'); $catalog->supportedTag(); }