feat: automated stable releases, versioned ISOs, and Hub version badge

- Add release-stable.sh script with automatic tagging, CHANGELOG updates,
  GitHub/Gitea release creation, and VERSION file management
- Update iso/common.nix to include version from VERSION file in ISO filename
- Add VERSION file (current: 1.0.3)
- Polish OS version badge in Sovran Hub header (top-right)
- Update README.md download link to versioned ISO
- Update CHANGELOG.md with existing tags (v1.0.0 – v1.0.3)

Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
This commit is contained in:
naturallaw777
2026-07-29 15:11:59 +00:00
co-authored by arena-agent
parent ac6a2e26bb
commit c54823ff68
8 changed files with 436 additions and 2 deletions
+59
View File
@@ -0,0 +1,59 @@
# Changelog
All notable changes to Sovran_SystemsOS will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
---
## [1.0.3] - 2026-07-29
### Added
- Incremental stable updates
## [1.0.2] - 2026-07-29
### Added
- Incremental stable updates
## [1.0.1] - 2026-07-29
### Added
- Incremental stable updates
## [1.0.0] - 2026-07-29
### Added
- Initial stable release of Sovran_SystemsOS
- Full Bitcoin self-custody desktop environment
- Integrated Sparrow Wallet, Bisq, and Bisq 2
- Comprehensive Sovran Hub for service management
- NixOS-based operating system with privacy and sovereignty focus
- Support for Bitcoin Knots + BIP110, Electrs, LND, Ride The Lightning, BTCPay Server, and more
- Server + Desktop hybrid mode with Matrix, Nextcloud, VaultWarden, and other self-hosted services
- Automated installer with graphical GNOME desktop
- Tor integration and onion services for all major components
- Remote desktop (GNOME Remote Desktop) support
- Role-based system configuration (desktop, server, server+desktop)
- Detailed service credential management in the Sovran Hub
### Changed
- Moved from development/main branch to dedicated `stable` branch for production releases
### Security
- All services run with least-privilege principles where possible
- Secrets stored in `/var/lib/secrets/`
- Strong emphasis on user-controlled keys and self-sovereignty
---
## [Unreleased]
### Added
- (Nothing yet)
[1.0.3]: https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS/releases/tag/v1.0.3
[1.0.2]: https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS/releases/tag/v1.0.2
[1.0.1]: https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS/releases/tag/v1.0.1
[1.0.0]: https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS/releases/tag/v1.0.0
+1 -1
View File
@@ -21,7 +21,7 @@ Lightning infrastructure when you are ready.
**Privacy. Sovereignty. Bitcoin.** **Privacy. Sovereignty. Bitcoin.**
[Visit the Website](https://sovransystems.com) · [Visit the Website](https://sovransystems.com) ·
[Download the ISO](https://downloads.sovransystems.com/Sovran_SystemsOS.iso) · [Download the ISO](https://downloads.sovransystems.com/Sovran_SystemsOS-1.0.3.iso) ·
[Verify the Download](https://downloads.sovransystems.com/Sovran_SystemsOS.iso.sha256) · [Verify the Download](https://downloads.sovransystems.com/Sovran_SystemsOS.iso.sha256) ·
[Build from Source](#build-from-source) [Build from Source](#build-from-source)
+1
View File
@@ -0,0 +1 @@
1.0.3
+17
View File
@@ -2274,13 +2274,30 @@ async def api_logout(request: Request):
return response return response
def _get_sovran_version() -> str:
"""Read the OS version from the VERSION file."""
try:
with open("/etc/nixos/VERSION", "r") as f:
return f.read().strip()
except FileNotFoundError:
try:
with open("VERSION", "r") as f: # fallback for development
return f.read().strip()
except FileNotFoundError:
return "dev"
except Exception:
return "dev"
@app.get("/", response_class=HTMLResponse) @app.get("/", response_class=HTMLResponse)
async def index(request: Request): async def index(request: Request):
sovran_version = _get_sovran_version()
return templates.TemplateResponse( return templates.TemplateResponse(
request=request, request=request,
name="index.html", name="index.html",
context={ context={
"asset_version": ASSET_VERSION, "asset_version": ASSET_VERSION,
"sovran_version": sovran_version,
}, },
) )
@@ -45,6 +45,44 @@
letter-spacing: 0.03em; letter-spacing: 0.03em;
} }
/* ── OS Version Badge (polished) ───────────────────────────────── */
.os-version-badge {
display: inline-flex;
align-items: center;
background: linear-gradient(145deg, #1a2a22, #0f1a15);
border: 1px solid rgba(74, 222, 128, 0.25);
border-radius: 9999px;
padding: 2px 14px 2px 10px;
font-size: 0.75rem;
font-weight: 600;
color: #4ade80;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
margin-left: 12px;
white-space: nowrap;
}
.os-version-badge:hover {
border-color: rgba(74, 222, 128, 0.45);
box-shadow: 0 4px 8px rgba(74, 222, 128, 0.15);
transform: translateY(-1px);
}
.os-version-badge .version-label {
font-size: 0.68rem;
font-weight: 500;
color: #4ade80;
opacity: 0.85;
margin-right: 2px;
}
.os-version-badge .version-number {
font-family: 'JetBrains Mono', 'Fira Code', monospace;
font-weight: 700;
letter-spacing: 0.5px;
color: #86efac;
}
/* ── IP bar ─────────────────────────────────────────────────────── */ /* ── IP bar ─────────────────────────────────────────────────────── */
.ip-bar { .ip-bar {
@@ -22,6 +22,13 @@
<header class="header-bar"> <header class="header-bar">
<img src="/static/sovran-hub-icon.svg" alt="Sovran Hub" class="header-logo" /> <img src="/static/sovran-hub-icon.svg" alt="Sovran Hub" class="header-logo" />
<span class="title">Sovran_SystemsOS Hub</span> <span class="title">Sovran_SystemsOS Hub</span>
<!-- OS Version Badge -->
<span class="os-version-badge" id="os-version-badge" title="Sovran_SystemsOS Version">
<span class="version-label">v</span>
<span class="version-number" id="version-number">{{ sovran_version }}</span>
</span>
<div class="header-buttons"> <div class="header-buttons">
<span class="role-badge" id="role-badge">Loading…</span> <span class="role-badge" id="role-badge">Loading…</span>
<button class="btn btn-header-reboot" id="btn-header-reboot" title="Restart the entire computer">Reboot</button> <button class="btn btn-header-reboot" id="btn-header-reboot" title="Restart the entire computer">Reboot</button>
+10 -1
View File
@@ -3,6 +3,14 @@
let let
sovranSource = builtins.path { path = ../.; name = "sovran-systemsos"; }; sovranSource = builtins.path { path = ../.; name = "sovran-systemsos"; };
# Read version from VERSION file if it exists, otherwise fall back to "dev"
versionFile = if builtins.pathExists ../VERSION
then builtins.readFile ../VERSION
else "dev";
# Clean version (remove 'v' prefix and newlines)
cleanVersion = builtins.replaceStrings ["v" "\n" "\r"] [""] versionFile;
pythonEnv = pkgs.python3.withPackages (ps: [ ps.pygobject3 ps.pycairo ]); pythonEnv = pkgs.python3.withPackages (ps: [ ps.pygobject3 ps.pycairo ]);
installerPy = pkgs.writeShellScriptBin "sovran-install" '' installerPy = pkgs.writeShellScriptBin "sovran-install" ''
@@ -18,7 +26,8 @@ in
"${modulesPath}/installer/cd-dvd/installation-cd-graphical-gnome.nix" "${modulesPath}/installer/cd-dvd/installation-cd-graphical-gnome.nix"
]; ];
image.baseName = lib.mkForce "Sovran_SystemsOS"; # Dynamic ISO filename with version
image.baseName = lib.mkForce "Sovran_SystemsOS-${cleanVersion}";
isoImage.splashImage = ./assets/splash-logo.png; isoImage.splashImage = ./assets/splash-logo.png;
services.gnome.gnome-initial-setup.enable = false; services.gnome.gnome-initial-setup.enable = false;
+303
View File
@@ -0,0 +1,303 @@
#!/usr/bin/env bash
#
# release-stable.sh
# Automated stable release script for Sovran_SystemsOS
#
# Features:
# - Detects latest tag and suggests next version
# - Pushes main → stable on Gitea
# - Creates annotated tag
# - Auto-updates CHANGELOG.md
# - Creates releases on both GitHub and Gitea via API
#
# Usage:
# ./scripts/release-stable.sh [version] [--message "text"]
#
# Requirements:
# - gh CLI (for GitHub releases)
# - GITEA_TOKEN env var (for Gitea releases)
#
set -euo pipefail
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m'
# Configuration
GITEA_REMOTE="gitea"
GITHUB_REMOTE="origin"
GITEA_API_URL="https://git.sovransystems.com/api/v1"
CHANGELOG_FILE="CHANGELOG.md"
echo -e "${BLUE}╔════════════════════════════════════════════════════════════╗${NC}"
echo -e "${BLUE}║ Sovran_SystemsOS Automated Stable Release Script ║${NC}"
echo -e "${BLUE}╚════════════════════════════════════════════════════════════╝${NC}"
echo
# ─────────────────────────────────────────────────────────────────────────────
# Helper: Get latest tag
# ─────────────────────────────────────────────────────────────────────────────
get_latest_tag() {
git tag --list 'v*' --sort=-version:refname | head -1 || echo ""
}
# ─────────────────────────────────────────────────────────────────────────────
# Helper: Suggest next version (patch bump)
# ─────────────────────────────────────────────────────────────────────────────
suggest_next_version() {
local current="$1"
if [[ -z "$current" ]]; then
echo "1.0.0"
return
fi
# Remove 'v' prefix
local ver="${current#v}"
IFS='.' read -r major minor patch <<< "$ver"
# Default: bump patch
echo "${major}.$((minor)).$((patch + 1))"
}
# ─────────────────────────────────────────────────────────────────────────────
# Step 0: Fetch everything
# ─────────────────────────────────────────────────────────────────────────────
echo -e "${BLUE}Step 0: Fetching latest from all remotes...${NC}"
git fetch --all --tags --prune
LATEST_TAG=$(get_latest_tag)
NEXT_VERSION=$(suggest_next_version "$LATEST_TAG")
echo -e " Latest tag : ${CYAN}${LATEST_TAG:-none}${NC}"
echo -e " Suggested : ${GREEN}v${NEXT_VERSION}${NC}"
echo
# ─────────────────────────────────────────────────────────────────────────────
# Get version from user or argument
# ─────────────────────────────────────────────────────────────────────────────
if [ $# -ge 1 ] && [[ "$1" != --* ]]; then
VERSION="$1"
shift
else
read -rp "Enter version to release (default: ${NEXT_VERSION}): " input_version
VERSION="${input_version:-$NEXT_VERSION}"
fi
# Strip 'v' if user added it
VERSION="${VERSION#v}"
TAG="v${VERSION}"
# ─────────────────────────────────────────────────────────────────────────────
# Get release message
# ─────────────────────────────────────────────────────────────────────────────
RELEASE_MESSAGE=""
while [[ $# -gt 0 ]]; do
case $1 in
--message|-m)
RELEASE_MESSAGE="$2"
shift 2
;;
*)
shift
;;
esac
done
if [[ -z "$RELEASE_MESSAGE" ]]; then
echo
read -rp "Enter release message (or press Enter for default): " input_msg
if [[ -n "$input_msg" ]]; then
RELEASE_MESSAGE="$input_msg"
else
RELEASE_MESSAGE="Sovran_SystemsOS ${TAG} — Stable Release"
fi
fi
echo
echo -e "${YELLOW}════════════════════════════════════════════════════════════${NC}"
echo -e "${YELLOW} Preparing Release${NC}"
echo -e "${YELLOW}════════════════════════════════════════════════════════════${NC}"
echo " Version : ${TAG}"
echo " Message : ${RELEASE_MESSAGE}"
echo " Gitea Remote : ${GITEA_REMOTE}"
echo " GitHub Remote : ${GITHUB_REMOTE}"
echo
read -rp "Proceed with this release? (y/N): " confirm
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
echo "Aborted."
exit 0
fi
# ─────────────────────────────────────────────────────────────────────────────
# Step 1: Push main to stable
# ─────────────────────────────────────────────────────────────────────────────
echo
echo -e "${BLUE}Step 1: Pushing main → stable on Gitea...${NC}"
git push "${GITEA_REMOTE}" main:stable --force-with-lease
# ─────────────────────────────────────────────────────────────────────────────
# Step 2: Create annotated tag
# ─────────────────────────────────────────────────────────────────────────────
echo
echo -e "${BLUE}Step 2: Creating annotated tag ${TAG}...${NC}"
git tag -a "${TAG}" -m "${RELEASE_MESSAGE}
- Stable release of Sovran_SystemsOS
- See CHANGELOG.md for full details"
git push "${GITEA_REMOTE}" "${TAG}"
# Update VERSION file for ISO builds
echo "${VERSION}" > VERSION
git add VERSION
git commit -m "chore: bump VERSION to ${TAG} for ISO naming" || true
echo -e " ${GREEN}${NC} VERSION file updated to ${VERSION}"
# ─────────────────────────────────────────────────────────────────────────────
# Step 3: Auto-update CHANGELOG.md
# ─────────────────────────────────────────────────────────────────────────────
echo
echo -e "${BLUE}Step 3: Updating ${CHANGELOG_FILE}...${NC}"
TODAY=$(date +%Y-%m-%d)
# Create new changelog entry
NEW_ENTRY="## [${VERSION}] - ${TODAY}
### Added
- (Add new features here)
### Changed
- (Add changes here)
### Fixed
- (Add bug fixes here)
[${VERSION}]: ${GITEA_API_URL%/*}/Sovran_Systems/Sovran_SystemsOS/releases/tag/${TAG}
"
# Prepend to changelog (after the header)
if [ -f "$CHANGELOG_FILE" ]; then
# Backup
cp "$CHANGELOG_FILE" "${CHANGELOG_FILE}.bak"
# Insert new section after the first --- line
awk -v new_entry="$NEW_ENTRY" '
BEGIN { printed=0 }
/^---$/ && !printed {
print
print ""
print new_entry
printed=1
next
}
{ print }
' "$CHANGELOG_FILE" > "${CHANGELOG_FILE}.tmp" && mv "${CHANGELOG_FILE}.tmp" "$CHANGELOG_FILE"
echo -e " ${GREEN}${NC} CHANGELOG.md updated with new section for ${TAG}"
else
echo -e " ${YELLOW}${NC} CHANGELOG.md not found — skipping"
fi
# Commit the changelog update
git add "$CHANGELOG_FILE"
if git diff --cached --quiet; then
echo " (No changes to commit in changelog)"
else
git commit -m "docs: update CHANGELOG.md for ${TAG}"
echo -e " ${GREEN}${NC} Committed changelog update"
# Ask if user wants to push
echo
read -rp "Push the changelog commit to GitHub now? (y/N): " push_confirm
if [[ "$push_confirm" =~ ^[Yy]$ ]]; then
echo -e "${BLUE}Pushing changelog commit...${NC}"
git push "${GITHUB_REMOTE}" main
echo -e " ${GREEN}${NC} Changelog pushed to GitHub"
else
echo " (Changelog commit left local — remember to push later)"
fi
fi
# ─────────────────────────────────────────────────────────────────────────────
# Step 4: Create GitHub Release via gh CLI
# ─────────────────────────────────────────────────────────────────────────────
echo
echo -e "${BLUE}Step 4: Creating GitHub Release...${NC}"
if command -v gh &>/dev/null; then
if gh release create "${TAG}" \
--repo naturallaw777/Sovran_SystemsOS \
--title "${TAG}" \
--notes "${RELEASE_MESSAGE}" \
--target main 2>/dev/null; then
echo -e " ${GREEN}${NC} GitHub release created successfully"
else
echo -e " ${YELLOW}${NC} GitHub release may already exist or failed"
fi
else
echo -e " ${YELLOW}${NC} gh CLI not found — skipping GitHub release"
fi
# ─────────────────────────────────────────────────────────────────────────────
# Step 5: Create Gitea Release via API
# ─────────────────────────────────────────────────────────────────────────────
echo
echo -e "${BLUE}Step 5: Creating Gitea Release via API...${NC}"
# ── Gitea Token Handling ─────────────────────────────────────────────────────
if [[ -z "${GITEA_TOKEN:-}" ]]; then
echo
echo -e "${YELLOW}GITEA_TOKEN is not set.${NC}"
read -rsp "Enter your Gitea API token (input will be hidden): " GITEA_TOKEN
echo
if [[ -z "$GITEA_TOKEN" ]]; then
echo -e " ${YELLOW}${NC} No token provided — skipping Gitea release"
GITEA_TOKEN=""
fi
fi
if [[ -n "${GITEA_TOKEN:-}" ]]; then
GITEA_REPO="Sovran_Systems/Sovran_SystemsOS"
RESPONSE=$(curl -s -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d "{
\"tag_name\": \"${TAG}\",
\"name\": \"${TAG}\",
\"body\": \"${RELEASE_MESSAGE}\",
\"draft\": false,
\"prerelease\": false
}" \
"${GITEA_API_URL}/repos/${GITEA_REPO}/releases" 2>/dev/null || echo "")
if echo "$RESPONSE" | grep -q '"id"'; then
echo -e " ${GREEN}${NC} Gitea release created successfully"
else
echo -e " ${YELLOW}${NC} Gitea release creation failed or already exists"
echo " Response: $RESPONSE"
fi
fi
# ─────────────────────────────────────────────────────────────────────────────
# Final Summary
# ─────────────────────────────────────────────────────────────────────────────
echo
echo -e "${GREEN}╔════════════════════════════════════════════════════════════╗${NC}"
echo -e "${GREEN}║ ✅ Release ${TAG} completed successfully! ║${NC}"
echo -e "${GREEN}╚════════════════════════════════════════════════════════════╝${NC}"
echo
echo "Next manual steps (recommended):"
echo " • Review and enhance the new section in CHANGELOG.md"
echo " • Push the changelog commit: git push origin main"
echo " • Verify releases on both GitHub and Gitea"
echo
echo -e "${CYAN}Tag created: ${TAG}${NC}"
git show "${TAG}" --quiet