#!/usr/bin/env bash set -euo pipefail # ── Configuration ────────────────────────────────────────────── GITEA_URL="https://code.crashlabs.io" REPO="dave/storkit" BINARY_NAME="storkit" # ── 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 " 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 # ── Bump version in Cargo.toml ──────────────────────────────── CARGO_TOML="${SCRIPT_DIR}/server/Cargo.toml" if ! grep -q "^version = " "$CARGO_TOML"; then echo "Error: Could not find version field in ${CARGO_TOML}" exit 1 fi sed -i '' "s/^version = \".*\"/version = \"${VERSION}\"/" "$CARGO_TOML" echo "==> Bumped ${CARGO_TOML} to ${VERSION}" PACKAGE_JSON="${SCRIPT_DIR}/frontend/package.json" sed -i '' "s/\"version\": \".*\"/\"version\": \"${VERSION}\"/" "$PACKAGE_JSON" echo "==> Bumped ${PACKAGE_JSON} to ${VERSION}" git add "$CARGO_TOML" "$PACKAGE_JSON" git commit -m "Bump version to ${VERSION}" 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}"/ # ── Changelog ────────────────────────────────────────────────── echo "==> Generating changelog..." PREV_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") if [ -n "$PREV_TAG" ]; then LOG_RANGE="${PREV_TAG}..HEAD" RANGE="${PREV_TAG}...${TAG}" else LOG_RANGE="" RANGE="initial...${TAG}" fi # Extract completed stories/bugs/refactors from merge commits. # Matches both the current "storkit:" prefix and the legacy "story-kit:" prefix. # Deduplicate (a story may have been merged more than once after reverts). MERGE_RE="^(storkit|story-kit): merge " if [ -n "$LOG_RANGE" ]; then MERGED_RAW=$(git log "$LOG_RANGE" --pretty=format:"%s" --no-merges \ | grep -E "$MERGE_RE" | sed -E "s/$MERGE_RE//" | sort -u) else MERGED_RAW=$(git log --pretty=format:"%s" --no-merges \ | grep -E "$MERGE_RE" | sed -E "s/$MERGE_RE//" | 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 -Ev "^(storkit|story-kit): " \ | grep -Ev "^Revert \"(storkit|story-kit): " \ | grep -v "^Bump version" \ | sed 's/^/- /') else MANUAL=$(git log --pretty=format:"%s" --no-merges \ | grep -Ev "^(storkit|story-kit): " \ | grep -Ev "^Revert \"(storkit|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")" fi 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 RELEASE_BODY="${RELEASE_BODY} **Full diff:** ${GITEA_URL}/${REPO}/compare/${RANGE}" echo "$RELEASE_BODY" # ── Tag & Push ───────────────────────────────────────────────── echo "==> Tagging ${TAG}..." git tag -a "$TAG" -m "Release ${TAG}" git push origin "$TAG" # ── Create Gitea Release ────────────────────────────────────── echo "==> Creating release on Gitea..." 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") RELEASE_RESPONSE=$(curl -sf -X POST \ -H "Authorization: token ${GITEA_TOKEN}" \ -H "Content-Type: application/json" \ "${GITEA_URL}/api/v1/repos/${REPO}/releases" \ -d "$RELEASE_JSON") 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}"