mirror of
https://github.com/warmbly/warmbly.git
synced 2026-09-06 00:01:24 +00:00
feat: install the CLI in one command from warmbly.com/cli.sh or cli.ps1, with checksum-verified archives, a Homebrew formula and Scoop manifest generated alongside them, a ghcr cli image, and a make cli-check that installs from a local mirror and proves a tampered checksum stops the install
This commit is contained in:
@@ -35,6 +35,7 @@ jobs:
|
||||
make: ${{ steps.filter.outputs.make }}
|
||||
ios: ${{ steps.filter.outputs.ios }}
|
||||
installer: ${{ steps.filter.outputs.installer }}
|
||||
cli-installer: ${{ steps.filter.outputs.cli-installer }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dorny/paths-filter@v3
|
||||
@@ -75,6 +76,12 @@ jobs:
|
||||
- 'site/public/install.sh'
|
||||
- 'site/public/install.sh.sha256'
|
||||
- 'scripts/check-installer.sh'
|
||||
cli-installer:
|
||||
- 'site/public/cli.sh'
|
||||
- 'site/public/cli.sh.sha256'
|
||||
- 'site/public/cli.ps1'
|
||||
- 'scripts/check-cli-installer.sh'
|
||||
- 'scripts/build-cli.sh'
|
||||
|
||||
migrations-ci:
|
||||
name: Migrations
|
||||
@@ -249,6 +256,22 @@ jobs:
|
||||
- name: Check the installer
|
||||
run: ./scripts/check-installer.sh
|
||||
|
||||
cli-installer-ci:
|
||||
name: CLI Installer CI
|
||||
needs: changes
|
||||
if: needs.changes.outputs.cli-installer == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# dash is what /bin/sh is on Debian and Ubuntu, which is what most people
|
||||
# will pipe this into. Ubuntu runners already have shellcheck and pwsh.
|
||||
- name: Install dash
|
||||
run: sudo apt-get update && sudo apt-get install -y dash
|
||||
|
||||
- name: Check the CLI installer
|
||||
run: ./scripts/check-cli-installer.sh
|
||||
|
||||
make-ci:
|
||||
name: Make App CI
|
||||
needs: changes
|
||||
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
service: [backend, consumer, worker, forms, updater]
|
||||
service: [backend, consumer, worker, forms, updater, cli]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -214,9 +214,46 @@ jobs:
|
||||
-t ${{ env.IMAGE_PREFIX }}/${{ matrix.service }}:prod \
|
||||
$(printf '${{ env.IMAGE_PREFIX }}/${{ matrix.service }}@sha256:%s ' *)
|
||||
|
||||
# The `warmbly` CLI is a plain static binary, so it cross-compiles for every
|
||||
# platform on one runner.
|
||||
#
|
||||
# Assets are named WITHOUT the version, so
|
||||
# releases/latest/download/warmbly_linux_amd64.tar.gz always resolves. That is
|
||||
# what lets the install script find the newest build with no GitHub API call,
|
||||
# which matters because the unauthenticated API is rate limited and a curl
|
||||
# installer that fails on a busy CI runner is not an installer.
|
||||
build-cli:
|
||||
name: Build CLI
|
||||
needs: validate-tag
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Cross-compile and package
|
||||
env:
|
||||
VERSION: ${{ github.ref_name }}
|
||||
COMMIT: ${{ github.sha }}
|
||||
BUILT_AT: ${{ github.event.head_commit.timestamp }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
./scripts/build-cli.sh dist
|
||||
|
||||
- name: Upload
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: warmbly-cli
|
||||
path: dist/
|
||||
retention-days: 1
|
||||
|
||||
create-release:
|
||||
name: Create GitHub Release
|
||||
needs: [validate-tag, build-go, build-frontend, merge-native]
|
||||
needs: [validate-tag, build-go, build-frontend, merge-native, build-cli]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -228,6 +265,12 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Download the CLI binaries
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: warmbly-cli
|
||||
path: /tmp/cli
|
||||
|
||||
# The installer verifies what it pulled against this file, so it is what
|
||||
# makes "curl | sh" checkable after the fact rather than only before it.
|
||||
# One line per service, because the thing that reads it is a POSIX shell.
|
||||
@@ -284,6 +327,18 @@ jobs:
|
||||
Add `--wizard` to be asked where each store lives, what is kept and
|
||||
for how long, and how it is backed up.
|
||||
|
||||
## The warmbly CLI
|
||||
|
||||
```
|
||||
curl -fsSL https://warmbly.com/cli.sh | sh # macOS, Linux
|
||||
irm https://warmbly.com/cli.ps1 | iex # Windows
|
||||
brew install warmbly/tap/warmbly # Homebrew
|
||||
scoop install warmbly # Scoop
|
||||
```
|
||||
|
||||
Or take an archive below and unpack it yourself; `checksums.txt`
|
||||
verifies every one of them. Already installed? `warmbly upgrade`.
|
||||
|
||||
`images.json` below lists the manifest digest of every image in this
|
||||
release. The installer checks what it pulled against it, and you can
|
||||
too:
|
||||
@@ -307,6 +362,7 @@ jobs:
|
||||
| Admin | `${{ env.IMAGE_PREFIX }}/admin:${{ github.ref_name }}` |
|
||||
| Forms | `${{ env.IMAGE_PREFIX }}/forms:${{ github.ref_name }}` |
|
||||
| Updater | `${{ env.IMAGE_PREFIX }}/updater:${{ github.ref_name }}` |
|
||||
| CLI | `${{ env.IMAGE_PREFIX }}/cli:${{ github.ref_name }}` |
|
||||
|
||||
## Deployment
|
||||
|
||||
@@ -314,12 +370,50 @@ jobs:
|
||||
EOF
|
||||
} > /tmp/release-body.md
|
||||
|
||||
# The formula and manifest are generated with the archives, so their
|
||||
# checksums can never drift from what they describe. Pushing them is
|
||||
# skipped, loudly, when the tap token is not configured: a release must
|
||||
# not fail because a downstream package repo is not set up yet.
|
||||
- name: Publish the Homebrew formula and Scoop manifest
|
||||
env:
|
||||
TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
|
||||
TAG: ${{ github.ref_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -z "${TAP_TOKEN:-}" ]; then
|
||||
echo "HOMEBREW_TAP_TOKEN is not set; skipping the tap push."
|
||||
echo "The formula and manifest are still attached to the release."
|
||||
exit 0
|
||||
fi
|
||||
# A prerelease must never become the default `brew install`.
|
||||
case "$TAG" in
|
||||
*-*) echo "$TAG is a prerelease; not updating the taps."; exit 0 ;;
|
||||
esac
|
||||
git config --global user.name "warmbly-release"
|
||||
git config --global user.email "release@warmbly.com"
|
||||
git clone --depth 1 \
|
||||
"https://x-access-token:${TAP_TOKEN}@github.com/warmbly/homebrew-tap.git" /tmp/tap
|
||||
mkdir -p /tmp/tap/Formula /tmp/tap/bucket
|
||||
cp /tmp/cli/warmbly.rb /tmp/tap/Formula/warmbly.rb
|
||||
cp /tmp/cli/warmbly.json /tmp/tap/bucket/warmbly.json
|
||||
cd /tmp/tap
|
||||
git add Formula/warmbly.rb bucket/warmbly.json
|
||||
if git diff --cached --quiet; then
|
||||
echo "the tap already describes $TAG"
|
||||
else
|
||||
git commit -m "warmbly $TAG"
|
||||
git push
|
||||
echo "pushed warmbly $TAG to the tap"
|
||||
fi
|
||||
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ github.ref_name }}
|
||||
name: ${{ github.ref_name }}
|
||||
body_path: /tmp/release-body.md
|
||||
files: /tmp/images.json
|
||||
files: |
|
||||
/tmp/images.json
|
||||
/tmp/cli/*
|
||||
draft: false
|
||||
prerelease: ${{ contains(github.ref_name, '-') }}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
/migrate
|
||||
/updater
|
||||
/warmblyctl
|
||||
/warmbly
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
@@ -28,6 +29,10 @@ profile.cov
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
vendor/
|
||||
|
||||
# Local CLI builds (make warmbly / make warmbly-dist)
|
||||
/bin/
|
||||
/dist/
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
go.work.sum
|
||||
|
||||
@@ -42,7 +42,7 @@ PROTO_GEN_FILES := $(PROTO_DIR)/tasks.pb.go
|
||||
restart restart-go restart-all infra infra-down app app-down app-logs \
|
||||
backend forms forms-web consumer worker run dev tracking realtime web \
|
||||
admin site docs grant-admin revoke-admin gen-key installer-sha installer-check installer-demo \
|
||||
db-reset db-wipe migrate
|
||||
db-reset db-wipe migrate warmbly warmbly-dist cli-sha cli-check
|
||||
|
||||
setup-tools:
|
||||
@echo "Installing required Go tools into $(GO_BIN)"
|
||||
@@ -50,6 +50,33 @@ setup-tools:
|
||||
GOBIN=$(GO_BIN) go install google.golang.org/protobuf/cmd/protoc-gen-go@$(PROTOC_GEN_GO_VERSION)
|
||||
GOBIN=$(GO_BIN) go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@$(PROTOC_GEN_GO_GRPC_VERSION)
|
||||
|
||||
# Build the `warmbly` CLI into ./bin, stamped with this checkout's version so
|
||||
# `warmbly version` reports something meaningful. This is the customer CLI; the
|
||||
# operator one (warmblyctl) ships in the backend image and runs there.
|
||||
warmbly:
|
||||
@mkdir -p bin
|
||||
go build -ldflags="-s -w \
|
||||
-X github.com/warmbly/warmbly/internal/version.Version=$(WARMBLY_BUILD_VERSION) \
|
||||
-X github.com/warmbly/warmbly/internal/version.Commit=$(WARMBLY_BUILD_COMMIT) \
|
||||
-X github.com/warmbly/warmbly/internal/version.BuiltAt=$(WARMBLY_BUILD_TIME)" \
|
||||
-o bin/warmbly ./cmd/cli
|
||||
@echo "built bin/warmbly ($(WARMBLY_BUILD_VERSION))"
|
||||
@echo "put it on your PATH: sudo install -m 0755 bin/warmbly /usr/local/bin/warmbly"
|
||||
|
||||
# Everything a release publishes for the CLI: an archive per platform, the
|
||||
# checksums, and the Homebrew and Scoop manifests. Same script the release
|
||||
# workflow runs, so an artifact can be reproduced locally.
|
||||
warmbly-dist:
|
||||
./scripts/build-cli.sh dist
|
||||
|
||||
# The published installer at https://warmbly.com/cli.sh. Regenerate the
|
||||
# checksum after any edit to it; CI fails when the two disagree.
|
||||
cli-sha:
|
||||
@cd site/public && sha256sum cli.sh > cli.sh.sha256 && cat cli.sh.sha256
|
||||
|
||||
cli-check:
|
||||
@./scripts/check-cli-installer.sh
|
||||
|
||||
# Format all Go code. CI's golangci-lint enforces gofmt, so this is the
|
||||
# formatting signal to run before committing, not `go build`.
|
||||
fmt:
|
||||
|
||||
@@ -32,7 +32,8 @@ RUN --mount=type=cache,target=/go/pkg/mod \
|
||||
CGO_ENABLED=$CGO GOOS=$TARGETOS GOARCH=$TARGETARCH go build -tags "$TAGS" -ldflags="-s -w -X github.com/warmbly/warmbly/internal/version.Version=$VERSION -X github.com/warmbly/warmbly/internal/version.Commit=$COMMIT -X github.com/warmbly/warmbly/internal/version.BuiltAt=$BUILT_AT" -o /out/backend ./cmd/backend; \
|
||||
CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags="-s -w -X github.com/warmbly/warmbly/internal/version.Version=$VERSION -X github.com/warmbly/warmbly/internal/version.Commit=$COMMIT -X github.com/warmbly/warmbly/internal/version.BuiltAt=$BUILT_AT" -o /out/seed ./cmd/seed; \
|
||||
CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags="-s -w -X github.com/warmbly/warmbly/internal/version.Version=$VERSION -X github.com/warmbly/warmbly/internal/version.Commit=$COMMIT -X github.com/warmbly/warmbly/internal/version.BuiltAt=$BUILT_AT" -o /out/migrate ./cmd/migrate; \
|
||||
CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags="-s -w -X github.com/warmbly/warmbly/internal/version.Version=$VERSION -X github.com/warmbly/warmbly/internal/version.Commit=$COMMIT -X github.com/warmbly/warmbly/internal/version.BuiltAt=$BUILT_AT" -o /out/warmblyctl ./cmd/warmblyctl
|
||||
CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags="-s -w -X github.com/warmbly/warmbly/internal/version.Version=$VERSION -X github.com/warmbly/warmbly/internal/version.Commit=$COMMIT -X github.com/warmbly/warmbly/internal/version.BuiltAt=$BUILT_AT" -o /out/warmblyctl ./cmd/warmblyctl; \
|
||||
CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags="-s -w -X github.com/warmbly/warmbly/internal/version.Version=$VERSION -X github.com/warmbly/warmbly/internal/version.Commit=$COMMIT -X github.com/warmbly/warmbly/internal/version.BuiltAt=$BUILT_AT" -o /out/warmbly ./cmd/cli
|
||||
|
||||
# Runtime stage
|
||||
FROM alpine:3.23
|
||||
@@ -60,6 +61,10 @@ COPY --from=builder /out/migrate /app/migrate
|
||||
# `docker compose exec backend warmblyctl status` and not a path.
|
||||
COPY --from=builder /out/warmblyctl /usr/local/bin/warmblyctl
|
||||
|
||||
# The customer CLI ships alongside it, so an operator who has exec on the box
|
||||
# can drive the product as well as recover it without installing anything.
|
||||
COPY --from=builder /out/warmbly /usr/local/bin/warmbly
|
||||
|
||||
# Installer script the worker orchestrator uploads + runs over SSH, and serves
|
||||
# at GET /worker-install.sh. The mode is explicit because COPY otherwise keeps
|
||||
# the checkout's: on a filesystem without POSIX permissions that is 0700, and
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
# The `warmbly` CLI as an image, for CI jobs and anywhere installing a binary
|
||||
# is more trouble than pulling one:
|
||||
#
|
||||
# docker run --rm -e WARMBLY_TOKEN ghcr.io/warmbly/warmbly/cli campaign list
|
||||
#
|
||||
# Distroless-style: the CLI is a static binary that talks to one HTTPS API, so
|
||||
# the runtime needs certificates, timezone data and nothing else.
|
||||
FROM --platform=$BUILDPLATFORM golang:1.25-alpine AS builder
|
||||
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
ARG VERSION=""
|
||||
ARG COMMIT=""
|
||||
ARG BUILT_AT=""
|
||||
|
||||
WORKDIR /app
|
||||
COPY go.mod go.sum ./
|
||||
RUN --mount=type=cache,target=/go/pkg/mod go mod download
|
||||
|
||||
COPY . .
|
||||
RUN --mount=type=cache,target=/go/pkg/mod \
|
||||
--mount=type=cache,target=/root/.cache/go-build \
|
||||
CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build \
|
||||
-ldflags="-s -w \
|
||||
-X github.com/warmbly/warmbly/internal/version.Version=$VERSION \
|
||||
-X github.com/warmbly/warmbly/internal/version.Commit=$COMMIT \
|
||||
-X github.com/warmbly/warmbly/internal/version.BuiltAt=$BUILT_AT" \
|
||||
-o /out/warmbly ./cmd/cli
|
||||
|
||||
FROM alpine:3.23
|
||||
|
||||
RUN apk add --no-cache ca-certificates tzdata && adduser -D -u 1000 warmbly
|
||||
|
||||
COPY --from=builder /out/warmbly /usr/local/bin/warmbly
|
||||
|
||||
# A container has no browser, so `warmbly auth login` cannot finish here.
|
||||
# WARMBLY_TOKEN is the documented way in, and the config directory is a volume
|
||||
# mount point for anyone who would rather bring their hosts.yml.
|
||||
ENV WARMBLY_CONFIG_DIR=/home/warmbly/.config/warmbly \
|
||||
WARMBLY_NO_UPDATE_CHECK=1
|
||||
|
||||
USER warmbly
|
||||
WORKDIR /home/warmbly
|
||||
|
||||
ENTRYPOINT ["warmbly"]
|
||||
CMD ["--help"]
|
||||
Executable
+180
@@ -0,0 +1,180 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Builds the `warmbly` CLI for every platform we publish, packages each one,
|
||||
# and writes the manifests the package managers read.
|
||||
#
|
||||
# Run by the release workflow and by `make cli-dist`, so a release artifact can
|
||||
# be reproduced locally byte for byte given the same VERSION and COMMIT.
|
||||
#
|
||||
# ./scripts/build-cli.sh dist
|
||||
#
|
||||
# Assets are named without the version on purpose: the install script resolves
|
||||
# https://github.com/warmbly/warmbly/releases/latest/download/warmbly_<os>_<arch>.tar.gz
|
||||
# with no GitHub API call, and the unauthenticated API's rate limit is exactly
|
||||
# what breaks a curl installer on a shared CI runner.
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
OUT=${1:-dist}
|
||||
REPO=warmbly/warmbly
|
||||
MODULE=github.com/warmbly/warmbly
|
||||
|
||||
VERSION=${VERSION:-$(git describe --tags --always --dirty 2>/dev/null || echo dev)}
|
||||
COMMIT=${COMMIT:-$(git rev-parse HEAD 2>/dev/null || echo "")}
|
||||
BUILT_AT=${BUILT_AT:-$(date -u +%Y-%m-%dT%H:%M:%SZ)}
|
||||
|
||||
# Every platform the install script and the package managers know how to ask
|
||||
# for. Keep this list and the one in site/public/cli.sh in step; the installer
|
||||
# check verifies they agree.
|
||||
PLATFORMS="darwin/amd64 darwin/arm64 linux/amd64 linux/arm64 windows/amd64 windows/arm64"
|
||||
|
||||
LDFLAGS="-s -w
|
||||
-X ${MODULE}/internal/version.Version=${VERSION}
|
||||
-X ${MODULE}/internal/version.Commit=${COMMIT}
|
||||
-X ${MODULE}/internal/version.BuiltAt=${BUILT_AT}"
|
||||
|
||||
rm -rf "$OUT"
|
||||
mkdir -p "$OUT"
|
||||
|
||||
# Completions ship inside every archive so the install script can drop them in
|
||||
# without running the binary it just downloaded, which it cannot do for a
|
||||
# cross-platform install anyway.
|
||||
stage_completions() {
|
||||
local host_bin=$1 dest=$2
|
||||
mkdir -p "$dest"
|
||||
for shell in bash zsh fish powershell; do
|
||||
"$host_bin" completion "$shell" > "$dest/warmbly.$shell" 2>/dev/null || true
|
||||
done
|
||||
}
|
||||
|
||||
echo "building warmbly ${VERSION}"
|
||||
|
||||
host_bin="$OUT/.host/warmbly"
|
||||
mkdir -p "$OUT/.host"
|
||||
# shellcheck disable=SC2086
|
||||
go build -ldflags="$LDFLAGS" -o "$host_bin" ./cmd/cli
|
||||
|
||||
completions="$OUT/.completions"
|
||||
stage_completions "$host_bin" "$completions"
|
||||
|
||||
for target in $PLATFORMS; do
|
||||
os=${target%/*}
|
||||
arch=${target#*/}
|
||||
ext=""
|
||||
if [ "$os" = "windows" ]; then ext=".exe"; fi
|
||||
|
||||
stage="$OUT/.stage/warmbly_${os}_${arch}"
|
||||
mkdir -p "$stage"
|
||||
echo " $os/$arch"
|
||||
# shellcheck disable=SC2086
|
||||
CGO_ENABLED=0 GOOS="$os" GOARCH="$arch" \
|
||||
go build -ldflags="$LDFLAGS" -o "$stage/warmbly${ext}" ./cmd/cli
|
||||
|
||||
cp LICENSE README.md "$stage/"
|
||||
cp -r "$completions" "$stage/completions"
|
||||
|
||||
if [ "$os" = "windows" ]; then
|
||||
(cd "$stage" && zip -qr "../../warmbly_${os}_${arch}.zip" .)
|
||||
else
|
||||
tar -czf "$OUT/warmbly_${os}_${arch}.tar.gz" -C "$stage" .
|
||||
fi
|
||||
done
|
||||
|
||||
rm -rf "$OUT/.stage" "$OUT/.host" "$OUT/.completions"
|
||||
|
||||
(cd "$OUT" && sha256sum warmbly_* > checksums.txt)
|
||||
echo
|
||||
cat "$OUT/checksums.txt"
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
# Package manager manifests
|
||||
#
|
||||
# Written here rather than by hand so the checksums in them can never drift
|
||||
# from the archives they describe, which is the failure mode that makes a tap
|
||||
# install fail for everyone at once.
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
sum_for() { awk -v f="$1" '$2 == f { print $1 }' "$OUT/checksums.txt"; }
|
||||
|
||||
BASE="https://github.com/${REPO}/releases/download/${VERSION}"
|
||||
|
||||
cat > "$OUT/warmbly.rb" <<EOF
|
||||
# Homebrew formula for the Warmbly CLI. Generated by scripts/build-cli.sh;
|
||||
# the release workflow pushes it to the warmbly/homebrew-tap repository.
|
||||
class Warmbly < Formula
|
||||
desc "Warmbly from the command line: campaigns, contacts, mailboxes, inbox"
|
||||
homepage "https://warmbly.com"
|
||||
version "${VERSION#v}"
|
||||
license "Apache-2.0"
|
||||
|
||||
on_macos do
|
||||
on_intel do
|
||||
url "${BASE}/warmbly_darwin_amd64.tar.gz"
|
||||
sha256 "$(sum_for warmbly_darwin_amd64.tar.gz)"
|
||||
end
|
||||
on_arm do
|
||||
url "${BASE}/warmbly_darwin_arm64.tar.gz"
|
||||
sha256 "$(sum_for warmbly_darwin_arm64.tar.gz)"
|
||||
end
|
||||
end
|
||||
|
||||
on_linux do
|
||||
on_intel do
|
||||
url "${BASE}/warmbly_linux_amd64.tar.gz"
|
||||
sha256 "$(sum_for warmbly_linux_amd64.tar.gz)"
|
||||
end
|
||||
on_arm do
|
||||
url "${BASE}/warmbly_linux_arm64.tar.gz"
|
||||
sha256 "$(sum_for warmbly_linux_arm64.tar.gz)"
|
||||
end
|
||||
end
|
||||
|
||||
def install
|
||||
bin.install "warmbly"
|
||||
bash_completion.install "completions/warmbly.bash" => "warmbly"
|
||||
zsh_completion.install "completions/warmbly.zsh" => "_warmbly"
|
||||
fish_completion.install "completions/warmbly.fish" => "warmbly.fish"
|
||||
end
|
||||
|
||||
test do
|
||||
assert_match "warmbly", shell_output("#{bin}/warmbly version")
|
||||
end
|
||||
end
|
||||
EOF
|
||||
|
||||
cat > "$OUT/warmbly.json" <<EOF
|
||||
{
|
||||
"version": "${VERSION#v}",
|
||||
"description": "Warmbly from the command line: campaigns, contacts, mailboxes, inbox",
|
||||
"homepage": "https://warmbly.com",
|
||||
"license": "Apache-2.0",
|
||||
"architecture": {
|
||||
"64bit": {
|
||||
"url": "${BASE}/warmbly_windows_amd64.zip",
|
||||
"hash": "$(sum_for warmbly_windows_amd64.zip)"
|
||||
},
|
||||
"arm64": {
|
||||
"url": "${BASE}/warmbly_windows_arm64.zip",
|
||||
"hash": "$(sum_for warmbly_windows_arm64.zip)"
|
||||
}
|
||||
},
|
||||
"bin": "warmbly.exe",
|
||||
"checkver": {
|
||||
"github": "https://github.com/${REPO}"
|
||||
},
|
||||
"autoupdate": {
|
||||
"architecture": {
|
||||
"64bit": {
|
||||
"url": "https://github.com/${REPO}/releases/download/v\$version/warmbly_windows_amd64.zip"
|
||||
},
|
||||
"arm64": {
|
||||
"url": "https://github.com/${REPO}/releases/download/v\$version/warmbly_windows_arm64.zip"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
echo
|
||||
echo "wrote $OUT/ (archives, checksums.txt, warmbly.rb, warmbly.json)"
|
||||
Executable
+182
@@ -0,0 +1,182 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Checks the CLI installer served at https://warmbly.com/cli.sh, and its
|
||||
# Windows counterpart at /cli.ps1.
|
||||
#
|
||||
# Both are served verbatim out of site/public, so this runs against the exact
|
||||
# bytes a `curl -fsSL https://warmbly.com/cli.sh | sh` executes:
|
||||
#
|
||||
# * it parses as POSIX sh, in dash and not only in bash
|
||||
# * shellcheck has nothing to say about it
|
||||
# * --help and --dry-run work with no network and no terminal, because that
|
||||
# is how someone reads it before trusting it, and --dry-run writes nothing
|
||||
# * a real install against a local mirror produces a working binary, puts it
|
||||
# on PATH and writes completions
|
||||
# * a tampered checksum stops the install rather than warning about it
|
||||
# * --uninstall removes exactly what it wrote and nothing else
|
||||
# * the platforms it will download match the ones we actually build
|
||||
# * the published checksum matches, so "download, verify, read, run" verifies
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
SCRIPT=site/public/cli.sh
|
||||
PS_SCRIPT=site/public/cli.ps1
|
||||
SUMFILE=site/public/cli.sh.sha256
|
||||
|
||||
fail() { printf '\n\033[31m✗\033[0m %s\n' "$*" >&2; exit 1; }
|
||||
pass() { printf '\033[32m✓\033[0m %s\n' "$*"; }
|
||||
|
||||
[[ -f $SCRIPT ]] || fail "$SCRIPT is missing"
|
||||
[[ -f $PS_SCRIPT ]] || fail "$PS_SCRIPT is missing"
|
||||
|
||||
# The script is executed by whatever /bin/sh is on the machine, which on Debian
|
||||
# and Ubuntu is dash. Checking with bash alone would let a bashism through to
|
||||
# exactly the hosts this is aimed at.
|
||||
if command -v dash >/dev/null 2>&1; then
|
||||
dash -n "$SCRIPT" || fail "the installer is not valid POSIX sh (dash -n)"
|
||||
pass "parses as POSIX sh"
|
||||
else
|
||||
sh -n "$SCRIPT" || fail "the installer does not parse"
|
||||
pass "parses (dash not installed; POSIX check was approximate)"
|
||||
fi
|
||||
|
||||
if command -v shellcheck >/dev/null 2>&1; then
|
||||
shellcheck -s sh "$SCRIPT" || fail "shellcheck found problems in the installer"
|
||||
pass "shellcheck clean ($(shellcheck --version | awk '/^version:/ {print $2}'))"
|
||||
else
|
||||
echo "· shellcheck not installed; skipped"
|
||||
fi
|
||||
|
||||
# --help must work before anything is set up, which is where an unbound
|
||||
# variable under set -u would otherwise hide.
|
||||
sh "$SCRIPT" --help >/dev/null || fail "--help failed"
|
||||
pass "--help works"
|
||||
|
||||
work=$(mktemp -d)
|
||||
trap 'rm -rf "$work"' EXIT
|
||||
|
||||
# --dry-run reaches its end with no network and, above all, writes nothing.
|
||||
HOME="$work/dryhome" SHELL=/bin/bash sh "$SCRIPT" --dry-run --no-color --dir "$work/dryhome/bin" >/dev/null 2>&1 \
|
||||
|| fail "--dry-run failed"
|
||||
[[ ! -e "$work/dryhome" ]] || fail "--dry-run created $work/dryhome; it must write nothing"
|
||||
pass "--dry-run runs and writes nothing"
|
||||
|
||||
# Every platform the installer will ask for has to be one we build, and the
|
||||
# other way round. A mismatch is a 404 for whoever runs it on that machine.
|
||||
script_platforms=$(sed -n 's/^PLATFORMS="\(.*\)"$/\1/p' "$SCRIPT" | tr ' ' '\n' | sort)
|
||||
build_platforms=$(sed -n 's/^PLATFORMS="\(.*\)"$/\1/p' scripts/build-cli.sh | tr ' ' '\n' | sed 's|/|_|' | grep -v '^windows' | sort)
|
||||
if [[ "$script_platforms" != "$build_platforms" ]]; then
|
||||
fail "cli.sh and scripts/build-cli.sh disagree about platforms:
|
||||
installer builds for:
|
||||
$script_platforms
|
||||
release builds:
|
||||
$build_platforms"
|
||||
fi
|
||||
pass "installer and release agree on platforms"
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
# A real install, against a mirror on disk. file:// keeps this offline, which
|
||||
# is what lets it run in CI without reaching GitHub.
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
mirror="$work/mirror"
|
||||
mkdir -p "$mirror" "$work/stage/completions"
|
||||
|
||||
# A stand-in for the real binary: the installer only needs something that runs
|
||||
# and answers `version`, and building the real CLI here would make this check
|
||||
# a minute slower for nothing.
|
||||
cat > "$work/stage/warmbly" <<'STUB'
|
||||
#!/bin/sh
|
||||
[ "${1:-}" = version ] && echo "warmbly v0.0.0-test (test)" && exit 0
|
||||
exit 0
|
||||
STUB
|
||||
chmod +x "$work/stage/warmbly"
|
||||
echo "# completions" > "$work/stage/completions/warmbly.bash"
|
||||
echo "# completions" > "$work/stage/completions/warmbly.zsh"
|
||||
echo "# completions" > "$work/stage/completions/warmbly.fish"
|
||||
cp LICENSE "$work/stage/" 2>/dev/null || echo license > "$work/stage/LICENSE"
|
||||
|
||||
host_os=$(uname -s | tr '[:upper:]' '[:lower:]')
|
||||
case "$(uname -m)" in
|
||||
x86_64|amd64) host_arch=amd64 ;;
|
||||
arm64|aarch64) host_arch=arm64 ;;
|
||||
*) host_arch=amd64 ;;
|
||||
esac
|
||||
asset="warmbly_${host_os}_${host_arch}.tar.gz"
|
||||
tar -czf "$mirror/$asset" -C "$work/stage" .
|
||||
( cd "$mirror" && sha256sum "$asset" > checksums.txt )
|
||||
|
||||
home="$work/home"
|
||||
mkdir -p "$home"
|
||||
HOME="$home" SHELL=/bin/bash sh "$SCRIPT" \
|
||||
--base-url "file://$mirror" --dir "$home/bin" --no-color >/dev/null 2>&1 \
|
||||
|| fail "installing from a local mirror failed"
|
||||
|
||||
[[ -x "$home/bin/warmbly" ]] || fail "the installer did not produce $home/bin/warmbly"
|
||||
[[ "$("$home/bin/warmbly" version)" == "warmbly v0.0.0-test (test)" ]] || fail "the installed binary does not run"
|
||||
pass "installs a working binary from a mirror"
|
||||
|
||||
grep -q 'warmbly CLI installer' "$home/.bash_profile" 2>/dev/null || grep -q 'warmbly CLI installer' "$home/.bashrc" 2>/dev/null \
|
||||
|| fail "the installer did not put the install directory on PATH"
|
||||
pass "puts the install directory on PATH"
|
||||
|
||||
[[ -f "$home/.local/share/bash-completion/completions/warmbly" ]] || fail "no bash completions were written"
|
||||
pass "writes shell completions"
|
||||
|
||||
# A second run must not append the PATH line again.
|
||||
HOME="$home" SHELL=/bin/bash sh "$SCRIPT" \
|
||||
--base-url "file://$mirror" --dir "$home/bin" --no-color >/dev/null 2>&1 \
|
||||
|| fail "the second install run failed"
|
||||
occurrences=$(grep -c 'warmbly CLI installer' "$home/.bash_profile" 2>/dev/null || true)
|
||||
[[ "${occurrences:-0}" -le 1 ]] || fail "re-running appended the PATH line again ($occurrences times)"
|
||||
pass "re-running is idempotent"
|
||||
|
||||
# A tampered archive must stop the install, not warn about it.
|
||||
bad="$work/badmirror"
|
||||
mkdir -p "$bad"
|
||||
cp "$mirror/$asset" "$bad/"
|
||||
sed 's/^[0-9a-f]\{64\}/0000000000000000000000000000000000000000000000000000000000000000/' \
|
||||
"$mirror/checksums.txt" > "$bad/checksums.txt"
|
||||
badhome="$work/badhome"
|
||||
if HOME="$badhome" sh "$SCRIPT" --base-url "file://$bad" --dir "$badhome/bin" --no-color >/dev/null 2>&1; then
|
||||
fail "a checksum mismatch did not stop the install"
|
||||
fi
|
||||
[[ ! -e "$badhome/bin/warmbly" ]] || fail "a checksum mismatch still installed the binary"
|
||||
pass "refuses to install on a checksum mismatch"
|
||||
|
||||
# --uninstall removes what it wrote, and leaves the credentials alone.
|
||||
mkdir -p "$home/.config/warmbly"
|
||||
echo "token" > "$home/.config/warmbly/hosts.yml"
|
||||
HOME="$home" SHELL=/bin/bash sh "$SCRIPT" --uninstall --dir "$home/bin" --no-color >/dev/null 2>&1 \
|
||||
|| fail "--uninstall failed"
|
||||
[[ ! -e "$home/bin/warmbly" ]] || fail "--uninstall left the binary behind"
|
||||
[[ ! -e "$home/.local/share/bash-completion/completions/warmbly" ]] || fail "--uninstall left completions behind"
|
||||
[[ -f "$home/.config/warmbly/hosts.yml" ]] || fail "--uninstall removed the credentials; it must not"
|
||||
pass "--uninstall removes the binary and completions, and keeps credentials"
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
# The Windows half. Ubuntu runners ship pwsh, so this is a real parse there.
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
if command -v pwsh >/dev/null 2>&1; then
|
||||
pwsh -NoProfile -Command "
|
||||
\$errors = \$null
|
||||
[System.Management.Automation.Language.Parser]::ParseFile('$PWD/$PS_SCRIPT', [ref]\$null, [ref]\$errors) | Out-Null
|
||||
if (\$errors) { \$errors | ForEach-Object { Write-Host \$_ }; exit 1 }
|
||||
" || fail "$PS_SCRIPT does not parse as PowerShell"
|
||||
pass "cli.ps1 parses as PowerShell"
|
||||
else
|
||||
echo "· pwsh not installed; skipped the PowerShell parse"
|
||||
fi
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
# The published checksum, which is what makes "download, verify, read, run" a
|
||||
# real alternative to piping into a shell.
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
[[ -f $SUMFILE ]] || fail "$SUMFILE is missing. Run: make cli-sha"
|
||||
( cd site/public && sha256sum -c "$(basename "$SUMFILE")" >/dev/null ) \
|
||||
|| fail "$SUMFILE does not match $SCRIPT. Run: make cli-sha"
|
||||
pass "published checksum matches"
|
||||
|
||||
printf '\n\033[32mAll CLI installer checks passed.\033[0m\n'
|
||||
@@ -0,0 +1,266 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Installs the warmbly CLI on Windows.
|
||||
|
||||
.DESCRIPTION
|
||||
One static binary, no toolchain, no admin rights. Downloads the archive for
|
||||
this machine's architecture from the GitHub release, checks it against the
|
||||
published checksum, unpacks it into a per-user directory and puts that
|
||||
directory on the user PATH.
|
||||
|
||||
Re-running it upgrades in place.
|
||||
|
||||
.EXAMPLE
|
||||
irm https://warmbly.com/cli.ps1 | iex
|
||||
|
||||
.EXAMPLE
|
||||
& ([scriptblock]::Create((irm https://warmbly.com/cli.ps1))) -Version v1.4.0
|
||||
|
||||
.EXAMPLE
|
||||
& ([scriptblock]::Create((irm https://warmbly.com/cli.ps1))) -Uninstall
|
||||
|
||||
.LINK
|
||||
https://docs.warmbly.com/api/cli/
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
# Where the binary goes. Defaults to a per-user directory so nothing here
|
||||
# needs an elevated shell.
|
||||
[string]$Dir = $env:WARMBLY_INSTALL_DIR,
|
||||
|
||||
# A release tag to pin, for example v1.4.0. Defaults to the newest release.
|
||||
[string]$Version = $env:WARMBLY_CLI_VERSION,
|
||||
|
||||
# Download from a mirror of the release assets instead of GitHub, for an
|
||||
# egress-restricted network.
|
||||
[string]$BaseUrl = $env:WARMBLY_CLI_BASE_URL,
|
||||
|
||||
# Leave the user PATH alone.
|
||||
[switch]$NoModifyPath,
|
||||
|
||||
# Print what would happen and change nothing.
|
||||
[switch]$DryRun,
|
||||
|
||||
# Remove the binary and its PATH entry.
|
||||
[switch]$Uninstall,
|
||||
|
||||
# Reinstall even when the version already matches.
|
||||
[switch]$Force
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Set-StrictMode -Version Latest
|
||||
|
||||
$Repo = 'warmbly/warmbly'
|
||||
$Releases = "https://github.com/$Repo/releases"
|
||||
$Docs = 'https://docs.warmbly.com/api/cli/'
|
||||
|
||||
function Write-Step { param($m) Write-Host "> $m" -ForegroundColor Cyan }
|
||||
function Write-Ok { param($m) Write-Host "✓ $m" -ForegroundColor Green }
|
||||
function Write-Warn { param($m) Write-Host "! $m" -ForegroundColor Yellow }
|
||||
function Write-Fail { param($m) Write-Host "✗ $m" -ForegroundColor Red; exit 1 }
|
||||
|
||||
# Windows on ARM runs amd64 binaries under emulation, but a native build is
|
||||
# published, so the architecture is read rather than assumed.
|
||||
function Get-Arch {
|
||||
$arch = $env:PROCESSOR_ARCHITECTURE
|
||||
if ($env:PROCESSOR_ARCHITEW6432) { $arch = $env:PROCESSOR_ARCHITEW6432 }
|
||||
switch ($arch) {
|
||||
'AMD64' { return 'amd64' }
|
||||
'ARM64' { return 'arm64' }
|
||||
default {
|
||||
Write-Fail @"
|
||||
No published build for $arch.
|
||||
We publish amd64 and arm64. Build from source with:
|
||||
go install github.com/$Repo/cmd/cli@latest
|
||||
"@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Get-AssetUrl {
|
||||
param($Name)
|
||||
if ($BaseUrl) { return "$($BaseUrl.TrimEnd('/'))/$Name" }
|
||||
if ($Version) { return "$Releases/download/$Version/$Name" }
|
||||
return "$Releases/latest/download/$Name"
|
||||
}
|
||||
|
||||
function Get-InstallDir {
|
||||
if ($Dir) { return $Dir }
|
||||
return (Join-Path $env:LOCALAPPDATA 'Warmbly\bin')
|
||||
}
|
||||
|
||||
# The user PATH is read from the registry rather than from $env:PATH, because
|
||||
# the session copy already has machine entries merged in and writing that back
|
||||
# would move machine-wide entries into the user scope.
|
||||
function Add-ToUserPath {
|
||||
param($Target)
|
||||
|
||||
$current = [Environment]::GetEnvironmentVariable('Path', 'User')
|
||||
if ($null -eq $current) { $current = '' }
|
||||
$entries = $current -split ';' | Where-Object { $_ -ne '' }
|
||||
|
||||
if ($entries -contains $Target) {
|
||||
Write-Ok "$Target is already on your PATH"
|
||||
return
|
||||
}
|
||||
if ($NoModifyPath) {
|
||||
Write-Warn "$Target is not on your PATH. Add it yourself, or re-run without -NoModifyPath."
|
||||
return
|
||||
}
|
||||
if ($DryRun) {
|
||||
Write-Host " would add $Target to the user PATH"
|
||||
return
|
||||
}
|
||||
|
||||
$updated = (@($entries) + $Target) -join ';'
|
||||
[Environment]::SetEnvironmentVariable('Path', $updated, 'User')
|
||||
# The registry change reaches new processes only, so this session gets the
|
||||
# entry too. Without it, the very next command in this window fails.
|
||||
$env:Path = "$env:Path;$Target"
|
||||
Write-Ok "added $Target to your PATH"
|
||||
Write-Warn 'Open a new terminal for other programs to see it.'
|
||||
}
|
||||
|
||||
function Install-Completions {
|
||||
param($Source)
|
||||
|
||||
$completion = Join-Path $Source 'completions\warmbly.powershell'
|
||||
if (-not (Test-Path $completion)) { return }
|
||||
|
||||
$profilePath = $PROFILE.CurrentUserAllHosts
|
||||
$marker = '# Added by the warmbly CLI installer'
|
||||
|
||||
if ($DryRun) {
|
||||
Write-Host " would add completions to $profilePath"
|
||||
return
|
||||
}
|
||||
if ((Test-Path $profilePath) -and (Select-String -Path $profilePath -Pattern ([regex]::Escape($marker)) -Quiet)) {
|
||||
return
|
||||
}
|
||||
|
||||
$dest = Join-Path (Get-InstallDir) 'warmbly.completion.ps1'
|
||||
Copy-Item $completion $dest -Force
|
||||
|
||||
New-Item -ItemType Directory -Force -Path (Split-Path $profilePath) | Out-Null
|
||||
Add-Content -Path $profilePath -Value "`n$marker`n. `"$dest`""
|
||||
Write-Ok "wrote completions and referenced them from $profilePath"
|
||||
}
|
||||
|
||||
function Invoke-Uninstall {
|
||||
$target = Get-InstallDir
|
||||
$exe = Join-Path $target 'warmbly.exe'
|
||||
$removed = $false
|
||||
|
||||
if (Test-Path $exe) {
|
||||
if ($DryRun) { Write-Host "would remove $exe" }
|
||||
else { Remove-Item $exe -Force; Write-Ok "removed $exe" }
|
||||
$removed = $true
|
||||
}
|
||||
|
||||
$completion = Join-Path $target 'warmbly.completion.ps1'
|
||||
if (Test-Path $completion) {
|
||||
if (-not $DryRun) { Remove-Item $completion -Force }
|
||||
$removed = $true
|
||||
}
|
||||
|
||||
if (-not $DryRun) {
|
||||
$current = [Environment]::GetEnvironmentVariable('Path', 'User')
|
||||
if ($current) {
|
||||
$kept = $current -split ';' | Where-Object { $_ -ne '' -and $_ -ne $target }
|
||||
[Environment]::SetEnvironmentVariable('Path', ($kept -join ';'), 'User')
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $removed) { Write-Warn "nothing to remove: no warmbly.exe in $target" }
|
||||
|
||||
$config = Join-Path $env:APPDATA 'warmbly'
|
||||
if (Test-Path $config) {
|
||||
Write-Host ''
|
||||
Write-Host "Your sign-ins are still in $config."
|
||||
Write-Host "Remove them with: Remove-Item -Recurse '$config'"
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-Install {
|
||||
$arch = Get-Arch
|
||||
$target = Get-InstallDir
|
||||
$asset = "warmbly_windows_$arch.zip"
|
||||
|
||||
Write-Step 'Installing the warmbly CLI'
|
||||
Write-Host " platform: windows/$arch"
|
||||
Write-Host " version: $(if ($Version) { $Version } else { 'latest' })"
|
||||
Write-Host " into: $target"
|
||||
Write-Host ''
|
||||
|
||||
$exe = Join-Path $target 'warmbly.exe'
|
||||
if ((Test-Path $exe) -and $Version -and -not $Force) {
|
||||
$current = (& $exe version 2>$null | Select-Object -First 1) -split ' ' | Select-Object -Index 1
|
||||
if ($current -eq $Version) {
|
||||
Write-Ok "warmbly $current is already installed in $target"
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if ($DryRun) {
|
||||
Write-Host "would download $(Get-AssetUrl $asset)"
|
||||
Write-Host "would verify it against $(Get-AssetUrl 'checksums.txt')"
|
||||
Write-Host "would install $exe"
|
||||
Install-Completions ''
|
||||
Add-ToUserPath $target
|
||||
return
|
||||
}
|
||||
|
||||
$tmp = Join-Path ([System.IO.Path]::GetTempPath()) ("warmbly-" + [guid]::NewGuid())
|
||||
New-Item -ItemType Directory -Force -Path $tmp | Out-Null
|
||||
try {
|
||||
Write-Step "Downloading $asset"
|
||||
$zip = Join-Path $tmp $asset
|
||||
try {
|
||||
Invoke-WebRequest -Uri (Get-AssetUrl $asset) -OutFile $zip -UseBasicParsing
|
||||
} catch {
|
||||
Write-Fail "could not download $(Get-AssetUrl $asset)`nIf you pinned -Version, check the tag exists: $Releases"
|
||||
}
|
||||
|
||||
# The checksum is why this is safer than a bare download: a truncated
|
||||
# transfer and a tampered one are indistinguishable to Expand-Archive.
|
||||
try {
|
||||
$sums = Join-Path $tmp 'checksums.txt'
|
||||
Invoke-WebRequest -Uri (Get-AssetUrl 'checksums.txt') -OutFile $sums -UseBasicParsing
|
||||
$want = (Select-String -Path $sums -Pattern ([regex]::Escape($asset)) | Select-Object -First 1).Line -split '\s+' | Select-Object -First 1
|
||||
$got = (Get-FileHash $zip -Algorithm SHA256).Hash.ToLower()
|
||||
if (-not $want) {
|
||||
Write-Warn "checksums.txt has no entry for $asset; continuing without verification"
|
||||
} elseif ($want.ToLower() -ne $got) {
|
||||
Write-Fail "checksum mismatch for $asset.`n expected $want`n got $got`nNothing was installed."
|
||||
} else {
|
||||
Write-Ok 'checksum verified'
|
||||
}
|
||||
} catch {
|
||||
Write-Warn 'could not fetch checksums.txt; continuing without verification'
|
||||
}
|
||||
|
||||
Write-Step 'Unpacking'
|
||||
$unpacked = Join-Path $tmp 'x'
|
||||
Expand-Archive -Path $zip -DestinationPath $unpacked -Force
|
||||
$source = Join-Path $unpacked 'warmbly.exe'
|
||||
if (-not (Test-Path $source)) { Write-Fail 'the archive did not contain warmbly.exe' }
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $target | Out-Null
|
||||
Copy-Item $source $exe -Force
|
||||
|
||||
$installed = (& $exe version 2>$null | Select-Object -First 1)
|
||||
Write-Ok "installed $installed to $exe"
|
||||
|
||||
Install-Completions $unpacked
|
||||
Add-ToUserPath $target
|
||||
|
||||
Write-Host ''
|
||||
Write-Host 'Next: warmbly auth login'
|
||||
Write-Host "Docs: $Docs"
|
||||
} finally {
|
||||
Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
if ($Uninstall) { Invoke-Uninstall } else { Invoke-Install }
|
||||
@@ -0,0 +1,548 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# curl -fsSL https://warmbly.com/cli.sh | sh
|
||||
#
|
||||
# Installs the `warmbly` CLI on this machine: one static binary, no Go
|
||||
# toolchain, no package manager, no root. It downloads the archive for your
|
||||
# platform from the GitHub release, checks it against the published checksum,
|
||||
# and puts the binary somewhere on your PATH.
|
||||
#
|
||||
# sh cli.sh --help every flag, and the environment variable for each
|
||||
# sh cli.sh --dry-run print exactly what it would do, touch nothing
|
||||
# sh cli.sh --uninstall remove the binary and the completions it wrote
|
||||
#
|
||||
# What it does, in full:
|
||||
#
|
||||
# * detects your OS and CPU, and stops with a real message if we publish no
|
||||
# build for it rather than downloading something that cannot run
|
||||
# * resolves the newest release (or the one you pin with --version)
|
||||
# * downloads warmbly_<os>_<arch>.tar.gz and checksums.txt, and REFUSES to
|
||||
# install if the two disagree
|
||||
# * installs to ~/.local/bin by default, which needs no sudo. Nothing else on
|
||||
# your system is touched
|
||||
# * writes shell completions, and tells you the one line to add to your shell
|
||||
# profile if the install directory is not already on PATH
|
||||
#
|
||||
# Re-running it upgrades in place and says so when there is nothing to do.
|
||||
#
|
||||
# Verify before running, if you would rather:
|
||||
#
|
||||
# curl -fsSLO https://warmbly.com/cli.sh
|
||||
# curl -fsSLO https://warmbly.com/cli.sh.sha256
|
||||
# sha256sum -c cli.sh.sha256
|
||||
# less cli.sh && sh cli.sh
|
||||
#
|
||||
# https://docs.warmbly.com/api/cli/
|
||||
|
||||
set -eu
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
# Constants
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
REPO="warmbly/warmbly"
|
||||
BIN="warmbly"
|
||||
DOCS="https://docs.warmbly.com/api/cli/"
|
||||
RELEASES="https://github.com/${REPO}/releases"
|
||||
|
||||
# Every platform scripts/build-cli.sh publishes. The two lists have to agree:
|
||||
# a platform here with no archive downloads a 404, and one missing here is a
|
||||
# build nobody can install.
|
||||
PLATFORMS="darwin_amd64 darwin_arm64 linux_amd64 linux_arm64"
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
# Options. Every one is also an environment variable, so the same install runs
|
||||
# from Ansible, cloud-init, a Dockerfile or an agent with no keyboard.
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
DIR=${WARMBLY_INSTALL_DIR:-}
|
||||
VERSION=${WARMBLY_CLI_VERSION:-}
|
||||
# Where the archives come from. Overridable so an air-gapped or
|
||||
# egress-restricted network can mirror the release assets internally and still
|
||||
# use this exact script.
|
||||
BASE_URL=${WARMBLY_CLI_BASE_URL:-}
|
||||
NO_MODIFY_PATH=${WARMBLY_NO_MODIFY_PATH:-}
|
||||
NO_COMPLETIONS=${WARMBLY_NO_COMPLETIONS:-}
|
||||
DRY_RUN=""
|
||||
UNINSTALL=""
|
||||
FORCE=""
|
||||
QUIET=""
|
||||
USE_COLOR=1
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
# Output
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
setup_colors() {
|
||||
if [ -n "$USE_COLOR" ] && [ -t 2 ] && [ "${TERM:-dumb}" != "dumb" ] && [ -z "${NO_COLOR:-}" ]; then
|
||||
C_RESET=$(printf '\033[0m')
|
||||
C_DIM=$(printf '\033[2m')
|
||||
C_BOLD=$(printf '\033[1m')
|
||||
C_RED=$(printf '\033[31m')
|
||||
C_GREEN=$(printf '\033[32m')
|
||||
C_YELLOW=$(printf '\033[33m')
|
||||
C_CYAN=$(printf '\033[36m')
|
||||
else
|
||||
C_RESET=""; C_DIM=""; C_BOLD=""; C_RED=""; C_GREEN=""; C_YELLOW=""; C_CYAN=""
|
||||
fi
|
||||
}
|
||||
|
||||
say() { [ -n "$QUIET" ] || printf '%s\n' "$*" >&2; }
|
||||
step() { [ -n "$QUIET" ] || printf '%s>%s %s\n' "$C_CYAN" "$C_RESET" "$*" >&2; }
|
||||
ok() { [ -n "$QUIET" ] || printf '%s✓%s %s\n' "$C_GREEN" "$C_RESET" "$*" >&2; }
|
||||
warn() { printf '%s!%s %s\n' "$C_YELLOW" "$C_RESET" "$*" >&2; }
|
||||
die() { printf '%s✗%s %s\n' "$C_RED" "$C_RESET" "$*" >&2; exit 1; }
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Install the warmbly CLI.
|
||||
|
||||
Usage:
|
||||
curl -fsSL https://warmbly.com/cli.sh | sh
|
||||
curl -fsSL https://warmbly.com/cli.sh | sh -s -- [flags]
|
||||
|
||||
Flags:
|
||||
--dir PATH Where to install the binary (WARMBLY_INSTALL_DIR)
|
||||
Default: \$HOME/.local/bin
|
||||
--version VERSION Install a specific release tag (WARMBLY_CLI_VERSION)
|
||||
Default: the newest release
|
||||
--base-url URL Download from a mirror of the release
|
||||
assets instead of GitHub (WARMBLY_CLI_BASE_URL)
|
||||
--no-modify-path Never touch a shell profile (WARMBLY_NO_MODIFY_PATH)
|
||||
--no-completions Do not write shell completions (WARMBLY_NO_COMPLETIONS)
|
||||
--force Reinstall even if the version already matches
|
||||
--uninstall Remove the binary and its completions
|
||||
--dry-run Print what would happen, change nothing
|
||||
--quiet Only errors
|
||||
--no-color Never colourise output
|
||||
--help This
|
||||
|
||||
Examples:
|
||||
curl -fsSL https://warmbly.com/cli.sh | sh
|
||||
curl -fsSL https://warmbly.com/cli.sh | sh -s -- --dir /usr/local/bin
|
||||
curl -fsSL https://warmbly.com/cli.sh | sh -s -- --version v1.4.0
|
||||
curl -fsSL https://warmbly.com/cli.sh | sh -s -- --uninstall
|
||||
|
||||
After installing, sign in:
|
||||
|
||||
warmbly auth login
|
||||
|
||||
Docs: ${DOCS}
|
||||
EOF
|
||||
}
|
||||
|
||||
parse_args() {
|
||||
while [ $# -gt 0 ]; do
|
||||
case $1 in
|
||||
--dir) shift; [ $# -gt 0 ] || die "--dir needs a path"; DIR=$1 ;;
|
||||
--dir=*) DIR=${1#*=} ;;
|
||||
--version) shift; [ $# -gt 0 ] || die "--version needs a release tag"; VERSION=$1 ;;
|
||||
--version=*) VERSION=${1#*=} ;;
|
||||
--base-url) shift; [ $# -gt 0 ] || die "--base-url needs a URL"; BASE_URL=$1 ;;
|
||||
--base-url=*) BASE_URL=${1#*=} ;;
|
||||
--no-modify-path) NO_MODIFY_PATH=1 ;;
|
||||
--no-completions) NO_COMPLETIONS=1 ;;
|
||||
--force) FORCE=1 ;;
|
||||
--uninstall) UNINSTALL=1 ;;
|
||||
--dry-run) DRY_RUN=1 ;;
|
||||
--quiet|-q) QUIET=1 ;;
|
||||
--no-color) USE_COLOR="" ;;
|
||||
--help|-h) usage; exit 0 ;;
|
||||
*) usage >&2; die "unknown option $1" ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
# Platform
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
detect_platform() {
|
||||
os=$(uname -s 2>/dev/null || echo unknown)
|
||||
arch=$(uname -m 2>/dev/null || echo unknown)
|
||||
|
||||
case $os in
|
||||
Linux) OS=linux ;;
|
||||
Darwin) OS=darwin ;;
|
||||
MINGW*|MSYS*|CYGWIN*)
|
||||
die "this script installs the Unix build.
|
||||
On Windows run this in PowerShell instead:
|
||||
irm https://warmbly.com/cli.ps1 | iex" ;;
|
||||
*) die "no published build for $os. Build from source with: go install github.com/${REPO}/cmd/cli@latest" ;;
|
||||
esac
|
||||
|
||||
case $arch in
|
||||
x86_64|amd64) ARCH=amd64 ;;
|
||||
arm64|aarch64) ARCH=arm64 ;;
|
||||
*) die "no published build for $arch on $OS.
|
||||
We publish amd64 and arm64. Build from source with:
|
||||
go install github.com/${REPO}/cmd/cli@latest" ;;
|
||||
esac
|
||||
|
||||
TARGET="${OS}_${ARCH}"
|
||||
for known in $PLATFORMS; do
|
||||
if [ "$known" = "$TARGET" ]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
die "no published build for $TARGET"
|
||||
}
|
||||
|
||||
# fetch writes a URL to a file. curl and wget are both accepted because a
|
||||
# minimal container image has exactly one of them and it is never the one you
|
||||
# assumed.
|
||||
fetch() {
|
||||
url=$1
|
||||
dest=$2
|
||||
if [ -n "$DOWNLOADER" ] && [ "$DOWNLOADER" = curl ]; then
|
||||
curl -fsSL --retry 3 --retry-delay 1 -o "$dest" "$url"
|
||||
else
|
||||
wget -q -O "$dest" "$url"
|
||||
fi
|
||||
}
|
||||
|
||||
require_downloader() {
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
DOWNLOADER=curl
|
||||
elif command -v wget >/dev/null 2>&1; then
|
||||
DOWNLOADER=wget
|
||||
else
|
||||
die "neither curl nor wget is installed, so there is nothing to download with"
|
||||
fi
|
||||
}
|
||||
|
||||
# sha256_of prints a file's checksum with whichever tool the host has. macOS
|
||||
# ships shasum, Linux ships sha256sum, Alpine ships both or neither.
|
||||
sha256_of() {
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
sha256sum "$1" | awk '{print $1}'
|
||||
elif command -v shasum >/dev/null 2>&1; then
|
||||
shasum -a 256 "$1" | awk '{print $1}'
|
||||
elif command -v openssl >/dev/null 2>&1; then
|
||||
openssl dgst -sha256 "$1" | awk '{print $NF}'
|
||||
else
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
# Install directory
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
# A literal tilde is what this matches: someone who typed --dir '~/bin' inside
|
||||
# quotes meant their home directory, not a folder called "~".
|
||||
# shellcheck disable=SC2088
|
||||
expand_tilde() {
|
||||
case $DIR in
|
||||
"~/"*) DIR="${HOME}/${DIR#\~/}" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# resolve_dir picks where the binary goes. ~/.local/bin is the default because
|
||||
# it needs no sudo and is on PATH by default on most modern distributions;
|
||||
# piping an installer into a shell should never need root.
|
||||
resolve_dir() {
|
||||
if [ -z "$DIR" ]; then
|
||||
DIR="${HOME:-/root}/.local/bin"
|
||||
fi
|
||||
expand_tilde
|
||||
}
|
||||
|
||||
# on_path answers whether DIR is already searched, so we only talk about shell
|
||||
# profiles when there is a real problem to solve.
|
||||
on_path() {
|
||||
case ":${PATH}:" in
|
||||
*":${DIR}:"*) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# profile_file is the file a login shell reads, chosen from $SHELL rather than
|
||||
# the shell running this script: this runs under sh no matter what the person
|
||||
# actually uses.
|
||||
profile_file() {
|
||||
shell_name=$(basename "${SHELL:-sh}")
|
||||
case $shell_name in
|
||||
zsh) printf '%s' "${ZDOTDIR:-$HOME}/.zshrc" ;;
|
||||
bash)
|
||||
if [ -f "$HOME/.bashrc" ]; then
|
||||
printf '%s' "$HOME/.bashrc"
|
||||
else
|
||||
printf '%s' "$HOME/.bash_profile"
|
||||
fi ;;
|
||||
fish) printf '%s' "$HOME/.config/fish/config.fish" ;;
|
||||
*) printf '%s' "$HOME/.profile" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# The single quotes below are the point: $PATH has to reach the profile
|
||||
# unexpanded, so it still resolves every time the shell reads it.
|
||||
# shellcheck disable=SC2016
|
||||
path_line() {
|
||||
shell_name=$(basename "${SHELL:-sh}")
|
||||
case $shell_name in
|
||||
fish) printf 'fish_add_path %s' "$DIR" ;;
|
||||
*) printf 'export PATH="%s:$PATH"' "$DIR" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ensure_on_path appends the PATH line to the right profile, once. The marker
|
||||
# comment is what makes a second run a no-op instead of a growing file.
|
||||
ensure_on_path() {
|
||||
if on_path; then
|
||||
return 0
|
||||
fi
|
||||
line=$(path_line)
|
||||
if [ -n "$NO_MODIFY_PATH" ]; then
|
||||
warn "$DIR is not on your PATH. Add this yourself:"
|
||||
say " $line"
|
||||
return 0
|
||||
fi
|
||||
|
||||
profile=$(profile_file)
|
||||
if [ -n "$DRY_RUN" ]; then
|
||||
say " would add to $profile: $line"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ -f "$profile" ] && grep -q "warmbly CLI" "$profile" 2>/dev/null; then
|
||||
ok "$profile already has the PATH line"
|
||||
else
|
||||
mkdir -p "$(dirname "$profile")"
|
||||
{
|
||||
printf '\n# Added by the warmbly CLI installer\n'
|
||||
printf '%s\n' "$line"
|
||||
} >> "$profile"
|
||||
ok "added $DIR to your PATH in $profile"
|
||||
fi
|
||||
warn "open a new terminal, or run: $line"
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
# Completions
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
# completion_dir is where the shell looks without any configuration. When there
|
||||
# is no such place we say nothing rather than writing a file that is never read.
|
||||
completion_dir() {
|
||||
shell_name=$(basename "${SHELL:-sh}")
|
||||
case $shell_name in
|
||||
bash)
|
||||
if [ -d "$HOME/.local/share/bash-completion/completions" ] || [ "$1" = create ]; then
|
||||
printf '%s' "$HOME/.local/share/bash-completion/completions/warmbly"
|
||||
fi ;;
|
||||
zsh)
|
||||
printf '%s' "${ZDOTDIR:-$HOME}/.zfunc/_warmbly" ;;
|
||||
fish)
|
||||
printf '%s' "$HOME/.config/fish/completions/warmbly.fish" ;;
|
||||
*) printf '' ;;
|
||||
esac
|
||||
}
|
||||
|
||||
install_completions() {
|
||||
if [ -n "$NO_COMPLETIONS" ]; then
|
||||
return 0
|
||||
fi
|
||||
shell_name=$(basename "${SHELL:-sh}")
|
||||
src=""
|
||||
case $shell_name in
|
||||
bash) src="$1/completions/warmbly.bash" ;;
|
||||
zsh) src="$1/completions/warmbly.zsh" ;;
|
||||
fish) src="$1/completions/warmbly.fish" ;;
|
||||
*) return 0 ;;
|
||||
esac
|
||||
|
||||
dest=$(completion_dir create)
|
||||
[ -n "$dest" ] || return 0
|
||||
|
||||
# The dry run has no unpacked archive to copy from, so it reports the
|
||||
# destination rather than testing for a source that cannot exist yet.
|
||||
if [ -n "$DRY_RUN" ]; then
|
||||
say " would write $shell_name completions to $dest"
|
||||
return 0
|
||||
fi
|
||||
[ -f "$src" ] || return 0
|
||||
mkdir -p "$(dirname "$dest")"
|
||||
cp "$src" "$dest"
|
||||
ok "wrote $shell_name completions to $dest"
|
||||
if [ "$shell_name" = zsh ]; then
|
||||
say " ${C_DIM}zsh needs ~/.zfunc on its fpath: add \`fpath+=~/.zfunc\` above compinit${C_RESET}"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
# Uninstall
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
do_uninstall() {
|
||||
resolve_dir
|
||||
target="$DIR/$BIN"
|
||||
removed=""
|
||||
|
||||
if [ -f "$target" ]; then
|
||||
if [ -n "$DRY_RUN" ]; then
|
||||
say "would remove $target"
|
||||
else
|
||||
rm -f "$target"
|
||||
ok "removed $target"
|
||||
fi
|
||||
removed=1
|
||||
fi
|
||||
|
||||
for c in "$HOME/.local/share/bash-completion/completions/warmbly" \
|
||||
"${ZDOTDIR:-$HOME}/.zfunc/_warmbly" \
|
||||
"$HOME/.config/fish/completions/warmbly.fish"; do
|
||||
if [ -f "$c" ]; then
|
||||
if [ -n "$DRY_RUN" ]; then
|
||||
say "would remove $c"
|
||||
else
|
||||
rm -f "$c"
|
||||
ok "removed $c"
|
||||
fi
|
||||
removed=1
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$removed" ]; then
|
||||
warn "nothing to remove: no warmbly found in $DIR"
|
||||
fi
|
||||
|
||||
# Deliberately left alone: it holds the credentials, and someone
|
||||
# reinstalling in a minute should not have to sign in again.
|
||||
if [ -d "${XDG_CONFIG_HOME:-$HOME/.config}/warmbly" ]; then
|
||||
say ""
|
||||
say "Your sign-ins are still in ${XDG_CONFIG_HOME:-$HOME/.config}/warmbly."
|
||||
say "Remove them with: rm -rf ${XDG_CONFIG_HOME:-$HOME/.config}/warmbly"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
# Install
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
# archive_url builds the download URL. The version-less asset names are what
|
||||
# let "latest" resolve with no GitHub API call, so the install works on a CI
|
||||
# runner whose IP has already spent the unauthenticated rate limit.
|
||||
archive_url() {
|
||||
name=$1
|
||||
if [ -n "$BASE_URL" ]; then
|
||||
printf '%s/%s' "${BASE_URL%/}" "$name"
|
||||
elif [ -n "$VERSION" ]; then
|
||||
printf '%s/download/%s/%s' "$RELEASES" "$VERSION" "$name"
|
||||
else
|
||||
printf '%s/latest/download/%s' "$RELEASES" "$name"
|
||||
fi
|
||||
}
|
||||
|
||||
installed_version() {
|
||||
if [ -x "$DIR/$BIN" ]; then
|
||||
"$DIR/$BIN" version 2>/dev/null | head -1 | awk '{print $2}'
|
||||
fi
|
||||
}
|
||||
|
||||
do_install() {
|
||||
detect_platform
|
||||
resolve_dir
|
||||
|
||||
archive="warmbly_${TARGET}.tar.gz"
|
||||
url=$(archive_url "$archive")
|
||||
sums_url=$(archive_url "checksums.txt")
|
||||
|
||||
step "Installing the warmbly CLI"
|
||||
say " platform: ${OS}/${ARCH}"
|
||||
say " version: ${VERSION:-latest}"
|
||||
say " into: ${DIR}"
|
||||
say ""
|
||||
|
||||
current=$(installed_version)
|
||||
if [ -n "$current" ] && [ -z "$FORCE" ] && [ -n "$VERSION" ] && [ "$current" = "$VERSION" ]; then
|
||||
ok "warmbly $current is already installed in $DIR"
|
||||
say " ${C_DIM}--force reinstalls it anyway${C_RESET}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ -n "$DRY_RUN" ]; then
|
||||
say "would download $url"
|
||||
say "would verify it against $sums_url"
|
||||
say "would install $DIR/$BIN"
|
||||
install_completions "" || true
|
||||
ensure_on_path
|
||||
return 0
|
||||
fi
|
||||
|
||||
tmp=$(mktemp -d 2>/dev/null || mktemp -d -t warmbly)
|
||||
# The trap is set before the first write, so an interrupted install leaves
|
||||
# nothing behind in /tmp.
|
||||
trap 'rm -rf "$tmp"' EXIT INT TERM
|
||||
|
||||
step "Downloading $archive"
|
||||
if ! fetch "$url" "$tmp/$archive"; then
|
||||
die "could not download $url
|
||||
If you pinned --version, check the tag exists: ${RELEASES}"
|
||||
fi
|
||||
|
||||
# The checksum is the whole reason this is safer than a bare curl into tar:
|
||||
# a truncated download and a tampered one look the same to tar.
|
||||
if fetch "$sums_url" "$tmp/checksums.txt" 2>/dev/null; then
|
||||
want=$(awk -v f="$archive" '$2 == f || $2 == "*"f { print $1 }' "$tmp/checksums.txt" | head -1)
|
||||
got=$(sha256_of "$tmp/$archive")
|
||||
if [ -z "$want" ]; then
|
||||
warn "checksums.txt has no entry for $archive; continuing without verification"
|
||||
elif [ -z "$got" ]; then
|
||||
warn "no sha256 tool on this machine, so the download was not verified"
|
||||
elif [ "$want" != "$got" ]; then
|
||||
die "checksum mismatch for $archive.
|
||||
expected $want
|
||||
got $got
|
||||
Nothing was installed. Try again, and if it happens twice report it: ${RELEASES}"
|
||||
else
|
||||
ok "checksum verified"
|
||||
fi
|
||||
else
|
||||
warn "could not fetch checksums.txt; continuing without verification"
|
||||
fi
|
||||
|
||||
step "Unpacking"
|
||||
mkdir -p "$tmp/x"
|
||||
tar -xzf "$tmp/$archive" -C "$tmp/x" || die "the archive could not be unpacked"
|
||||
[ -f "$tmp/x/$BIN" ] || die "the archive did not contain $BIN"
|
||||
|
||||
mkdir -p "$DIR" 2>/dev/null || die "could not create $DIR.
|
||||
Pick somewhere writable with --dir, for example: --dir \$HOME/bin"
|
||||
|
||||
# install(1) is not on every minimal image, so this is cp plus chmod, done
|
||||
# to a temporary name and moved into place: replacing a running binary with
|
||||
# a rename is atomic, overwriting one in place is not.
|
||||
cp "$tmp/x/$BIN" "$DIR/.$BIN.new" || die "could not write to $DIR.
|
||||
Pick somewhere writable with --dir, or re-run with sudo if $DIR is system-owned."
|
||||
chmod 0755 "$DIR/.$BIN.new"
|
||||
mv -f "$DIR/.$BIN.new" "$DIR/$BIN"
|
||||
|
||||
version_now=$("$DIR/$BIN" version 2>/dev/null | head -1 || echo "")
|
||||
ok "installed ${version_now:-warmbly} to $DIR/$BIN"
|
||||
|
||||
install_completions "$tmp/x" || true
|
||||
ensure_on_path
|
||||
|
||||
say ""
|
||||
say "${C_BOLD}Next:${C_RESET} warmbly auth login"
|
||||
say "${C_DIM}Docs: ${DOCS}${C_RESET}"
|
||||
return 0
|
||||
}
|
||||
|
||||
main() {
|
||||
parse_args "$@"
|
||||
setup_colors
|
||||
require_downloader
|
||||
|
||||
if [ -n "$UNINSTALL" ]; then
|
||||
do_uninstall
|
||||
return 0
|
||||
fi
|
||||
do_install
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -0,0 +1 @@
|
||||
30e5c97559826b3f10f70b4d5efd2b175120b9d98e0472069824d6aecfa924bf cli.sh
|
||||
Reference in New Issue
Block a user