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
122 changes: 122 additions & 0 deletions .github/workflows/install-script.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
name: Install Script

# Exercises install.sh (the `curl … | sh` Linux installer) against the real
# GitHub Releases on every change to it, and weekly so a change to the
# release assets (names, archive layout, checksums.txt) that breaks the
# installer is caught without waiting for a user report.

on:
pull_request:
paths:
- install.sh
- .github/workflows/install-script.yml
push:
branches:
- main
paths:
- install.sh
- .github/workflows/install-script.yml
schedule:
- cron: "0 6 * * 1"
workflow_dispatch:
Comment thread
spark2ignite marked this conversation as resolved.

permissions:
contents: read

jobs:
shellcheck:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Shellcheck install.sh
run: shellcheck -s sh install.sh

install:
strategy:
fail-fast: false
matrix:
runner: [ubuntu-latest, ubuntu-24.04-arm]
image: ["ubuntu:24.04", "debian:12", "fedora:41"]
runs-on: ${{ matrix.runner }}

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Resolve latest release
id: latest
env:
GH_TOKEN: ${{ github.token }}
run: echo "version=$(gh release view --repo "$GITHUB_REPOSITORY" --json tagName -q .tagName | sed 's/^v//')" >> "$GITHUB_OUTPUT"

# Inside a stock container, as a non-root user: the default install
# goes to ~/.local/bin with completions, a pinned version is honored,
# and --system installs the distro package.
- name: Install in ${{ matrix.image }}
env:
IMAGE: ${{ matrix.image }}
LATEST: ${{ steps.latest.outputs.version }}
run: |
docker run --rm -e LATEST -v "$PWD/install.sh:/install.sh:ro" "$IMAGE" sh -euc '
if command -v apt-get >/dev/null; then
apt-get update -qq && apt-get install -y -qq curl ca-certificates >/dev/null
else
dnf install -y -q util-linux shadow-utils >/dev/null # su, useradd
fi
useradd -m tester

su tester -c "sh /install.sh"
su tester -c "~/.local/bin/dci --version" | grep -F "version $LATEST"
su tester -c "test -f ~/.local/share/bash-completion/completions/dci && test -f ~/.config/fish/completions/dci.fish && test -f ~/.local/share/zsh/site-functions/_dci"

su tester -c "sh /install.sh --version 2.7.5"
su tester -c "~/.local/bin/dci --version" | grep -F "version 2.7.5"

sh /install.sh --system
/usr/bin/dci --version | grep -F "version $LATEST"
'

edge-systems:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Alpine gets a clear glibc error
run: |
set +e
out=$(docker run --rm -v "$PWD/install.sh:/install.sh:ro" alpine:3.20 sh -c 'apk add -q curl && sh /install.sh' 2>&1)
status=$?
set -e
echo "$out"
test "$status" -ne 0
echo "$out" | grep -F "dci needs glibc"

# A musl loader on a glibc host (Debian's musl-tools) must not trip the
# glibc check.
- name: Debian with musl-tools installs
run: |
docker run --rm -v "$PWD/install.sh:/install.sh:ro" debian:12 sh -euc '
apt-get update -qq && apt-get install -y -qq curl ca-certificates musl-tools >/dev/null
ls /lib/ld-musl-*
sh /install.sh
~/.local/bin/dci --version
'

# Too-old glibc: a clear error naming the floor, and an existing dci is
# left in place rather than replaced by a binary that cannot start.
- name: Old glibc keeps the existing dci
run: |
docker run --rm -v "$PWD/install.sh:/install.sh:ro" ubuntu:20.04 sh -euc '
apt-get update -qq && apt-get install -y -qq curl ca-certificates >/dev/null
mkdir -p ~/.local/bin && printf "#!/bin/sh\necho old dci\n" > ~/.local/bin/dci && chmod +x ~/.local/bin/dci
if out=$(sh /install.sh 2>&1); then echo "$out"; echo "install unexpectedly succeeded"; exit 1; fi
echo "$out"
echo "$out" | grep -F "needs glibc 2.34 or newer"
test "$(~/.local/bin/dci)" = "old dci"
test -z "$(ls -A ~/.local/bin | grep -v "^dci$")"
'
4 changes: 4 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ jobs:
run: |
gh workflow run "Sync Package Manifests" --field tag="$RELEASE_TAG"
gh workflow run "Post-release Verify" --field tag="$RELEASE_TAG"
# install.sh reads the latest release's asset names and archive
# layout; GITHUB_TOKEN-published releases trigger no workflows, so
# run its live test against the new release explicitly.
gh workflow run "Install Script"

# Help Center changelog delivery (CHANGELOG-SPEC.md §7): hand the tag to
# doiteng/scribe, whose deterministic job renders CHANGELOG.md into the
Expand Down
38 changes: 38 additions & 0 deletions DISTRIBUTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ This document covers how `dci` releases are built, published, and distributed ac
| Channel | Install command |
|------------------|---------------------------------------------------------------------------------|
| GitHub Releases | Download from [Releases](https://github.com/doitintl/dci-cli/releases/latest) |
| Linux installer | `curl -fsSL https://raw.githubusercontent.com/doitintl/dci-cli/main/install.sh \| sh` |
| Homebrew (macOS) | `brew install doitintl/dci-cli/dci` |
| Scoop (Windows) | `scoop bucket add doitintl https://github.com/doitintl/dci-cli && scoop install dci` |
| WinGet (Windows) | `winget install DoiT.dci` |
Expand All @@ -28,6 +29,7 @@ This document covers how `dci` releases are built, published, and distributed ac
- **`release.yml`** — builds binaries, archives, `.deb`/`.rpm` packages, and publishes a GitHub Release.
- **`sync-manifests.yml`** — renders and commits Homebrew and Scoop manifests to this repo, and submits a PR to [microsoft/winget-pkgs](https://github.com/microsoft/winget-pkgs) for WinGet.
- **`post-release-verify.yml`** — smoke-tests Homebrew, `.deb`, `.rpm`, and Windows zip installs.
- **`install-script.yml`** (not tag-triggered) — runs `install.sh` against the live releases on every change to it and weekly; see [Linux installer](#linux-installer).

## Release Artifacts

Expand Down Expand Up @@ -62,13 +64,49 @@ Shell completions:
- `post-release-verify.yml` asserts the completion files land in the Homebrew,
`.deb`, `.rpm`, and Windows zip installs.

## Linux Installer

`install.sh` at the repo root is the one-command Linux install, served straight
from `main` via raw.githubusercontent.com. It is not part of the release build:
it reads whatever the latest release publishes.

- **Version and checksum come from `checksums.txt`.** With no `--version`, the
script downloads `releases/latest/download/checksums.txt` and takes the
version from the archive name in it, so it never calls the GitHub API (whose
unauthenticated limit shared Cloud Shell and CI IPs hit). A checksum mismatch
or a missing checksum tool is a hard failure.
- **It depends on the release asset names** `dci_<version>_linux_<arch>.tar.gz`
/ `.deb` / `.rpm` and on the archive layout (`dci` at the root,
`completions/`). Renaming either in `.goreleaser.yaml` breaks the installer;
`install-script.yml` catches that weekly at the latest.
- **Default install is `~/.local/bin`** (no root; the only location that
survives a Cloud Shell restart). `dci update` classifies it as a standalone
install and replaces it in place. `--system` installs the `.deb`/`.rpm`
instead, which `dci update` then upgrades through the package.
- **glibc only.** The binary links against glibc (2.34+, the newest `GLIBC_`
symbol version in the cgo build), so the script refuses systems without
glibc (Alpine) up front, detecting glibc by its presence rather than by a
musl loader (Debian's `musl-tools` installs one on glibc hosts). It also
runs the staged binary before replacing anything, so a too-old glibc gets
a clear error and keeps the existing `dci`. If the build image's glibc
moves, update `GLIBC_HINT` in the script and the README.
- **Re-tested after every release.** `release.yml` dispatches
`install-script.yml` alongside the other post-release workflows (a
`GITHUB_TOKEN`-published release triggers none on its own).
- **Truncation-safe.** All logic is in functions and `main "$@"` is the last
line, so a partial download of the script cannot run.
- Changes must pass `shellcheck -s sh install.sh` (POSIX sh: it runs under
dash on Debian/Ubuntu).

## Repository Layout (Distribution Files)

```
.goreleaser.yaml # GoReleaser v2 build config
.github/workflows/release.yml # Tag-triggered release
.github/workflows/sync-manifests.yml
.github/workflows/post-release-verify.yml
.github/workflows/install-script.yml # Tests install.sh against live releases
install.sh # One-command Linux installer (served from main)
packaging/render.sh # Template renderer for manifests
packaging/completions.sh # Generates shell completion scripts
packaging/homebrew/dci.rb.tmpl
Expand Down
27 changes: 22 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
## Installation

```bash
# macOS (Homebrew)
# Linux (glibc 2.34+: Ubuntu 22.04+, Debian 12+, RHEL 9+, Fedora, Cloud Shell, WSL)
curl -fsSL https://raw.githubusercontent.com/doitintl/dci-cli/main/install.sh | sh

# macOS (Homebrew; also works with Homebrew on Linux)
brew install doitintl/dci-cli/dci

# Windows (WinGet)
Expand All @@ -24,6 +27,19 @@ sudo dpkg -i dci_*_linux_amd64.deb
sudo rpm -i dci_*_linux_amd64.rpm
```

The Linux install command downloads the release for your machine, verifies its checksum, and installs `dci` to `~/.local/bin` with tab completion for bash, zsh, and fish. It needs no `sudo`, and it is the right choice for Google Cloud Shell, which keeps only your home directory when the session restarts. Options go after `sh -s --`:

```bash
# A specific version
curl -fsSL https://raw.githubusercontent.com/doitintl/dci-cli/main/install.sh | sh -s -- --version 2.8.0

# A different directory
curl -fsSL https://raw.githubusercontent.com/doitintl/dci-cli/main/install.sh | sh -s -- --dir ~/bin

# The .deb/.rpm package, system-wide (uses sudo)
curl -fsSL https://raw.githubusercontent.com/doitintl/dci-cli/main/install.sh | sh -s -- --system
```

Prebuilt binaries for all platforms are available on the [Releases](https://github.com/doitintl/dci-cli/releases/latest) page.

## Getting Started
Expand Down Expand Up @@ -147,9 +163,10 @@ DCI_NO_RESOLVE=1 dci ... # turn name resolution off entirely (for scripts

## Shell Completion

Tab completion for commands and flags is installed automatically by Homebrew
and the `.deb`/`.rpm` packages (bash, zsh, and fish). The release archives also
ship the scripts under `completions/`.
Tab completion for commands and flags is installed automatically by Homebrew,
the `.deb`/`.rpm` packages, and the Linux install command (bash, zsh, and fish;
for zsh, the install command prints the one `fpath` line to add to `~/.zshrc`).
The release archives also ship the scripts under `completions/`.

On Windows (WinGet or Scoop), add this line to your PowerShell profile:

Expand Down Expand Up @@ -333,7 +350,7 @@ Run `dci update` (alias: `dci upgrade`) to update in place on any OS. It knows h

- **Homebrew / Scoop / WinGet** installs run the package manager's own upgrade command for you.
- **`.deb`/`.rpm`** installs download the new package, verify its checksum, and run the install step with `sudo` (you'll be asked for your password).
- **Standalone binaries** (tarball, `~/bin`, CI) are replaced directly after validating the release checksum.
- **Standalone binaries** (the Linux install command, tarball, `~/bin`, CI) are replaced directly after validating the release checksum.

Useful flags: `--check` only reports whether an update is available, `--yes` skips the confirmation, and `--version vX.Y.Z` pins or rolls back a standalone install to a specific release. In agent mode the command never installs without `--yes` and reports a JSON result.

Expand Down
Loading
Loading