#!/usr/bin/env bash # TrainUs release script — local steps only. # Prepares a release commit, tag, source archive, and a draft release notes file. # Everything that touches Forgejo or a remote server is left to you (see checklist at the end). # Usage: ./scripts/release.sh set -euo pipefail GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m' RED='\033[0;31m'; DIM='\033[2m' info() { echo -e " ${GREEN}✓${NC} $*"; } doing() { echo -e "\n${CYAN}▶${NC} $*"; } warn() { echo -e " ${YELLOW}⚠${NC} $*"; } error() { echo -e "\n${RED}✗ Error:${NC} $*" >&2; exit 1; } SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" cd "$ROOT_DIR" # Print a recovery hint if the script dies between the version bump and the commit. BUMPED=0 COMMITTED=0 on_exit() { if [[ $? -ne 0 && "$BUMPED" == "1" && "$COMMITTED" == "0" ]]; then echo -e "\n${YELLOW}⚠ Release interrupted after the version bump.${NC}" >&2 echo " To undo it: git checkout -- package.json package-lock.json" >&2 fi } trap on_exit EXIT # ── Prerequisites ───────────────────────────────────────────────────────────── echo "" echo -e "${BOLD}Checking prerequisites...${NC}" command -v npm >/dev/null 2>&1 || error "npm is not installed." command -v git >/dev/null 2>&1 || error "git is not installed." info "npm and git are available." if [[ -n "$(git status --porcelain)" ]]; then error "Working tree is not clean. Commit or stash your changes before releasing." fi info "Working tree is clean." CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) if [[ "$CURRENT_BRANCH" != "main" ]]; then warn "Current branch is '$CURRENT_BRANCH', not 'main'." fi info "On branch: $CURRENT_BRANCH" # ── Quality gate ────────────────────────────────────────────────────────────── echo "" echo -e "${BOLD}Quality gate${NC}" echo " Running: npm run lint" npm run lint --silent || error "Lint failed. Fix the issues before releasing." info "Lint passed." echo " Running: npm run format:check" npm run format:check --silent || error "Formatting check failed. Run 'npm run format' and commit." info "Formatting is clean." echo -n " Run the Playwright test suite now? (slow) [Y/n] " read -r RUN_TESTS if [[ "$RUN_TESTS" =~ ^[Nn] ]]; then warn "Tests skipped — make sure the suite passed recently." else echo " Running: npm run test:parallel" npm run test:parallel || error "Tests failed. Fix them before releasing." info "Tests passed on Firefox and Chromium." fi # ── Commits since last release ──────────────────────────────────────────────── echo "" echo -e "${BOLD}Changes since last release${NC}" LAST_TAG=$(git describe --tags --abbrev=0 HEAD 2>/dev/null || echo "") if [[ -n "$LAST_TAG" ]]; then echo " Last release: $LAST_TAG" echo " Commits since $LAST_TAG:" echo "" COMMIT_LOG=$(git log "${LAST_TAG}..HEAD" --oneline --no-merges) if [[ -z "$COMMIT_LOG" ]]; then warn "No commits since $LAST_TAG. Are you sure you want to release?" else while IFS= read -r line; do echo -e " ${DIM}${line}${NC}" done <<< "$COMMIT_LOG" fi else COMMIT_LOG="" info "No previous tag — this is the first release." fi echo "" # ── Version ─────────────────────────────────────────────────────────────────── echo -e "${BOLD}Version${NC}" CURRENT_VERSION=$(node -p "require('./package.json').version") echo " Current version: $CURRENT_VERSION" echo -n " New version (semver, e.g. 1.1.0): " read -r VERSION [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] \ || error "Invalid format. Use MAJOR.MINOR.PATCH without a 'v' prefix." TAG="v$VERSION" mkdir -p tmp ARCHIVE="tmp/trainus-${TAG}-src.zip" NOTES_FILE="tmp/trainus-${TAG}-notes.md" git tag --list | grep -q "^${TAG}$" \ && error "Tag $TAG already exists locally." # ── Plan ────────────────────────────────────────────────────────────────────── echo "" echo -e "${BOLD}The script will perform the following steps in order:${NC}" echo "" echo " 1. Bump version in package.json and package-lock.json" echo " $CURRENT_VERSION → $VERSION" echo "" echo " 2. Build the production app" echo " npm run build → dist/" echo "" echo " 3. Create a release commit on branch '$CURRENT_BRANCH'" echo " git commit -m \"chore: release $TAG\"" echo "" echo " 4. Create an annotated git tag (local only, not pushed)" echo " git tag -a $TAG -m \"Release $TAG\"" echo "" echo " 5. Create a source archive of the current commit" echo " $ARCHIVE (tracked files only, no node_modules, no dist)" echo "" echo " 6. Generate a draft release notes file for you to edit" echo " $NOTES_FILE (pre-filled draft to edit)" echo "" echo -e "${YELLOW}Nothing will be pushed or sent anywhere. All steps are local.${NC}" echo "" echo -n " Continue? [y/N] " read -r CONFIRM [[ "$CONFIRM" =~ ^[Yy]$ ]] || { echo " Aborted."; exit 0; } # ── Step 1 — Bump version ───────────────────────────────────────────────────── doing "Step 1/6 — Bumping version in package.json and package-lock.json" echo " Running: npm version $VERSION --no-git-tag-version" npm version "$VERSION" --no-git-tag-version --silent BUMPED=1 info "package.json and package-lock.json updated to $VERSION." # ── Step 2 — Build ──────────────────────────────────────────────────────────── doing "Step 2/6 — Building the production app" echo " Running: npm run build" npm run build info "Build complete. Output is in dist/." # ── Step 3 — Commit ─────────────────────────────────────────────────────────── doing "Step 3/6 — Creating release commit" echo " Running: git add package.json package-lock.json" echo " git commit -m \"chore: release $TAG\"" git add package.json package-lock.json git commit -m "chore: release $TAG" COMMITTED=1 info "Commit created: chore: release $TAG" # ── Step 4 — Tag ────────────────────────────────────────────────────────────── doing "Step 4/6 — Creating annotated git tag" echo " Running: git tag -a $TAG -m \"Release $TAG\"" git tag -a "$TAG" -m "Release $TAG" info "Tag $TAG created locally." # ── Step 5 — Archive ────────────────────────────────────────────────────────── doing "Step 5/6 — Creating source archive" echo " Running: git archive --format=zip HEAD --output=$ARCHIVE" git archive --format=zip HEAD --output="$ARCHIVE" info "Archive created: $ARCHIVE" echo " Contains: all files tracked by git at commit $(git rev-parse --short HEAD)." echo " Excludes: node_modules/, dist/, .git/, and any untracked files." # ── Step 6 — Release notes file ─────────────────────────────────────────────── doing "Step 6/6 — Generating draft release notes" echo " Writing: $NOTES_FILE" { echo "# TrainUs $TAG — Release notes" echo "" if [[ -z "$LAST_TAG" ]]; then echo "First release $VERSION." else echo "" echo "" echo "## What's new" echo "" echo "" echo "" echo "-" echo "" echo "## Bug fixes" echo "" echo "" echo "" echo "-" echo "" echo "## Breaking changes" echo "" echo "None." echo "" echo "---" echo "" echo "## Commits since $LAST_TAG" echo "" echo "" echo "" if [[ -n "$COMMIT_LOG" ]]; then while IFS= read -r line; do echo "- $line" done <<< "$COMMIT_LOG" else echo "- (none)" fi fi } > "$NOTES_FILE" info "Draft release notes written to $NOTES_FILE." echo " Open it in your editor, rewrite the notes, then paste into Forgejo." # ── Done — Manual checklist ─────────────────────────────────────────────────── echo "" echo -e "${GREEN}${BOLD}Local release steps complete.${NC}" echo "" echo -e "${BOLD}Artifacts created:${NC}" echo " $ARCHIVE ← attach to the Forgejo release" echo " $NOTES_FILE ← edit and paste as the release body" echo "" echo -e "${BOLD}Manual steps remaining:${NC}" echo "" echo -e " ${BOLD}1. Edit the release notes${NC}" echo " Open $NOTES_FILE and rewrite the notes into polished prose." echo "" echo -e " ${BOLD}2. Push the commit and tag to Forgejo${NC}" echo " git push origin $CURRENT_BRANCH" echo " git push origin $TAG" echo "" echo -e " ${BOLD}3. Create the release on Forgejo${NC}" echo " Go to your repository → Releases → New release" echo " Tag: $TAG" echo " Title: TrainUs $TAG" echo " Body: paste the contents of $NOTES_FILE" echo " Attach: $ARCHIVE" echo " Publish the release." echo "" echo -e " ${BOLD}4. Deploy the app${NC}" echo " Upload the contents of dist/ to your static host." echo "" echo -e " ${BOLD}5. Deploy the website${NC}" echo " npm run site:build" echo " Upload the contents of website/public/ to your website host." echo "" echo -e " ${BOLD}6. Publish the release blog post${NC}" echo " Duplicate website/content/blog/release-template.en.md" echo " Rename it: $(date +%Y-%m-%d)-release-${TAG}.en.md" echo " Fill in the placeholders — set draft: false when ready." echo " Write the French version yourself." echo " Rebuild and redeploy the website (step 5)." echo "" echo -e " ${BOLD}7. Clean up local artifacts (optional — tmp/ is gitignored)${NC}" echo " Once uploaded and published: rm $ARCHIVE $NOTES_FILE" echo ""