Origin moved to the private forge; the public repo is now a mirror with an unknown sync cadence. The release flow creates the GitHub release directly, and a missing tag there would be silently created at the default-branch head - the wrong commit. The script now pushes branch+tag to the mirror itself and hard-stops if the tag's commit on the mirror differs from local, before anything irreversible (npm publish) happens.
232 lines
8.1 KiB
Bash
Executable file
232 lines
8.1 KiB
Bash
Executable file
#!/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://github.com/soulcraftlabs/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 (source of truth) and the public GitHub mirror
|
||
echo -e "${BLUE}8️⃣ Pushing to origin...${NC}"
|
||
git push --follow-tags origin "$CURRENT_BRANCH"
|
||
echo -e "${GREEN}✅ Pushed to origin${NC}\n"
|
||
|
||
# The public GitHub repo is a mirror of origin with an unknown sync cadence.
|
||
# `gh release create` below targets GitHub directly: if the new tag hasn't
|
||
# reached GitHub yet, gh would CREATE it — pointed at GitHub's default-branch
|
||
# head, i.e. the wrong commit. Push branch+tag to GitHub explicitly, then
|
||
# verify the tag resolves there to the same commit before any release is cut.
|
||
GITHUB_URL="https://github.com/soulcraftlabs/brainy.git"
|
||
echo -e "${BLUE}8️⃣½ Pushing to the public GitHub mirror...${NC}"
|
||
git push --follow-tags "$GITHUB_URL" "$CURRENT_BRANCH"
|
||
LOCAL_TAG_SHA="$(git rev-parse "v${NEW_VERSION}^{}")"
|
||
GITHUB_TAG_SHA="$(git ls-remote --tags "$GITHUB_URL" "v${NEW_VERSION}^{}" | cut -f1)"
|
||
if [ "$LOCAL_TAG_SHA" != "$GITHUB_TAG_SHA" ]; then
|
||
echo -e "${RED}❌ Tag v${NEW_VERSION} on GitHub (${GITHUB_TAG_SHA:-absent}) does not match local (${LOCAL_TAG_SHA}) — aborting before npm publish. Fix the mirror, then re-run.${NC}"
|
||
exit 1
|
||
fi
|
||
echo -e "${GREEN}✅ GitHub mirror has the tag at the right commit${NC}\n"
|
||
|
||
# Step 10: Publish to npm
|
||
echo -e "${BLUE}9️⃣ Publishing to npm (dist-tag: ${NPM_TAG})...${NC}"
|
||
npm publish --tag "$NPM_TAG"
|
||
# Brainy is the only PUBLIC @soulcraft package — verify visibility after every publish.
|
||
npm access get status @soulcraft/brainy || true
|
||
echo -e "${GREEN}✅ Published to npm${NC}\n"
|
||
|
||
# Step 11: Create GitHub release
|
||
echo -e "${BLUE}🔟 Creating GitHub release...${NC}"
|
||
if [ "$PRERELEASE" = true ]; then
|
||
gh release create "v${NEW_VERSION}" --generate-notes --prerelease
|
||
else
|
||
gh release create "v${NEW_VERSION}" --generate-notes
|
||
fi
|
||
echo -e "${GREEN}✅ GitHub release created${NC}\n"
|
||
|
||
# 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 "🐙 GitHub: ${BLUE}https://github.com/soulcraftlabs/brainy/releases/tag/v${NEW_VERSION}${NC}"
|