Skip to content
Merged
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
51 changes: 24 additions & 27 deletions docs/distribution.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
<https://durable-workflow.com/public-artifact-compatibility-evidence.json>.
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

Expand Down Expand Up @@ -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/<release>/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/<release>/<platform-asset>` | 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.
Expand Down Expand Up @@ -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=<release>` to select an exact release instead of the supported
channel. This is also the required path for an intentional downgrade. The
Use `--tag=<release>` 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.

Expand All @@ -233,18 +230,18 @@ in each invocation:

```console
$ dw upgrade
Upgraded dw to <supported-release>
Upgraded dw to <stable-release>
path: /home/user/.local/bin/dw

$ dw upgrade
dw is already at <supported-release>
dw is already at <stable-release>

$ dw upgrade --force
Upgraded dw to <supported-release>
Upgraded dw to <stable-release>
path: /home/user/.local/bin/dw

$ dw upgrade
dw <newer-release> is newer than the supported release <supported-release>; no change was made
dw <newer-release> is newer than the latest stable release <stable-release>; no change was made

$ dw upgrade --tag="$OLDER_RELEASE" --dry-run
Would downgrade <newer-release> -> <older-release>
Expand Down
36 changes: 4 additions & 32 deletions scripts/install.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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 {
Expand Down
51 changes: 6 additions & 45 deletions scripts/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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}"

Expand Down Expand Up @@ -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}"
Expand Down
8 changes: 4 additions & 4 deletions src/Commands/UpgradeCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -58,7 +58,7 @@ protected function configure(): void
<info>dw upgrade --dry-run</info>
<info>dw upgrade --output=json</info>
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(
Expand Down Expand Up @@ -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,
),
Expand Down Expand Up @@ -295,7 +295,7 @@ private function renderHuman(OutputInterface $output, array $payload): void
$output->writeln(sprintf('<info>dw is already at %s</info>', (string) ($payload['current_version'] ?? 'unknown')));
break;
case 'newer':
$output->writeln(sprintf('<info>%s</info>', (string) ($payload['reason'] ?? 'The installed dw release is newer than the supported release; no change was made.')));
$output->writeln(sprintf('<info>%s</info>', (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';
Expand Down
31 changes: 11 additions & 20 deletions src/Support/ReleaseCatalog.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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([
Expand All @@ -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
{
Expand All @@ -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;
Expand Down
Loading
Loading