release: refuse upload over existing tag unless --replace

scripts/build-release.sh --upload now checks for an existing release
at the given tag before POSTing a new one. If found and --replace is
not set, errors out with a clear message.

This prevents the silent-overwrite problem: a user on v2.9.7 running
./scripts/upgrade.sh sees "no update available" when the v2.9.7
release's tarball contents have silently changed. Version tags should
be immutable once published.

--replace is still available for deliberate test-bench iteration
(DELETEs the existing release, then POSTs). Documented as destructive
in the --help output and DEV_WORKFLOW.md.

Bunker Admin
This commit is contained in:
bunker-admin 2026-04-16 13:06:06 -06:00
parent 6e01d580b2
commit c2f12aa2bf
2 changed files with 38 additions and 0 deletions

View File

@ -157,8 +157,17 @@ Packages only runtime files (~9 MB) — no source code, no node_modules:
# Preview contents without creating tarball
./scripts/build-release.sh --dry-run
# --upload refuses to overwrite an existing tag. To deliberately replace
# a release (destructive — users on that tag see no upgrade signal):
./scripts/build-release.sh --tag v2.2.0 --upload --replace
```
**Version hygiene:** bump the tag when changing release contents. Overwriting
an existing release silently breaks upgrade checks for users already on that
version — they see "no update available" even though the tarball they'd
download differs.
The tarball contains:
- `docker-compose.yml` (copy of `docker-compose.prod.yml` — image-only, no build blocks)
- `.env.example`, `config.sh` (configuration wizard)

View File

@ -12,6 +12,8 @@
# --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
#
@ -28,6 +30,7 @@ TAG=""
OUTPUT_DIR="${PROJECT_DIR}/releases"
UPLOAD=false
DRY_RUN=false
REPLACE=false
# --- Colors ---
if [[ -t 1 ]] && [[ -z "${NO_COLOR:-}" ]]; then
@ -48,6 +51,7 @@ while [[ $# -gt 0 ]]; do
--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/^# \?//'
@ -223,6 +227,31 @@ if [[ "$UPLOAD" == "true" ]]; 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" \