Files
bare-operating-system/scripts/gitea-rolling-release.sh
T
Raven Scott 33c8a9f197
Release rolling / release (push) Successful in 8m34s
further updates
2026-07-31 19:00:14 -04:00

244 lines
7.5 KiB
Bash
Executable File

#!/usr/bin/env bash
# Build bare-os-seeder + bare-os-booter for all 64-bit hosts and publish a rolling Gitea release.
# Also assembles by-arch tarballs for operators / offline pear stage.
#
# Required (unless DRY_RUN=1):
# RELEASE_TOKEN — Gitea PAT with repository release write
# Optional:
# GITEA_URL / GITEA_OWNER / GITEA_REPO
# BARE_OS_HOSTS / BARE_OS_SEEDER_HOSTS / BARE_OS_BOOTER_HOSTS
# RELEASE_TAG (default: rolling)
# SKIP_PEAR_STAGE=1 — default in CI; skip pear-ci (stage offline via pear-stage-by-arch.sh)
# DRY_RUN=1
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
VERSION="$(node -p "require('./package.json').version")"
STAMP="$(date -u +%Y%m%dT%H%M%SZ)"
SHA="$(git rev-parse --short HEAD 2>/dev/null || echo unknown)"
TAG="${RELEASE_TAG:-rolling}"
RELEASE_TITLE="${RELEASE_NAME:-bare-os rolling}"
DIST="$ROOT/dist/release"
DEFAULT_HOSTS="$(node -p "require('./scripts/hosts.cjs').ALL_64.join(',')")"
rm -rf "$DIST"
mkdir -p "$DIST"
log() { echo "[release] $*"; }
detect_remote() {
local url
url="$(git remote get-url origin 2>/dev/null || true)"
if [[ "$url" =~ git@([^:]+):([^/]+)/([^/.]+) ]]; then
local host="${BASH_REMATCH[1]}"
# [email protected]:owner/repo → https://git.ssh.surf
if [[ "$host" == "ssh.surf" ]]; then host="git.ssh.surf"; fi
echo "https://${host}" "${BASH_REMATCH[2]}" "${BASH_REMATCH[3]}"
elif [[ "$url" =~ https?://([^/]+)/([^/]+)/([^/.]+) ]]; then
echo "https://${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" "${BASH_REMATCH[3]}"
else
echo "" "" ""
fi
}
read -r DETECTED_URL DETECTED_OWNER DETECTED_REPO <<<"$(detect_remote)"
GITEA_URL="${GITEA_URL:-${DETECTED_URL:-}}"
GITEA_OWNER="${GITEA_OWNER:-${DETECTED_OWNER:-}}"
GITEA_REPO="${GITEA_REPO:-${DETECTED_REPO:-bare-operating-system}}"
export BARE_OS_HOSTS="${BARE_OS_HOSTS:-$DEFAULT_HOSTS}"
export BARE_OS_SEEDER_HOSTS="${BARE_OS_SEEDER_HOSTS:-$BARE_OS_HOSTS}"
export BARE_OS_BOOTER_HOSTS="${BARE_OS_BOOTER_HOSTS:-$BARE_OS_HOSTS}"
# --- build kernel image into seeder mirror ---
log "building kernel image (coreutils / openssh / bare-libs / parity)"
npm run maintainer:kernel-image
# --- binaries ---
log "building seeder+booter binaries for: $BARE_OS_HOSTS"
node scripts/make.cjs both
sha_file() {
local f="$1"
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "$f"
else
shasum -a 256 "$f"
fi
}
stage_product() {
local product="$1"
local host="$2"
local name="bare-os-${product}"
local dir="$ROOT/out/${name}-${host}"
local bin="$name"
[[ "$host" == win32-* ]] && bin="${name}.exe"
if [[ ! -d "$dir" ]]; then
log "WARN: missing $dir"
return 1
fi
if [[ ! -f "$dir/$bin" ]]; then
local f
f="$(find "$dir" -maxdepth 2 -type f \( -name "$name" -o -name "${name}.exe" \) | head -1 || true)"
if [[ -n "$f" ]]; then
bin="$(basename "$f")"
cp -f "$f" "$dir/$bin" 2>/dev/null || true
else
log "WARN: no binary in $dir"
return 1
fi
fi
local archive="${name}-${VERSION}-${host}.tar.gz"
tar -C "$dir" -czf "$DIST/$archive" .
(cd "$DIST" && sha_file "$archive" >"${archive}.sha256")
log "staged $archive"
}
SEEDER_OK=0
BOOTER_OK=0
SEEDER_FAIL=0
BOOTER_FAIL=0
IFS=',' read -ra SHOSTS <<<"$BARE_OS_SEEDER_HOSTS"
for h in "${SHOSTS[@]}"; do
h="${h// /}"
[[ -z "$h" ]] && continue
if stage_product seeder "$h"; then SEEDER_OK=$((SEEDER_OK + 1)); else SEEDER_FAIL=$((SEEDER_FAIL + 1)); fi
done
IFS=',' read -ra BHOSTS <<<"$BARE_OS_BOOTER_HOSTS"
for h in "${BHOSTS[@]}"; do
h="${h// /}"
[[ -z "$h" ]] && continue
if stage_product booter "$h"; then BOOTER_OK=$((BOOTER_OK + 1)); else BOOTER_FAIL=$((BOOTER_FAIL + 1)); fi
done
# --- by-arch tarballs (pear-ci stage/seed is offline — see scripts/pear-stage-by-arch.sh) ---
export SKIP_PEAR_STAGE="${SKIP_PEAR_STAGE:-1}"
log "assembling by-arch (SKIP_PEAR_STAGE=$SKIP_PEAR_STAGE)"
chmod +x scripts/pear-stage-by-arch.sh
bash scripts/pear-stage-by-arch.sh
cat >"$DIST/RELEASE_NOTES.md" <<EOF
# bare-operating-system ${VERSION} (${TAG})
- Commit: \`${SHA}\`
- Built: ${STAMP}
- Seeder archives: ${SEEDER_OK} (failed: ${SEEDER_FAIL})
- Booter archives: ${BOOTER_OK} (failed: ${BOOTER_FAIL})
## Hosts (64-bit only)
\`linux-x64\`, \`linux-arm64\`, \`darwin-x64\`, \`darwin-arm64\`, \`win32-x64\`, \`win32-arm64\`
## Seeder (Bare standalone + kernel/)
\`\`\`bash
tar -xzf bare-os-seeder-${VERSION}-linux-x64.tar.gz
./bare-os-seeder # or bare-os-seeder.exe on Windows
# kernel/ must sit next to the binary (included in the archive)
\`\`\`
## Booter (Bare standalone)
\`\`\`bash
tar -xzf bare-os-booter-${VERSION}-linux-x64.tar.gz
./bare-os-booter
./bare-os-booter --datadir /var/lib/bare-os # optional host state root
# Keep qvac-backends/ next to the binary (ggml CPU/Vulkan .so for agent/llama.cpp)
\`\`\`
## Host updates
Rolling Gitea tarballs are the supported update path for these binaries.
\`pear-runtime\` is **not** embedded by default (avoids CodeRange OOM on constrained hosts).
Rebuild with \`node scripts/bare-standalone.cjs --with-ota\` only if you need in-process pear:// OTA.
Optional: \`BARE_OS_HOST_DATA\` / \`--datadir\` for Corestore location (default \`~/.bare-os\`).
by-arch trees: \`by-arch-seeder.tar.gz\`, \`by-arch-booter.tar.gz\`
(pear-ci stage/seed is offline — not run in this CI job)
## Checksums
See \`*.sha256\` beside each archive.
EOF
log "artifacts in $DIST:"
ls -la "$DIST" || true
if [[ "$SEEDER_OK" -eq 0 || "$BOOTER_OK" -eq 0 ]]; then
log "ERROR: need at least one seeder and one booter archive"
exit 1
fi
if [[ "${DRY_RUN:-0}" == "1" ]]; then
log "DRY_RUN=1 — skip Gitea upload"
exit 0
fi
if [[ -z "${RELEASE_TOKEN:-}" ]]; then
log "ERROR: RELEASE_TOKEN is required (or DRY_RUN=1)"
exit 1
fi
if [[ -z "${GITEA_URL}" || -z "${GITEA_OWNER}" || -z "${GITEA_REPO}" ]]; then
log "ERROR: GITEA_URL / GITEA_OWNER / GITEA_REPO must be set"
exit 1
fi
API="${GITEA_URL%/}/api/v1/repos/${GITEA_OWNER}/${GITEA_REPO}"
AUTH="Authorization: token ${RELEASE_TOKEN}"
log "Gitea API: $API tag=$TAG"
REL_JSON="$(curl -fsSL -H "$AUTH" "$API/releases/tags/${TAG}" 2>/dev/null || true)"
if [[ -n "$REL_JSON" ]]; then
REL_ID="$(node -e "try{const j=JSON.parse(process.argv[1]);console.log(j.id||'')}catch{console.log('')}" "$REL_JSON")"
if [[ -n "$REL_ID" ]]; then
log "deleting previous release id=$REL_ID"
curl -fsSL -X DELETE -H "$AUTH" "$API/releases/$REL_ID" >/dev/null || true
fi
fi
curl -fsSL -X DELETE -H "$AUTH" "$API/tags/${TAG}" >/dev/null 2>&1 || true
CREATE_BODY="$(node -e "
const notes=require('fs').readFileSync(process.argv[1],'utf8');
console.log(JSON.stringify({
tag_name: process.argv[2],
name: process.argv[3],
body: notes,
draft: false,
prerelease: true,
target_commitish: process.env.GITHUB_SHA || process.env.GITEA_SHA || 'main'
}));
" "$DIST/RELEASE_NOTES.md" "$TAG" "${RELEASE_TITLE} ${VERSION} ${SHA}")"
CREATE_RESP="$(curl -fsSL -X POST -H "$AUTH" -H 'Content-Type: application/json' \
-d "$CREATE_BODY" "$API/releases")"
REL_ID="$(node -e "console.log(JSON.parse(process.argv[1]).id)" "$CREATE_RESP")"
log "created release id=$REL_ID"
shopt -s nullglob
for f in "$DIST"/*; do
[[ -f "$f" ]] || continue
base="$(basename "$f")"
case "$base" in
*.tar.gz|*.sha256|*.md) ;;
*) continue ;;
esac
log "upload $base"
name_q="$(node -e "console.log(encodeURIComponent(process.argv[1]))" "$base")"
curl -fsSL -X POST -H "$AUTH" \
-F "attachment=@${f}" \
"$API/releases/${REL_ID}/assets?name=${name_q}" \
>/dev/null
done
log "release published: ${GITEA_URL}/${GITEA_OWNER}/${GITEA_REPO}/releases/tag/${TAG}"