brainy/scripts/release.sh

252 lines
9.5 KiB
Bash
Executable file
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/bin/bash
set -e # Exit on error
# Brainy Release Script
# Simple, reliable release workflow: build → test → commit → push → publish → release
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Parse arguments
RELEASE_TYPE="${1:-patch}" # patch, minor, or major
SKIP_TESTS=false
DRY_RUN=false
for arg in "$@"; do
case $arg in
--skip-tests)
SKIP_TESTS=true
;;
--dry-run)
DRY_RUN=true
;;
esac
done
# An explicit version (e.g. "8.0.0-rc.1") may be passed in place of patch/minor/major.
EXPLICIT_VERSION=""
if [[ "$RELEASE_TYPE" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then
EXPLICIT_VERSION="$RELEASE_TYPE"
fi
# Release on whatever branch we're on — RC lines live on feature branches, not main.
CURRENT_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
echo -e "${BLUE}🚀 Brainy Release Script${NC}"
echo -e "${BLUE}Release type: ${RELEASE_TYPE}${NC}"
echo -e "${BLUE}Branch: ${CURRENT_BRANCH}${NC}\n"
if [ "$DRY_RUN" = true ]; then
echo -e "${YELLOW}⚠️ DRY RUN MODE - No changes will be made${NC}\n"
fi
if [ "$SKIP_TESTS" = true ]; then
echo -e "${YELLOW}⚠️ SKIPPING TESTS - Use with caution!${NC}\n"
fi
# Step 1: Verify clean git state
echo -e "${BLUE}1⃣ Checking git status...${NC}"
if [ -n "$(git status --porcelain)" ]; then
echo -e "${RED}❌ Working directory not clean. Commit or stash changes first.${NC}"
exit 1
fi
echo -e "${GREEN}✅ Working directory clean${NC}\n"
# Step 2: Build
echo -e "${BLUE}2⃣ Building project...${NC}"
if [ "$DRY_RUN" = false ]; then
npm run build
fi
echo -e "${GREEN}✅ Build successful${NC}\n"
# Step 3: Test
if [ "$SKIP_TESTS" = false ]; then
echo -e "${BLUE}3⃣ Running tests...${NC}"
if [ "$DRY_RUN" = false ]; then
# Full CI gate: unit + integration (test:ci = test:ci-unit && test:ci-integration).
npm run test:ci
fi
echo -e "${GREEN}✅ Tests passed${NC}\n"
else
echo -e "${YELLOW}3⃣ Skipping tests...${NC}\n"
fi
# Step 4: Get current and new version
CURRENT_VERSION=$(node -p "require('./package.json').version")
echo -e "${BLUE}Current version: ${CURRENT_VERSION}${NC}"
# Calculate new version
IFS='.' read -r -a VERSION_PARTS <<< "$CURRENT_VERSION"
MAJOR="${VERSION_PARTS[0]}"
MINOR="${VERSION_PARTS[1]}"
PATCH="${VERSION_PARTS[2]}"
if [ -n "$EXPLICIT_VERSION" ]; then
NEW_VERSION="$EXPLICIT_VERSION"
else
case $RELEASE_TYPE in
major)
NEW_VERSION="$((MAJOR + 1)).0.0"
;;
minor)
NEW_VERSION="${MAJOR}.$((MINOR + 1)).0"
;;
patch)
NEW_VERSION="${MAJOR}.${MINOR}.$((PATCH + 1))"
;;
*)
echo -e "${RED}❌ Invalid release type: ${RELEASE_TYPE}${NC}"
echo "Usage: ./scripts/release.sh [patch|minor|major|<explicit-version>] [--dry-run]"
exit 1
;;
esac
fi
# A version with a hyphen (e.g. 8.0.0-rc.1) is a prerelease: publish under the 'rc'
# dist-tag (NOT 'latest') and mark the GitHub release as a prerelease.
PRERELEASE=false
NPM_TAG="latest"
if [[ "$NEW_VERSION" == *"-"* ]]; then
PRERELEASE=true
NPM_TAG="rc"
fi
echo -e "${BLUE}New version: ${NEW_VERSION}${NC}"
if [ "$PRERELEASE" = true ]; then
echo -e "${YELLOW}⚠️ Prerelease → npm dist-tag '${NPM_TAG}', GitHub prerelease${NC}"
fi
echo ""
if [ "$DRY_RUN" = true ]; then
echo -e "${YELLOW}DRY RUN: Would release version ${NEW_VERSION}${NC}"
exit 0
fi
# Step 5: Bump version in package files
echo -e "${BLUE}4⃣ Bumping version to ${NEW_VERSION}...${NC}"
npm version $NEW_VERSION --no-git-tag-version
echo -e "${GREEN}✅ Version bumped${NC}\n"
# Step 6: Update CHANGELOG
echo -e "${BLUE}5⃣ Updating CHANGELOG...${NC}"
# Get commits since last tag
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
if [ -z "$LAST_TAG" ]; then
COMMITS=$(git log --oneline --pretty=format:"- %s (%h)")
else
COMMITS=$(git log ${LAST_TAG}..HEAD --oneline --pretty=format:"- %s (%h)")
fi
# Create new changelog entry
CHANGELOG_ENTRY="### [${NEW_VERSION}](https://source.soulcraft.com/soulcraft/brainy/compare/v${CURRENT_VERSION}...v${NEW_VERSION}) ($(date +%Y-%m-%d))
${COMMITS}
"
# Prepend to CHANGELOG.md after header
if [ -f "CHANGELOG.md" ]; then
# Read header (first 4 lines)
HEADER=$(head -n 4 CHANGELOG.md)
# Read rest of file
REST=$(tail -n +5 CHANGELOG.md)
# Write new CHANGELOG
echo "$HEADER" > CHANGELOG.md
echo "" >> CHANGELOG.md
echo "$CHANGELOG_ENTRY" >> CHANGELOG.md
echo "" >> CHANGELOG.md
echo "$REST" >> CHANGELOG.md
fi
echo -e "${GREEN}✅ CHANGELOG updated${NC}\n"
# Step 7: Create release commit
echo -e "${BLUE}6⃣ Creating release commit...${NC}"
git add package.json package-lock.json CHANGELOG.md
git commit -m "chore(release): ${NEW_VERSION}"
echo -e "${GREEN}✅ Release commit created${NC}\n"
# Step 8: Create git tag
# Annotated (-a) so `git push --follow-tags` below actually pushes it. A lightweight tag is
# skipped by --follow-tags, which leaves the tag local-only and makes `gh release create` fail.
echo -e "${BLUE}7⃣ Creating git tag v${NEW_VERSION}...${NC}"
git tag -a "v${NEW_VERSION}" -m "Release v${NEW_VERSION}"
echo -e "${GREEN}✅ Tag created${NC}\n"
# Step 9: Push to origin — the forge is the one home (ruled 2026-07-23; the
# old public GitHub repo is archived history, no longer part of any release).
echo -e "${BLUE}8⃣ Pushing to origin...${NC}"
git push --follow-tags origin "$CURRENT_BRANCH"
echo -e "${GREEN}✅ Pushed to origin${NC}\n"
# Step 10: Forge publish is CI's job now, not the laptop's — a tag push (just
# above) triggers .forgejo/workflows/publish-forge.yml, which builds and
# publishes on the forge's own runner (datacenter-side: seconds, not the
# laptop's WAN timing out on an 87MB tarball PUT). The laptop holds no forge
# publish credential anymore; it only waits for CI's result before trusting
# the forge/npmjs pair enough to publish the storefront leg.
FORGE_NPM_REG="https://source.soulcraft.com/api/packages/soulcraft/npm/"
FORGE_POLL_INTERVAL_S=15
FORGE_POLL_MAX_ATTEMPTS=40 # 40 × 15s = 10 minutes
echo -e "${BLUE}9⃣ Waiting for CI to publish v${NEW_VERSION} to the forge registry (home)...${NC}"
FORGE_LANDED=false
for ((attempt = 1; attempt <= FORGE_POLL_MAX_ATTEMPTS; attempt++)); do
LANDED_VERSION=$(npm view "@soulcraft/brainy@${NEW_VERSION}" version "--@soulcraft:registry=${FORGE_NPM_REG}" 2>/dev/null || echo "")
if [ "$LANDED_VERSION" = "$NEW_VERSION" ]; then
FORGE_LANDED=true
break
fi
echo -e "${YELLOW} … not yet on the forge (attempt ${attempt}/${FORGE_POLL_MAX_ATTEMPTS}); retrying in ${FORGE_POLL_INTERVAL_S}s${NC}"
sleep "$FORGE_POLL_INTERVAL_S"
done
if [ "$FORGE_LANDED" = true ]; then
echo -e "${GREEN}✅ CI published v${NEW_VERSION} to the forge${NC}\n"
else
echo -e "${RED}❌ CI forge publish did not land — check the workflow run on The Source; the pair must not diverge.${NC}"
echo -e "${RED} v${NEW_VERSION} was tagged and pushed, but @soulcraft/brainy@${NEW_VERSION} never became visible on the${NC}"
echo -e "${RED} forge registry after ${FORGE_POLL_MAX_ATTEMPTS} attempts, ${FORGE_POLL_INTERVAL_S}s apart. Aborting before npmjs.${NC}"
exit 1
fi
echo -e "${BLUE}9⃣½ Publishing to npmjs (storefront, dist-tag: ${NPM_TAG})...${NC}"
npm publish --tag "$NPM_TAG" "--@soulcraft:registry=https://registry.npmjs.org/"
# Brainy is the only PUBLIC @soulcraft package — verify visibility after every publish.
npm access get status @soulcraft/brainy "--@soulcraft:registry=https://registry.npmjs.org/" || true
echo -e "${GREEN}✅ Published to npmjs${NC}\n"
# Step 11: Release object on the forge (presentational — the tag, CHANGELOG,
# and RELEASES.md are the record; this just gives the forge UI a release page).
echo -e "${BLUE}🔟 Creating forge release...${NC}"
if [ -n "${FORGEJO_RELEASE_TOKEN:-}" ]; then
if curl -sf -X POST "https://source.soulcraft.com/api/v1/repos/soulcraft/brainy/releases" \
-H "Authorization: token ${FORGEJO_RELEASE_TOKEN}" -H "Content-Type: application/json" \
-d "{\"tag_name\":\"v${NEW_VERSION}\",\"name\":\"v${NEW_VERSION}\",\"prerelease\":${PRERELEASE}}" >/dev/null; then
echo -e "${GREEN}✅ Forge release created${NC}\n"
else
echo -e "${RED}⚠️ Forge release API call failed — tag + CHANGELOG remain the record; create the release page via the forge UI if wanted${NC}\n"
fi
else
echo -e "${RED}⚠️ FORGEJO_RELEASE_TOKEN unset — no release page created; tag + CHANGELOG remain the record${NC}\n"
fi
# Step 12: Push public docs to the soulcraft.com docs ingest door
# (VENUE-DOCS-RELEASE-PUSH). Skips with a loud warning when
# DOCS_INGEST_SECRET is unset; fails loudly (without undoing the publish —
# that already happened) when a push errors, so the docs site never
# silently trails npm.
echo -e "${BLUE}1⃣2⃣ Pushing public docs to soulcraft.com/docs...${NC}"
if node scripts/push-docs.js; then
echo -e "${GREEN}✅ Docs push step done${NC}\n"
else
echo -e "${RED}❌ Docs push FAILED — soulcraft.com/docs trails npm until re-run or interim sync${NC}\n"
fi
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${GREEN}🎉 Release ${NEW_VERSION} complete!${NC}"
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo ""
echo -e "📦 npm: ${BLUE}https://www.npmjs.com/package/@soulcraft/brainy/v/${NEW_VERSION}${NC}"
echo -e "🏠 Forge: ${BLUE}https://source.soulcraft.com/soulcraft/brainy/releases/tag/v${NEW_VERSION}${NC}"