Monorepo for Aesthetic.Computer
aesthetic.computer
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