From c2f12aa2bf503d1a8298a8551d7594714d4f8e86 Mon Sep 17 00:00:00 2001 From: bunker-admin Date: Thu, 16 Apr 2026 13:06:06 -0600 Subject: [PATCH] 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 --- DEV_WORKFLOW.md | 9 +++++++++ scripts/build-release.sh | 29 +++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/DEV_WORKFLOW.md b/DEV_WORKFLOW.md index 589b4435..1fde0fc6 100644 --- a/DEV_WORKFLOW.md +++ b/DEV_WORKFLOW.md @@ -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) diff --git a/scripts/build-release.sh b/scripts/build-release.sh index 1c813945..8bb59790 100755 --- a/scripts/build-release.sh +++ b/scripts/build-release.sh @@ -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" \