Ruled today: the project's one public home is source.soulcraft.com. The old public repo is archived history and no longer part of any release. - package.json repository/homepage/bugs now point at the forge (this is what the npm page links as Repository/Homepage/Issues) - README CI badge reads the forge pipeline; CONTRIBUTING drops the mirror paragraph (forge account or email patch were already the ruled contribution paths) - release.sh: mirror push + external release step removed; publishes go forge-first (box-held write token, temp userconfig so the token never hits argv; a forge-publish failure aborts before the storefront so the pair can never diverge), then npmjs with the scope-override pin (the fleet npmrc maps @soulcraft to the forge and scope mappings beat --registry); release page created via forge API when a token is present, loud skip otherwise; changelog compare links point home - dead external CI workflow removed (.forgejo/workflows/ci.yml is the live pipeline) Historical CHANGELOG links to the archive stay as written - history is history and the archive serves them read-only.
249 lines
9 KiB
Bash
Executable file
249 lines
9 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://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: Publish — forge FIRST (home), npmjs second (the world's storefront).
|
||
# The fleet-wide ~/.npmrc maps the @soulcraft scope to the forge registry, and
|
||
# a scope mapping BEATS `--registry` on the command line — so each publish
|
||
# names its registry via the scope override explicitly. Nothing implicit.
|
||
FORGE_NPM_REG="https://source.soulcraft.com/api/packages/soulcraft/npm/"
|
||
FORGE_NPM_TOKEN_FILE="$HOME/.config/soulcraft/npm-publish-brainy.token"
|
||
echo -e "${BLUE}9️⃣ Publishing to the forge registry (home)...${NC}"
|
||
if [ -f "$FORGE_NPM_TOKEN_FILE" ]; then
|
||
TMPRC="$(mktemp)"
|
||
chmod 600 "$TMPRC"
|
||
{
|
||
echo "@soulcraft:registry=${FORGE_NPM_REG}"
|
||
echo "//source.soulcraft.com/api/packages/soulcraft/npm/:_authToken=$(cat "$FORGE_NPM_TOKEN_FILE")"
|
||
} > "$TMPRC"
|
||
if npm publish --tag "$NPM_TAG" --userconfig "$TMPRC"; then
|
||
echo -e "${GREEN}✅ Published to the forge${NC}\n"
|
||
else
|
||
rm -f "$TMPRC"
|
||
echo -e "${RED}❌ Forge publish FAILED — aborting before npmjs so the pair never diverges. Fix and re-run.${NC}"
|
||
exit 1
|
||
fi
|
||
rm -f "$TMPRC"
|
||
else
|
||
echo -e "${RED}❌ Forge publish token missing (${FORGE_NPM_TOKEN_FILE}) — aborting. The forge is home; publish it first or restage the token.${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}"
|