2025-10-01 13:26:04 -07:00
#!/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
2025-10-01 13:51:47 -07:00
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
2025-10-01 13:26:04 -07:00
feat(8.0): id-normalization (#18) + aggregation min/max delete-safety + RC-safe release
- id-normalization (#18): `brain.newId()` (UUID v7) + v7 default ids; non-UUID string ids are
transparently normalized to a stable UUID v5 on creation AND every lookup — get/update/remove,
relate(from,to), related, find({connected}), getMany, removeMany, the transact op-handler, and
db.with() overlays — with the caller's original key preserved under `_originalId`. The engine only
ever sees a UUID; real UUIDs pass through untouched. universal/uuid.ts gains v5/v7/isUUID +
BRAINY_ID_NAMESPACE; new utils/idNormalization.ts (coerceNewEntityId / resolveEntityId).
- aggregation min/max delete-safety: `queryAggregate` no longer leaves a stale min/max after
deleting the current extreme — min/max now track a value multiset and recompute in-memory (no
entity scan; that scan was the prior delete-then-hang a consumer reported). Other metrics were
already exact across deletes. Regression test proves resolve-after-delete + correct new min/max.
- release.sh RC-safe: explicit version arg, prerelease detection (npm `--tag rc` + GitHub
`--prerelease`), full `test:ci` gate (unit + integration), current-branch push, npm-access
verification after publish.
- native-engine references point at `@soulcraft/cor` (8.0's partner) in user-facing strings/docs.
Unit 1461/0 · integration 599/0 · tsc clean.
2026-06-20 14:40:57 -07:00
# 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) "
2025-10-01 13:26:04 -07:00
echo -e " ${ BLUE } 🚀 Brainy Release Script ${ NC } "
feat(8.0): id-normalization (#18) + aggregation min/max delete-safety + RC-safe release
- id-normalization (#18): `brain.newId()` (UUID v7) + v7 default ids; non-UUID string ids are
transparently normalized to a stable UUID v5 on creation AND every lookup — get/update/remove,
relate(from,to), related, find({connected}), getMany, removeMany, the transact op-handler, and
db.with() overlays — with the caller's original key preserved under `_originalId`. The engine only
ever sees a UUID; real UUIDs pass through untouched. universal/uuid.ts gains v5/v7/isUUID +
BRAINY_ID_NAMESPACE; new utils/idNormalization.ts (coerceNewEntityId / resolveEntityId).
- aggregation min/max delete-safety: `queryAggregate` no longer leaves a stale min/max after
deleting the current extreme — min/max now track a value multiset and recompute in-memory (no
entity scan; that scan was the prior delete-then-hang a consumer reported). Other metrics were
already exact across deletes. Regression test proves resolve-after-delete + correct new min/max.
- release.sh RC-safe: explicit version arg, prerelease detection (npm `--tag rc` + GitHub
`--prerelease`), full `test:ci` gate (unit + integration), current-branch push, npm-access
verification after publish.
- native-engine references point at `@soulcraft/cor` (8.0's partner) in user-facing strings/docs.
Unit 1461/0 · integration 599/0 · tsc clean.
2026-06-20 14:40:57 -07:00
echo -e " ${ BLUE } Release type: ${ RELEASE_TYPE } ${ NC } "
echo -e " ${ BLUE } Branch: ${ CURRENT_BRANCH } ${ NC } \n "
2025-10-01 13:26:04 -07:00
2025-10-01 13:51:47 -07:00
if [ " $DRY_RUN " = true ] ; then
2025-10-01 13:26:04 -07:00
echo -e " ${ YELLOW } ⚠️ DRY RUN MODE - No changes will be made ${ NC } \n "
fi
2025-10-01 13:51:47 -07:00
if [ " $SKIP_TESTS " = true ] ; then
echo -e " ${ YELLOW } ⚠️ SKIPPING TESTS - Use with caution! ${ NC } \n "
fi
2025-10-01 13:26:04 -07:00
# 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 } "
2025-10-01 13:51:47 -07:00
if [ " $DRY_RUN " = false ] ; then
2025-10-01 13:26:04 -07:00
npm run build
fi
echo -e " ${ GREEN } ✅ Build successful ${ NC } \n "
# Step 3: Test
2025-10-01 13:51:47 -07:00
if [ " $SKIP_TESTS " = false ] ; then
echo -e " ${ BLUE } 3️ ⃣ Running tests... ${ NC } "
if [ " $DRY_RUN " = false ] ; then
feat(8.0): id-normalization (#18) + aggregation min/max delete-safety + RC-safe release
- id-normalization (#18): `brain.newId()` (UUID v7) + v7 default ids; non-UUID string ids are
transparently normalized to a stable UUID v5 on creation AND every lookup — get/update/remove,
relate(from,to), related, find({connected}), getMany, removeMany, the transact op-handler, and
db.with() overlays — with the caller's original key preserved under `_originalId`. The engine only
ever sees a UUID; real UUIDs pass through untouched. universal/uuid.ts gains v5/v7/isUUID +
BRAINY_ID_NAMESPACE; new utils/idNormalization.ts (coerceNewEntityId / resolveEntityId).
- aggregation min/max delete-safety: `queryAggregate` no longer leaves a stale min/max after
deleting the current extreme — min/max now track a value multiset and recompute in-memory (no
entity scan; that scan was the prior delete-then-hang a consumer reported). Other metrics were
already exact across deletes. Regression test proves resolve-after-delete + correct new min/max.
- release.sh RC-safe: explicit version arg, prerelease detection (npm `--tag rc` + GitHub
`--prerelease`), full `test:ci` gate (unit + integration), current-branch push, npm-access
verification after publish.
- native-engine references point at `@soulcraft/cor` (8.0's partner) in user-facing strings/docs.
Unit 1461/0 · integration 599/0 · tsc clean.
2026-06-20 14:40:57 -07:00
# Full CI gate: unit + integration (test:ci = test:ci-unit && test:ci-integration).
npm run test:ci
2025-10-01 13:51:47 -07:00
fi
echo -e " ${ GREEN } ✅ Tests passed ${ NC } \n "
else
echo -e " ${ YELLOW } 3️ ⃣ Skipping tests... ${ NC } \n "
2025-10-01 13:26:04 -07:00
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] } "
feat(8.0): id-normalization (#18) + aggregation min/max delete-safety + RC-safe release
- id-normalization (#18): `brain.newId()` (UUID v7) + v7 default ids; non-UUID string ids are
transparently normalized to a stable UUID v5 on creation AND every lookup — get/update/remove,
relate(from,to), related, find({connected}), getMany, removeMany, the transact op-handler, and
db.with() overlays — with the caller's original key preserved under `_originalId`. The engine only
ever sees a UUID; real UUIDs pass through untouched. universal/uuid.ts gains v5/v7/isUUID +
BRAINY_ID_NAMESPACE; new utils/idNormalization.ts (coerceNewEntityId / resolveEntityId).
- aggregation min/max delete-safety: `queryAggregate` no longer leaves a stale min/max after
deleting the current extreme — min/max now track a value multiset and recompute in-memory (no
entity scan; that scan was the prior delete-then-hang a consumer reported). Other metrics were
already exact across deletes. Regression test proves resolve-after-delete + correct new min/max.
- release.sh RC-safe: explicit version arg, prerelease detection (npm `--tag rc` + GitHub
`--prerelease`), full `test:ci` gate (unit + integration), current-branch push, npm-access
verification after publish.
- native-engine references point at `@soulcraft/cor` (8.0's partner) in user-facing strings/docs.
Unit 1461/0 · integration 599/0 · tsc clean.
2026-06-20 14:40:57 -07:00
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 ""
2025-10-01 13:26:04 -07:00
2025-10-01 13:51:47 -07:00
if [ " $DRY_RUN " = true ] ; then
2025-10-01 13:26:04 -07:00
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
2026-05-26 11:44:14 -07:00
# 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.
2025-10-01 13:26:04 -07:00
echo -e " ${ BLUE } 7️ ⃣ Creating git tag v ${ NEW_VERSION } ... ${ NC } "
2026-05-26 11:44:14 -07:00
git tag -a " v ${ NEW_VERSION } " -m " Release v ${ NEW_VERSION } "
2025-10-01 13:26:04 -07:00
echo -e " ${ GREEN } ✅ Tag created ${ NC } \n "
2026-07-23 10:01:28 -07:00
# Step 9: Push to origin (source of truth) and the public GitHub mirror
echo -e " ${ BLUE } 8️ ⃣ Pushing to origin... ${ NC } "
feat(8.0): id-normalization (#18) + aggregation min/max delete-safety + RC-safe release
- id-normalization (#18): `brain.newId()` (UUID v7) + v7 default ids; non-UUID string ids are
transparently normalized to a stable UUID v5 on creation AND every lookup — get/update/remove,
relate(from,to), related, find({connected}), getMany, removeMany, the transact op-handler, and
db.with() overlays — with the caller's original key preserved under `_originalId`. The engine only
ever sees a UUID; real UUIDs pass through untouched. universal/uuid.ts gains v5/v7/isUUID +
BRAINY_ID_NAMESPACE; new utils/idNormalization.ts (coerceNewEntityId / resolveEntityId).
- aggregation min/max delete-safety: `queryAggregate` no longer leaves a stale min/max after
deleting the current extreme — min/max now track a value multiset and recompute in-memory (no
entity scan; that scan was the prior delete-then-hang a consumer reported). Other metrics were
already exact across deletes. Regression test proves resolve-after-delete + correct new min/max.
- release.sh RC-safe: explicit version arg, prerelease detection (npm `--tag rc` + GitHub
`--prerelease`), full `test:ci` gate (unit + integration), current-branch push, npm-access
verification after publish.
- native-engine references point at `@soulcraft/cor` (8.0's partner) in user-facing strings/docs.
Unit 1461/0 · integration 599/0 · tsc clean.
2026-06-20 14:40:57 -07:00
git push --follow-tags origin " $CURRENT_BRANCH "
2026-07-23 10:01:28 -07:00
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 "
2025-10-01 13:26:04 -07:00
# Step 10: Publish to npm
feat(8.0): id-normalization (#18) + aggregation min/max delete-safety + RC-safe release
- id-normalization (#18): `brain.newId()` (UUID v7) + v7 default ids; non-UUID string ids are
transparently normalized to a stable UUID v5 on creation AND every lookup — get/update/remove,
relate(from,to), related, find({connected}), getMany, removeMany, the transact op-handler, and
db.with() overlays — with the caller's original key preserved under `_originalId`. The engine only
ever sees a UUID; real UUIDs pass through untouched. universal/uuid.ts gains v5/v7/isUUID +
BRAINY_ID_NAMESPACE; new utils/idNormalization.ts (coerceNewEntityId / resolveEntityId).
- aggregation min/max delete-safety: `queryAggregate` no longer leaves a stale min/max after
deleting the current extreme — min/max now track a value multiset and recompute in-memory (no
entity scan; that scan was the prior delete-then-hang a consumer reported). Other metrics were
already exact across deletes. Regression test proves resolve-after-delete + correct new min/max.
- release.sh RC-safe: explicit version arg, prerelease detection (npm `--tag rc` + GitHub
`--prerelease`), full `test:ci` gate (unit + integration), current-branch push, npm-access
verification after publish.
- native-engine references point at `@soulcraft/cor` (8.0's partner) in user-facing strings/docs.
Unit 1461/0 · integration 599/0 · tsc clean.
2026-06-20 14:40:57 -07:00
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
2025-10-01 13:26:04 -07:00
echo -e " ${ GREEN } ✅ Published to npm ${ NC } \n "
# Step 11: Create GitHub release
echo -e " ${ BLUE } 🔟 Creating GitHub release... ${ NC } "
feat(8.0): id-normalization (#18) + aggregation min/max delete-safety + RC-safe release
- id-normalization (#18): `brain.newId()` (UUID v7) + v7 default ids; non-UUID string ids are
transparently normalized to a stable UUID v5 on creation AND every lookup — get/update/remove,
relate(from,to), related, find({connected}), getMany, removeMany, the transact op-handler, and
db.with() overlays — with the caller's original key preserved under `_originalId`. The engine only
ever sees a UUID; real UUIDs pass through untouched. universal/uuid.ts gains v5/v7/isUUID +
BRAINY_ID_NAMESPACE; new utils/idNormalization.ts (coerceNewEntityId / resolveEntityId).
- aggregation min/max delete-safety: `queryAggregate` no longer leaves a stale min/max after
deleting the current extreme — min/max now track a value multiset and recompute in-memory (no
entity scan; that scan was the prior delete-then-hang a consumer reported). Other metrics were
already exact across deletes. Regression test proves resolve-after-delete + correct new min/max.
- release.sh RC-safe: explicit version arg, prerelease detection (npm `--tag rc` + GitHub
`--prerelease`), full `test:ci` gate (unit + integration), current-branch push, npm-access
verification after publish.
- native-engine references point at `@soulcraft/cor` (8.0's partner) in user-facing strings/docs.
Unit 1461/0 · integration 599/0 · tsc clean.
2026-06-20 14:40:57 -07:00
if [ " $PRERELEASE " = true ] ; then
gh release create " v ${ NEW_VERSION } " --generate-notes --prerelease
else
gh release create " v ${ NEW_VERSION } " --generate-notes
fi
2025-10-01 13:26:04 -07:00
echo -e " ${ GREEN } ✅ GitHub release created ${ NC } \n "
2026-07-18 14:02:23 -07:00
# 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
2025-10-01 13:26:04 -07:00
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 } "