Monorepo for Aesthetic.Computer aesthetic.computer
at main 256 lines 11 kB view raw
1#!/bin/bash 2# upload-release.sh — Upload a built notepat vmlinuz to Digital Ocean Spaces 3# Publishes: native-notepat-latest.vmlinuz, .sha256, .version, and updates releases.json 4# 5# Usage: ./upload-release.sh [vmlinuz_path] 6# ./upload-release.sh --image [image_path] # Upload template disk image only 7# Credentials auto-loaded from aesthetic-computer-vault/fedac/native/upload.env 8 9set -e 10 11SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" 12IMAGE_ONLY=0 13if [ "${1:-}" = "--image" ]; then 14 IMAGE_ONLY=1 15 IMAGE_PATH="${2:-${SCRIPT_DIR}/../build/ac-os.img}" 16 if [ ! -f "$IMAGE_PATH" ]; then 17 echo "Error: image not found at $IMAGE_PATH" >&2 18 exit 1 19 fi 20 VMLINUZ="/dev/null" # not used but needed for cred loading below 21else 22 VMLINUZ="${1:-${SCRIPT_DIR}/../build/vmlinuz}" 23 if [ ! -f "$VMLINUZ" ]; then 24 echo "Error: vmlinuz not found at $VMLINUZ" >&2 25 exit 1 26 fi 27fi 28 29# Credentials — check env (set by ac-os), session cache, plaintext vault, or GPG decrypt 30if [ -z "${DO_SPACES_KEY:-}" ] || [ -z "${DO_SPACES_SECRET:-}" ]; then 31 # Session cache (written by ac-os load_vault_creds) 32 [ -f "/tmp/.ac-upload-env" ] && { set -a; source "/tmp/.ac-upload-env"; set +a; } 33fi 34if [ -z "${DO_SPACES_KEY:-}" ] || [ -z "${DO_SPACES_SECRET:-}" ]; then 35 # Plaintext vault file 36 VAULT_ENV="${SCRIPT_DIR}/../../../aesthetic-computer-vault/fedac/native/upload.env" 37 [ -f "$VAULT_ENV" ] && { set -a; source "$VAULT_ENV"; set +a; } 38fi 39if [ -z "${DO_SPACES_KEY:-}" ] || [ -z "${DO_SPACES_SECRET:-}" ]; then 40 # GPG decrypt from vault 41 for gpg_file in \ 42 "${SCRIPT_DIR}/../upload.env.gpg" \ 43 "${SCRIPT_DIR}/../../../aesthetic-computer-vault/fedac/native/upload.env.gpg"; do 44 if [ -f "$gpg_file" ]; then 45 echo "[upload] Decrypting $(basename "$gpg_file")..." 46 DECRYPTED=$(gpg --pinentry-mode loopback -d "$gpg_file" 2>/dev/null | grep "=") || true 47 if [ -n "$DECRYPTED" ]; then 48 set -a; eval "$DECRYPTED"; set +a 49 break 50 fi 51 fi 52 done 53fi 54 55: "${DO_SPACES_KEY:?DO_SPACES_KEY not set}" 56: "${DO_SPACES_SECRET:?DO_SPACES_SECRET not set}" 57DO_SPACES_BUCKET="${DO_SPACES_BUCKET:-releases-aesthetic-computer}" 58DO_SPACES_REGION="${DO_SPACES_REGION:-sfo3}" 59 60BASE_URL="https://${DO_SPACES_BUCKET}.${DO_SPACES_REGION}.digitaloceanspaces.com" 61 62# Build version string from git. Dirty/conflicted uploads are blocked by default. 63GIT_ROOT=$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || true) 64GIT_CWD="$SCRIPT_DIR" 65GIT_NATIVE_PATHS=() 66if [ -n "$GIT_ROOT" ]; then 67 GIT_CWD="$GIT_ROOT" 68 GIT_NATIVE_PATHS=(fedac/native fedac/nixos) 69fi 70GIT_HASH="${AC_GIT_HASH:-$(git -C "$GIT_CWD" rev-parse --short HEAD 2>/dev/null || echo "unknown")}" 71CONFLICT_FILES=$(git -C "$GIT_CWD" diff --name-only --diff-filter=U -- "${GIT_NATIVE_PATHS[@]}" 2>/dev/null || true) 72if [ -n "$CONFLICT_FILES" ]; then 73 echo "Error: refusing upload with unresolved merge conflicts:" >&2 74 echo "$CONFLICT_FILES" >&2 75 exit 1 76fi 77DIRTY_TRACKED=0 78if ! git -C "$GIT_CWD" diff --quiet HEAD -- "${GIT_NATIVE_PATHS[@]}" 2>/dev/null; then 79 DIRTY_TRACKED=1 80fi 81if [ "$DIRTY_TRACKED" -eq 1 ] && [ "${ALLOW_DIRTY_UPLOAD:-0}" != "1" ]; then 82 echo "Error: refusing dirty upload. Commit/stash/reset fedac/native or fedac/nixos changes first." >&2 83 git -C "$GIT_CWD" status --porcelain --untracked-files=no -- "${GIT_NATIVE_PATHS[@]}" 2>/dev/null >&2 || true 84 echo "Override only for emergencies: ALLOW_DIRTY_UPLOAD=1 ./scripts/upload-release.sh ..." >&2 85 exit 1 86fi 87if [ "$DIRTY_TRACKED" -eq 1 ]; then 88 GIT_HASH="${GIT_HASH}-dirty" 89fi 90BUILD_TS="${AC_BUILD_TS:-$(date -u '+%Y-%m-%dT%H:%M')}" 91# Format must match AC_GIT_HASH "-" AC_BUILD_TS in js-bindings.c / Makefile 92VERSION="${GIT_HASH}-${BUILD_TS}" 93 94# Compute SHA256 95SHA256=$(sha256sum "$VMLINUZ" | awk '{print $1}') 96SIZE=$(stat -c%s "$VMLINUZ") 97 98# Build name: prefer AC_BUILD_NAME (set by oven/Makefile at compile time) 99# so the uploaded name matches what the kernel displays on boot. 100# Falls back to MongoDB counter if not set (local builds). 101BUILD_NAME="${AC_BUILD_NAME:-}" 102BUILD_NUM="" 103if [ -z "$BUILD_NAME" ] && command -v node &>/dev/null; then 104 NAME_JSON=$(node "$SCRIPT_DIR/track-build.mjs" next-name 2>/dev/null || echo '{}') 105 BUILD_NAME=$(echo "$NAME_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('name',''))" 2>/dev/null || true) 106 BUILD_NUM=$(echo "$NAME_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('buildNum',''))" 2>/dev/null || true) 107fi 108if [ -z "$BUILD_NAME" ]; then 109 BUILD_NAME="local-$(date -u +%s)" 110fi 111 112echo "Uploading notepat release: $VERSION" 113echo " name: $BUILD_NAME (#${BUILD_NUM:-?})" 114echo " vmlinuz: $(du -sh "$VMLINUZ" | cut -f1)" 115echo " sha256: $SHA256" 116 117# Helper: upload file to DO Spaces via curl (AWS Sig v2) 118# x-amz-acl must be in CanonicalizedAmzHeaders in the string-to-sign 119do_upload() { 120 local src="$1" 121 local dest_key="$2" 122 local content_type="${3:-application/octet-stream}" 123 local acl="public-read" 124 125 local date_val 126 date_val=$(date -u '+%a, %d %b %Y %H:%M:%S GMT') 127 local md5_val 128 md5_val=$(openssl md5 -binary < "$src" | base64) 129 local sig 130 sig=$(printf 'PUT\n%s\n%s\n%s\nx-amz-acl:%s\n/%s/%s' \ 131 "$md5_val" "$content_type" "$date_val" "$acl" "$DO_SPACES_BUCKET" "$dest_key" \ 132 | openssl dgst -sha1 -hmac "$DO_SPACES_SECRET" -binary | base64) 133 134 # Use -T (upload-file) for streaming — avoids loading entire file into memory 135 # (--data-binary loads the whole file into RAM, OOM on 1GB+ files) 136 curl -sf -X PUT \ 137 -H "Date: $date_val" \ 138 -H "Content-Type: $content_type" \ 139 -H "Content-MD5: $md5_val" \ 140 -H "x-amz-acl: $acl" \ 141 -H "Authorization: AWS ${DO_SPACES_KEY}:${sig}" \ 142 -T "$src" \ 143 "${BASE_URL}/${dest_key}" \ 144 && echo " uploaded: $dest_key" \ 145 || { echo " ERROR uploading $dest_key" >&2; return 1; } 146} 147 148# Write .version and .sha256 to temp files 149TMP=$(mktemp -d) 150trap "rm -rf $TMP" EXIT 151 152# Include build name in version string for display on device 153# Line 1: version string, Line 2: kernel size in bytes 154FULL_VERSION="${BUILD_NAME} ${VERSION}" 155printf '%s\n%s' "$FULL_VERSION" "$SIZE" > "$TMP/version.txt" 156printf '%s' "$SHA256" > "$TMP/sha256.txt" 157 158# OTA channel prefix (empty for default C build, "cl-" for Common Lisp variant) 159CHANNEL_PREFIX="" 160if [ -n "${OTA_CHANNEL:-}" ]; then 161 CHANNEL_PREFIX="${OTA_CHANNEL}-" 162 echo " channel: ${OTA_CHANNEL}" 163fi 164 165# Image-only mode: upload disk image + version + sha256, then exit 166if [ "$IMAGE_ONLY" = "1" ]; then 167 IMAGE_SHA256=$(sha256sum "$IMAGE_PATH" | awk '{print $1}') 168 IMAGE_SIZE=$(stat -c%s "$IMAGE_PATH" 2>/dev/null || stat -f%z "$IMAGE_PATH") 169 printf '%s\n%s' "${FULL_VERSION}" "$IMAGE_SIZE" > "$TMP/version.txt" 170 printf '%s' "$IMAGE_SHA256" > "$TMP/sha256.txt" 171 echo "Uploading image: $(du -sh "$IMAGE_PATH" | cut -f1) sha256=${IMAGE_SHA256:0:16}..." 172 do_upload "$TMP/version.txt" "os/${CHANNEL_PREFIX}native-notepat-latest.version" "text/plain" 173 do_upload "$TMP/sha256.txt" "os/${CHANNEL_PREFIX}native-notepat-latest.sha256" "text/plain" 174 do_upload "$IMAGE_PATH" "os/${CHANNEL_PREFIX}native-notepat-latest.img" "application/octet-stream" 175 echo "Image published: ${BASE_URL}/os/${CHANNEL_PREFIX}native-notepat-latest.img" 176 exit 0 177fi 178 179# Upload files (vmlinuz last — it's large, others are the canary) 180do_upload "$TMP/version.txt" "os/${CHANNEL_PREFIX}native-notepat-latest.version" "text/plain" 181do_upload "$TMP/sha256.txt" "os/${CHANNEL_PREFIX}native-notepat-latest.sha256" "text/plain" 182do_upload "$VMLINUZ" "os/${CHANNEL_PREFIX}native-notepat-latest.vmlinuz" "application/octet-stream" 183 184# Fetch existing releases.json (or start fresh) 185RELEASES_JSON="$TMP/releases.json" 186curl -sf "${BASE_URL}/os/${CHANNEL_PREFIX}releases.json" -o "$RELEASES_JSON" 2>/dev/null \ 187 || echo '{"releases":[]}' > "$RELEASES_JSON" 188 189# Append new entry (keep last 50) 190COMMIT_MSG="${AC_COMMIT_MSG:-$(git -C "$GIT_CWD" log -1 --format="%s" 2>/dev/null || echo "")}" 191BUILD_HANDLE="${AC_HANDLE:-}" 192 193python3 - "$RELEASES_JSON" "$FULL_VERSION" "$SHA256" "$SIZE" "$GIT_HASH" "$BUILD_TS" "$BUILD_NAME" "$CHANNEL_PREFIX" "$COMMIT_MSG" "$BUILD_HANDLE" <<'PYEOF' 194import sys, json 195path, version, sha256, size, git_hash, build_ts, name, channel_prefix, commit_msg, handle = sys.argv[1:] 196with open(path) as f: 197 data = json.load(f) 198releases = data.get("releases", []) 199entry = { 200 "version": version, 201 "name": name, 202 "sha256": sha256, 203 "size": int(size), 204 "git_hash": git_hash, 205 "build_ts": build_ts, 206 "url": f"https://releases-aesthetic-computer.sfo3.digitaloceanspaces.com/os/{channel_prefix}native-notepat-latest.vmlinuz", 207 "commit_msg": commit_msg, 208 "handle": handle, 209} 210releases.insert(0, entry) 211data["releases"] = releases[:50] 212data["latest"] = version 213data["latest_name"] = name 214with open(path, "w") as f: 215 json.dump(data, f, indent=2) 216PYEOF 217 218do_upload "$RELEASES_JSON" "os/${CHANNEL_PREFIX}releases.json" "application/json" 219 220# Record build in MongoDB 221if command -v node &>/dev/null; then 222 BUILD_NUM_JSON="${BUILD_NUM:-null}" 223 echo "{\"name\":\"$BUILD_NAME\",\"buildNum\":$BUILD_NUM_JSON,\"version\":\"$FULL_VERSION\",\"sha256\":\"$SHA256\",\"size\":$SIZE,\"git_hash\":\"$GIT_HASH\",\"build_ts\":\"$BUILD_TS\",\"url\":\"${BASE_URL}/os/native-notepat-latest.vmlinuz\"}" \ 224 | node "$SCRIPT_DIR/track-build.mjs" record 2>&1 || true 225fi 226 227# Upload slim kernel + initramfs for universal Mac/ThinkPad boot 228SLIM_SIBLING="$(dirname "$VMLINUZ")/vmlinuz-slim" 229INITRAMFS_SIBLING="$(dirname "$VMLINUZ")/initramfs.cpio.gz" 230if [ -f "$SLIM_SIBLING" ]; then 231 echo " Uploading slim kernel ($(du -sh "$SLIM_SIBLING" | cut -f1))..." 232 do_upload "$SLIM_SIBLING" "os/${CHANNEL_PREFIX}native-notepat-latest.vmlinuz-slim" "application/octet-stream" 233fi 234if [ -f "$INITRAMFS_SIBLING" ]; then 235 echo " Uploading initramfs ($(du -sh "$INITRAMFS_SIBLING" | cut -f1))..." 236 do_upload "$INITRAMFS_SIBLING" "os/${CHANNEL_PREFIX}native-notepat-latest.initramfs.cpio.gz" "application/octet-stream" 237fi 238 239# Also upload a template disk image if it exists (non-fatal) 240IMAGE_SIBLING="$(dirname "$VMLINUZ")/ac-os.img" 241if [ -f "$IMAGE_SIBLING" ]; then 242 echo " Uploading image ($(du -sh "$IMAGE_SIBLING" | cut -f1))..." 243 do_upload "$IMAGE_SIBLING" "os/${CHANNEL_PREFIX}native-notepat-latest.img" "application/octet-stream" || echo " Image upload failed (non-fatal)" 244fi 245 246echo "" 247echo "Release published: $BUILD_NAME ($FULL_VERSION)" 248echo " ${BASE_URL}/os/${CHANNEL_PREFIX}native-notepat-latest.vmlinuz" 249if [ -f "$SLIM_SIBLING" ]; then 250 echo " ${BASE_URL}/os/${CHANNEL_PREFIX}native-notepat-latest.vmlinuz-slim" 251 echo " ${BASE_URL}/os/${CHANNEL_PREFIX}native-notepat-latest.initramfs.cpio.gz" 252fi 253echo " ${BASE_URL}/os/${CHANNEL_PREFIX}releases.json" 254if [ -f "$IMAGE_SIBLING" ]; then 255 echo " ${BASE_URL}/os/${CHANNEL_PREFIX}native-notepat-latest.img" 256fi