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

on:
workflow_call:
inputs:
tag:
description: Release tag whose installer should be published
required: true
type: string
secrets:
CLOUDFLARE_R2_ACCESS_KEY_ID:
required: true
CLOUDFLARE_R2_SECRET_ACCESS_KEY:
required: true
workflow_dispatch:
inputs:
tag:
description: Existing release tag to publish or repair
required: true
type: string

permissions:
contents: read

concurrency:
group: publish-cli-installer-${{ inputs.tag }}
cancel-in-progress: false

jobs:
publish:
name: Publish get.stackdome.com/cli.sh
runs-on: ubuntu-latest
environment: installer-production
env:
AWS_ACCESS_KEY_ID: ${{ secrets.CLOUDFLARE_R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_R2_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: auto
R2_ACCOUNT_ID: ${{ vars.CLOUDFLARE_R2_ACCOUNT_ID }}
R2_BUCKET: ${{ vars.CLOUDFLARE_R2_BUCKET }}
RELEASE_TAG: ${{ inputs.tag }}
steps:
- name: Validate publishing configuration
run: |
if [[ ! "${RELEASE_TAG}" =~ ^v[A-Za-z0-9._-]+$ ]]; then
echo "Invalid release tag: ${RELEASE_TAG}" >&2
exit 1
fi
test -n "${R2_ACCOUNT_ID}" || {
echo "Repository variable CLOUDFLARE_R2_ACCOUNT_ID is required" >&2
exit 1
}
test -n "${R2_BUCKET}" || {
echo "Repository variable CLOUDFLARE_R2_BUCKET is required" >&2
exit 1
}

- name: Check out release tag
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
ref: refs/tags/${{ inputs.tag }}

- name: Validate installer syntax
run: sh -n install.sh

- name: Publish immutable installer
env:
R2_ENDPOINT: https://${{ vars.CLOUDFLARE_R2_ACCOUNT_ID }}.r2.cloudflarestorage.com
run: |
published_installer="$(mktemp)"
head_error="$(mktemp)"
trap 'rm -f "${published_installer}" "${head_error}"' EXIT

if aws s3api head-object \
--bucket "${R2_BUCKET}" \
--key "cli/${RELEASE_TAG}/cli.sh" \
--endpoint-url "${R2_ENDPOINT}" >/dev/null 2>"${head_error}"; then
aws s3 cp "s3://${R2_BUCKET}/cli/${RELEASE_TAG}/cli.sh" "${published_installer}" \
--endpoint-url "${R2_ENDPOINT}"
if ! cmp install.sh "${published_installer}"; then
echo "Refusing to overwrite immutable installer cli/${RELEASE_TAG}/cli.sh" >&2
exit 1
fi
elif grep -Eq '\(404\)|Not Found|NoSuchKey' "${head_error}"; then
aws s3 cp install.sh "s3://${R2_BUCKET}/cli/${RELEASE_TAG}/cli.sh" \
--endpoint-url "${R2_ENDPOINT}" \
--content-type "text/x-shellscript; charset=utf-8" \
--cache-control "public,max-age=31536000,immutable"
else
cat "${head_error}" >&2
exit 1
fi

aws s3 cp "s3://${R2_BUCKET}/cli/${RELEASE_TAG}/cli.sh" "${published_installer}" \
--endpoint-url "${R2_ENDPOINT}"
cmp install.sh "${published_installer}"
sh -n "${published_installer}"

- name: Publish stable alias last
env:
R2_ENDPOINT: https://${{ vars.CLOUDFLARE_R2_ACCOUNT_ID }}.r2.cloudflarestorage.com
run: |
aws s3 cp install.sh "s3://${R2_BUCKET}/cli.sh" \
--endpoint-url "${R2_ENDPOINT}" \
--content-type "text/x-shellscript; charset=utf-8" \
--cache-control "no-store"

- name: Verify public installer
run: |
public_installer="$(mktemp)"
response_headers="$(mktemp)"
trap 'rm -f "${public_installer}" "${response_headers}"' EXIT
curl --retry 5 --retry-all-errors -fsSL \
-D "${response_headers}" \
-o "${public_installer}" \
https://get.stackdome.com/cli.sh
cmp install.sh "${public_installer}"
sh -n "${public_installer}"
grep -qi '^content-type: text/x-shellscript' "${response_headers}"
grep -qi '^cache-control: no-store' "${response_headers}"
85 changes: 71 additions & 14 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,6 @@ jobs:
goarch: amd64
- goos: linux
goarch: arm64
- goos: windows
goarch: amd64
- goos: windows
goarch: arm64
steps:
- name: Check out repository
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
Expand All @@ -113,18 +109,11 @@ jobs:
build_date="$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
mkdir -p package dist
binary="stackdome"
if [ "${GOOS}" = "windows" ]; then
binary="stackdome.exe"
fi
go build -trimpath \
-ldflags "-s -w -X main.Version=${version} -X main.GitCommit=${commit} -X main.BuildDate=${build_date}" \
-o "package/${binary}" ./cmd/stackdome
cp LICENSE package/LICENSE
if [ "${GOOS}" = "windows" ]; then
(cd package && zip -q "../dist/stackdome_${version}_${GOOS}_${GOARCH}.zip" "${binary}" LICENSE)
else
tar -C package -czf "dist/stackdome_${version}_${GOOS}_${GOARCH}.tar.gz" "${binary}" LICENSE
fi
tar -C package -czf "dist/stackdome_${version}_${GOOS}_${GOARCH}.tar.gz" "${binary}" LICENSE

- name: Verify release metadata
if: matrix.goos == 'linux' && matrix.goarch == 'amd64'
Expand Down Expand Up @@ -169,23 +158,91 @@ jobs:

- name: Generate checksum manifest
working-directory: dist
run: sha256sum *.tar.gz *.zip > checksums.txt
env:
RELEASE_TAG: ${{ needs.prepare.outputs.tag }}
run: |
expected_archives=(
"stackdome_${RELEASE_TAG}_darwin_amd64.tar.gz"
"stackdome_${RELEASE_TAG}_darwin_arm64.tar.gz"
"stackdome_${RELEASE_TAG}_linux_amd64.tar.gz"
"stackdome_${RELEASE_TAG}_linux_arm64.tar.gz"
)
for archive in "${expected_archives[@]}"; do
test -f "${archive}" || {
echo "Missing release archive: ${archive}" >&2
exit 1
}
done
archive_count="$(find . -maxdepth 1 -type f -name '*.tar.gz' | wc -l | tr -d ' ')"
if [ "${archive_count}" != "4" ]; then
echo "Expected exactly 4 release archives, found ${archive_count}" >&2
exit 1
fi
sha256sum "${expected_archives[@]}" > checksums.txt
checksum_count="$(wc -l < checksums.txt | tr -d ' ')"
if [ "${checksum_count}" != "4" ]; then
echo "Expected exactly 4 checksums, found ${checksum_count}" >&2
exit 1
fi

- name: Publish release
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ needs.prepare.outputs.tag }}
run: |
expected_assets=(
"stackdome_${RELEASE_TAG}_darwin_amd64.tar.gz"
"stackdome_${RELEASE_TAG}_darwin_arm64.tar.gz"
"stackdome_${RELEASE_TAG}_linux_amd64.tar.gz"
"stackdome_${RELEASE_TAG}_linux_arm64.tar.gz"
"checksums.txt"
)

if gh release view "${RELEASE_TAG}" >/dev/null 2>&1; then
release_is_draft="$(gh release view "${RELEASE_TAG}" --json isDraft --jq .isDraft)"
else
gh release create "${RELEASE_TAG}" --draft --verify-tag --generate-notes --title "Stackdome CLI ${RELEASE_TAG}"
release_is_draft=true
fi

gh release upload "${RELEASE_TAG}" dist/*.tar.gz dist/*.zip --clobber
while IFS= read -r asset; do
is_expected=false
for expected_asset in "${expected_assets[@]}"; do
if [[ "${asset}" == "${expected_asset}" ]]; then
is_expected=true
break
fi
done
if [[ "${is_expected}" == "false" ]]; then
echo "Removing obsolete release asset: ${asset}"
gh release delete-asset "${RELEASE_TAG}" "${asset}" --yes
fi
done < <(gh release view "${RELEASE_TAG}" --json assets --jq '.assets[].name')

gh release upload "${RELEASE_TAG}" dist/*.tar.gz --clobber
gh release upload "${RELEASE_TAG}" dist/checksums.txt --clobber

mapfile -t actual_assets < <(
gh release view "${RELEASE_TAG}" --json assets --jq '.assets[].name' | sort
)
mapfile -t sorted_expected_assets < <(printf '%s\n' "${expected_assets[@]}" | sort)
if [[ "${actual_assets[*]}" != "${sorted_expected_assets[*]}" ]]; then
echo "Release assets do not match the Linux/macOS publication contract" >&2
printf 'Expected: %s\n' "${sorted_expected_assets[*]}" >&2
printf 'Actual: %s\n' "${actual_assets[*]}" >&2
exit 1
fi

if [[ "${release_is_draft}" == "true" ]]; then
gh release edit "${RELEASE_TAG}" --draft=false
fi

publish-installer:
name: Publish CLI installer
needs:
- prepare
- publish
uses: ./.github/workflows/publish-cli-installer.yml
with:
tag: ${{ needs.prepare.outputs.tag }}
secrets: inherit
41 changes: 0 additions & 41 deletions .github/workflows/windows-installer.yml

This file was deleted.

7 changes: 0 additions & 7 deletions AGENTS.md

This file was deleted.

43 changes: 12 additions & 31 deletions INSTALL.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# Install the Stackdome CLI

The installers download the latest GitHub release for the current platform and
verify its SHA-256 checksum before installing it.
The installer downloads the latest GitHub release for the current platform and
verifies its SHA-256 checksum before installing it.

## macOS and Linux

```sh
curl -fsSL https://raw.githubusercontent.com/Stackdome/stackdome-cli/main/install.sh | sh
curl -fsSL https://get.stackdome.com/cli.sh | sh
```

For agents and CI, download first so a network failure cannot be hidden by
Expand All @@ -15,7 +15,7 @@ pipeline exit-status behavior:
```sh
installer_file=$(mktemp)
trap 'rm -f "$installer_file"' EXIT
curl -fsSL https://raw.githubusercontent.com/Stackdome/stackdome-cli/main/install.sh -o "$installer_file"
curl -fsSL https://get.stackdome.com/cli.sh -o "$installer_file"
sh "$installer_file"
```

Expand All @@ -25,32 +25,13 @@ when that directory is writable, otherwise it uses `$HOME/.local/bin`.
To install a specific version or directory:

```sh
curl -fsSL https://raw.githubusercontent.com/Stackdome/stackdome-cli/main/install.sh \
| STACKDOME_VERSION=v0.0.1-alpha STACKDOME_INSTALL_DIR="$HOME/.local/bin" sh
```

## Windows PowerShell

```powershell
irm https://raw.githubusercontent.com/Stackdome/stackdome-cli/main/install.ps1 | iex
```

For agents and CI, fetch the script before evaluating it so download errors are
terminal:

```powershell
$installer = Invoke-RestMethod -ErrorAction Stop https://raw.githubusercontent.com/Stackdome/stackdome-cli/main/install.ps1
& ([ScriptBlock]::Create([string]$installer))
installer_file=$(mktemp)
trap 'rm -f "$installer_file"' EXIT
curl -fsSL https://get.stackdome.com/cli.sh -o "$installer_file"
sh "$installer_file" \
--version v0.0.2-alpha \
--install-dir "$HOME/.local/bin"
```

The installer supports AMD64 and ARM64. By default it installs to
`%LOCALAPPDATA%\Programs\Stackdome\bin` and adds that directory to the user
`PATH`.

To install a specific version or directory:

```powershell
$env:STACKDOME_VERSION = 'v0.0.1-alpha'
$env:STACKDOME_INSTALL_DIR = "$env:LOCALAPPDATA\Programs\Stackdome\bin"
irm https://raw.githubusercontent.com/Stackdome/stackdome-cli/main/install.ps1 | iex
```
`STACKDOME_VERSION` and `STACKDOME_INSTALL_DIR` provide the same settings for
automation. Explicit flags take precedence over environment variables.
Loading
Loading