Restructure release workflow: dev prepares, Mac builds+uploads
Two scripts: - prepare-release.sh (run by me/Kiro on dev machine): Writes releases/vX.Y.Z.md, bumps packageVersion, commits, tags, pushes - release.sh (run by you on Mac): Reads version from current git tag, reads notes from releases/<tag>.md, builds DMG, creates Gitea release, uploads DMG. No user input needed — just run ./release.sh Workflow: Dev: ./prepare-release.sh 1.0.0 'Release notes' Mac: git fetch --tags && git checkout v1.0.0 && ./release.sh
This commit is contained in:
68
prepare-release.sh
Executable file
68
prepare-release.sh
Executable file
@@ -0,0 +1,68 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# prepare-release.sh — prepare a release commit and tag (run on dev machine)
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./prepare-release.sh 1.0.0 "Release notes here..."
|
||||||
|
#
|
||||||
|
# This script:
|
||||||
|
# 1. Writes release notes to releases/v<version>.md
|
||||||
|
# 2. Updates packageVersion in build.gradle.kts
|
||||||
|
# 3. Commits both
|
||||||
|
# 4. Creates and pushes the annotated tag
|
||||||
|
#
|
||||||
|
# After this, the Mac operator runs ./release.sh to build and upload.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [ $# -lt 2 ]; then
|
||||||
|
echo "Usage: $0 <version> <release-notes>"
|
||||||
|
echo " Example: $0 1.0.0 'First release with Android and iPhone support'"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
VERSION="$1"
|
||||||
|
TAG="v${VERSION}"
|
||||||
|
NOTES="$2"
|
||||||
|
|
||||||
|
# ── Pre-flight ────────────────────────────────────────────────────────────────
|
||||||
|
if ! git diff --quiet || ! git diff --cached --quiet; then
|
||||||
|
echo "ERROR: Uncommitted changes. Commit or stash first."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if git tag | grep -q "^${TAG}$"; then
|
||||||
|
echo "ERROR: Tag $TAG already exists."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Write release notes ───────────────────────────────────────────────────────
|
||||||
|
mkdir -p releases
|
||||||
|
NOTES_FILE="releases/${TAG}.md"
|
||||||
|
cat > "$NOTES_FILE" << EOF
|
||||||
|
$NOTES
|
||||||
|
EOF
|
||||||
|
echo "→ Wrote $NOTES_FILE"
|
||||||
|
|
||||||
|
# ── Update version ────────────────────────────────────────────────────────────
|
||||||
|
sed -i "s/packageVersion = \"[^\"]*\"/packageVersion = \"${VERSION}\"/" build.gradle.kts
|
||||||
|
echo "→ Updated packageVersion to $VERSION"
|
||||||
|
|
||||||
|
# ── Commit ────────────────────────────────────────────────────────────────────
|
||||||
|
git add "$NOTES_FILE" build.gradle.kts
|
||||||
|
git commit -m "Release $TAG"
|
||||||
|
echo "→ Committed release files"
|
||||||
|
|
||||||
|
# ── Tag ───────────────────────────────────────────────────────────────────────
|
||||||
|
git tag -a "$TAG" -m "$NOTES"
|
||||||
|
echo "→ Created tag $TAG"
|
||||||
|
|
||||||
|
# ── Push ──────────────────────────────────────────────────────────────────────
|
||||||
|
echo "→ Pushing to origin..."
|
||||||
|
GIT_ASKPASS='' GIT_TERMINAL_PROMPT=0 git push origin HEAD:refs/heads/main 2>&1 || \
|
||||||
|
echo " (push to main may require running from Mac — tag is local)"
|
||||||
|
GIT_ASKPASS='' GIT_TERMINAL_PROMPT=0 git push origin "$TAG" 2>&1 || \
|
||||||
|
echo " (tag push may require running from Mac)"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✓ Release $TAG prepared."
|
||||||
|
echo " On your Mac: git fetch --tags && git checkout $TAG && ./release.sh"
|
||||||
149
release.sh
149
release.sh
@@ -1,113 +1,60 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# release.sh — build and publish a PhotoPhetch release to Gitea
|
# release.sh — build and publish a PhotoPhetch release to Gitea
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage (on your Mac after pulling tagged commit):
|
||||||
# ./release.sh 1.0.0 "First release with Android and iPhone support"
|
# ./release.sh
|
||||||
# ./release.sh 1.1.0 # prompts for release notes
|
|
||||||
#
|
#
|
||||||
# Requires:
|
# The release version and notes are read from the current git tag and
|
||||||
# - curl (built into macOS)
|
# releases/<tag>.md — both committed by the dev machine before tagging.
|
||||||
# - git
|
#
|
||||||
# - ./gradlew (Gradle wrapper in this directory)
|
# Requires GITEA_TOKEN environment variable.
|
||||||
# - GITEA_TOKEN environment variable, or it will prompt
|
|
||||||
# Get a token: https://git.bolenpad.com/user/settings/applications
|
# Get a token: https://git.bolenpad.com/user/settings/applications
|
||||||
|
# Add to ~/.zshrc: export GITEA_TOKEN=your_token_here
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# ── Config ──────────────────────────────────────────────────────────────────
|
# ── Config ───────────────────────────────────────────────────────────────────
|
||||||
GITEA_URL="https://git.bolenpad.com"
|
GITEA_URL="https://git.bolenpad.com"
|
||||||
GITEA_USER="kbolen"
|
GITEA_USER="kbolen"
|
||||||
GITEA_REPO="photophetch"
|
GITEA_REPO="photophetch"
|
||||||
|
|
||||||
# ── Arguments ───────────────────────────────────────────────────────────────
|
# ── Resolve version from current git tag ─────────────────────────────────────
|
||||||
if [ $# -lt 1 ]; then
|
TAG=$(git describe --exact-match --tags HEAD 2>/dev/null || echo "")
|
||||||
echo "Usage: $0 <version> [release-notes]"
|
if [ -z "$TAG" ]; then
|
||||||
echo " Example: $0 1.0.0 'First stable release'"
|
echo "ERROR: HEAD is not on a tag. Pull the latest and checkout the release tag."
|
||||||
|
echo " git fetch --tags && git checkout v1.0.0"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
VERSION="$1"
|
VERSION="${TAG#v}" # strip leading 'v'
|
||||||
TAG="v${VERSION}"
|
echo "→ Building release $TAG (version $VERSION)"
|
||||||
|
|
||||||
# Release notes: from argument, or auto-generate from git log
|
# ── Read release notes from releases/<tag>.md ────────────────────────────────
|
||||||
if [ $# -ge 2 ]; then
|
NOTES_FILE="releases/${TAG}.md"
|
||||||
NOTES="$2"
|
if [ ! -f "$NOTES_FILE" ]; then
|
||||||
else
|
echo "ERROR: Release notes file not found: $NOTES_FILE"
|
||||||
# Auto-generate from git log since last tag (or all commits if no tags yet)
|
echo " The dev machine should have committed this before tagging."
|
||||||
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
|
|
||||||
if [ -n "$LAST_TAG" ]; then
|
|
||||||
RAW_LOG=$(git log "${LAST_TAG}..HEAD" --oneline --no-merges)
|
|
||||||
LOG_RANGE="since $LAST_TAG"
|
|
||||||
else
|
|
||||||
RAW_LOG=$(git log --oneline --no-merges | head -30)
|
|
||||||
LOG_RANGE="all commits"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "Auto-generated release notes ($LOG_RANGE):"
|
|
||||||
echo "──────────────────────────────────────────"
|
|
||||||
echo "$RAW_LOG"
|
|
||||||
echo "──────────────────────────────────────────"
|
|
||||||
echo ""
|
|
||||||
echo "Accept these as release notes? [y=yes, e=edit, q=quit]"
|
|
||||||
read -r CHOICE
|
|
||||||
|
|
||||||
case "$CHOICE" in
|
|
||||||
y|Y)
|
|
||||||
NOTES="$RAW_LOG"
|
|
||||||
;;
|
|
||||||
e|E)
|
|
||||||
TMPFILE=$(mktemp /tmp/photophetch-release-notes.XXXXXX.md)
|
|
||||||
echo "$RAW_LOG" > "$TMPFILE"
|
|
||||||
${EDITOR:-nano} "$TMPFILE"
|
|
||||||
NOTES=$(cat "$TMPFILE")
|
|
||||||
rm -f "$TMPFILE"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Aborted."
|
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
|
||||||
esac
|
|
||||||
fi
|
fi
|
||||||
|
NOTES=$(cat "$NOTES_FILE")
|
||||||
|
echo "→ Release notes: $NOTES_FILE"
|
||||||
|
|
||||||
# Gitea token
|
# ── Gitea token ───────────────────────────────────────────────────────────────
|
||||||
if [ -z "${GITEA_TOKEN:-}" ]; then
|
if [ -z "${GITEA_TOKEN:-}" ]; then
|
||||||
echo "GITEA_TOKEN not set."
|
read -rsp "GITEA_TOKEN not set. Enter token: " GITEA_TOKEN
|
||||||
read -rsp "Enter Gitea API token: " GITEA_TOKEN
|
|
||||||
echo
|
echo
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── Pre-flight checks ────────────────────────────────────────────────────────
|
# ── Check packageVersion matches tag ─────────────────────────────────────────
|
||||||
echo "→ Checking for uncommitted changes..."
|
GRADLE_VERSION=$(grep 'packageVersion' build.gradle.kts | sed 's/.*"\(.*\)".*/\1/')
|
||||||
if ! git diff --quiet || ! git diff --cached --quiet; then
|
if [ "$GRADLE_VERSION" != "$VERSION" ]; then
|
||||||
echo "ERROR: Uncommitted changes exist. Commit or stash them first."
|
echo "WARNING: build.gradle.kts packageVersion ($GRADLE_VERSION) != tag version ($VERSION)"
|
||||||
exit 1
|
echo "Continue anyway? [y/N]"
|
||||||
|
read -r CONFIRM
|
||||||
|
[[ "$CONFIRM" == "y" || "$CONFIRM" == "Y" ]] || exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "→ Checking we're on main..."
|
# ── Build DMG ─────────────────────────────────────────────────────────────────
|
||||||
BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
|
||||||
if [ "$BRANCH" != "main" ]; then
|
|
||||||
echo "ERROR: Not on main branch (currently on '$BRANCH'). Switch to main first."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "→ Checking tag $TAG doesn't already exist..."
|
|
||||||
if git tag | grep -q "^${TAG}$"; then
|
|
||||||
echo "ERROR: Tag $TAG already exists."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── Update version in build.gradle.kts ──────────────────────────────────────
|
|
||||||
echo "→ Updating version to $VERSION in build.gradle.kts..."
|
|
||||||
sed -i.bak "s/packageVersion = \"[^\"]*\"/packageVersion = \"${VERSION}\"/" build.gradle.kts
|
|
||||||
rm -f build.gradle.kts.bak
|
|
||||||
|
|
||||||
# ── Commit version bump ──────────────────────────────────────────────────────
|
|
||||||
echo "→ Committing version bump..."
|
|
||||||
git add build.gradle.kts
|
|
||||||
git commit -m "Bump version to $VERSION"
|
|
||||||
|
|
||||||
# ── Build DMG ───────────────────────────────────────────────────────────────
|
|
||||||
echo "→ Building DMG (this takes a few minutes)..."
|
echo "→ Building DMG (this takes a few minutes)..."
|
||||||
./gradlew packageDmg
|
./gradlew packageDmg
|
||||||
|
|
||||||
@@ -119,16 +66,20 @@ fi
|
|||||||
DMG_NAME=$(basename "$DMG_PATH")
|
DMG_NAME=$(basename "$DMG_PATH")
|
||||||
echo "→ Built: $DMG_PATH"
|
echo "→ Built: $DMG_PATH"
|
||||||
|
|
||||||
# ── Tag and push ─────────────────────────────────────────────────────────────
|
# ── Check release doesn't already exist ──────────────────────────────────────
|
||||||
echo "→ Creating tag $TAG..."
|
EXISTING=$(curl -s -o /dev/null -w "%{http_code}" \
|
||||||
git tag -a "$TAG" -m "Release $TAG"
|
"${GITEA_URL}/api/v1/repos/${GITEA_USER}/${GITEA_REPO}/releases/tags/${TAG}" \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}")
|
||||||
|
if [ "$EXISTING" = "200" ]; then
|
||||||
|
echo "ERROR: Release $TAG already exists on Gitea."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
echo "→ Pushing commits and tag..."
|
# ── Create Gitea release ──────────────────────────────────────────────────────
|
||||||
git push origin main
|
|
||||||
git push origin "$TAG"
|
|
||||||
|
|
||||||
# ── Create Gitea release ─────────────────────────────────────────────────────
|
|
||||||
echo "→ Creating Gitea release $TAG..."
|
echo "→ Creating Gitea release $TAG..."
|
||||||
|
# Escape notes for JSON
|
||||||
|
NOTES_JSON=$(echo "$NOTES" | python3 -c "import sys,json; print(json.dumps(sys.stdin.read()))")
|
||||||
|
|
||||||
RELEASE_RESPONSE=$(curl -s -X POST \
|
RELEASE_RESPONSE=$(curl -s -X POST \
|
||||||
"${GITEA_URL}/api/v1/repos/${GITEA_USER}/${GITEA_REPO}/releases" \
|
"${GITEA_URL}/api/v1/repos/${GITEA_USER}/${GITEA_REPO}/releases" \
|
||||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
@@ -136,7 +87,7 @@ RELEASE_RESPONSE=$(curl -s -X POST \
|
|||||||
-d "{
|
-d "{
|
||||||
\"tag_name\": \"${TAG}\",
|
\"tag_name\": \"${TAG}\",
|
||||||
\"name\": \"PhotoPhetch ${TAG}\",
|
\"name\": \"PhotoPhetch ${TAG}\",
|
||||||
\"body\": \"${NOTES}\",
|
\"body\": ${NOTES_JSON},
|
||||||
\"draft\": false,
|
\"draft\": false,
|
||||||
\"prerelease\": false
|
\"prerelease\": false
|
||||||
}")
|
}")
|
||||||
@@ -149,7 +100,7 @@ if [ -z "$RELEASE_ID" ]; then
|
|||||||
fi
|
fi
|
||||||
echo "→ Created release ID $RELEASE_ID"
|
echo "→ Created release ID $RELEASE_ID"
|
||||||
|
|
||||||
# ── Upload DMG ───────────────────────────────────────────────────────────────
|
# ── Upload DMG ────────────────────────────────────────────────────────────────
|
||||||
echo "→ Uploading $DMG_NAME..."
|
echo "→ Uploading $DMG_NAME..."
|
||||||
UPLOAD_RESPONSE=$(curl -s -X POST \
|
UPLOAD_RESPONSE=$(curl -s -X POST \
|
||||||
"${GITEA_URL}/api/v1/repos/${GITEA_USER}/${GITEA_REPO}/releases/${RELEASE_ID}/assets?name=${DMG_NAME}" \
|
"${GITEA_URL}/api/v1/repos/${GITEA_USER}/${GITEA_REPO}/releases/${RELEASE_ID}/assets?name=${DMG_NAME}" \
|
||||||
@@ -159,12 +110,10 @@ UPLOAD_RESPONSE=$(curl -s -X POST \
|
|||||||
|
|
||||||
DOWNLOAD_URL=$(echo "$UPLOAD_RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin).get('browser_download_url',''))" 2>/dev/null)
|
DOWNLOAD_URL=$(echo "$UPLOAD_RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin).get('browser_download_url',''))" 2>/dev/null)
|
||||||
if [ -z "$DOWNLOAD_URL" ]; then
|
if [ -z "$DOWNLOAD_URL" ]; then
|
||||||
echo "WARNING: Upload may have failed. Response:"
|
echo "WARNING: Upload response unexpected:"
|
||||||
echo "$UPLOAD_RESPONSE"
|
echo "$UPLOAD_RESPONSE"
|
||||||
else
|
else
|
||||||
|
echo ""
|
||||||
echo "✓ Released: $DOWNLOAD_URL"
|
echo "✓ Released: $DOWNLOAD_URL"
|
||||||
|
echo " Release page: ${GITEA_URL}/${GITEA_USER}/${GITEA_REPO}/releases/tag/${TAG}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "✓ Done! PhotoPhetch $TAG released."
|
|
||||||
echo " Release page: ${GITEA_URL}/${GITEA_USER}/${GITEA_REPO}/releases/tag/${TAG}"
|
|
||||||
|
|||||||
0
releases/.gitkeep
Normal file
0
releases/.gitkeep
Normal file
Reference in New Issue
Block a user