#!/bin/bash # Create or update a shared Forgejo release with branded macOS assets. set -e VERSION="" BRANDS=() FORGEJO_USER="${FORGEJO_USER}" FORGEJO_PASS="${FORGEJO_PASS}" FORGEJO_URL="https://git.him-tools.de" REPO="HIM-public/webdrop-bridge" CLEAR_CREDS=false DRY_RUN=false PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" BRAND_HELPER="$PROJECT_ROOT/build/scripts/brand_config.py" MANIFEST_OUTPUT="$PROJECT_ROOT/build/dist/release-manifest.json" LOCAL_MANIFEST_OUTPUT="$PROJECT_ROOT/build/dist/release-manifest.local.json" EXISTING_MANIFEST_OUTPUT="$PROJECT_ROOT/build/dist/release-manifest.existing.json" LOCAL_DATA_OUTPUT="$PROJECT_ROOT/build/dist/release-data.local.json" while [[ $# -gt 0 ]]; do case $1 in -v|--version) VERSION="$2"; shift 2 ;; -u|--url) FORGEJO_URL="$2"; shift 2 ;; --brand) BRANDS+=("$2"); shift 2 ;; --clear-credentials) CLEAR_CREDS=true; shift ;; --dry-run) DRY_RUN=true; shift ;; *) echo "Unknown option: $1"; exit 1 ;; esac done if [ "$CLEAR_CREDS" = true ]; then unset FORGEJO_USER unset FORGEJO_PASS echo "[OK] Credentials cleared from this session" exit 0 fi if [ -z "$VERSION" ]; then VERSION="$(python3 -c "from pathlib import Path; import sys; sys.path.insert(0, str(Path(r'$PROJECT_ROOT/build/scripts').resolve())); from version_utils import get_current_version; print(get_current_version())")" fi get_release_notes() { python3 -c "from pathlib import Path; import sys; sys.path.insert(0, str(Path(r'$PROJECT_ROOT/build/scripts').resolve())); from version_utils import get_release_notes; print(get_release_notes('$VERSION'))" } LOCAL_ARGS=("$BRAND_HELPER" "local-release-data" "--platform" "macos" "--version" "$VERSION") if [ ${#BRANDS[@]} -gt 0 ]; then LOCAL_ARGS+=("--brands" "${BRANDS[@]}") fi python3 "${LOCAL_ARGS[@]}" > "$LOCAL_DATA_OUTPUT" ARTIFACTS=() while IFS= read -r artifact; do if [ -n "$artifact" ]; then ARTIFACTS+=("$artifact") fi done < <(python3 - "$LOCAL_DATA_OUTPUT" "$LOCAL_MANIFEST_OUTPUT" <<'PY' import json import sys from pathlib import Path data = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8")) Path(sys.argv[2]).write_text(json.dumps(data["manifest"], indent=2), encoding="utf-8") for artifact in data["artifacts"]: print(artifact) PY ) VALIDATION_RESULT=$(python3 - "$LOCAL_DATA_OUTPUT" "$LOCAL_MANIFEST_OUTPUT" <<'PY' import json import sys from pathlib import Path data = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8")) manifest = json.loads(Path(sys.argv[2]).read_text(encoding="utf-8")) selected_brands = data.get("brands", []) manifest_brands = manifest.get("brands", {}) missing = [] for brand_id in selected_brands: platform_entry = manifest_brands.get(brand_id, {}).get("macos-universal", {}) installer_name = platform_entry.get("installer", "") if not installer_name: missing.append(brand_id) if missing: print("MISSING:" + ",".join(missing)) else: print("OK") PY ) if [[ "$VALIDATION_RESULT" == MISSING:* ]]; then MISSING_BRANDS="${VALIDATION_RESULT#MISSING:}" echo "ERROR: release-manifest.json is missing macos-universal installer entries for brand(s): $MISSING_BRANDS" echo "Build the missing brand(s) first or check artifact naming/version before creating the release." exit 1 fi if [ ${#BRANDS[@]} -gt 0 ]; then FOUND_BRANDS_CSV=$(python3 - "$LOCAL_DATA_OUTPUT" <<'PY' import json import sys from pathlib import Path data = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8")) print(",".join(data.get("brands", []))) PY ) for REQUESTED_BRAND in "${BRANDS[@]}"; do if [[ ",$FOUND_BRANDS_CSV," != *",$REQUESTED_BRAND,"* ]]; then echo "ERROR: Requested brand '$REQUESTED_BRAND' has no local macOS artifact for version $VERSION" echo "Run build/scripts/build_macos.sh --brand $REQUESTED_BRAND first, then retry release creation." exit 1 fi done fi for ARTIFACT in "${ARTIFACTS[@]}"; do if [ -f "$ARTIFACT" ] && [ "${ARTIFACT##*.}" = "dmg" ]; then DMG_SIZE=$(du -m "$ARTIFACT" | cut -f1) echo "macOS artifact: $(basename "$ARTIFACT") ($DMG_SIZE MB)" fi done if [ ${#ARTIFACTS[@]} -eq 0 ]; then echo "ERROR: No local macOS artifacts found" exit 1 fi if [ "$DRY_RUN" = true ]; then cp "$LOCAL_MANIFEST_OUTPUT" "$MANIFEST_OUTPUT" DISCOVERED_BRANDS=$(python3 - "$LOCAL_DATA_OUTPUT" <<'PY' import json import sys from pathlib import Path data = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8")) print(", ".join(data.get("brands", [])) or "") PY ) echo "[DRY RUN] No network requests or uploads will be performed." echo "[DRY RUN] Release tag: v$VERSION" echo "[DRY RUN] Release URL: $FORGEJO_URL/$REPO/releases/tag/v$VERSION" echo "[DRY RUN] Discovered brands: $DISCOVERED_BRANDS" echo "[DRY RUN] Artifacts that would be uploaded:" for ARTIFACT in "${ARTIFACTS[@]}"; do echo " - $ARTIFACT" done echo "[DRY RUN] Local manifest preview: $MANIFEST_OUTPUT" exit 0 fi if [ -z "$FORGEJO_USER" ] || [ -z "$FORGEJO_PASS" ]; then echo "Forgejo credentials not found. Enter your credentials:" if [ -z "$FORGEJO_USER" ]; then read -r -p "Username: " FORGEJO_USER fi if [ -z "$FORGEJO_PASS" ]; then read -r -s -p "Password: " FORGEJO_PASS echo "" fi export FORGEJO_USER export FORGEJO_PASS fi BASIC_AUTH=$(echo -n "${FORGEJO_USER}:${FORGEJO_PASS}" | base64) RELEASE_URL="$FORGEJO_URL/api/v1/repos/$REPO/releases" RELEASE_LOOKUP_URL="$FORGEJO_URL/api/v1/repos/$REPO/releases/tags/v$VERSION" RELEASE_RESPONSE_FILE=$(mktemp) HTTP_CODE=$(curl -s -o "$RELEASE_RESPONSE_FILE" -w "%{http_code}" -H "Authorization: Basic $BASIC_AUTH" "$RELEASE_LOOKUP_URL") if [ "$HTTP_CODE" = "200" ]; then RELEASE_ID=$(python3 - "$RELEASE_RESPONSE_FILE" <<'PY' import json import sys from pathlib import Path payload = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8")) print(payload.get("id", "")) PY ) else RELEASE_ID="" fi if [ -z "$RELEASE_ID" ]; then RELEASE_BODY="$(get_release_notes)" RELEASE_DATA=$(RELEASE_BODY="$RELEASE_BODY" VERSION="$VERSION" python3 - <<'PY' import json import os print(json.dumps({ "tag_name": f"v{os.environ['VERSION']}", "name": f"WebDropBridge v{os.environ['VERSION']}", "body": os.environ["RELEASE_BODY"], "draft": False, "prerelease": False, })) PY ) HTTP_CODE=$(curl -s -o "$RELEASE_RESPONSE_FILE" -w "%{http_code}" -X POST \ -H "Authorization: Basic $BASIC_AUTH" \ -H "Content-Type: application/json" \ -d "$RELEASE_DATA" \ "$RELEASE_URL") if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "201" ]; then RELEASE_ID=$(python3 - "$RELEASE_RESPONSE_FILE" <<'PY' import json import sys from pathlib import Path payload = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8")) print(payload.get("id", "")) PY ) fi fi if [ -z "$RELEASE_ID" ]; then echo "ERROR creating or finding release" cat "$RELEASE_RESPONSE_FILE" exit 1 fi MANIFEST_URL=$(python3 - "$RELEASE_RESPONSE_FILE" <<'PY' import json import sys from pathlib import Path payload = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8")) for asset in payload.get("assets", []): if asset.get("name") == "release-manifest.json": print(asset.get("browser_download_url", "")) break PY ) if [ -n "$MANIFEST_URL" ]; then curl -s -H "Authorization: Basic $BASIC_AUTH" "$MANIFEST_URL" -o "$EXISTING_MANIFEST_OUTPUT" python3 "$BRAND_HELPER" merge-manifests --base "$EXISTING_MANIFEST_OUTPUT" --overlay "$LOCAL_MANIFEST_OUTPUT" --output "$MANIFEST_OUTPUT" >/dev/null else cp "$LOCAL_MANIFEST_OUTPUT" "$MANIFEST_OUTPUT" fi ARTIFACTS+=("$MANIFEST_OUTPUT") UPLOAD_URL="$FORGEJO_URL/api/v1/repos/$REPO/releases/$RELEASE_ID/assets" for ARTIFACT in "${ARTIFACTS[@]}"; do ASSET_NAME="$(basename "$ARTIFACT")" EXISTING_ASSET_ID=$(python3 - "$RELEASE_RESPONSE_FILE" "$ASSET_NAME" <<'PY' import json import sys from pathlib import Path payload = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8")) asset_name = sys.argv[2] for asset in payload.get("assets", []): if asset.get("name") == asset_name: print(asset.get("id", "")) break PY ) if [ -n "$EXISTING_ASSET_ID" ]; then curl -s -X DELETE \ -H "Authorization: Basic $BASIC_AUTH" \ "$FORGEJO_URL/api/v1/repos/$REPO/releases/$RELEASE_ID/assets/$EXISTING_ASSET_ID" >/dev/null echo "[OK] Replaced existing asset $ASSET_NAME" fi HTTP_CODE=$(curl -s -w "%{http_code}" -X POST \ -H "Authorization: Basic $BASIC_AUTH" \ -F "attachment=@$ARTIFACT" \ "$UPLOAD_URL" \ -o /tmp/curl_response.txt) if [ "$HTTP_CODE" -eq 201 ] || [ "$HTTP_CODE" -eq 200 ]; then echo "[OK] Uploaded $ASSET_NAME" else echo "ERROR uploading $ASSET_NAME (HTTP $HTTP_CODE)" cat /tmp/curl_response.txt exit 1 fi done echo "" echo "[OK] Release complete!" echo "View at: $FORGEJO_URL/$REPO/releases/tag/v$VERSION"