AkurAI Build
Menu

BifrOSt

public

Latest change 396b54c4e84791c369e5492a4639fe02b58b56b6 - Add AkurAI Build CI/CD pipeline for ISO delivery by Ólafur Búi Ólafsson

#!/usr/bin/env bash
#
# publish-iso.sh - Publish a qualified BifrOSt ISO to the AkurAI Build public
# download endpoint and refresh the bifrost.olibuijr.com landing page.
#
# Invoked by the approval-gated `publish` job of .akurai.yml, but also runnable
# by hand on Titan. It performs the delivery side of an ISO release:
#
#   1. Install the ISO + a SHA-256 sidecar into the AkurAI Build release store
#      ($AKURAI_BUILD_HOSTED/releases/BifrOSt/), which the controller serves at
#      https://akurai-build.olibuijr.com/downloads/BifrOSt/<name>. Both the
#      versioned name (bifrost-<version>-x86_64.iso) and a stable alias
#      (bifrost-x86_64.iso) are published, each with its own .sha256.
#   2. Verify the public download endpoint returns 200 with the expected digest.
#   3. Update the landing page's runtime content (BifrOSt-Web data/content.db):
#      the version string and download links in the home/is and home/en rows.
#      The server re-reads content.db per request, so no rebuild is needed.
#   4. Verify the public page advertises the new version.
#
# ISOs are large (multiple GiB) and MUST NOT travel through the pipeline
# `artifacts:` mechanism (512 MiB/run cap); the package job stages the ISO on
# Titan's filesystem and this script reads it from there.
#
set -Eeuo pipefail

REPO="BifrOSt"
VERSION=""
ISO=""
RELEASES_DIR="${AKURAI_BUILD_HOSTED:-$HOME/.local/share/akurai-build}/releases/$REPO"
WEB_DB="/opt/bifrost-web/current/data/content.db"
BASE_URL="https://akurai-build.olibuijr.com/downloads/$REPO"
PAGE_URL="https://bifrost.olibuijr.com/"

usage() {
    cat <<'USAGE'
Usage: publish-iso.sh --version X.Y.0 --iso PATH [options]

Publish a qualified BifrOSt ISO to the AkurAI Build downloads endpoint and
refresh the bifrost.olibuijr.com landing page.

Options:
  --version X.Y.0     Release version (must match the ISO); required
  --iso PATH          Path to the qualified ISO on Titan; required
  --releases-dir DIR  AkurAI Build release store (default: $AKURAI_BUILD_HOSTED/releases/BifrOSt)
  --web-db PATH       Deployed BifrOSt-Web content.db (default: /opt/bifrost-web/current/data/content.db)
  --base-url URL      Public downloads base (default: https://akurai-build.olibuijr.com/downloads/BifrOSt)
  --skip-page         Publish the ISO only; do not touch the landing page
  -h, --help          Show this help
USAGE
}

die() { printf 'publish-iso: %s\n' "$*" >&2; exit 1; }
log() { printf '\n==> %s\n' "$*"; }

SKIP_PAGE=0
while (( $# )); do
    case "$1" in
        --version) [[ $# -ge 2 ]] || die "$1 needs a value"; VERSION="$2"; shift 2 ;;
        --iso) [[ $# -ge 2 ]] || die "$1 needs a value"; ISO="$2"; shift 2 ;;
        --releases-dir) [[ $# -ge 2 ]] || die "$1 needs a value"; RELEASES_DIR="$2"; shift 2 ;;
        --web-db) [[ $# -ge 2 ]] || die "$1 needs a value"; WEB_DB="$2"; shift 2 ;;
        --base-url) [[ $# -ge 2 ]] || die "$1 needs a value"; BASE_URL="$2"; shift 2 ;;
        --skip-page) SKIP_PAGE=1; shift ;;
        -h|--help) usage; exit 0 ;;
        *) usage >&2; die "unknown argument: $1" ;;
    esac
done

[[ -n "$VERSION" ]] || { usage >&2; die "--version is required"; }
[[ "$VERSION" == *.*.0 ]] || die "refusing to publish $VERSION: ISO releases are minor versions only (X.Y.0)"
[[ -n "$ISO" && -f "$ISO" ]] || die "ISO not found: ${ISO:-<unset>}"

NAME="bifrost-${VERSION}-x86_64.iso"
ALIAS="bifrost-x86_64.iso"

log "Computing source digest"
DIGEST="$(sha256sum "$ISO" | cut -d' ' -f1)"
printf '    %s  %s\n' "$DIGEST" "$NAME"

log "Installing into release store: $RELEASES_DIR"
mkdir -p "$RELEASES_DIR"
tmp="$RELEASES_DIR/.$NAME.tmp.$$"
install -m 0644 "$ISO" "$tmp"
[[ "$(sha256sum "$tmp" | cut -d' ' -f1)" == "$DIGEST" ]] || { rm -f "$tmp"; die "copy digest mismatch"; }
mv -f "$tmp" "$RELEASES_DIR/$NAME"
printf '%s  %s\n' "$DIGEST" "$NAME" > "$RELEASES_DIR/$NAME.sha256"
ln -sf "$NAME" "$RELEASES_DIR/$ALIAS"
printf '%s  %s\n' "$DIGEST" "$ALIAS" > "$RELEASES_DIR/$ALIAS.sha256"
log "Published $NAME (+ stable alias $ALIAS)"

log "Verifying public download endpoint"
served_len="$(curl -fsIL "$BASE_URL/$NAME" | awk 'tolower($1)=="content-length:"{print $2}' | tr -d '\r' | tail -1)"
local_len="$(stat -c %s "$RELEASES_DIR/$NAME")"
[[ -n "$served_len" && "$served_len" == "$local_len" ]] || die "served content-length ($served_len) != local ($local_len)"
served_sha="$(curl -fsSL "$BASE_URL/$NAME.sha256" | cut -d' ' -f1)"
[[ "$served_sha" == "$DIGEST" ]] || die "served sha256 ($served_sha) != built ($DIGEST)"
log "Download endpoint serves $NAME ($served_len bytes, sha $DIGEST)"

if (( SKIP_PAGE )); then
    log "Skipping landing-page update (--skip-page)"
    exit 0
fi

if [[ ! -f "$WEB_DB" ]]; then
    printf 'publish-iso: WARNING landing-page content.db not found at %s; ISO published but page not refreshed.\n' "$WEB_DB" >&2
    exit 0
fi

log "Refreshing landing page content: $WEB_DB"
# content.db is root-owned under /opt; the server re-reads it per request.
sudo VERSION="$VERSION" NAME="$NAME" BASE_URL="$BASE_URL" WEB_DB="$WEB_DB" python3 - <<'PY'
import json, os, re, sqlite3
version = os.environ["VERSION"]
name = os.environ["NAME"]
url = f'{os.environ["BASE_URL"]}/{name}'
db = os.environ["WEB_DB"]
conn = sqlite3.connect(db)
try:
    changed = 0
    for (locale,) in conn.execute("SELECT locale FROM page_content WHERE page='home'").fetchall():
        (payload,) = conn.execute(
            "SELECT payload FROM page_content WHERE page='home' AND locale=?", (locale,)
        ).fetchone()
        data = json.loads(payload)
        # Old version, taken from the release-facts version line (value like 'v0.2.2').
        old = None
        for fact in data.get("release_facts", []):
            m = re.fullmatch(r"v?(\d+\.\d+\.\d+)", str(fact.get("value", "")))
            if m:
                old = m.group(1)
                fact["value"] = f"v{version}"
        for section in ("hero", "download"):
            block = data.get(section, {})
            for key, val in list(block.items()):
                if not isinstance(val, str):
                    continue
                new = val.replace(f"bifrost-{old}-x86_64.iso", name) if old else val
                if old:
                    new = re.sub(rf"v{re.escape(old)}\b", f"v{version}", new)
                if key.endswith("href") and ("downloads/" in val and name.split('-')[0] in val):
                    new = url
                block[key] = new
        conn.execute(
            "UPDATE page_content SET payload=?, updated_at=datetime('now') WHERE page='home' AND locale=?",
            (json.dumps(data, ensure_ascii=False), locale),
        )
        changed += 1
    conn.commit()
    print(f"    updated {changed} locale row(s) to v{version}")
finally:
    conn.close()
PY

log "Verifying public page advertises v$VERSION"
if curl -fsSL "$PAGE_URL" | grep -q "$NAME"; then
    log "Landing page now links $NAME"
else
    printf 'publish-iso: WARNING page did not show %s yet (CDN/cache?); verify %s manually.\n' "$NAME" "$PAGE_URL" >&2
fi