Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
3f362d3
feat: add bootstrap installer
Feb 6, 2026
1874407
docs: clarify quick install branch ref
Feb 6, 2026
2fe39da
docs: fix quick install branch example
Feb 6, 2026
fd3c7af
chore: update maintainer email
Feb 6, 2026
dfd308f
Initial plan (#5)
Copilot Feb 6, 2026
cca8a1d
Fix curl error handling and revert maintainer emails to project addre…
Copilot Feb 8, 2026
bd62efa
Initial plan (#7)
Copilot Feb 8, 2026
20e7869
Initial plan (#8)
Copilot Feb 8, 2026
3c7547e
docs: prioritize pinned version install for supply-chain security (#10)
Copilot Feb 8, 2026
06b639f
Require explicit ref in install.sh for supply-chain security (#11)
Copilot Feb 8, 2026
0098c8e
Add SHA256 verification to bootstrap installer (#12)
Copilot Feb 8, 2026
410c628
Revert maintainer emails to project address in packaging metadata (#13)
Copilot Feb 8, 2026
50960a3
Update install.sh
senet Feb 8, 2026
6673047
Update install.sh
senet Feb 8, 2026
78d4ca7
Add macOS compatibility for SHA256 verification in installer (#14)
Copilot Feb 8, 2026
934ccee
Update scripts/release.sh
senet Feb 8, 2026
e49f322
Update README.md
senet Feb 8, 2026
24f8373
Update README.md
senet Feb 8, 2026
eeb2322
Update CONTRIBUTING.md
senet Feb 8, 2026
ddc9def
Update install.sh
senet Feb 8, 2026
ff40a18
Update install.sh
senet Feb 8, 2026
c966ba6
Update README.md
senet Feb 8, 2026
8fb34e7
Update libexec/kctl-env-list-remote
senet Feb 8, 2026
518570d
Update scripts/release.sh
senet Feb 8, 2026
17d9280
Update README.md
senet Feb 8, 2026
af8be08
Update install.sh
senet Feb 8, 2026
517c844
fix: robust checksum parsing in installer
Feb 8, 2026
3642d1f
chore: make install.sh executable
Feb 8, 2026
0d4aa37
docs: adjust installer and release guidance
Feb 8, 2026
a4ae27b
Update install.sh
senet Feb 8, 2026
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

All notable changes to this project will be documented in this file. This project adheres to Semantic Versioning.

## [Unreleased]
### Added
- `install.sh` bootstrap installer (installs kctl-env into `~/.kctl-env` or `$KCTL_ENV_ROOT`)
- README “Quick install” section

## [v0.1.0] - 2026-01-07
### Added
- Initial scaffold: bin/kubectl shim with env/.kubectl-version/global/auto resolution and TTL cache
Expand All @@ -15,3 +20,5 @@ All notable changes to this project will be documented in this file. This projec
- Git repository initialized, branch protections configured (squash/rebase, linear history, delete branch on merge)

[v0.1.0]: https://github.com/senet/kctl-env/releases/tag/v0.1.0

[Unreleased]: https://github.com/senet/kctl-env/compare/v0.1.0...HEAD
16 changes: 16 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,22 @@
git tag -a v0.1.1 -m "v0.1.1 release"
git push origin v0.1.1
```
5. Generate and publish checksums for the release:
```sh
# Generate SHA256 checksum for the source tarball (Linux / GNU coreutils)
curl -fsSL https://github.com/senet/kctl-env/archive/refs/tags/v0.1.1.tar.gz | sha256sum > v0.1.1.tar.gz.sha256
# On macOS (BSD userland), use shasum instead:
# curl -fsSL https://github.com/senet/kctl-env/archive/refs/tags/v0.1.1.tar.gz | shasum -a 256 > v0.1.1.tar.gz.sha256

# Create the GitHub release and attach the checksum asset
# Note: this repo uses immutable releases, so you must attach assets at
# creation time (you can't upload them later).
# Note: GitHub automatically provides the source tarball at:
# https://github.com/senet/kctl-env/archive/refs/tags/v0.1.1.tar.gz
# We only need to upload the checksum file as an additional asset.
gh release create v0.1.1 --title "v0.1.1" --notes "Release v0.1.1" v0.1.1.tar.gz.sha256
```
This enables secure installation with SHA256 verification.

## Branch protections (recommended)

Expand Down
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,29 @@ Pure Bash, zero-deps kubectl version manager with fast shims and tfenv-style UX.

## Installation

### Quick install (recommended)

Install from the `main` branch (recommended until the next tagged release that includes `install.sh`):

```sh
curl -fsSL https://raw.githubusercontent.com/senet/kctl-env/main/install.sh | bash -s -- main
```
Comment thread
senet marked this conversation as resolved.

Pin a specific version (recommended for reproducibility and supply-chain security after the next release):

```sh
curl -fsSL https://raw.githubusercontent.com/senet/kctl-env/vX.Y.Z/install.sh | bash -s -- vX.Y.Z
```

If you're testing from a feature branch, replace `main` in the URL (URL-encoding the branch name if it contains `/`) and pass the branch name as the ref:

```sh
curl -fsSL https://raw.githubusercontent.com/senet/kctl-env/feat%2Feasy-install/install.sh | bash -s -- feat/easy-install
```


**Security note**: Tagged releases (v*) require SHA256 checksum verification by default and will fail if the checksum asset is missing, unless you explicitly set `KCTL_ENV_SKIP_VERIFY=1` to skip verification. Branch installations skip verification as GitHub does not provide checksums for auto-generated archives.

1. Clone or extract kctl-env to a directory (e.g., `~/.kctl-env`).
2. Add the `bin/` directory to your `PATH`:

Expand Down
5 changes: 4 additions & 1 deletion bin/kctl-env
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
#!/usr/bin/env bash
set -euo pipefail

KCTL_ENV_ROOT="${KCTL_ENV_ROOT:-$HOME/.kctl-env}"
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
default_root="$(cd -- "$script_dir/.." && pwd)"

KCTL_ENV_ROOT="${KCTL_ENV_ROOT:-$default_root}"
LIBEXEC="$KCTL_ENV_ROOT/libexec"

usage() {
Expand Down
5 changes: 4 additions & 1 deletion bin/kubectl
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
set -euo pipefail

# --- Config ---
KCTL_ENV_ROOT="${KCTL_ENV_ROOT:-$HOME/.kctl-env}"
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
default_root="$(cd -- "$script_dir/.." && pwd)"

KCTL_ENV_ROOT="${KCTL_ENV_ROOT:-$default_root}"
KCTL_CACHE="$KCTL_ENV_ROOT/cache"
KCTL_VERSIONS="$KCTL_ENV_ROOT/versions"
KCTL_GLOBAL_VERSION_FILE="$KCTL_ENV_ROOT/version"
Expand Down
211 changes: 211 additions & 0 deletions install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
#!/usr/bin/env bash
# kctl-env bootstrap installer
# - Requires standard Unix tools: bash, curl, tar, coreutils (incl. install, mktemp, head, basename), find, awk
# - SHA256 verification uses sha256sum (Linux), shasum (macOS), or openssl (fallback)
# - Installs into $KCTL_ENV_ROOT (default: ~/.kctl-env)
# - Preserves existing runtime dirs (versions/, cache/)

set -euo pipefail

REPO_OWNER="senet"
REPO_NAME="kctl-env"

KCTL_ENV_ROOT="${KCTL_ENV_ROOT:-$HOME/.kctl-env}"
KCTL_ENV_REF="${KCTL_ENV_REF:-}"
KCTL_ENV_SKIP_VERIFY="${KCTL_ENV_SKIP_VERIFY:-}"

usage() {
cat <<EOF
kctl-env installer

Usage:
./install.sh <ref>

Arguments:
ref Git ref to install (required; tag like v0.1.1, or branch like main)

Environment:
KCTL_ENV_ROOT Install root (default: ~/.kctl-env)
KCTL_ENV_REF Alternative to passing ref as argument (required if ref not provided)

Examples:
./install.sh v0.1.1
KCTL_ENV_ROOT="$HOME/.kctl-env" ./install.sh main
KCTL_ENV_REF=v0.1.1 ./install.sh
EOF
}

ref_from_input="${1:-}"
if [[ "${ref_from_input:-}" == "-h" || "${ref_from_input:-}" == "--help" ]]; then
usage
exit 0
fi

ref="${ref_from_input:-$KCTL_ENV_REF}"
if [[ -z "$ref" ]]; then
echo "Error: No ref specified. For supply-chain security, you must explicitly specify a version tag or branch." >&2
echo "Usage: $0 <ref> (e.g., v0.1.1 or main)" >&2
echo "See: curl -fsSL https://raw.githubusercontent.com/${REPO_OWNER}/${REPO_NAME}/main/install.sh | bash -s -- <ref>" >&2
exit 1
fi
Comment thread
senet marked this conversation as resolved.

archive_url=""
case "$ref" in
v*) archive_url="https://github.com/${REPO_OWNER}/${REPO_NAME}/archive/refs/tags/${ref}.tar.gz" ;;
*) archive_url="https://github.com/${REPO_OWNER}/${REPO_NAME}/archive/refs/heads/${ref}.tar.gz" ;;
esac

require_cmd() {
command -v "$1" >/dev/null 2>&1 || { echo "Missing required command: $1" >&2; exit 1; }
}

# Compute SHA256 hash using available tools
# Returns hash on stdout, exits non-zero on error
compute_sha256() {
local file="$1"
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "$file" | awk '{print $1}'
elif command -v shasum >/dev/null 2>&1; then
shasum -a 256 "$file" | awk '{print $1}'
elif command -v openssl >/dev/null 2>&1; then
openssl dgst -sha256 "$file" | awk '{print $NF}'
else
echo "Error: No SHA256 tool found (tried: sha256sum, shasum, openssl)" >&2
exit 1
fi
}

require_cmd install
require_cmd mktemp
require_cmd curl
require_cmd find
require_cmd head
require_cmd tar
require_cmd awk
require_cmd basename
require_cmd tr

Comment thread
senet marked this conversation as resolved.
tmpdir="$(mktemp -d "${TMPDIR:-/tmp}/kctl-env.XXXXXX")"
trap 'rm -rf "$tmpdir"' EXIT

archive="$tmpdir/src.tar.gz"

echo "Downloading $archive_url"
curl -fsSL "$archive_url" -o "$archive"

# Verify integrity for tagged releases
# For tags (v*), try to fetch and verify SHA256 checksum from GitHub releases
# For branches, skip checksum (GitHub doesn't provide checksums for auto-generated tarballs)
if [[ "$ref" == v* ]]; then
if [[ -n "${KCTL_ENV_SKIP_VERIFY:-}" ]]; then
echo "Warning: Checksum verification explicitly skipped via KCTL_ENV_SKIP_VERIFY" >&2
else
checksum_url="https://github.com/${REPO_OWNER}/${REPO_NAME}/releases/download/${ref}/${ref}.tar.gz.sha256"
echo "Verifying checksum..."
if curl -fsSL "$checksum_url" -o "$tmpdir/checksum.sha256" 2>/dev/null; then
# Extract just the hash (first field) and verify manually
expected_hash="$(awk 'NR==1{print $1; exit}' "$tmpdir/checksum.sha256" | tr '[:upper:]' '[:lower:]')"
actual_hash="$(compute_sha256 "$archive" | tr '[:upper:]' '[:lower:]')"

# Validate that hashes were extracted successfully
if [[ -z "$expected_hash" || -z "$actual_hash" ]]; then
echo "Error: Failed to extract checksum values" >&2
echo "The checksum file may be empty or malformed" >&2
exit 1
fi

if [[ ! "$expected_hash" =~ ^[0-9a-f]{64}$ ]]; then
echo "Error: Checksum file is malformed (expected SHA256 hex)." >&2
echo "Got: $expected_hash" >&2
exit 1
fi

if [[ "$expected_hash" != "$actual_hash" ]]; then
echo "Checksum verification failed for $ref" >&2
echo "Expected: $expected_hash" >&2
echo "Actual: $actual_hash" >&2
echo "This may indicate a compromised download or release." >&2
exit 1
fi
echo "Checksum verified successfully"
else
echo "Error: No checksum found for $ref at $checksum_url" >&2
echo "For security, this installer requires SHA256 verification for tagged releases." >&2
echo "If you still want to install this older release without checksums, you can re-run with:" >&2
echo " Local file: KCTL_ENV_SKIP_VERIFY=1 ./install.sh $ref" >&2
echo " Via curl: KCTL_ENV_SKIP_VERIFY=1 bash -s -- $ref" >&2
exit 1
fi
fi
else
echo "Note: Checksum verification skipped for branch '$ref' (not available for auto-generated archives)"
fi

# Extract
mkdir -p "$tmpdir/src"
tar -xzf "$archive" -C "$tmpdir/src"

Comment thread
senet marked this conversation as resolved.
# Determine extracted directory (choose first top-level subdirectory deterministically)
src_root=""
for d in "$tmpdir/src"/*; do
if [[ -d "$d" ]]; then
src_root="$d"
break
fi
done
if [[ -z "${src_root:-}" || ! -d "$src_root" ]]; then
echo "Failed to locate extracted source directory" >&2
exit 1
fi

# Create target directories (preserve versions/cache if already exist)
mkdir -p "$KCTL_ENV_ROOT" \
"$KCTL_ENV_ROOT/bin" \
"$KCTL_ENV_ROOT/libexec" \
"$KCTL_ENV_ROOT/etc" \
"$KCTL_ENV_ROOT/packaging" \
"$KCTL_ENV_ROOT/scripts" \
"$KCTL_ENV_ROOT/versions" \
"$KCTL_ENV_ROOT/cache"

# Install core scripts
install -m 0755 "$src_root/bin/kctl-env" "$KCTL_ENV_ROOT/bin/kctl-env"
install -m 0755 "$src_root/bin/kubectl" "$KCTL_ENV_ROOT/bin/kubectl"

# libexec scripts
for f in "$src_root"/libexec/*; do
[[ -f "$f" ]] || continue
install -m 0755 "$f" "$KCTL_ENV_ROOT/libexec/$(basename "$f")"
done

# Completion scripts (optional)
if [[ -f "$src_root/etc/kctl-env-completion.bash" ]]; then
install -m 0644 "$src_root/etc/kctl-env-completion.bash" "$KCTL_ENV_ROOT/etc/kctl-env-completion.bash"
fi
if [[ -f "$src_root/etc/kctl-env-completion.zsh" ]]; then
install -m 0644 "$src_root/etc/kctl-env-completion.zsh" "$KCTL_ENV_ROOT/etc/kctl-env-completion.zsh"
fi

# Scripts (optional)
if [[ -f "$src_root/scripts/release.sh" ]]; then
install -m 0755 "$src_root/scripts/release.sh" "$KCTL_ENV_ROOT/scripts/release.sh"
fi

# Docs + metadata (best-effort)
for f in README.md CHANGELOG.md VERSION; do
if [[ -f "$src_root/$f" ]]; then
install -m 0644 "$src_root/$f" "$KCTL_ENV_ROOT/$f"
fi
done

echo
echo "Installed kctl-env into: $KCTL_ENV_ROOT"
echo
echo "Next steps:"
echo " 1) Add to PATH:"
echo " export PATH=\"$KCTL_ENV_ROOT/bin:\$PATH\""
echo " 2) Install kubectl:"
echo " kctl-env install latest"
echo " 3) Select version:"
echo " kctl-env use latest"
echo
59 changes: 53 additions & 6 deletions libexec/kctl-env-list-remote
Original file line number Diff line number Diff line change
@@ -1,10 +1,57 @@
#!/usr/bin/env bash
set -euo pipefail

# List available kubectl releases from dl.k8s.io
# Zero-deps: parse HTML index
# List available kubectl releases.
#
# Kubernetes' dl.k8s.io no longer exposes a browsable release index, so we use
# the GitHub tags API and parse it with POSIX tools (no jq).
#
# Notes:
# - Unauthenticated GitHub API requests are rate-limited.
# - Increase limits by exporting GITHUB_TOKEN.

curl -fsSL https://dl.k8s.io/release/ |
grep -Eo 'v[0-9]+\.[0-9]+\.[0-9]+' |
sort -Vr |
uniq
MAX_PAGES="${KCTL_LIST_REMOTE_MAX_PAGES:-5}"
PER_PAGE=100

api_base="https://api.github.com/repos/kubernetes/kubernetes/tags"

curl_args=(-sSLf)
if [[ -n "${GITHUB_TOKEN:-}" ]]; then
curl_args+=(-H "Authorization: Bearer ${GITHUB_TOKEN}")
fi

page=1
while [[ "$page" -le "$MAX_PAGES" ]]; do
json="$(curl "${curl_args[@]}" "${api_base}?per_page=${PER_PAGE}&page=${page}")" || {
echo "kctl-env: failed to query GitHub tags API (page ${page})." >&2
echo "Hint: export GITHUB_TOKEN to avoid rate limits, or set KCTL_LIST_REMOTE_MAX_PAGES lower." >&2
exit 1
}

# Extract stable semver tags like v1.29.0
tags="$(printf '%s' "$json" |
grep -Eo '"name"[[:space:]]*:[[:space:]]*"v[0-9]+\.[0-9]+\.[0-9]+"' |
sed -E 's/.*"(v[0-9]+\.[0-9]+\.[0-9]+)".*/\1/'
)"

[[ -z "$tags" ]] && break
printf '%s\n' "$tags"
page=$((page + 1))
done |
if sort -V </dev/null >/dev/null 2>&1; then
# Prefer GNU sort's version sort when available.
sort -Vr | uniq
else
# POSIX fallback: sort semantic versions vMAJOR.MINOR.PATCH numerically.
awk -F. '
{
ver = $0
# Strip leading "v" from major component.
sub(/^v/, "", $1)
# Print: original major minor patch
printf "%s %d %d %d\n", ver, $1, $2, $3
}
' |
sort -k2,2nr -k3,3nr -k4,4nr |
awk '!seen[$1]++ { print $1 }'
fi
12 changes: 12 additions & 0 deletions scripts/release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,15 @@ git add VERSION "$changelog_file" "$spec_file"
git commit -m "release: bump to v$new_version"

echo "Release files updated and committed. Open a PR if on a release branch."
echo
echo "After merging and creating the tag, remember to:"
echo " 1. Create a GitHub release for v$new_version"
echo " 2. Generate and attach checksum for the source tarball:"
echo " # On Linux / systems with GNU coreutils:"
echo " curl -fsSL https://github.com/senet/kctl-env/archive/refs/tags/v$new_version.tar.gz | sha256sum > v$new_version.tar.gz.sha256"
Comment thread
senet marked this conversation as resolved.
echo " # On macOS (no sha256sum by default):"
echo " curl -fsSL https://github.com/senet/kctl-env/archive/refs/tags/v$new_version.tar.gz | shasum -a 256 > v$new_version.tar.gz.sha256"
echo " 3. Create the release and attach the checksum asset (immutable releases):"
echo " gh release create v$new_version --title \"v$new_version\" --notes \"Release v$new_version\" v$new_version.tar.gz.sha256"
echo " This enables secure installation with: curl -fsSL https://raw.githubusercontent.com/senet/kctl-env/v$new_version/install.sh | bash -s -- v$new_version"