mirror of
https://github.com/supabase/supabase.git
synced 2026-09-03 07:28:20 +08:00
693 lines
24 KiB
Bash
Executable File
693 lines
24 KiB
Bash
Executable File
#!/bin/sh
|
|
#
|
|
# Update an existing self-hosted Supabase deployment in place.
|
|
#
|
|
# The deployment directory mixes vendor-owned files (docker-compose.yml, the
|
|
# override files, volumes/*, scripts, .env.example) with user-owned state
|
|
# (.env, docker-compose.override.yml, volumes/db/data, volumes/storage, etc.).
|
|
# This script pulls a newer version of the Supabase files on top of yours
|
|
# using a 3-way merge against the version you started from, so local edits
|
|
# survive and genuine conflicts are surfaced rather than silently overwritten.
|
|
#
|
|
# The version you started from is recorded in .supabase-version (written by
|
|
# setup.sh). If it is missing, pass --from <ref> or follow the printed guidance.
|
|
#
|
|
# What it never touches: .env values you set, docker-compose.override.yml, and
|
|
# the data directories (volumes/db/data, volumes/storage, etc.). New keys from
|
|
# .env.example are appended to your .env; existing values are kept as-is.
|
|
#
|
|
# By default it updates to the latest self-hosted/v* release tag (or 'master'
|
|
# until the first tag exists). Pass --to to pin a specific tag/branch.
|
|
#
|
|
# Usage:
|
|
# sh update.sh # update to the latest release tag
|
|
# sh update.sh --dry-run # show what would change, write nothing
|
|
# sh update.sh --to <tag> # update to a specific tag/branch
|
|
# sh update.sh --from <ref> # base to merge from (if no .supabase-version)
|
|
# sh update.sh --yes # don't prompt, even on breaking changes
|
|
#
|
|
# Env:
|
|
# SUPABASE_REPO_URL Override the upstream repo (default: github supabase/supabase)
|
|
#
|
|
# Documentation: https://supabase.com/docs/guides/self-hosting/updating
|
|
#
|
|
|
|
set -e
|
|
|
|
cd "$(dirname "$0")"
|
|
|
|
# Pipeline (see main at the bottom):
|
|
# resolve refs -> fetch base+target snapshots -> [report-only exit]
|
|
# -> build manifest gate -> confirm_gate (before any writes)
|
|
# -> backup → merge vendor files + .env keys -> summary → stamp
|
|
#
|
|
# Three trees for every vendor file path:
|
|
# - base - upstream at BASE_REF (.supabase-version ref=, or --from)
|
|
# - target - upstream at TARGET_REF (--to, or latest self-hosted/v* tag)
|
|
# - user's - the deployment directory (cwd); _not_ a git checkout
|
|
#
|
|
# Git is only used to fetch snapshots (fetch_snapshot) and to run git merge-file.
|
|
#
|
|
# Breaking-change gate (upgrades.json on the target snapshot):
|
|
# - Keyed by version (e.g. "0.7.0"); window = entries in (BASE_VER, TARGET_VER],
|
|
# where the bounds are the base/target refs reduced to bare semver
|
|
# (self-hosted/vX.Y.Z -> X.Y.Z) and compared with sort -V.
|
|
# - If a bound is not a release tag (a commit SHA, or "master"), that side of
|
|
# the window is left open and all applicable entries are shown with a warning.
|
|
# - Prompt when an entry has breaking:true or a gate script; runs before
|
|
# backup/merge so abort leaves the deployment untouched.
|
|
# - CHANGELOG.md is never parsed (update.sh only points users at it); routine
|
|
# "requires compose update" items are applied by the merge, not listed here.
|
|
#
|
|
# User-owned paths skipped during merge are defined in .gitignore (loaded from the
|
|
# target snapshot). git check-ignore --no-index applies negation rules (e.g.
|
|
# volumes/functions/** ignored except volumes/functions/main/index.ts).
|
|
# .git/ is excluded here only - never listed in .gitignore.
|
|
#
|
|
# .env is never 3-way merged: append missing keys from .env.example only.
|
|
|
|
# --- globals (set during main) -----------------------------------------------
|
|
|
|
REPO_URL="${SUPABASE_REPO_URL:-https://github.com/supabase/supabase}"
|
|
STAMP_FILE=".supabase-version"
|
|
SELF_NAME=$(basename "$0")
|
|
DRY_RUN=0
|
|
ASSUME_YES=0
|
|
TO_REF=""
|
|
FROM_REF=""
|
|
|
|
TARGET_REF=""
|
|
BASE_REF=""
|
|
BASE_VER=""
|
|
TARGET_VER=""
|
|
REPORT_ONLY=0
|
|
|
|
TMP_ROOT=""
|
|
TARGET_DIR=""
|
|
BASE_DIR=""
|
|
REPORT=""
|
|
ENV_ADDED=""
|
|
ENV_REMOVED=""
|
|
GATE_REPORT=""
|
|
GATE_REQUIRED=0
|
|
IGNORE_FILE=""
|
|
IGNORE_GIT_DIR=""
|
|
|
|
# --- logging -----------------------------------------------------------------
|
|
|
|
log() { printf "===> %s\n" "$*"; }
|
|
warn() { printf "WARNING: %s\n" "$*" >&2; }
|
|
die() { printf "ERROR: %s\n" "$*" >&2; exit 1; }
|
|
|
|
print_help() {
|
|
awk 'NR==1 {next} /^#/ {sub(/^# ?/,""); print; next} {exit}' "$0"
|
|
}
|
|
|
|
# --- small helpers -----------------------------------------------------------
|
|
|
|
# load_ignore_file - vendor/user split from the target snapshot's .gitignore.
|
|
# Uses a throwaway git dir so check-ignore works on deployment trees that are
|
|
# not git repos (and on Apple Git, which rejects --git-dir=/dev/null).
|
|
load_ignore_file() {
|
|
if [ -f "$TARGET_DIR/.gitignore" ]; then
|
|
IGNORE_FILE="$TARGET_DIR/.gitignore"
|
|
elif [ -f .gitignore ]; then
|
|
IGNORE_FILE="$(pwd)/.gitignore"
|
|
else
|
|
IGNORE_FILE=""
|
|
warn "No .gitignore found; only .git paths are excluded from the merge."
|
|
return 0
|
|
fi
|
|
case "$IGNORE_FILE" in
|
|
/*) ;;
|
|
*) IGNORE_FILE="$(cd "$(dirname "$IGNORE_FILE")" && pwd)/$(basename "$IGNORE_FILE")" ;;
|
|
esac
|
|
IGNORE_GIT_DIR="$TMP_ROOT/ignore-git"
|
|
git init -q "$IGNORE_GIT_DIR"
|
|
}
|
|
|
|
# is_excluded <relpath> - true when the path is user-owned and must not merge.
|
|
is_excluded() {
|
|
case "$1" in
|
|
.git|.git/*) return 0 ;;
|
|
esac
|
|
[ -n "$IGNORE_FILE" ] || return 1
|
|
git -C "$IGNORE_GIT_DIR" -c "core.excludesfile=$IGNORE_FILE" \
|
|
check-ignore -q --no-index "$1" 2>/dev/null
|
|
}
|
|
|
|
read_stamp_ref() {
|
|
[ -f "$STAMP_FILE" ] || return 1
|
|
val=$(grep -E '^ref=' "$STAMP_FILE" 2>/dev/null | head -n1 | cut -d= -f2- | tr -d "\r\"' ")
|
|
if [ -z "$val" ]; then
|
|
val=$(grep -vE '^[[:space:]]*#' "$STAMP_FILE" 2>/dev/null \
|
|
| grep -vE '^[[:space:]]*$' | head -n1 | tr -d "\r\"' ")
|
|
fi
|
|
[ -n "$val" ] && printf '%s' "$val"
|
|
}
|
|
|
|
# env_has_key <key> <file> - true if the key appears as KEY=, even commented
|
|
# (e.g. "#GOOGLE_ENABLED="), so we never re-add a key the user disabled on purpose.
|
|
env_has_key() {
|
|
grep -qE "^[[:space:]]*#?[[:space:]]*$1=" "$2" 2>/dev/null
|
|
}
|
|
|
|
record() { printf '%s:%s\n' "$1" "$2" >> "$REPORT"; }
|
|
|
|
count_status() { grep -cE "^$1:" "$REPORT" 2>/dev/null || true; }
|
|
|
|
list_status() {
|
|
grep -E "^$1:" "$REPORT" 2>/dev/null | cut -d: -f2- | sed 's/^/ /'
|
|
}
|
|
|
|
# --- upstream snapshots ------------------------------------------------------
|
|
|
|
_sparse_init() {
|
|
git -C "$1" init -q
|
|
git -C "$1" remote add origin "$REPO_URL"
|
|
git -C "$1" config core.sparseCheckout true
|
|
git -C "$1" sparse-checkout init --cone >/dev/null 2>&1
|
|
git -C "$1" sparse-checkout set docker >/dev/null 2>&1
|
|
}
|
|
|
|
# fetch_snapshot <ref> <dest_dir>
|
|
# Materializes ./docker at <ref> into <dest_dir> via a shallow fetch. Also the
|
|
# seam an artifact source (tarball + sha256) would slot into later.
|
|
fetch_snapshot() {
|
|
_ref="$1"
|
|
_dest="$2"
|
|
_work=$(mktemp -d "$TMP_ROOT/fetch.XXXXXX")
|
|
if _sparse_init "$_work" \
|
|
&& git -C "$_work" fetch --depth=1 --filter=blob:none -q origin "$_ref" 2>/dev/null \
|
|
&& git -C "$_work" checkout -q FETCH_HEAD 2>/dev/null \
|
|
&& [ -d "$_work/docker" ]; then
|
|
mkdir -p "$_dest"
|
|
cp -rf "$_work/docker/." "$_dest/"
|
|
rm -rf "$_work"
|
|
else
|
|
rm -rf "$_work"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
latest_release_tag() {
|
|
git ls-remote --tags --refs "$REPO_URL" 2>/dev/null \
|
|
| sed 's#^.*refs/tags/##' \
|
|
| grep -E '^self-hosted/v[0-9]' \
|
|
| sort -V | tail -n1
|
|
}
|
|
|
|
list_files() {
|
|
( cd "$1" && find . -type f | sed 's|^\./||' | grep -vE '^\.git(/|$)' | sort )
|
|
}
|
|
|
|
# normalize_version <ref> - reduce a ref to bare semver (0.7.0) for comparison,
|
|
# or empty when it is not a self-hosted release tag (commit SHA, "master", …).
|
|
normalize_version() {
|
|
_v="${1#refs/tags/}"
|
|
_v="${_v#self-hosted/}"
|
|
_v="${_v#v}"
|
|
case "$_v" in
|
|
[0-9]*.[0-9]*) printf '%s' "$_v" ;;
|
|
*) ;;
|
|
esac
|
|
}
|
|
|
|
# ver_gt A B - true when version A is strictly greater than B (sort -V order).
|
|
ver_gt() {
|
|
if [ "$1" = "$2" ]; then return 1; fi
|
|
[ "$(printf '%s\n%s\n' "$1" "$2" | sort -V | tail -n1)" = "$1" ]
|
|
}
|
|
|
|
# ver_in_window VER - true when BASE_VER < VER <= TARGET_VER. An empty bound
|
|
# leaves that side open (over-reports a gate rather than hiding one).
|
|
ver_in_window() {
|
|
if [ -n "$TARGET_VER" ] && ver_gt "$1" "$TARGET_VER"; then return 1; fi
|
|
if [ -n "$BASE_VER" ] && ! ver_gt "$1" "$BASE_VER"; then return 1; fi
|
|
return 0
|
|
}
|
|
|
|
# --- ref resolution ----------------------------------------------------------
|
|
|
|
resolve_target_ref() {
|
|
if [ -n "$TO_REF" ]; then
|
|
TARGET_REF="$TO_REF"
|
|
return
|
|
fi
|
|
TARGET_REF=$(latest_release_tag)
|
|
if [ -n "$TARGET_REF" ]; then
|
|
log "Latest release tag: $TARGET_REF"
|
|
else
|
|
TARGET_REF="master"
|
|
warn "No self-hosted/v* release tags found; targeting 'master'. Pin a version with --to."
|
|
fi
|
|
}
|
|
|
|
resolve_base_ref() {
|
|
REPORT_ONLY=0
|
|
if [ -n "$FROM_REF" ]; then
|
|
BASE_REF="$FROM_REF"
|
|
elif BASE_REF=$(read_stamp_ref) && [ -n "$BASE_REF" ]; then
|
|
:
|
|
else
|
|
REPORT_ONLY=1
|
|
BASE_REF=""
|
|
fi
|
|
}
|
|
|
|
print_report_only_guidance() {
|
|
warn "No $STAMP_FILE found and no --from <ref> given; cannot determine the version you started from."
|
|
cat >&2 <<EOF
|
|
|
|
Without a base version a safe 3-way merge is not possible. To fix this, find the
|
|
version your deployment was based on and record it, then re-run:
|
|
|
|
1. Identify your base - the self-hosted/vX.Y.Z release (or commit) your files
|
|
came from. Cross-reference the image tags in docker-compose.yml / .env
|
|
against versions.md, or the newest CHANGELOG.md section you remember pulling.
|
|
2. Write it to $STAMP_FILE, e.g.:
|
|
printf 'ref=self-hosted/v0.7.0\n' > $STAMP_FILE
|
|
3. Re-run: sh update.sh
|
|
|
|
Or supply it inline for this run: sh update.sh --from <commit-sha-or-tag>
|
|
|
|
Continuing in REPORT-ONLY mode. NOTE: this is NOT the full set of changes -
|
|
without a base version it can only list files and .env keys that are entirely
|
|
NEW to you; it CANNOT show which existing files would change or conflict. Record
|
|
a base and re-run for the real preview. Nothing will be written.
|
|
EOF
|
|
}
|
|
|
|
fetch_snapshots() {
|
|
TARGET_DIR="$TMP_ROOT/target"
|
|
log "Fetching target snapshot ($TARGET_REF)"
|
|
fetch_snapshot "$TARGET_REF" "$TARGET_DIR" \
|
|
|| die "Could not fetch target snapshot '$TARGET_REF' from $REPO_URL"
|
|
|
|
if [ "$REPORT_ONLY" = "1" ]; then
|
|
return
|
|
fi
|
|
|
|
BASE_DIR="$TMP_ROOT/base"
|
|
log "Fetching base snapshot ($BASE_REF)"
|
|
fetch_snapshot "$BASE_REF" "$BASE_DIR" \
|
|
|| die "Could not fetch base snapshot '$BASE_REF' from $REPO_URL"
|
|
}
|
|
|
|
run_report_only() {
|
|
log "Files in '$TARGET_REF' you do NOT have yet (brand-new only; existing files that changed are NOT shown here):"
|
|
while IFS= read -r f; do
|
|
is_excluded "$f" && continue
|
|
[ -f "$f" ] || echo " + $f"
|
|
done <<EOF
|
|
$(list_files "$TARGET_DIR")
|
|
EOF
|
|
echo ""
|
|
log ".env keys in the new .env.example that your .env is missing (add these):"
|
|
if [ -f "$TARGET_DIR/.env.example" ]; then
|
|
while IFS= read -r k; do
|
|
env_has_key "$k" .env || echo " + $k"
|
|
done <<EOF
|
|
$(grep -E '^[A-Za-z_][A-Za-z0-9_]*=' "$TARGET_DIR/.env.example" | cut -d= -f1)
|
|
EOF
|
|
fi
|
|
echo ""
|
|
warn "This is NOT the full update. To see what would actually change, record a base version."
|
|
warn "See the guidance above and re-run the script. Nothing was written."
|
|
}
|
|
|
|
# --- breaking-change gate (manifest-driven, before any writes) ---------------
|
|
# Reads docker/upgrades.json from the target snapshot; gating keys off manifest
|
|
# entries in (BASE_VER, TARGET_VER].
|
|
|
|
build_gate_report() {
|
|
_manifest="$TARGET_DIR/upgrades.json"
|
|
GATE_REQUIRED=0
|
|
: > "$GATE_REPORT"
|
|
|
|
if [ ! -f "$_manifest" ]; then
|
|
return 0
|
|
fi
|
|
if ! jq -e 'type == "object"' "$_manifest" >/dev/null 2>&1; then
|
|
die "$_manifest is present but is not a valid JSON object; refusing to update without a working breaking-change gate. Please report this to the maintainers."
|
|
fi
|
|
|
|
if [ -z "$BASE_VER" ] || [ -z "$TARGET_VER" ]; then
|
|
warn "Base or target is not a self-hosted/vX.Y.Z tag; cannot compute an exact update window."
|
|
warn "Showing all applicable manual-action releases."
|
|
warn "Review which ones apply to your deployment."
|
|
fi
|
|
|
|
for k in $(jq -r 'keys[]' "$_manifest" 2>/dev/null); do
|
|
case "$k" in [0-9]*.[0-9]*) ;; *) continue ;; esac
|
|
ver_in_window "$k" || continue
|
|
|
|
breaking=$(jq -r --arg k "$k" '.[$k].breaking // false' "$_manifest")
|
|
gate=$(jq -r --arg k "$k" '.[$k].gate // empty' "$_manifest")
|
|
url=$(jq -r --arg k "$k" '.[$k].migration_guide_url // empty' "$_manifest")
|
|
reqs=$(jq -r --arg k "$k" '.[$k].requires[]? // empty' "$_manifest")
|
|
|
|
if [ "$breaking" = "true" ] || [ -n "$gate" ]; then
|
|
GATE_REQUIRED=1
|
|
fi
|
|
|
|
{
|
|
[ "$breaking" = "true" ] && echo "[$k] BREAKING" || echo "[$k]"
|
|
[ -n "$gate" ] && echo " gate: '$gate' must be run first (see the steps below for the exact command)"
|
|
[ -n "$url" ] && echo " guide: $url"
|
|
[ -n "$reqs" ] && printf '%s\n' "$reqs" | sed 's/^/ - /'
|
|
} >> "$GATE_REPORT"
|
|
done
|
|
# Explicit success: the loop's last iteration can end on a false test (e.g.
|
|
# an entry with no 'requires'), which would otherwise make this function
|
|
# return non-zero and abort the whole script under 'set -e'.
|
|
return 0
|
|
}
|
|
|
|
confirm_gate() {
|
|
[ "$GATE_REQUIRED" = "1" ] || return 0
|
|
[ "$DRY_RUN" != "1" ] || return 0
|
|
[ "$ASSUME_YES" != "1" ] || return 0
|
|
|
|
echo "" >&2
|
|
warn "This update requires manual action - review before continuing:"
|
|
sed 's/^/ /' "$GATE_REPORT" >&2
|
|
echo "" >&2
|
|
|
|
if { : > /dev/tty; } 2>/dev/null; then
|
|
printf "Have you completed the required steps and want to continue? [y/N]: " > /dev/tty
|
|
read -r reply < /dev/tty
|
|
case "$reply" in
|
|
y|Y|yes|YES) return 0 ;;
|
|
*) die "Aborted by user. Nothing was modified." ;;
|
|
esac
|
|
fi
|
|
die "Breaking/gated changes present and no controlling terminal to confirm. Re-run with --yes to proceed."
|
|
}
|
|
|
|
# --- backup ------------------------------------------------------------------
|
|
|
|
take_backup() {
|
|
if [ "$DRY_RUN" = "1" ]; then
|
|
log "Dry run: no backup taken, nothing will be written."
|
|
return 0
|
|
fi
|
|
mkdir -p backups
|
|
_backup="backups/pre-update-$(date +%Y%m%d-%H%M%S).tgz"
|
|
log "Backing up current configuration to $_backup (excluding data directories)"
|
|
tar czf "$_backup" \
|
|
--exclude='./backups' \
|
|
--exclude='./volumes/db/data' \
|
|
--exclude='./volumes/storage' \
|
|
. 2>/dev/null || warn "Backup archive reported errors; review $_backup before relying on it."
|
|
warn "This does NOT back up your database. Back it up separately before updating."
|
|
}
|
|
|
|
# --- vendor file merge -------------------------------------------------------
|
|
|
|
apply_file() {
|
|
[ "$DRY_RUN" = "1" ] && return 0
|
|
_dir=$(dirname "$1")
|
|
[ "$_dir" = "." ] || mkdir -p "$_dir"
|
|
cp -f "$2" "$1"
|
|
}
|
|
|
|
# Per-file 3-way merge (u=yours, b=base snapshot, t=target snapshot):
|
|
# no t → keep u; report removed-upstream
|
|
# no u → copy t; report new
|
|
# u == t → report unchanged
|
|
# u == b → copy t; report updated (user never edited)
|
|
# else → git merge-file u b t; report merged-clean or CONFLICT
|
|
# If b had no file, an empty file stands in for b.
|
|
merge_one_file() {
|
|
f="$1"
|
|
empty="$2"
|
|
merged="$TMP_ROOT/merged.out"
|
|
|
|
b="$BASE_DIR/$f"
|
|
t="$TARGET_DIR/$f"
|
|
u="$f"
|
|
|
|
if [ ! -f "$t" ]; then
|
|
[ -f "$u" ] && record "removed-upstream" "$f"
|
|
return 0
|
|
fi
|
|
|
|
if [ ! -f "$u" ]; then
|
|
apply_file "$f" "$t"
|
|
record "new" "$f"
|
|
return 0
|
|
fi
|
|
|
|
if cmp -s "$u" "$t"; then
|
|
record "unchanged" "$f"
|
|
return 0
|
|
fi
|
|
|
|
base_for_merge="$b"
|
|
[ -f "$base_for_merge" ] || base_for_merge="$empty"
|
|
|
|
if cmp -s "$u" "$base_for_merge"; then
|
|
apply_file "$f" "$t"
|
|
record "updated" "$f"
|
|
return 0
|
|
fi
|
|
|
|
if git merge-file -p -q \
|
|
-L "yours ($f)" -L "base" -L "new ($TARGET_REF)" \
|
|
"$u" "$base_for_merge" "$t" > "$merged" 2>/dev/null; then
|
|
[ "$DRY_RUN" != "1" ] && cp -f "$merged" "$u"
|
|
record "merged-clean" "$f"
|
|
elif [ -s "$merged" ]; then
|
|
# Non-zero exit with output = a normal conflict (markers written).
|
|
[ "$DRY_RUN" != "1" ] && cp -f "$merged" "$u"
|
|
record "CONFLICT" "$f"
|
|
else
|
|
# git merge-file errored and produced no output; keep the user's file
|
|
# intact rather than truncating it to empty.
|
|
record "merge-failed" "$f"
|
|
fi
|
|
}
|
|
|
|
# The running script is a vendor file too, but overwriting it in place would
|
|
# corrupt this process (the shell reads $0 as it runs). Never write it directly:
|
|
# if the target ships a different version, stage it as <name>.dist to review.
|
|
stage_self_update() {
|
|
_t="$TARGET_DIR/$SELF_NAME"
|
|
if [ ! -f "$_t" ] || cmp -s "$SELF_NAME" "$_t"; then
|
|
record "unchanged" "$SELF_NAME"
|
|
return 0
|
|
fi
|
|
[ "$DRY_RUN" = "1" ] || cp -f "$_t" "$SELF_NAME.dist"
|
|
record "self-staged" "$SELF_NAME"
|
|
}
|
|
|
|
merge_vendor_files() {
|
|
_empty="$TMP_ROOT/empty"
|
|
: > "$_empty"
|
|
: > "$REPORT"
|
|
|
|
while IFS= read -r f; do
|
|
[ -n "$f" ] || continue
|
|
is_excluded "$f" && continue
|
|
if [ "$f" = "$SELF_NAME" ]; then
|
|
stage_self_update
|
|
continue
|
|
fi
|
|
merge_one_file "$f" "$_empty"
|
|
done <<EOF
|
|
$( { list_files "$BASE_DIR"; list_files "$TARGET_DIR"; } | sort -u )
|
|
EOF
|
|
}
|
|
|
|
# --- .env key-union merge ----------------------------------------------------
|
|
|
|
merge_env_file() {
|
|
_example="$TARGET_DIR/.env.example"
|
|
: > "$ENV_ADDED"
|
|
: > "$ENV_REMOVED"
|
|
[ -f "$_example" ] || return 0
|
|
|
|
while IFS= read -r k; do
|
|
env_has_key "$k" .env || echo "$k" >> "$ENV_ADDED"
|
|
done <<EOF
|
|
$(grep -E '^[A-Za-z_][A-Za-z0-9_]*=' "$_example" | cut -d= -f1)
|
|
EOF
|
|
|
|
while IFS= read -r k; do
|
|
env_has_key "$k" "$_example" || echo "$k" >> "$ENV_REMOVED"
|
|
done <<EOF
|
|
$(grep -E '^[A-Za-z_][A-Za-z0-9_]*=' .env | cut -d= -f1)
|
|
EOF
|
|
|
|
if [ ! -s "$ENV_ADDED" ] || [ "$DRY_RUN" = "1" ]; then
|
|
return 0
|
|
fi
|
|
|
|
{
|
|
echo ""
|
|
echo "############"
|
|
echo "# Added by update.sh on $(date +%Y-%m-%d) from .env.example ($TARGET_REF)."
|
|
echo "# Review and set values as needed."
|
|
echo "############"
|
|
while IFS= read -r k; do
|
|
grep -E "^${k}=" "$_example" | head -n1
|
|
done < "$ENV_ADDED"
|
|
} >> .env
|
|
}
|
|
|
|
# --- summary and finish ------------------------------------------------------
|
|
|
|
print_summary() {
|
|
echo ""
|
|
if [ "$DRY_RUN" = "1" ]; then
|
|
log "DRY RUN - the following WOULD change (nothing was written):"
|
|
else
|
|
log "Update applied. Summary:"
|
|
fi
|
|
printf " updated: %s\n" "$(count_status updated)"
|
|
printf " new: %s\n" "$(count_status new)"
|
|
printf " merged (clean): %s\n" "$(count_status merged-clean)"
|
|
printf " CONFLICTS: %s\n" "$(count_status CONFLICT)"
|
|
printf " merge failures: %s\n" "$(count_status merge-failed)"
|
|
printf " removed upstream: %s\n" "$(count_status removed-upstream)"
|
|
printf " env keys added: %s\n" "$( [ -s "$ENV_ADDED" ] && wc -l < "$ENV_ADDED" | tr -d ' ' || echo 0 )"
|
|
|
|
if [ "$(count_status CONFLICT)" != "0" ]; then
|
|
echo ""
|
|
warn "Files with merge conflicts (edit these and remove the <<<<<<< ======= >>>>>>> markers):"
|
|
list_status CONFLICT
|
|
fi
|
|
if [ "$(count_status merge-failed)" != "0" ]; then
|
|
echo ""
|
|
warn "Files git could not merge (left unchanged - update these manually):"
|
|
list_status merge-failed
|
|
fi
|
|
if [ "$(count_status merged-clean)" != "0" ]; then
|
|
echo ""
|
|
log "Files merged cleanly (review recommended):"
|
|
list_status merged-clean
|
|
fi
|
|
if [ "$(count_status removed-upstream)" != "0" ]; then
|
|
echo ""
|
|
log "Removed upstream but kept in place (you may no longer need these):"
|
|
list_status removed-upstream
|
|
fi
|
|
if [ "$(count_status self-staged)" != "0" ]; then
|
|
echo ""
|
|
if [ "$DRY_RUN" = "1" ]; then
|
|
log "$SELF_NAME differs from the version in '$TARGET_REF'; a real run would stage that version as $SELF_NAME.dist (the running script is never overwritten in place)."
|
|
else
|
|
log "$SELF_NAME differs from the version in '$TARGET_REF', staged as $SELF_NAME.dist (the running script was not modified)."
|
|
log "Review it, then swap it in if needed: mv $SELF_NAME.dist $SELF_NAME"
|
|
fi
|
|
fi
|
|
if [ -s "$ENV_ADDED" ]; then
|
|
echo ""
|
|
log ".env keys added (review values):"
|
|
sed 's/^/ + /' "$ENV_ADDED"
|
|
fi
|
|
if [ -s "$ENV_REMOVED" ]; then
|
|
echo ""
|
|
log ".env keys you have that are gone from the new .env.example (review/remove manually):"
|
|
sed 's/^/ - /' "$ENV_REMOVED"
|
|
fi
|
|
if [ -s "$GATE_REPORT" ]; then
|
|
echo ""
|
|
log "Required manual steps for this update (from upgrades.json):"
|
|
sed 's/^/ /' "$GATE_REPORT"
|
|
fi
|
|
echo ""
|
|
log "For what changed in this update, see CHANGELOG.md (from $BASE_REF to $TARGET_REF)."
|
|
}
|
|
|
|
write_stamp() {
|
|
{
|
|
echo "# Supabase self-hosted version stamp. Managed by setup.sh / update.sh."
|
|
echo "# Do not commit or edit by hand. Records the ref this deployment was based on."
|
|
echo "ref=$TARGET_REF"
|
|
} > "$STAMP_FILE"
|
|
}
|
|
|
|
print_next_steps() {
|
|
echo ""
|
|
log "Next steps:"
|
|
echo " 1. Review the changes above (compare against the latest backup in backups/ if needed)."
|
|
echo " 2. sh run.sh pull"
|
|
echo " 3. sh run.sh recreate"
|
|
}
|
|
|
|
# --- main --------------------------------------------------------------------
|
|
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--dry-run) DRY_RUN=1; shift ;;
|
|
--yes|-y) ASSUME_YES=1; shift ;;
|
|
--to) [ $# -ge 2 ] || die "--to requires a <tag> argument."; TO_REF="$2"; shift 2 ;;
|
|
--from) [ $# -ge 2 ] || die "--from requires a <ref> argument."; FROM_REF="$2"; shift 2 ;;
|
|
-h|--help) print_help; exit 0 ;;
|
|
*) echo "Unknown option: $1" >&2; print_help; exit 1 ;;
|
|
esac
|
|
done
|
|
|
|
[ -f docker-compose.yml ] || die "docker-compose.yml not found in $(pwd). Run this from your deployment directory."
|
|
[ -f .env ] || die ".env not found in $(pwd). This does not look like a configured deployment."
|
|
command -v git >/dev/null 2>&1 || die "git is required but was not found on PATH."
|
|
command -v jq >/dev/null 2>&1 || die "jq is required but was not found on PATH."
|
|
|
|
resolve_target_ref
|
|
resolve_base_ref
|
|
TARGET_VER=$(normalize_version "$TARGET_REF")
|
|
BASE_VER=$(normalize_version "$BASE_REF")
|
|
[ "$REPORT_ONLY" = "1" ] && print_report_only_guidance
|
|
|
|
TMP_ROOT=$(mktemp -d)
|
|
REPORT="$TMP_ROOT/report"
|
|
ENV_ADDED="$TMP_ROOT/env_added"
|
|
ENV_REMOVED="$TMP_ROOT/env_removed"
|
|
GATE_REPORT="$TMP_ROOT/gate_report"
|
|
trap 'rm -rf "$TMP_ROOT"' EXIT INT TERM
|
|
|
|
fetch_snapshots
|
|
load_ignore_file
|
|
|
|
if [ "$REPORT_ONLY" = "1" ]; then
|
|
run_report_only
|
|
build_gate_report
|
|
if [ -s "$GATE_REPORT" ]; then
|
|
echo ""
|
|
log "Breaking changes / required manual steps in this range (from upgrades.json):"
|
|
sed 's/^/ /' "$GATE_REPORT"
|
|
warn "Record a base version (see above) and re-run to apply with the gate enforced."
|
|
fi
|
|
exit 0
|
|
fi
|
|
|
|
build_gate_report
|
|
confirm_gate
|
|
|
|
take_backup
|
|
merge_vendor_files
|
|
merge_env_file
|
|
print_summary
|
|
|
|
if [ "$DRY_RUN" = "1" ]; then
|
|
echo ""
|
|
log "Dry run complete. Re-run without --dry-run to apply."
|
|
exit 0
|
|
fi
|
|
|
|
if [ "$(count_status CONFLICT)" != "0" ] || [ "$(count_status merge-failed)" != "0" ]; then
|
|
echo ""
|
|
warn "Update applied WITH CONFLICTS. Resolve the files listed above (remove the"
|
|
warn "<<<<<<< ======= >>>>>>> markers, or fix the files git could not merge)"
|
|
warn "before starting the stack."
|
|
warn "The version stamp was NOT advanced; it will update on your next clean run."
|
|
warn "Exiting with status 2."
|
|
exit 2
|
|
fi
|
|
|
|
write_stamp
|
|
print_next_steps
|
|
log "Update applied cleanly."
|