banger/scripts/publish-kernel.sh
Thales Maciel fa95849f5a
Phase 5: kernel catalog publish flow + docs
Manual publish flow for the kernel catalog, designed for the current
no-CI, private-repo state of banger.

scripts/publish-kernel.sh <name>:
 - Reads $BANGER_KERNELS_DIR/<name>/ (the canonical layout produced by
   `banger kernel import`).
 - Pulls distro / arch / kernel_version from the local manifest.
 - Packages vmlinux + optional initrd.img + optional modules/ as
   <name>-<arch>.tar.zst with zstd -19.
 - Computes sha256 + size.
 - rclone copyto -> r2:banger-kernels/<file>.
 - HEAD-checks https://kernels.thaloco.com/<file> to catch
   public-access misconfig before declaring success.
 - jq-patches internal/kernelcat/catalog.json: replaces any prior
   entry with the same name, then sorts entries by name.
 - Prints next-step git+make commands; does not commit or rebuild
   automatically.

Environment overrides RCLONE_REMOTE / RCLONE_BUCKET / BASE_URL /
BANGER_KERNELS_DIR for non-default setups.

docs/kernel-catalog.md covers the architecture (embedded JSON +
external tarballs), end-user flow, the add/update/remove playbook,
naming and tarball-layout conventions, the trust model (sha256 in
embedded catalog catches transport/swap; no signing yet), and where
the bucket lives.

README.md gains a kernel-catalog example next to the existing image
register example. AGENTS.md points at publish-kernel.sh and the docs.

.gitignore now excludes .env so accidental drops of R2 credentials
don't follow into commits.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 15:56:56 -03:00

139 lines
4.5 KiB
Bash
Executable file

#!/usr/bin/env bash
# publish-kernel.sh
#
# Package an entry from the local banger kernel catalog as a tar.zst,
# upload it to the public R2 bucket, and patch internal/kernelcat/catalog.json
# with the resulting URL + sha256 + size. Run after `banger kernel import`.
#
# Usage:
# scripts/publish-kernel.sh <name> [--description "..."]
#
# Environment overrides:
# RCLONE_REMOTE rclone remote to upload through (default: r2)
# RCLONE_BUCKET R2 bucket name (default: banger-kernels)
# BASE_URL public URL prefix for the bucket (default: https://kernels.thaloco.com)
# BANGER_KERNELS_DIR local catalog directory (default: ~/.local/state/banger/kernels)
set -euo pipefail
log() { printf '[publish-kernel] %s\n' "$*" >&2; }
die() { log "$*"; exit 1; }
usage() {
cat <<EOF
usage: scripts/publish-kernel.sh <name> [--description "<text>"]
Reads the locally-imported kernel at \$BANGER_KERNELS_DIR/<name>/, packages
it as <name>-<arch>.tar.zst, uploads to R2, and updates
internal/kernelcat/catalog.json.
Run \`banger kernel import <name> --from <build-dir> --distro <d> --arch <a>\`
first.
EOF
}
RCLONE_REMOTE="${RCLONE_REMOTE:-r2}"
RCLONE_BUCKET="${RCLONE_BUCKET:-banger-kernels}"
BASE_URL="${BASE_URL:-https://kernels.thaloco.com}"
BANGER_KERNELS_DIR="${BANGER_KERNELS_DIR:-$HOME/.local/state/banger/kernels}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
CATALOG_FILE="$REPO_ROOT/internal/kernelcat/catalog.json"
NAME=""
DESCRIPTION=""
while [[ $# -gt 0 ]]; do
case "$1" in
-d|--description) DESCRIPTION="${2:-}"; shift 2;;
-h|--help) usage; exit 0;;
--) shift; break;;
-*) die "unknown flag: $1";;
*)
if [[ -z "$NAME" ]]; then
NAME="$1"; shift
else
die "unexpected positional arg: $1"
fi
;;
esac
done
[[ -n "$NAME" ]] || { usage; exit 1; }
for tool in jq rclone tar zstd sha256sum stat curl; do
command -v "$tool" >/dev/null 2>&1 || die "missing required tool: $tool"
done
[[ -f "$CATALOG_FILE" ]] || die "catalog file not found: $CATALOG_FILE"
SRC="$BANGER_KERNELS_DIR/$NAME"
[[ -d "$SRC" ]] || die "$SRC does not exist; run 'banger kernel import $NAME --from <dir>' first"
[[ -f "$SRC/vmlinux" ]] || die "$SRC/vmlinux missing"
[[ -f "$SRC/manifest.json" ]] || die "$SRC/manifest.json missing"
DISTRO="$(jq -r '.distro // ""' "$SRC/manifest.json")"
ARCH="$(jq -r '.arch // ""' "$SRC/manifest.json")"
KERNEL_VERSION="$(jq -r '.kernel_version // ""' "$SRC/manifest.json")"
[[ -n "$ARCH" ]] || ARCH="x86_64"
STAGE="$(mktemp -d)"
trap 'rm -rf "$STAGE"' EXIT
TARBALL_NAME="${NAME}-${ARCH}.tar.zst"
TARBALL="$STAGE/$TARBALL_NAME"
INCLUDES=(vmlinux)
[[ -f "$SRC/initrd.img" ]] && INCLUDES+=(initrd.img)
[[ -d "$SRC/modules" ]] && INCLUDES+=(modules)
log "packaging ${INCLUDES[*]} from $SRC"
( cd "$SRC" && tar -cf - "${INCLUDES[@]}" ) | zstd -19 --long -T0 -q -o "$TARBALL"
SHA256="$(sha256sum "$TARBALL" | awk '{print $1}')"
SIZE="$(stat -c '%s' "$TARBALL")"
HUMAN_SIZE="$(numfmt --to=iec --suffix=B "$SIZE" 2>/dev/null || echo "${SIZE}B")"
log "tarball $TARBALL_NAME: $HUMAN_SIZE, sha256 $SHA256"
log "uploading to $RCLONE_REMOTE:$RCLONE_BUCKET/$TARBALL_NAME"
rclone copyto "$TARBALL" "$RCLONE_REMOTE:$RCLONE_BUCKET/$TARBALL_NAME"
URL="$BASE_URL/$TARBALL_NAME"
log "verifying $URL is reachable"
HEAD_STATUS="$(curl -fsSI -o /dev/null -w '%{http_code}' "$URL" || true)"
if [[ "$HEAD_STATUS" != "200" ]]; then
die "uploaded tarball is not publicly reachable at $URL (HTTP $HEAD_STATUS); check bucket public-access config"
fi
log "patching $CATALOG_FILE"
NEW_ENTRY="$(jq -n \
--arg name "$NAME" \
--arg distro "$DISTRO" \
--arg arch "$ARCH" \
--arg kver "$KERNEL_VERSION" \
--arg url "$URL" \
--arg sha "$SHA256" \
--argjson size "$SIZE" \
--arg desc "$DESCRIPTION" \
'{
name: $name,
distro: $distro,
arch: $arch,
kernel_version: $kver,
tarball_url: $url,
tarball_sha256: $sha,
size_bytes: $size,
description: $desc
} | with_entries(select(.value != null and .value != ""))')"
CATALOG_TMP="$(mktemp)"
jq --arg name "$NAME" --argjson new "$NEW_ENTRY" '
.version = (.version // 1)
| .entries = (((.entries // []) | map(select(.name != $name))) + [$new])
| .entries |= sort_by(.name)
' "$CATALOG_FILE" > "$CATALOG_TMP"
mv "$CATALOG_TMP" "$CATALOG_FILE"
log "done"
log "next steps:"
log " git diff -- $CATALOG_FILE"
log " git add $CATALOG_FILE && git commit -m 'kernel catalog: add/update $NAME'"
log " make build # rebuild banger so the new catalog is embedded"