Skip to content

feat: install scripts, Linux packages, a container image and automated releases - #2

Merged
fuzcap merged 7 commits into
mainfrom
feat/install-and-packaging
Nov 9, 2026
Merged

fuzcap merged 7 commits into
mainfrom
feat/install-and-packaging

Conversation

@tomflenner

@tomflenner backtrack5r3 (tomflenner) commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

What does this PR change?

Everything needed to install kaiten with one command on each platform, and to package every release:

  • install/install.sh (Linux, macOS) and install/install.ps1 (Windows): detect OS and architecture, download the release archive, verify its SHA-256 against the release's checksums.txt, install the binary. Both accept a version and an install directory; the shell script uses sudo only when the target directory actually needs it, and the PowerShell one installs under %LOCALAPPDATA%\Programs\kaiten and adds it to the user PATH, no administrator rights.
  • GoReleaser (.goreleaser.yaml) now also produces: .deb, .rpm and .apk packages (binary, completions, LICENSE and NOTICE in each format's convention); a multi-platform ghcr.io/kaitencloud/cli image on a distroless non-root base. The archives now carry shell completions too.
  • Archives and packages are named kaiten_<os>_<arch>.* without the version, so releases/latest/download/<asset> is a stable URL for all twelve of them. The install scripts rely on that.
  • Automated releases with release-please, modeled on open-feature/cli. release.yml now runs on every push to main: release-please keeps one chore: release X.Y.Z PR open with the next semver version (computed from Conventional Commits) and the CHANGELOG.md entry. Merging that PR tags vX.Y.Z and creates the GitHub release, and the goreleaser job in the same run attaches the archives, packages and the GHCR image, plus SLSA build-provenance attestations. The hand-pushed tag trigger is gone; workflow_dispatch re-publishes an existing tag if GoReleaser fails after tagging. Pre-1.0: feat/breaking → minor, fix → patch, first release 0.1.0.
  • pr-title.yml: a PR title check (Conventional Commits) on pull_request_target, like DCO, since the squash-merge title is what release-please reads. CONTRIBUTING and the PR template say what each type releases.
  • ci.yml (lint job): goreleaser check, shellcheck on the shell installer and a PowerShell parse of the Windows one, so a broken installer or release config fails the PR rather than the tag.
  • Taskfile.yml: task release:snapshot builds the whole release into dist/ without a tag; task release:check runs the same checks as CI (GoReleaser, shellcheck, PowerShell parse).
  • README: an Installation section per platform (macOS, Linux, Windows, container, Go, binaries, pinning a version, shell completion) and a Releasing subsection describing the release-please flow and its one-time setup.

Before merging: one-time setup

  1. Create a GitHub App (org-owned), install it on kaitencloud/cli with Contents, Pull requests and Issues read/write. Put its ID in the Actions variable RELEASE_APP_ID and its private key in the Actions secret RELEASE_APP_PRIVATE_KEY. Without it the release PR would be opened by GITHUB_TOKEN, which starts no workflow, so the required checks would never run on it.
  2. Merge this PR with squash, so the changelog reads its title rather than the individual commits.
  3. After the first release: make the cli GHCR package public. Keep immutable releases off (GoReleaser attaches assets to the release release-please already published).

Recommended, not required: add PR title to the ruleset's required checks, restrict merges to squash, and add a tag ruleset on v* that only the release App can create.

Modeled on dapr/cli (per-OS install scripts, "latest release" resolution) and open-feature/cli (container image, checksum verification in the script).

Why?

A public CLI is only as usable as its install path. Until now the options were go install or unpacking an archive by hand.

Testing

  • goreleaser check (v2.12.7, the pinned version): clean, no deprecation warnings (uses dockers_v2, not the phased-out dockers).
  • goreleaser release --snapshot locally: all six binaries, archives, .deb/.rpm/.apk and both container images build. Verified with dpkg -c, rpm -qlp/rpm -qLp and tar -t that every package carries the binary, the completions, LICENSE and NOTICE; ran the arm64 image.
  • install/install.sh against the snapshot served over local HTTP: installs and prints the version; creates a missing nested install directory as the user, not root; refuses a tampered checksum and a missing checksum entry without writing anything; builds the right GitHub URL for a pinned version.
  • install/install.ps1 under pwsh on macOS against the same server: downloads, verifies, extracts and places kaiten.exe; refuses a tampered checksum before extracting; rejects an unsupported architecture. (Running the .exe is the one step that cannot be exercised off Windows.)
  • shellcheck --shell=sh, PowerShell parser, go build, go vet, go test, golangci-lint: all clean.
  • After the rebase on main and the release-please changes: task release:check passes (and fails on a deliberately broken install.ps1); goreleaser release --snapshot --skip=publish,docker builds all 12 assets with changelog.disable and release.mode: append; actionlint is clean on release.yml and pr-title.yml. release-please itself can only run once this is on main: the first push opens the chore: release 0.1.0 PR.

Not in this PR

  • Homebrew and Scoop were in the first revision and are dropped for now (second and third commits): neither kaitencloud/homebrew-tap nor kaitencloud/scoop-bucket exists yet. The removed blocks are in commit e417a77 if they come back. Publishing still needs nothing beyond GITHUB_TOKEN; only release-please uses the App token.
  • No release yet, so releases/latest/download/… returns 404 until the first release PR is merged.

Checklist

  • I have read CONTRIBUTING.md.
  • Every commit in this PR includes a valid DCO Signed-off-by line.
  • I have the right to submit all material in this PR.
  • I have not included secrets or confidential data.
  • I have updated tests where appropriate.
  • I have updated documentation where appropriate.
  • I have preserved required third-party licenses and attributions.

🤖 Generated with Claude Code

@Alexkuva
Alex (Alexkuva) self-requested a review October 1, 2026 11:12
Alexkuva
Alex (Alexkuva) previously approved these changes Oct 1, 2026
@tomflenner
backtrack5r3 (tomflenner) requested a review from a team as a code owner October 3, 2026 09:22
@tomflenner backtrack5r3 (tomflenner) changed the title feat: install scripts, Linux packages, Homebrew, Scoop and a container image feat: install scripts, Linux packages, Scoop and a container image Oct 3, 2026
@tomflenner backtrack5r3 (tomflenner) changed the title feat: install scripts, Linux packages, Scoop and a container image feat: install scripts, Linux packages and a container image Oct 3, 2026
…r image

install/install.sh (Linux, macOS) and install/install.ps1 (Windows) fetch
the release archive for the host's platform, verify it against the release's
checksums.txt and install the binary; both take a version and an install
directory, and the shell script uses sudo only when the target needs it.

GoReleaser now also produces .deb, .rpm and .apk packages with shell
completions, a Homebrew cask and a Scoop manifest for kaitencloud/homebrew-tap
and kaitencloud/scoop-bucket, and a multi-platform ghcr.io/kaitencloud/cli
image on a distroless base. Package and archive names carry no version, so
releases/latest/download/<asset> is a stable URL for every one of them.

The cask and the manifest are pushed only when HOMEBREW_TAP_GITHUB_TOKEN is
set and the tag is not a pre-release; otherwise they are left in dist/. CI
validates the GoReleaser configuration and both install scripts on every
pull request, and `task release:snapshot` builds the whole release locally.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: tomflenner <flennertom@gmail.com>
The tap does not exist yet, so the cask could not be published anyway. The
token the release passes for package-manager repositories is now named for
its one remaining user, SCOOP_BUCKET_GITHUB_TOKEN.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: tomflenner <flennertom@gmail.com>
Same reason as the Homebrew cask: the bucket does not exist yet. With it goes
the only token the release workflow needed beyond GITHUB_TOKEN.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: tomflenner <flennertom@gmail.com>
Every merge to main now runs release-please, which keeps one "chore: release
X.Y.Z" pull request open with the next version, computed from the Conventional
Commit messages since the last release, and the CHANGELOG.md entry. Merging it
tags vX.Y.Z and creates the GitHub release; the goreleaser job in the same
workflow then builds and attaches the archives, packages and image. Nobody
pushes a tag by hand any more, so the tag trigger goes.

release-please acts as a GitHub App (RELEASE_APP_ID, RELEASE_APP_PRIVATE_KEY):
a pull request opened with GITHUB_TOKEN starts no workflow, and the release pull
request has to pass the required checks like any other. GoReleaser keeps using
GITHUB_TOKEN.

GoReleaser appends its install footer to the notes release-please wrote
instead of generating a second changelog. Every archive, package and the
image get a build-provenance attestation. workflow_dispatch rebuilds an
existing tag if the goreleaser job fails after tagging.

Pre-1.0, feat and breaking changes bump the minor, fix the patch; the first
release is 0.1.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: tomflenner <flennertom@gmail.com>
release-please reads the commits that land on main, and a squash merge makes
the pull request title the commit message. The PR title check runs on
pull_request_target, like the DCO check, and checks no code out.
CONTRIBUTING.md and the pull request template say what each type releases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: tomflenner <flennertom@gmail.com>
README's Releasing section covers the release pull request, how commit types
map to versions, re-running a failed publish, and the maintainer setup: the
GitHub App, making the GHCR package public, keeping immutable releases off.
task release:check now parses install.ps1 as CI does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: tomflenner <flennertom@gmail.com>
@tomflenner backtrack5r3 (tomflenner) changed the title feat: install scripts, Linux packages and a container image feat: install scripts, Linux packages, a container image and automated releases Oct 13, 2026
@fuzcap
fuzcap merged commit e723f6b into main Nov 9, 2026
7 checks passed
@fuzcap
fuzcap deleted the feat/install-and-packaging branch November 9, 2026 22:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants