326 lines
12 KiB
Bash
Executable File
326 lines
12 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# =============================================================================
|
|
# Changemaker Lite V2 — Build Release Tarball
|
|
#
|
|
# Creates a lightweight release tarball (~1-2 MB) containing only runtime files
|
|
# needed to deploy with pre-built Docker images. No source code included.
|
|
#
|
|
# Usage:
|
|
# ./scripts/build-release.sh [OPTIONS]
|
|
#
|
|
# Options:
|
|
# --tag TAG Version tag (default: git describe or commit SHA)
|
|
# --output DIR Output directory (default: ./releases/)
|
|
# --upload Upload to Gitea Releases API after building
|
|
# --replace Delete + recreate an existing release with this tag
|
|
# (DESTRUCTIVE: users already on TAG see no upgrade signal)
|
|
# --dry-run Show what would be included without creating tarball
|
|
# --help Show this help
|
|
#
|
|
# Prerequisites:
|
|
# Run ./scripts/build-and-push.sh first to push Docker images to registry.
|
|
# =============================================================================
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
|
|
|
# --- Defaults ---
|
|
TAG=""
|
|
OUTPUT_DIR="${PROJECT_DIR}/releases"
|
|
UPLOAD=false
|
|
DRY_RUN=false
|
|
REPLACE=false
|
|
|
|
# --- Colors ---
|
|
if [[ -t 1 ]] && [[ -z "${NO_COLOR:-}" ]]; then
|
|
RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[0;33m'
|
|
BLUE='\033[0;34m' CYAN='\033[0;36m' BOLD='\033[1m' NC='\033[0m'
|
|
else
|
|
RED='' GREEN='' YELLOW='' BLUE='' CYAN='' BOLD='' NC=''
|
|
fi
|
|
|
|
info() { echo -e "${BLUE}[INFO]${NC} $*"; }
|
|
success() { echo -e "${GREEN}[OK]${NC} $*"; }
|
|
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
|
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
|
|
|
|
# --- Arg parser ---
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--tag) TAG="$2"; shift 2 ;;
|
|
--output) OUTPUT_DIR="$2"; shift 2 ;;
|
|
--upload) UPLOAD=true; shift ;;
|
|
--replace) REPLACE=true; shift ;;
|
|
--dry-run) DRY_RUN=true; shift ;;
|
|
--help|-h)
|
|
sed -n '2,20p' "$0" | grep '^#' | sed 's/^# \?//'
|
|
exit 0 ;;
|
|
*) error "Unknown option: $1"; exit 1 ;;
|
|
esac
|
|
done
|
|
|
|
# --- Determine version ---
|
|
cd "$PROJECT_DIR"
|
|
if [[ -z "$TAG" ]]; then
|
|
TAG="$(git describe --tags --always 2>/dev/null || git rev-parse --short HEAD)"
|
|
fi
|
|
COMMIT_SHA="$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")"
|
|
BUILD_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
|
|
echo -e "${BOLD}Changemaker Lite — Build Release${NC}"
|
|
echo " Tag: $TAG"
|
|
echo " Commit: $COMMIT_SHA"
|
|
echo " Date: $BUILD_DATE"
|
|
echo ""
|
|
|
|
# --- Create staging directory ---
|
|
STAGE_DIR="$(mktemp -d)/changemaker-lite"
|
|
mkdir -p "$STAGE_DIR"
|
|
|
|
# --- Write VERSION file ---
|
|
cat > "$STAGE_DIR/VERSION" << EOF
|
|
$TAG
|
|
$COMMIT_SHA
|
|
$BUILD_DATE
|
|
EOF
|
|
info "VERSION: $TAG ($COMMIT_SHA)"
|
|
|
|
# --- Copy production docker-compose ---
|
|
if [[ ! -f "$PROJECT_DIR/docker-compose.prod.yml" ]]; then
|
|
error "docker-compose.prod.yml not found. Generate it first."
|
|
exit 1
|
|
fi
|
|
|
|
# Fail the release if dev and prod compose files have drifted on critical
|
|
# healthcheck blocks — catches cases where one file was patched without
|
|
# the other (bit us on the api start_period fix).
|
|
if [[ -x "$PROJECT_DIR/scripts/validate-compose-parity.sh" ]]; then
|
|
if ! bash "$PROJECT_DIR/scripts/validate-compose-parity.sh"; then
|
|
error "Compose parity check failed. Aborting release build."
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
cp "$PROJECT_DIR/docker-compose.prod.yml" "$STAGE_DIR/docker-compose.yml"
|
|
info "docker-compose.yml (production)"
|
|
|
|
# --- Copy config files ---
|
|
cp "$PROJECT_DIR/.env.example" "$STAGE_DIR/"
|
|
cp "$PROJECT_DIR/config.sh" "$STAGE_DIR/"
|
|
info "Config files (.env.example, config.sh)"
|
|
|
|
# --- Copy scripts ---
|
|
mkdir -p "$STAGE_DIR/scripts"
|
|
|
|
# Init scripts (from api/prisma/ to scripts/)
|
|
cp "$PROJECT_DIR/api/prisma/init-nocodb-db.sh" "$STAGE_DIR/scripts/"
|
|
cp "$PROJECT_DIR/api/prisma/init-gancio-db.sh" "$STAGE_DIR/scripts/"
|
|
|
|
# Explicit allow/deny lists for scripts/*.sh. Every shell script in scripts/
|
|
# must appear in exactly one of these arrays — the parity check below aborts
|
|
# the build if anything is unaccounted for. This catches the "added a new
|
|
# script, forgot to add it to the release" drift bug.
|
|
RUNTIME_SCRIPTS=(
|
|
# Ship in the release tarball — user-facing or invoked by containers/upgrades
|
|
install.sh
|
|
nocodb-init.sh gitea-init.sh mkdocs-entrypoint.sh
|
|
backup.sh restore.sh
|
|
upgrade.sh upgrade-check.sh upgrade-watcher.sh
|
|
uninstall.sh test-deployment.sh
|
|
validate-env.sh pangolin-teardown.sh ccp-deregister.sh register-with-ccp.sh
|
|
update-env.sh
|
|
)
|
|
|
|
DEV_ONLY_SCRIPTS=(
|
|
# Deliberately NOT shipped — dev-machine tooling (build/release/mirror/CI)
|
|
build-and-push.sh build-release.sh
|
|
mirror-images.sh validate-compose-parity.sh
|
|
)
|
|
|
|
# --- Parity check: every scripts/*.sh must be classified ---
|
|
declare -A KNOWN_SCRIPTS
|
|
for s in "${RUNTIME_SCRIPTS[@]}" "${DEV_ONLY_SCRIPTS[@]}"; do
|
|
KNOWN_SCRIPTS[$s]=1
|
|
done
|
|
|
|
UNCLASSIFIED=()
|
|
for f in "$PROJECT_DIR"/scripts/*.sh; do
|
|
[[ -f "$f" ]] || continue
|
|
name=$(basename "$f")
|
|
if [[ -z "${KNOWN_SCRIPTS[$name]:-}" ]]; then
|
|
UNCLASSIFIED+=("$name")
|
|
fi
|
|
done
|
|
|
|
if [[ ${#UNCLASSIFIED[@]} -gt 0 ]]; then
|
|
error "build-release.sh parity check failed — unclassified scripts/*.sh:"
|
|
for name in "${UNCLASSIFIED[@]}"; do
|
|
echo " - $name" >&2
|
|
done
|
|
echo "" >&2
|
|
echo " Every script in scripts/*.sh must be classified as either:" >&2
|
|
echo " RUNTIME_SCRIPTS — ships in the release tarball" >&2
|
|
echo " DEV_ONLY_SCRIPTS — stays on dev/build machines only" >&2
|
|
echo " Edit scripts/build-release.sh and add each to the correct array." >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Copy the runtime scripts
|
|
for script in "${RUNTIME_SCRIPTS[@]}"; do
|
|
if [[ -f "$PROJECT_DIR/scripts/$script" ]]; then
|
|
cp "$PROJECT_DIR/scripts/$script" "$STAGE_DIR/scripts/"
|
|
fi
|
|
done
|
|
|
|
# MkDocs build trigger (python, not in the allowlist because it's .py)
|
|
if [[ -f "$PROJECT_DIR/scripts/mkdocs-build-trigger.py" ]]; then
|
|
cp "$PROJECT_DIR/scripts/mkdocs-build-trigger.py" "$STAGE_DIR/scripts/"
|
|
fi
|
|
|
|
# Systemd units
|
|
if [[ -d "$PROJECT_DIR/scripts/systemd" ]]; then
|
|
cp -r "$PROJECT_DIR/scripts/systemd" "$STAGE_DIR/scripts/"
|
|
fi
|
|
|
|
chmod +x "$STAGE_DIR/scripts/"*.sh 2>/dev/null || true
|
|
info "Scripts ($(ls "$STAGE_DIR/scripts/" | wc -l) files)"
|
|
|
|
# --- Copy configs ---
|
|
if [[ -d "$PROJECT_DIR/configs" ]]; then
|
|
cp -r "$PROJECT_DIR/configs" "$STAGE_DIR/"
|
|
# Ensure code-server skeleton dirs exist
|
|
mkdir -p "$STAGE_DIR/configs/code-server/.config"
|
|
mkdir -p "$STAGE_DIR/configs/code-server/.local"
|
|
info "Configs (homepage, prometheus, grafana, alertmanager, pangolin)"
|
|
fi
|
|
|
|
# --- Copy nginx templates (for reference) ---
|
|
mkdir -p "$STAGE_DIR/nginx/conf.d"
|
|
cp "$PROJECT_DIR"/nginx/conf.d/*.template "$STAGE_DIR/nginx/conf.d/" 2>/dev/null || true
|
|
info "Nginx templates (for reference)"
|
|
|
|
# --- Copy MkDocs starter docs ---
|
|
if [[ -d "$PROJECT_DIR/mkdocs" ]]; then
|
|
mkdir -p "$STAGE_DIR/mkdocs"
|
|
cp "$PROJECT_DIR/mkdocs/mkdocs.yml" "$STAGE_DIR/mkdocs/" 2>/dev/null || true
|
|
cp -r "$PROJECT_DIR/mkdocs/docs" "$STAGE_DIR/mkdocs/" 2>/dev/null || mkdir -p "$STAGE_DIR/mkdocs/docs"
|
|
cp -r "$PROJECT_DIR/mkdocs/overrides" "$STAGE_DIR/mkdocs/" 2>/dev/null || mkdir -p "$STAGE_DIR/mkdocs/overrides"
|
|
mkdir -p "$STAGE_DIR/mkdocs/.cache"
|
|
mkdir -p "$STAGE_DIR/mkdocs/site"
|
|
info "MkDocs (starter documentation)"
|
|
fi
|
|
|
|
# --- Copy public-web ---
|
|
if [[ -d "$PROJECT_DIR/public-web" ]]; then
|
|
cp -r "$PROJECT_DIR/public-web" "$STAGE_DIR/"
|
|
else
|
|
mkdir -p "$STAGE_DIR/public-web"
|
|
fi
|
|
info "Public web assets"
|
|
|
|
# --- Create empty data directories ---
|
|
mkdir -p "$STAGE_DIR/assets/uploads"
|
|
mkdir -p "$STAGE_DIR/assets/icons"
|
|
mkdir -p "$STAGE_DIR/assets/images"
|
|
mkdir -p "$STAGE_DIR/data/upgrade"
|
|
mkdir -p "$STAGE_DIR/media/local/inbox"
|
|
mkdir -p "$STAGE_DIR/media/local/thumbnails"
|
|
mkdir -p "$STAGE_DIR/media/local/photos"
|
|
mkdir -p "$STAGE_DIR/media/local/documents"
|
|
mkdir -p "$STAGE_DIR/media/public"
|
|
mkdir -p "$STAGE_DIR/local-files"
|
|
info "Data directories (empty)"
|
|
|
|
# --- Summary ---
|
|
echo ""
|
|
STAGE_SIZE=$(du -sh "$STAGE_DIR" | cut -f1)
|
|
FILE_COUNT=$(find "$STAGE_DIR" -type f | wc -l)
|
|
info "Staging: ${FILE_COUNT} files, ${STAGE_SIZE}"
|
|
|
|
if [[ "$DRY_RUN" == "true" ]]; then
|
|
echo ""
|
|
info "[DRY RUN] Would create: changemaker-lite-${TAG}.tar.gz"
|
|
find "$STAGE_DIR" -type f | sed "s|$STAGE_DIR/||" | sort
|
|
rm -rf "$(dirname "$STAGE_DIR")"
|
|
exit 0
|
|
fi
|
|
|
|
# --- Create tarball ---
|
|
mkdir -p "$OUTPUT_DIR"
|
|
TARBALL="${OUTPUT_DIR}/changemaker-lite-${TAG}.tar.gz"
|
|
|
|
tar czf "$TARBALL" -C "$(dirname "$STAGE_DIR")" "changemaker-lite"
|
|
rm -rf "$(dirname "$STAGE_DIR")"
|
|
|
|
TARBALL_SIZE=$(du -h "$TARBALL" | cut -f1)
|
|
success "Created: $TARBALL (${TARBALL_SIZE})"
|
|
|
|
# --- Upload to Gitea (optional) ---
|
|
if [[ "$UPLOAD" == "true" ]]; then
|
|
source "$PROJECT_DIR/.env" 2>/dev/null || true
|
|
# GITEA_REGISTRY_API_TOKEN is for the remote registry (gitea.bnkops.com)
|
|
# GITEA_API_TOKEN is for the local platform Gitea — do NOT use it here
|
|
GITEA_TOKEN="${GITEA_REGISTRY_API_TOKEN:-}"
|
|
# GITEA_URL is the internal Docker hostname — use GITEA_REGISTRY for external access
|
|
GITEA_REGISTRY_HOST="${GITEA_REGISTRY%%/*}" # strip /admin path → gitea.bnkops.com
|
|
GITEA_HOST="${GITEA_EXTERNAL_URL:-https://${GITEA_REGISTRY_HOST:-gitea.bnkops.com}}"
|
|
|
|
if [[ -z "$GITEA_TOKEN" ]]; then
|
|
warn "GITEA_REGISTRY_API_TOKEN not set — skipping upload"
|
|
warn "Set GITEA_REGISTRY_API_TOKEN in .env and re-run with --upload"
|
|
else
|
|
# Refuse to overwrite an existing release unless --replace is explicit.
|
|
# Silently overwriting a published tag breaks upgrade checks for users
|
|
# already on that version (they see "no update available" even though
|
|
# the tarball changed).
|
|
EXISTING_RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" \
|
|
"${GITEA_HOST}/api/v1/repos/admin/changemaker.lite/releases/tags/${TAG}" \
|
|
-H "Authorization: token ${GITEA_TOKEN}")
|
|
if [[ "$EXISTING_RESPONSE" == "200" ]]; then
|
|
if [[ "$REPLACE" != "true" ]]; then
|
|
error "Release ${TAG} already exists."
|
|
error " Bump the version to a new tag, or pass --replace to delete and recreate."
|
|
error " --replace is destructive: users on ${TAG} will see no upgrade signal but get different tarball contents."
|
|
exit 1
|
|
fi
|
|
warn "Release ${TAG} exists — deleting first (--replace)"
|
|
EXISTING_ID=$(curl -sf "${GITEA_HOST}/api/v1/repos/admin/changemaker.lite/releases/tags/${TAG}" \
|
|
-H "Authorization: token ${GITEA_TOKEN}" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])" 2>/dev/null || echo "")
|
|
if [[ -n "$EXISTING_ID" ]]; then
|
|
curl -sf -X DELETE \
|
|
"${GITEA_HOST}/api/v1/repos/admin/changemaker.lite/releases/${EXISTING_ID}" \
|
|
-H "Authorization: token ${GITEA_TOKEN}" >/dev/null
|
|
success "Deleted existing release ${TAG} (id ${EXISTING_ID})"
|
|
fi
|
|
fi
|
|
|
|
info "Creating Gitea release ${TAG}..."
|
|
RELEASE_RESPONSE=$(curl -sf -X POST \
|
|
"${GITEA_HOST}/api/v1/repos/admin/changemaker.lite/releases" \
|
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{\"tag_name\":\"${TAG}\",\"name\":\"Changemaker Lite ${TAG}\",\"body\":\"Release ${TAG} (${COMMIT_SHA})\"}" \
|
|
2>/dev/null || true)
|
|
|
|
RELEASE_ID=$(echo "$RELEASE_RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
|
|
|
if [[ -n "$RELEASE_ID" ]]; then
|
|
info "Uploading tarball to release ${RELEASE_ID}..."
|
|
curl -sf -X POST \
|
|
"${GITEA_HOST}/api/v1/repos/admin/changemaker.lite/releases/${RELEASE_ID}/assets" \
|
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
|
-F "attachment=@${TARBALL}" \
|
|
>/dev/null 2>&1
|
|
success "Uploaded to Gitea release ${TAG}"
|
|
else
|
|
warn "Failed to create release — upload manually at ${GITEA_HOST}/admin/changemaker.lite/releases"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
echo ""
|
|
success "Release ${TAG} ready."
|
|
echo " Tarball: $TARBALL"
|
|
echo " Install: tar xzf $(basename "$TARBALL") && cd changemaker-lite && bash config.sh"
|