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
118 changes: 118 additions & 0 deletions .github/workflows/installers.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
name: CLI installers

on:
pull_request:
paths:
- static/install.sh
- static/install.ps1
- scripts/installer-source.json
- scripts/check-installer-source.test.js
- .github/workflows/installers.yml
workflow_dispatch:

permissions:
contents: read

concurrency:
group: cli-installers-${{ github.ref }}
cancel-in-progress: true

env:
# The published stable CLI these runs install end to end.
CLI_VERSION: 2.3.5

jobs:
shell-installer:
name: install.sh (${{ matrix.os }})
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
timeout-minutes: 10
permissions:
contents: read
attestations: read
steps:
- name: Check out source
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
persist-credentials: false
- name: Install the stable CLI with signer-pinned attestations
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ env.CLI_VERSION }}
DURABLE_WORKFLOW_INSTALL_DIR: ${{ runner.temp }}/dw-bin
DURABLE_WORKFLOW_INSTALL_VERIFY_ATTESTATIONS: '1'
run: |
# The installer reports "not ready" (exit 1) unless dw resolves on PATH.
export PATH="$DURABLE_WORKFLOW_INSTALL_DIR:$PATH"
sh static/install.sh
dw --version | tee "$RUNNER_TEMP/version.txt"
grep -F "$CLI_VERSION" "$RUNNER_TEMP/version.txt"
- name: Refuse a release signed by another workflow
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ env.CLI_VERSION }}
DURABLE_WORKFLOW_INSTALL_DIR: ${{ runner.temp }}/dw-refused
DURABLE_WORKFLOW_INSTALL_VERIFY_ATTESTATIONS: '1'
run: |
# Same installer, but it only trusts a workflow that never signed the release.
sed 's/(release|release-rust)/(not-release)/' static/install.sh > "$RUNNER_TEMP/install-wrong-signer.sh"
! cmp -s static/install.sh "$RUNNER_TEMP/install-wrong-signer.sh"
export PATH="$DURABLE_WORKFLOW_INSTALL_DIR:$PATH"
if sh "$RUNNER_TEMP/install-wrong-signer.sh" > "$RUNNER_TEMP/refused.log" 2>&1; then
cat "$RUNNER_TEMP/refused.log"
echo 'installer accepted a release signed by an unexpected workflow' >&2
exit 1
fi
cat "$RUNNER_TEMP/refused.log"
# Refused by the attestation check, not by an earlier step.
grep -F 'Verifying GitHub artifact attestations' "$RUNNER_TEMP/refused.log"
grep -F 'verifying with issuer' "$RUNNER_TEMP/refused.log"
test ! -e "$DURABLE_WORKFLOW_INSTALL_DIR/dw"

powershell-installer:
name: install.ps1 (windows-latest)
runs-on: windows-latest
timeout-minutes: 10
permissions:
contents: read
attestations: read
steps:
- name: Check out source
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
persist-credentials: false
- name: Install the stable CLI with signer-pinned attestations
shell: pwsh
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ env.CLI_VERSION }}
DURABLE_WORKFLOW_INSTALL_VERIFY_ATTESTATIONS: '1'
run: |
$env:DURABLE_WORKFLOW_INSTALL_DIR = Join-Path $env:RUNNER_TEMP 'dw-bin'
& ./static/install.ps1
$version = & (Join-Path $env:DURABLE_WORKFLOW_INSTALL_DIR 'dw.exe') --version | Out-String
Write-Host $version
if (-not $version.Contains($env:CLI_VERSION)) { throw "Installed dw is not $env:CLI_VERSION" }
- name: Refuse a release signed by another workflow
shell: pwsh
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ env.CLI_VERSION }}
DURABLE_WORKFLOW_INSTALL_VERIFY_ATTESTATIONS: '1'
run: |
$env:DURABLE_WORKFLOW_INSTALL_DIR = Join-Path $env:RUNNER_TEMP 'dw-refused'
$wrong = Join-Path $env:RUNNER_TEMP 'install-wrong-signer.ps1'
(Get-Content -Raw ./static/install.ps1).Replace('(release|release-rust)', '(not-release)') | Set-Content -NoNewline $wrong
$refused = $false
try { & $wrong } catch {
if ("$_" -notlike 'Attestation verification failed*') { throw }
$refused = $true
Write-Host "Refused as expected: $_"
}
if (-not $refused) { throw 'installer accepted a release signed by an unexpected workflow' }
if (Test-Path (Join-Path $env:DURABLE_WORKFLOW_INSTALL_DIR 'dw.exe')) { throw 'refused install left dw.exe behind' }
# The expected gh failure leaves a nonzero exit code behind.
$global:LASTEXITCODE = 0
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"postinstall": "patch-package",
"prestart": "node scripts/check-dependency-security.js",
"start": "docusaurus start",
"build": "npm run check:dependency-security && npm run check:node-toolchain && node scripts/check-helm-chart-pairing.test.js && node scripts/check-install-sdk.test.js && node scripts/check-server-config-reference.test.js && node scripts/check-server-config-reference.js && node scripts/check-local-search.test.js && node scripts/check-doc-translation-updates.test.js && node scripts/check-doc-translations.js && node scripts/check-localized-links.test.js && docusaurus build && node scripts/generate-llms.js && node scripts/generate-llms-full.js",
"build": "npm run check:dependency-security && npm run check:node-toolchain && node scripts/check-helm-chart-pairing.test.js && node scripts/check-install-sdk.test.js && node scripts/check-installer-source.test.js && node scripts/check-server-config-reference.test.js && node scripts/check-server-config-reference.js && node scripts/check-local-search.test.js && node scripts/check-doc-translation-updates.test.js && node scripts/check-doc-translations.js && node scripts/check-localized-links.test.js && docusaurus build && node scripts/generate-llms.js && node scripts/generate-llms-full.js",
"check:node-toolchain": "node scripts/check-node-toolchain.js && node scripts/check-node-toolchain.test.js",
"check:dependency-security": "node scripts/check-dependency-security.js && node scripts/check-dependency-security.test.js && node scripts/check-open-browser.test.js",
"check:platform-protocol-specs": "node scripts/check-platform-protocol-specs.js",
Expand Down
31 changes: 31 additions & 0 deletions scripts/check-installer-source.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
const assert = require('node:assert/strict');
const crypto = require('node:crypto');
const fs = require('node:fs');
const path = require('node:path');
const test = require('node:test');

// The site serves durable-workflow.com/install.sh and /install.ps1. They must
// stay byte-identical to the CLI repository's installers at the recorded
// source commit, so a site copy cannot silently drift from the reviewed CLI
// installer (for example, losing its attestation signer pin).
const root = path.resolve(__dirname, '..');
const source = JSON.parse(fs.readFileSync(path.join(__dirname, 'installer-source.json'), 'utf8'));

test('the installer source names a CLI commit', () => {
assert.equal(source.source_repository, 'durable-workflow/cli');
assert.match(source.source_commit, /^[0-9a-f]{40}$/);
});

for (const [file, entry] of Object.entries(source.files)) {
test(`${file} matches ${source.source_repository}@${source.source_commit.slice(0, 8)}:${entry.source_path}`, () => {
const bytes = fs.readFileSync(path.join(root, file));
const actual = crypto.createHash('sha256').update(bytes).digest('hex');
assert.equal(actual, entry.sha256,
`${file} differs from the recorded CLI installer. Copy ${entry.source_path} from ` +
`${source.source_repository} byte for byte and update scripts/installer-source.json.`);
});
}

test('both public installers are pinned', () => {
assert.deepEqual(Object.keys(source.files).sort(), ['static/install.ps1', 'static/install.sh']);
});
14 changes: 14 additions & 0 deletions scripts/installer-source.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"source_repository": "durable-workflow/cli",
"source_commit": "3df282fa33ebe5e3319e68298aeef2c32d64c1af",
"files": {
"static/install.sh": {
"source_path": "scripts/install.sh",
"sha256": "5f17db134cd04cc455de1908df0d0d80d2e8e7de0d89731b58689fbb70313b18"
},
"static/install.ps1": {
"source_path": "scripts/install.ps1",
"sha256": "52d3ef117eaa360527924a54401cb01d1d4aedaa5ca19c28846148eb39dffd0b"
}
}
}
23 changes: 13 additions & 10 deletions static/install.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# irm https://durable-workflow.com/install.ps1 | iex
#
# Environment variables:
# $env:VERSION Release tag, supported, prerelease, or stable (default: stable).
# $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_INSTALL_VERIFY_ATTESTATIONS
Expand All @@ -25,13 +25,8 @@ $installDir = if ($env:DURABLE_WORKFLOW_INSTALL_DIR) {
Join-Path $env:USERPROFILE '.durable-workflow\bin'
}
$version = if ($env:VERSION) { $env:VERSION } else { 'stable' }
$version = if ($version -eq 'supported') {
'stable'
} elseif ($version -eq 'prerelease') {
throw 'Prerelease auto-selection ended with the stable 2.0 launch; set VERSION to an exact historical tag.'
} else {
$version
}
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 Expand Up @@ -102,8 +97,16 @@ try {
}

Write-Host '==> Verifying GitHub artifact attestations' -ForegroundColor Green
gh attestation verify $tmp --repo $repo
gh attestation verify $sums --repo $repo
# Only the repository's release workflows, run from main or a release
# tag on GitHub-hosted runners, can have signed a release asset.
$repoPattern = [regex]::Escape($repo)
$signer = "^https://github\.com/$repoPattern/\.github/workflows/(release|release-rust)\.yml@refs/(heads/main|tags/v?[0-9]+\.[0-9]+\.[0-9]+([-+][0-9A-Za-z.+-]+)?)$"
foreach ($subject in @($tmp, $sums)) {
gh attestation verify $subject --repo $repo --cert-identity-regex $signer --deny-self-hosted-runners
if ($LASTEXITCODE -ne 0) {
throw "Attestation verification failed for $subject."
}
}
}

Move-Item -Force -Path $tmp -Destination $dest
Expand Down
31 changes: 16 additions & 15 deletions static/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
# curl -fsSL https://durable-workflow.com/install.sh | sh
#
# Environment variables:
# VERSION Release tag, supported, prerelease, or stable (default: stable).
# 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.
Expand Down Expand Up @@ -61,13 +61,10 @@
asset="dw-${os}-${arch}"
command -v curl >/dev/null 2>&1 || err "curl is required"

if [ "$VERSION" = "supported" ]; then
VERSION="stable"
fi

if [ "$VERSION" = "prerelease" ]; then
err "prerelease auto-selection ended with the stable 2.0 launch; set VERSION to an exact historical tag"
fi
case "$VERSION" in

Check failure on line 64 in static/install.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add a default case (*) to handle unexpected values.

See more on https://sonarcloud.io/project/issues?id=durable-workflow_durable-workflow.github.io&issues=AaGk1wJ03GVjRyY5sSgx&open=AaGk1wJ03GVjRyY5sSgx&pullRequest=458
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 Expand Up @@ -130,13 +127,17 @@
command -v gh >/dev/null 2>&1 || err "gh is required when DURABLE_WORKFLOW_INSTALL_VERIFY_ATTESTATIONS=1"

info "Verifying GitHub artifact attestations"
if [ "$OUTPUT_MODE" = "json" ]; then
gh attestation verify "$tmp" --repo "$REPO" >&2
gh attestation verify "$sums" --repo "$REPO" >&2
else
gh attestation verify "$tmp" --repo "$REPO"
gh attestation verify "$sums" --repo "$REPO"
fi
# Only the repository's release workflows, run from main or a release
# tag on GitHub-hosted runners, can have signed a release asset.
repo_pattern=$(printf '%s' "$REPO" | sed 's/[.]/\\./g')
signer="^https://github\\.com/${repo_pattern}/\\.github/workflows/(release|release-rust)\\.yml@refs/(heads/main|tags/v?[0-9]+\\.[0-9]+\\.[0-9]+([-+][0-9A-Za-z.+-]+)?)\$"
for subject in "$tmp" "$sums"; do
if [ "$OUTPUT_MODE" = "json" ]; then
gh attestation verify "$subject" --repo "$REPO" --cert-identity-regex "$signer" --deny-self-hosted-runners >&2
else
gh attestation verify "$subject" --repo "$REPO" --cert-identity-regex "$signer" --deny-self-hosted-runners
fi
done
fi

chmod +x "$tmp"
Expand Down
Loading