2026-03-13 12:52:56 +00:00
|
|
|
#!/usr/bin/env bash
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
|
|
|
|
|
# ── Configuration ──────────────────────────────────────────────
|
|
|
|
|
GITEA_URL="https://code.crashlabs.io"
|
|
|
|
|
REPO="dave/story-kit"
|
|
|
|
|
BINARY_NAME="story-kit"
|
|
|
|
|
|
|
|
|
|
# ── Load .env if present ───────────────────────────────────────
|
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
|
|
|
if [ -f "${SCRIPT_DIR}/.env" ]; then
|
|
|
|
|
set -a
|
|
|
|
|
source "${SCRIPT_DIR}/.env"
|
|
|
|
|
set +a
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# ── Preflight ──────────────────────────────────────────────────
|
|
|
|
|
if [ -z "${GITEA_TOKEN:-}" ]; then
|
|
|
|
|
echo "Error: GITEA_TOKEN is not set."
|
|
|
|
|
echo "Create a token at ${GITEA_URL}/user/settings/applications"
|
|
|
|
|
echo "Then add to .env: GITEA_TOKEN=your_token"
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
VERSION="${1:-}"
|
|
|
|
|
if [ -z "$VERSION" ]; then
|
|
|
|
|
echo "Usage: script/release <version>"
|
|
|
|
|
echo "Example: script/release 0.2.0"
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
TAG="v${VERSION}"
|
|
|
|
|
|
|
|
|
|
if git rev-parse "$TAG" >/dev/null 2>&1; then
|
|
|
|
|
echo "Error: Tag ${TAG} already exists."
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
if ! command -v cross >/dev/null 2>&1; then
|
|
|
|
|
echo "Error: 'cross' is not installed. Run: cargo install cross"
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
if ! docker info >/dev/null 2>&1; then
|
|
|
|
|
echo "Error: Docker is not running. Start Docker Desktop first."
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
echo "==> Releasing ${TAG}"
|
|
|
|
|
|
|
|
|
|
# ── Build ──────────────────────────────────────────────────────
|
|
|
|
|
echo "==> Building macOS (native)..."
|
|
|
|
|
cargo build --release
|
|
|
|
|
|
|
|
|
|
echo "==> Building Linux (static musl via cross)..."
|
|
|
|
|
cross build --release --target x86_64-unknown-linux-musl
|
|
|
|
|
|
|
|
|
|
# ── Package ────────────────────────────────────────────────────
|
|
|
|
|
DIST="target/dist"
|
|
|
|
|
rm -rf "$DIST"
|
|
|
|
|
mkdir -p "$DIST"
|
|
|
|
|
|
|
|
|
|
cp "target/release/${BINARY_NAME}" "${DIST}/${BINARY_NAME}-macos-arm64"
|
|
|
|
|
cp "target/x86_64-unknown-linux-musl/release/${BINARY_NAME}" "${DIST}/${BINARY_NAME}-linux-amd64"
|
|
|
|
|
chmod +x "${DIST}"/*
|
|
|
|
|
|
|
|
|
|
echo "==> Binaries:"
|
|
|
|
|
ls -lh "${DIST}"/
|
|
|
|
|
|
2026-03-13 13:02:25 +00:00
|
|
|
# ── Changelog ──────────────────────────────────────────────────
|
|
|
|
|
echo "==> Generating changelog..."
|
|
|
|
|
PREV_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
|
|
|
|
|
if [ -n "$PREV_TAG" ]; then
|
2026-03-17 16:42:48 +00:00
|
|
|
LOG_RANGE="${PREV_TAG}..HEAD"
|
2026-03-13 13:02:25 +00:00
|
|
|
RANGE="${PREV_TAG}...${TAG}"
|
|
|
|
|
else
|
2026-03-17 16:42:48 +00:00
|
|
|
LOG_RANGE=""
|
2026-03-13 13:02:25 +00:00
|
|
|
RANGE="initial...${TAG}"
|
|
|
|
|
fi
|
|
|
|
|
|
2026-03-17 16:42:48 +00:00
|
|
|
# Extract completed stories/bugs/refactors from "story-kit: merge <id>" commits.
|
|
|
|
|
# Deduplicate (a story may have been merged more than once after reverts).
|
|
|
|
|
if [ -n "$LOG_RANGE" ]; then
|
|
|
|
|
MERGED_RAW=$(git log "$LOG_RANGE" --pretty=format:"%s" --no-merges \
|
|
|
|
|
| grep "^story-kit: merge " | sed 's/^story-kit: merge //' | sort -u)
|
|
|
|
|
else
|
|
|
|
|
MERGED_RAW=$(git log --pretty=format:"%s" --no-merges \
|
|
|
|
|
| grep "^story-kit: merge " | sed 's/^story-kit: merge //' | sort -u)
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# Categorise merged work items and format names.
|
|
|
|
|
FEATURES=""
|
|
|
|
|
FIXES=""
|
|
|
|
|
REFACTORS=""
|
|
|
|
|
while IFS= read -r item; do
|
|
|
|
|
[ -z "$item" ] && continue
|
|
|
|
|
# Strip the numeric prefix and type to get the human name.
|
|
|
|
|
name=$(echo "$item" | sed -E 's/^[0-9]+_(story|bug|refactor|spike)_//' | tr '_' ' ')
|
|
|
|
|
# Capitalise first letter.
|
|
|
|
|
name="$(echo "${name:0:1}" | tr '[:lower:]' '[:upper:]')${name:1}"
|
|
|
|
|
case "$item" in
|
|
|
|
|
*_bug_*) FIXES="${FIXES}- ${name}\n" ;;
|
|
|
|
|
*_refactor_*) REFACTORS="${REFACTORS}- ${name}\n" ;;
|
|
|
|
|
*) FEATURES="${FEATURES}- ${name}\n" ;;
|
|
|
|
|
esac
|
|
|
|
|
done <<< "$MERGED_RAW"
|
|
|
|
|
|
|
|
|
|
# Collect non-automation manual commits (direct fixes, version bumps, etc).
|
|
|
|
|
if [ -n "$LOG_RANGE" ]; then
|
|
|
|
|
MANUAL=$(git log "$LOG_RANGE" --pretty=format:"%s" --no-merges \
|
|
|
|
|
| grep -v "^story-kit: " \
|
|
|
|
|
| grep -v "^Revert \"story-kit: " \
|
|
|
|
|
| grep -v "^Bump version" \
|
|
|
|
|
| sed 's/^/- /')
|
|
|
|
|
else
|
|
|
|
|
MANUAL=$(git log --pretty=format:"%s" --no-merges \
|
|
|
|
|
| grep -v "^story-kit: " \
|
|
|
|
|
| grep -v "^Revert \"story-kit: " \
|
|
|
|
|
| grep -v "^Bump version" \
|
|
|
|
|
| sed 's/^/- /')
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# Assemble the release body.
|
|
|
|
|
RELEASE_BODY="## What's Changed"
|
|
|
|
|
|
|
|
|
|
if [ -n "$FEATURES" ]; then
|
|
|
|
|
RELEASE_BODY="${RELEASE_BODY}
|
|
|
|
|
|
|
|
|
|
### Features
|
|
|
|
|
$(echo -e "$FEATURES")"
|
2026-03-13 13:02:25 +00:00
|
|
|
fi
|
|
|
|
|
|
2026-03-17 16:42:48 +00:00
|
|
|
if [ -n "$FIXES" ]; then
|
|
|
|
|
RELEASE_BODY="${RELEASE_BODY}
|
|
|
|
|
|
|
|
|
|
### Bug Fixes
|
|
|
|
|
$(echo -e "$FIXES")"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
if [ -n "$REFACTORS" ]; then
|
|
|
|
|
RELEASE_BODY="${RELEASE_BODY}
|
|
|
|
|
|
|
|
|
|
### Refactors
|
|
|
|
|
$(echo -e "$REFACTORS")"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
if [ -n "$MANUAL" ]; then
|
|
|
|
|
RELEASE_BODY="${RELEASE_BODY}
|
|
|
|
|
|
|
|
|
|
### Other Changes
|
|
|
|
|
${MANUAL}"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
if [ -z "$FEATURES" ] && [ -z "$FIXES" ] && [ -z "$REFACTORS" ] && [ -z "$MANUAL" ]; then
|
|
|
|
|
RELEASE_BODY="${RELEASE_BODY}
|
|
|
|
|
|
|
|
|
|
- No changes since last release"
|
|
|
|
|
fi
|
2026-03-13 13:02:25 +00:00
|
|
|
|
2026-03-17 16:42:48 +00:00
|
|
|
RELEASE_BODY="${RELEASE_BODY}
|
2026-03-13 13:02:25 +00:00
|
|
|
|
|
|
|
|
**Full diff:** ${GITEA_URL}/${REPO}/compare/${RANGE}"
|
|
|
|
|
|
|
|
|
|
echo "$RELEASE_BODY"
|
|
|
|
|
|
2026-03-13 12:52:56 +00:00
|
|
|
# ── Tag & Push ─────────────────────────────────────────────────
|
|
|
|
|
echo "==> Tagging ${TAG}..."
|
|
|
|
|
git tag -a "$TAG" -m "Release ${TAG}"
|
|
|
|
|
git push origin "$TAG"
|
|
|
|
|
|
|
|
|
|
# ── Create Gitea Release ──────────────────────────────────────
|
|
|
|
|
echo "==> Creating release on Gitea..."
|
2026-03-13 13:02:25 +00:00
|
|
|
RELEASE_JSON=$(python3 -c "
|
|
|
|
|
import json, sys
|
|
|
|
|
print(json.dumps({
|
|
|
|
|
'tag_name': sys.argv[1],
|
|
|
|
|
'name': sys.argv[1],
|
|
|
|
|
'body': sys.argv[2]
|
|
|
|
|
}))
|
|
|
|
|
" "$TAG" "$RELEASE_BODY")
|
|
|
|
|
|
2026-03-13 12:52:56 +00:00
|
|
|
RELEASE_RESPONSE=$(curl -sf -X POST \
|
|
|
|
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
|
|
|
|
-H "Content-Type: application/json" \
|
|
|
|
|
"${GITEA_URL}/api/v1/repos/${REPO}/releases" \
|
2026-03-13 13:02:25 +00:00
|
|
|
-d "$RELEASE_JSON")
|
2026-03-13 12:52:56 +00:00
|
|
|
|
|
|
|
|
RELEASE_ID=$(echo "$RELEASE_RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
|
|
|
|
|
|
|
|
|
|
# ── Upload Binaries ───────────────────────────────────────────
|
|
|
|
|
for file in "${DIST}"/*; do
|
|
|
|
|
filename=$(basename "$file")
|
|
|
|
|
echo "==> Uploading ${filename}..."
|
|
|
|
|
curl -sf -X POST \
|
|
|
|
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
|
|
|
|
-F "attachment=@${file};filename=${filename}" \
|
|
|
|
|
"${GITEA_URL}/api/v1/repos/${REPO}/releases/${RELEASE_ID}/assets" > /dev/null
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
echo ""
|
|
|
|
|
echo "==> Done! Release ${TAG} published:"
|
|
|
|
|
echo " ${GITEA_URL}/${REPO}/releases/tag/${TAG}"
|