564 lines
25 KiB
Bash
Executable File
564 lines
25 KiB
Bash
Executable File
#!/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
|
|
|
|
# Always operate from the repository root, regardless of the caller's cwd.
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
cd "$REPO_ROOT"
|
|
|
|
# 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_DEFAULT="gitea"
|
|
GITHUB_REMOTE_DEFAULT="origin"
|
|
GITEA_API_URL="https://git.sovransystems.com/api/v1"
|
|
CHANGELOG_FILE="CHANGELOG.md"
|
|
|
|
# Auto-detect remotes
|
|
detect_remote() {
|
|
local preferred="$1"
|
|
local keyword="$2"
|
|
if git remote | grep -q -x "$preferred"; then
|
|
echo "$preferred"
|
|
return
|
|
fi
|
|
local found
|
|
found=$(git remote -v | grep -i "$keyword" | head -n 1 | awk '{print $1}')
|
|
if [[ -n "$found" ]]; then
|
|
echo "$found"
|
|
return
|
|
fi
|
|
echo "$preferred"
|
|
}
|
|
|
|
GITEA_REMOTE=$(detect_remote "$GITEA_REMOTE_DEFAULT" "sovransystems\|gitea")
|
|
GITHUB_REMOTE=$(detect_remote "$GITHUB_REMOTE_DEFAULT" "github")
|
|
|
|
echo -e "${BLUE}╔════════════════════════════════════════════════════════════╗${NC}"
|
|
echo -e "${BLUE}║ Sovran_SystemsOS Automated Stable Release Script ║${NC}"
|
|
echo -e "${BLUE}╚════════════════════════════════════════════════════════════╝${NC}"
|
|
echo
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Helper: Get latest tag
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# GitHub release tags are fetched into a private namespace instead of
|
|
# refs/tags/. GitHub and Gitea contain some same-named historical tags that
|
|
# point to different objects; sharing refs/tags/ would make either fetch fail
|
|
# with "would clobber existing tag".
|
|
GITHUB_TAG_NAMESPACE="refs/release-tags/github"
|
|
|
|
get_latest_tag() {
|
|
local exclude="${1:-}"
|
|
local tag
|
|
|
|
while IFS= read -r tag; do
|
|
if [[ -n "$exclude" && "$tag" == "v${exclude#v}" ]]; then
|
|
continue
|
|
fi
|
|
printf '%s\n' "$tag"
|
|
return 0
|
|
done < <(git for-each-ref \
|
|
--sort=-version:refname \
|
|
--format='%(refname:strip=3)' \
|
|
"${GITHUB_TAG_NAMESPACE}/v*")
|
|
|
|
return 0
|
|
}
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# 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: Preflight and fetch everything
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
echo -e "${BLUE}Step 0: Running release preflight...${NC}"
|
|
|
|
# A release must start from an exact, committed, reproducible tree. This also
|
|
# prevents release metadata generated below from being mixed with local work.
|
|
if [[ -n "$(git status --porcelain)" ]]; then
|
|
echo -e "${RED}Error: the working tree is not clean. Commit, stash, or remove local changes first.${NC}" >&2
|
|
git status --short >&2
|
|
exit 1
|
|
fi
|
|
|
|
for remote in "$GITHUB_REMOTE" "$GITEA_REMOTE"; do
|
|
if ! git remote | grep -q -x "$remote"; then
|
|
echo -e "${RED}Error: required remote '$remote' is not configured.${NC}" >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
for command_name in gh curl; do
|
|
if ! command -v "$command_name" >/dev/null 2>&1; then
|
|
echo -e "${RED}Error: required command '$command_name' is not installed.${NC}" >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
if ! gh auth status >/dev/null 2>&1; then
|
|
echo -e "${RED}Error: GitHub CLI is not authenticated. Run 'gh auth login' first.${NC}" >&2
|
|
exit 1
|
|
fi
|
|
if [[ -z "${GITEA_TOKEN:-}" ]]; then
|
|
echo
|
|
read -rsp "Enter your Gitea API token (input hidden): " GITEA_TOKEN
|
|
echo
|
|
fi
|
|
if [[ -z "${GITEA_TOKEN:-}" ]]; then
|
|
echo -e "${RED}Error: a GITEA_TOKEN with write:repository scope is required.${NC}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo -e " Fetching GitHub (${GITHUB_REMOTE}) and Gitea (${GITEA_REMOTE})..."
|
|
# Never fetch either host's tags into refs/tags/. Historical tags with the same
|
|
# name differ between the hosts, and Git correctly refuses to clobber them.
|
|
git fetch "$GITHUB_REMOTE" --prune --no-tags
|
|
git fetch "$GITEA_REMOTE" --prune --no-tags
|
|
git fetch "$GITHUB_REMOTE" --prune --no-tags \
|
|
"+refs/tags/*:${GITHUB_TAG_NAMESPACE}/*"
|
|
|
|
GITHUB_MAIN_REF="refs/remotes/${GITHUB_REMOTE}/main"
|
|
GITEA_STAGING_REF="refs/remotes/${GITEA_REMOTE}/staging-dev"
|
|
|
|
if ! git rev-parse --verify "$GITHUB_MAIN_REF" >/dev/null 2>&1; then
|
|
echo -e "${RED}Error: cannot resolve GitHub main at ${GITHUB_MAIN_REF}.${NC}" >&2
|
|
exit 1
|
|
fi
|
|
if ! git rev-parse --verify "$GITEA_STAGING_REF" >/dev/null 2>&1; then
|
|
echo -e "${RED}Error: cannot resolve Gitea staging-dev at ${GITEA_STAGING_REF}.${NC}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
HEAD_COMMIT=$(git rev-parse HEAD)
|
|
GITHUB_MAIN_COMMIT=$(git rev-parse "$GITHUB_MAIN_REF")
|
|
GITEA_STAGING_COMMIT=$(git rev-parse "$GITEA_STAGING_REF")
|
|
|
|
if [[ "$GITHUB_MAIN_COMMIT" != "$GITEA_STAGING_COMMIT" ]]; then
|
|
echo -e "${RED}Error: GitHub main and Gitea staging-dev are not synchronized.${NC}" >&2
|
|
echo " GitHub main : $GITHUB_MAIN_COMMIT" >&2
|
|
echo " Gitea staging-dev : $GITEA_STAGING_COMMIT" >&2
|
|
exit 1
|
|
fi
|
|
if [[ "$HEAD_COMMIT" != "$GITHUB_MAIN_COMMIT" ]]; then
|
|
echo -e "${RED}Error: local HEAD is not the synchronized release candidate.${NC}" >&2
|
|
echo " Local HEAD : $HEAD_COMMIT" >&2
|
|
echo " Remote HEAD : $GITHUB_MAIN_COMMIT" >&2
|
|
echo "Update/check out the synchronized commit, then run the script again." >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo -e " ${GREEN}✓${NC} Clean tree; GitHub main and Gitea staging-dev match local HEAD"
|
|
|
|
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}"
|
|
|
|
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
|
echo -e "${RED}Error: version must use MAJOR.MINOR.PATCH format (for example, 1.1.1).${NC}" >&2
|
|
exit 1
|
|
fi
|
|
remote_has_tag() {
|
|
local remote="$1"
|
|
[[ -n "$(git ls-remote --tags "$remote" "refs/tags/${TAG}" "refs/tags/${TAG}^{}")" ]]
|
|
}
|
|
|
|
if git show-ref --verify --quiet "refs/tags/${TAG}" || \
|
|
git show-ref --verify --quiet "${GITHUB_TAG_NAMESPACE}/${TAG}" || \
|
|
remote_has_tag "$GITHUB_REMOTE" || remote_has_tag "$GITEA_REMOTE"; then
|
|
echo -e "${RED}Error: tag ${TAG} already exists locally or on a remote.${NC}" >&2
|
|
echo "Refusing to move or overwrite an existing release tag." >&2
|
|
exit 1
|
|
fi
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# 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 headline (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
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Helper: Generate categorized release notes from commit history
|
|
# Groups commits since the last tag into Keep-a-Changelog sections based on
|
|
# conventional-commit prefixes (feat/fix/docs/security/etc.) and keywords.
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
generate_release_notes() {
|
|
local range="$1"
|
|
|
|
local added="" changed="" fixed="" security="" docs=""
|
|
|
|
while IFS= read -r subject; do
|
|
# Skip noise commits
|
|
case "$subject" in
|
|
"Initial plan"|"initial plan") continue ;;
|
|
"Merge pull request"*|"Merge branch"*) continue ;;
|
|
"chore: bump VERSION"*|"docs: update CHANGELOG"*) continue ;;
|
|
"Address code review"*|"Address review"*|"Address validation"*) continue ;;
|
|
esac
|
|
|
|
# Strip conventional-commit prefix for display
|
|
local clean
|
|
clean="$(echo "$subject" | sed -E 's/^(feat|fix|docs|chore|refactor|test|security|perf|style|ci|build)(\([^)]*\))?!?:[[:space:]]*//')"
|
|
# Capitalize first letter
|
|
clean="$(echo "${clean:0:1}" | tr '[:lower:]' '[:upper:]')${clean:1}"
|
|
|
|
case "$subject" in
|
|
security:*|security\(*) security+="- ${clean}"$'\n' ;;
|
|
feat:*|feat\(*) added+="- ${clean}"$'\n' ;;
|
|
fix:*|fix\(*|Fix\ *|fixed\ *) fixed+="- ${clean}"$'\n' ;;
|
|
docs:*|docs\(*) docs+="- ${clean}"$'\n' ;;
|
|
refactor:*|refactor\(*|chore:*|chore\(*|removed\ *|Updated\ *|updated\ *) changed+="- ${clean}"$'\n' ;;
|
|
test:*|test\(*) continue ;;
|
|
*) added+="- ${clean}"$'\n' ;;
|
|
esac
|
|
done < <(git log --no-merges --format='%s' "$range" 2>/dev/null | awk '!seen[$0]++')
|
|
|
|
local notes=""
|
|
if [[ -n "$added" ]]; then notes+=$'### Added\n'"$added"$'\n'; fi
|
|
if [[ -n "$changed" ]]; then notes+=$'### Changed\n'"$changed"$'\n'; fi
|
|
if [[ -n "$fixed" ]]; then notes+=$'### Fixed\n'"$fixed"$'\n'; fi
|
|
if [[ -n "$security" ]]; then notes+=$'### Security\n'"$security"$'\n'; fi
|
|
if [[ -n "$docs" ]]; then notes+=$'### Documentation\n'"$docs"$'\n'; fi
|
|
|
|
if [[ -z "$notes" ]]; then
|
|
notes=$'### Changed\n- Incremental stable updates\n'
|
|
fi
|
|
|
|
printf '%s' "$notes"
|
|
}
|
|
|
|
# Build notes from the previous canonical GitHub tag. Use its private ref so a
|
|
# conflicting local or Gitea tag with the same name cannot select the wrong
|
|
# commit.
|
|
PREV_TAG=$(get_latest_tag "$TAG")
|
|
PREV_TAG_REF="${GITHUB_TAG_NAMESPACE}/${PREV_TAG}"
|
|
if [[ -n "$PREV_TAG" ]] && git rev-parse -q --verify "$PREV_TAG_REF" >/dev/null; then
|
|
COMMIT_RANGE="${PREV_TAG_REF}..HEAD"
|
|
COMMIT_RANGE_DISPLAY="${PREV_TAG}..HEAD"
|
|
else
|
|
COMMIT_RANGE="HEAD"
|
|
COMMIT_RANGE_DISPLAY="HEAD"
|
|
fi
|
|
|
|
echo
|
|
echo -e "${BLUE}Generating draft release notes from ${COMMIT_RANGE_DISPLAY}...${NC}"
|
|
RELEASE_NOTES="$(generate_release_notes "$COMMIT_RANGE")"
|
|
|
|
# Let the user review/edit the generated notes before publishing
|
|
NOTES_FILE="$(mktemp "/tmp/release-notes-${TAG}.XXXXXX.md")"
|
|
trap 'rm -f "$NOTES_FILE"' EXIT
|
|
{
|
|
echo "## Sovran_SystemsOS ${TAG}"
|
|
echo
|
|
echo "${RELEASE_MESSAGE}"
|
|
echo
|
|
echo "$RELEASE_NOTES"
|
|
echo "**Full changelog:** [CHANGELOG.md](https://github.com/naturallaw777/Sovran_SystemsOS/blob/main/CHANGELOG.md)"
|
|
} > "$NOTES_FILE"
|
|
|
|
echo -e " ${GREEN}✓${NC} Draft notes written to: ${CYAN}${NOTES_FILE}${NC}"
|
|
echo
|
|
echo "──────────────── Draft Release Notes ────────────────"
|
|
cat "$NOTES_FILE"
|
|
echo "──────────────────────────────────────────────────────"
|
|
echo
|
|
read -rp "Edit the notes before publishing? (y/N): " edit_confirm
|
|
if [[ "$edit_confirm" =~ ^[Yy]$ ]]; then
|
|
"${EDITOR:-nano}" "$NOTES_FILE"
|
|
RELEASE_NOTES="$(sed -n '/^###/,$p' "$NOTES_FILE" | sed '/^\*\*Full changelog/d')"
|
|
fi
|
|
RELEASE_BODY="$(cat "$NOTES_FILE")"
|
|
|
|
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: Prepare and commit all release metadata
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
echo
|
|
echo -e "${BLUE}Step 1: Preparing release metadata for ${TAG}...${NC}"
|
|
|
|
# VERSION drives ISO naming.
|
|
echo "${VERSION}" > VERSION
|
|
|
|
# Update every versioned ISO filename in README.md.
|
|
README_FILE="README.md"
|
|
if [[ ! -f "$README_FILE" ]]; then
|
|
echo -e "${RED}Error: ${README_FILE} not found.${NC}" >&2
|
|
exit 1
|
|
fi
|
|
OLD_ISO_VER=$(grep -oE 'Sovran_SystemsOS-[0-9]+\.[0-9]+\.[0-9]+\.iso' "$README_FILE" \
|
|
| head -1 | sed 's/Sovran_SystemsOS-//; s/\.iso//')
|
|
if [[ -z "$OLD_ISO_VER" ]]; then
|
|
echo -e "${RED}Error: no versioned ISO reference found in ${README_FILE}.${NC}" >&2
|
|
exit 1
|
|
fi
|
|
if [[ "$OLD_ISO_VER" != "$VERSION" ]]; then
|
|
sed "s/Sovran_SystemsOS-${OLD_ISO_VER}/Sovran_SystemsOS-${VERSION}/g" \
|
|
"$README_FILE" > "$README_FILE.tmp"
|
|
mv "$README_FILE.tmp" "$README_FILE"
|
|
fi
|
|
|
|
# Add the changelog entry before tagging so the tag and stable branch contain it.
|
|
TODAY=$(date +%Y-%m-%d)
|
|
NEW_ENTRY="## [${VERSION}] - ${TODAY}
|
|
|
|
${RELEASE_NOTES}
|
|
[${VERSION}]: https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS/releases/tag/${TAG}
|
|
"
|
|
|
|
if [[ ! -f "$CHANGELOG_FILE" ]]; then
|
|
echo -e "${RED}Error: ${CHANGELOG_FILE} not found.${NC}" >&2
|
|
exit 1
|
|
fi
|
|
awk -v new_entry="$NEW_ENTRY" '
|
|
BEGIN { printed=0 }
|
|
/^---$/ && !printed {
|
|
print
|
|
print ""
|
|
print new_entry
|
|
printed=1
|
|
next
|
|
}
|
|
{ print }
|
|
END { if (!printed) exit 2 }
|
|
' "$CHANGELOG_FILE" > "${CHANGELOG_FILE}.tmp" || {
|
|
rm -f "${CHANGELOG_FILE}.tmp"
|
|
echo -e "${RED}Error: could not find the changelog insertion marker.${NC}" >&2
|
|
exit 1
|
|
}
|
|
mv "${CHANGELOG_FILE}.tmp" "$CHANGELOG_FILE"
|
|
|
|
git add VERSION "$README_FILE" "$CHANGELOG_FILE"
|
|
if git diff --cached --quiet; then
|
|
echo -e "${RED}Error: release preparation produced no changes.${NC}" >&2
|
|
exit 1
|
|
fi
|
|
git commit -m "chore(release): prepare ${TAG}"
|
|
RELEASE_COMMIT=$(git rev-parse HEAD)
|
|
|
|
echo -e " ${GREEN}✓${NC} VERSION, README, and CHANGELOG committed"
|
|
echo -e " Release commit: ${CYAN}${RELEASE_COMMIT}${NC}"
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Step 2: Publish the final release commit to every release branch
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
echo
|
|
echo -e "${BLUE}Step 2: Publishing the final release commit...${NC}"
|
|
|
|
# GitHub main and Gitea staging-dev were verified equal during preflight, so
|
|
# these are normal fast-forward pushes. Stable is an intentional promotion and
|
|
# uses a lease to prevent overwriting a branch changed since the fetch.
|
|
git push "$GITHUB_REMOTE" HEAD:main
|
|
git push "$GITEA_REMOTE" HEAD:staging-dev
|
|
git push "$GITEA_REMOTE" HEAD:stable --force-with-lease
|
|
|
|
# Verify all three branch tips before creating an immutable release tag.
|
|
remote_branch_commit() {
|
|
local remote="$1"
|
|
local branch="$2"
|
|
git ls-remote "$remote" "refs/heads/${branch}" | awk 'NR == 1 { print $1 }'
|
|
}
|
|
|
|
PUBLISHED_GITHUB=$(remote_branch_commit "$GITHUB_REMOTE" main)
|
|
PUBLISHED_STAGING=$(remote_branch_commit "$GITEA_REMOTE" staging-dev)
|
|
PUBLISHED_STABLE=$(remote_branch_commit "$GITEA_REMOTE" stable)
|
|
if [[ "$PUBLISHED_GITHUB" != "$RELEASE_COMMIT" || \
|
|
"$PUBLISHED_STAGING" != "$RELEASE_COMMIT" || \
|
|
"$PUBLISHED_STABLE" != "$RELEASE_COMMIT" ]]; then
|
|
echo -e "${RED}Error: post-push verification failed; no release tag was created.${NC}" >&2
|
|
echo " Expected : $RELEASE_COMMIT" >&2
|
|
echo " GitHub main : ${PUBLISHED_GITHUB:-missing}" >&2
|
|
echo " Gitea staging-dev : ${PUBLISHED_STAGING:-missing}" >&2
|
|
echo " Gitea stable : ${PUBLISHED_STABLE:-missing}" >&2
|
|
exit 1
|
|
fi
|
|
echo -e " ${GREEN}✓${NC} GitHub main, Gitea staging-dev, and Gitea stable all match"
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Step 3: Tag the final release commit and publish the tag
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
echo
|
|
echo -e "${BLUE}Step 3: Creating annotated tag ${TAG} on ${RELEASE_COMMIT}...${NC}"
|
|
git tag -a "$TAG" -m "${RELEASE_MESSAGE}
|
|
|
|
- Stable release of Sovran_SystemsOS
|
|
- See CHANGELOG.md for full details" "$RELEASE_COMMIT"
|
|
|
|
git push "$GITHUB_REMOTE" "$TAG"
|
|
git push "$GITEA_REMOTE" "$TAG"
|
|
|
|
# For an annotated tag, ^{} resolves the commit referenced by the tag object.
|
|
remote_tag_commit() {
|
|
local remote="$1"
|
|
git ls-remote "$remote" "refs/tags/${TAG}^{}" | awk 'NR == 1 { print $1 }'
|
|
}
|
|
GITHUB_TAG_COMMIT=$(remote_tag_commit "$GITHUB_REMOTE")
|
|
GITEA_TAG_COMMIT=$(remote_tag_commit "$GITEA_REMOTE")
|
|
if [[ "$GITHUB_TAG_COMMIT" != "$RELEASE_COMMIT" || "$GITEA_TAG_COMMIT" != "$RELEASE_COMMIT" ]]; then
|
|
echo -e "${RED}Error: published tag verification failed.${NC}" >&2
|
|
exit 1
|
|
fi
|
|
echo -e " ${GREEN}✓${NC} ${TAG} points to the final release commit on GitHub and Gitea"
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Step 4: Create GitHub release via gh CLI
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
echo
|
|
echo -e "${BLUE}Step 4: Creating GitHub release...${NC}"
|
|
|
|
title_suffix="${RELEASE_MESSAGE#Sovran_SystemsOS v* — }"
|
|
title_suffix="${title_suffix#Sovran_SystemsOS * — }"
|
|
gh release create "$TAG" \
|
|
--repo naturallaw777/Sovran_SystemsOS \
|
|
--title "${TAG} — ${title_suffix}" \
|
|
--notes-file "$NOTES_FILE"
|
|
echo -e " ${GREEN}✓${NC} GitHub release created successfully"
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Step 5: Create Gitea release via API
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
echo
|
|
echo -e "${BLUE}Step 5: Creating Gitea release via API...${NC}"
|
|
|
|
GITEA_REPO="Sovran_Systems/Sovran_SystemsOS"
|
|
|
|
# Build JSON safely because release notes may contain quotes and newlines.
|
|
if command -v jq >/dev/null 2>&1; then
|
|
PAYLOAD=$(jq -n \
|
|
--arg tag "$TAG" \
|
|
--arg name "$TAG — Stable Release" \
|
|
--arg body "$RELEASE_BODY" \
|
|
'{tag_name: $tag, name: $name, body: $body, draft: false, prerelease: false}')
|
|
else
|
|
PAYLOAD=$(python3 -c "import json,sys; print(json.dumps({'tag_name': sys.argv[1], 'name': sys.argv[1] + ' — Stable Release', 'body': open(sys.argv[2]).read(), 'draft': False, 'prerelease': False}))" "$TAG" "$NOTES_FILE")
|
|
fi
|
|
|
|
RESPONSE_FILE=$(mktemp "/tmp/gitea-release-${TAG}.XXXXXX.json")
|
|
HTTP_STATUS=$(curl -sS -o "$RESPONSE_FILE" -w '%{http_code}' -X POST \
|
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
|
-H "Content-Type: application/json" \
|
|
-d "$PAYLOAD" \
|
|
"${GITEA_API_URL}/repos/${GITEA_REPO}/releases")
|
|
|
|
if [[ "$HTTP_STATUS" != "201" ]]; then
|
|
echo -e "${RED}Error: Gitea release creation failed (HTTP ${HTTP_STATUS}).${NC}" >&2
|
|
cat "$RESPONSE_FILE" >&2
|
|
rm -f "$RESPONSE_FILE"
|
|
exit 1
|
|
fi
|
|
rm -f "$RESPONSE_FILE"
|
|
echo -e " ${GREEN}✓${NC} Gitea release created successfully"
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# 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 " • Build the installer ISO:"
|
|
echo " nix build .#nixosConfigurations.sovran_systemsos-iso.config.system.build.isoImage"
|
|
echo " • Package, verify, and upload ISO to CDN:"
|
|
echo " ./scripts/upload-cdn.sh --upload"
|
|
echo " • Verify the ISO download and checksum from the public CDN"
|
|
echo " • Verify releases on both GitHub and Gitea"
|
|
echo
|
|
echo -e "${CYAN}Tag created: ${TAG}${NC}"
|
|
git show "${TAG}" --quiet
|
|
|
|
# Clean up temp notes file
|
|
rm -f "${NOTES_FILE}"
|