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
|
2026-08-24 10:06:23 -07:00
|
|
|
|
# --source-only: the HOME leg only — tag, CI's publish to The Source, and the
|
|
|
|
|
|
# release page; NO storefront (npmjs) publish, NO pair verification, NO docs
|
|
|
|
|
|
# push. The pair-gate shape: a prerelease the fleet's other engine devDeps
|
|
|
|
|
|
# from our own registry while the pair is proven, never a public artifact.
|
|
|
|
|
|
# Refused for a non-prerelease version — a public floor is always a pair.
|
|
|
|
|
|
SOURCE_ONLY=false
|
2025-10-01 13:51:47 -07:00
|
|
|
|
|
|
|
|
|
|
for arg in "$@"; do
|
|
|
|
|
|
case $arg in
|
|
|
|
|
|
--skip-tests)
|
|
|
|
|
|
SKIP_TESTS=true
|
|
|
|
|
|
;;
|
|
|
|
|
|
--dry-run)
|
|
|
|
|
|
DRY_RUN=true
|
|
|
|
|
|
;;
|
2026-08-24 10:06:23 -07:00
|
|
|
|
--source-only)
|
|
|
|
|
|
SOURCE_ONLY=true
|
|
|
|
|
|
;;
|
2025-10-01 13:51:47 -07:00
|
|
|
|
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}"
|
2026-08-24 10:06:23 -07:00
|
|
|
|
echo "Usage: ./scripts/release.sh [patch|minor|major|<explicit-version>] [--dry-run] [--source-only (prereleases only)]"
|
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
|
|
|
|
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
|
2026-08-24 10:06:23 -07:00
|
|
|
|
if [ "$SOURCE_ONLY" = true ]; then
|
|
|
|
|
|
if [ "$PRERELEASE" != true ]; then
|
|
|
|
|
|
echo -e "${RED}❌ --source-only is for prereleases only: a non-prerelease version is a public floor and always ships as the byte-identical pair.${NC}"
|
|
|
|
|
|
exit 1
|
|
|
|
|
|
fi
|
|
|
|
|
|
echo -e "${YELLOW}⚠️ --source-only → The Source (home) ONLY: no npmjs publish, no pair verification, no docs push${NC}"
|
|
|
|
|
|
fi
|
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 ""
|
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
|
2026-07-23 11:09:43 -07:00
|
|
|
|
CHANGELOG_ENTRY="### [${NEW_VERSION}](https://source.soulcraft.com/soulcraft/brainy/compare/v${CURRENT_VERSION}...v${NEW_VERSION}) ($(date +%Y-%m-%d))
|
2025-10-01 13:26:04 -07:00
|
|
|
|
|
|
|
|
|
|
${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-08-04 10:56:21 -07:00
|
|
|
|
# Step 9: Push to origin — The Source is the one home (ruled 2026-07-23; the
|
2026-07-23 11:09:43 -07:00
|
|
|
|
# old public GitHub repo is archived history, no longer part of any release).
|
2026-08-12 16:56:08 -07:00
|
|
|
|
# TAG FIRST, branch second — deliberately two pushes: the runner is
|
|
|
|
|
|
# sequential, and a combined push can queue the release commit's ci.yml run
|
|
|
|
|
|
# AHEAD of the tag's publish-source run (observed on 10.0.0: the publish sat
|
|
|
|
|
|
# ~37 minutes behind a redundant CI run of the very commit the local gates
|
|
|
|
|
|
# had just proven). Pushing the tag alone queues the publish immediately;
|
|
|
|
|
|
# the branch push (and its ci.yml run) follows behind it, harmlessly.
|
|
|
|
|
|
echo -e "${BLUE}8️⃣ Pushing to origin (tag first — the publish must never queue behind CI)...${NC}"
|
|
|
|
|
|
git push origin "v${NEW_VERSION}"
|
|
|
|
|
|
git push origin "$CURRENT_BRANCH"
|
2026-07-23 10:01:28 -07:00
|
|
|
|
echo -e "${GREEN}✅ Pushed to origin${NC}\n"
|
|
|
|
|
|
|
2026-08-04 10:56:21 -07:00
|
|
|
|
# Step 10: The home publish (The Source, source.soulcraft.com) is CI's job
|
|
|
|
|
|
# now, not the laptop's — a tag push (just above) triggers
|
|
|
|
|
|
# .forgejo/workflows/publish-source.yml, which builds and publishes on The
|
|
|
|
|
|
# Source's own runner (datacenter-side: seconds, not the laptop's WAN timing
|
|
|
|
|
|
# out on an 87MB tarball PUT). The laptop holds no home-registry publish
|
|
|
|
|
|
# credential anymore; it only waits for CI's result before trusting the
|
|
|
|
|
|
# home/npmjs pair enough to publish the storefront leg.
|
|
|
|
|
|
SOURCE_NPM_REG="https://source.soulcraft.com/api/packages/soulcraft/npm/"
|
|
|
|
|
|
SOURCE_POLL_INTERVAL_S=15
|
|
|
|
|
|
SOURCE_POLL_MAX_ATTEMPTS=200 # 200 × 15s = 50 minutes — the runner is sequential and a busy day's ci.yml
|
2026-08-04 10:14:46 -07:00
|
|
|
|
# backlog has twice exceeded the old 20-minute window (8.10.3, 9.0.0);
|
|
|
|
|
|
# ci.yml no longer runs on tag pushes, but same-day branch pushes still queue ahead
|
2026-08-04 10:56:21 -07:00
|
|
|
|
echo -e "${BLUE}9️⃣ Waiting for CI to publish v${NEW_VERSION} to The Source registry (home)...${NC}"
|
|
|
|
|
|
SOURCE_LANDED=false
|
|
|
|
|
|
for ((attempt = 1; attempt <= SOURCE_POLL_MAX_ATTEMPTS; attempt++)); do
|
|
|
|
|
|
LANDED_VERSION=$(npm view "@soulcraft/brainy@${NEW_VERSION}" version "--@soulcraft:registry=${SOURCE_NPM_REG}" 2>/dev/null || echo "")
|
2026-07-27 11:11:53 -07:00
|
|
|
|
if [ "$LANDED_VERSION" = "$NEW_VERSION" ]; then
|
2026-08-04 10:56:21 -07:00
|
|
|
|
SOURCE_LANDED=true
|
2026-07-27 11:11:53 -07:00
|
|
|
|
break
|
2026-07-23 11:09:43 -07:00
|
|
|
|
fi
|
2026-08-04 10:56:21 -07:00
|
|
|
|
echo -e "${YELLOW} … not yet on The Source (attempt ${attempt}/${SOURCE_POLL_MAX_ATTEMPTS}); retrying in ${SOURCE_POLL_INTERVAL_S}s${NC}"
|
|
|
|
|
|
sleep "$SOURCE_POLL_INTERVAL_S"
|
2026-07-27 11:11:53 -07:00
|
|
|
|
done
|
|
|
|
|
|
|
2026-08-04 10:56:21 -07:00
|
|
|
|
if [ "$SOURCE_LANDED" = true ]; then
|
|
|
|
|
|
echo -e "${GREEN}✅ CI published v${NEW_VERSION} to The Source${NC}\n"
|
2026-07-23 11:09:43 -07:00
|
|
|
|
else
|
2026-08-04 10:56:21 -07:00
|
|
|
|
echo -e "${RED}❌ CI's home publish did not land — check the workflow run on The Source; the pair must not diverge.${NC}"
|
2026-07-27 11:11:53 -07:00
|
|
|
|
echo -e "${RED} v${NEW_VERSION} was tagged and pushed, but @soulcraft/brainy@${NEW_VERSION} never became visible on the${NC}"
|
2026-08-04 10:56:21 -07:00
|
|
|
|
echo -e "${RED} Source registry after ${SOURCE_POLL_MAX_ATTEMPTS} attempts, ${SOURCE_POLL_INTERVAL_S}s apart. Aborting before npmjs.${NC}"
|
2026-07-23 10:01:28 -07:00
|
|
|
|
exit 1
|
|
|
|
|
|
fi
|
2025-10-01 13:26:04 -07:00
|
|
|
|
|
2026-08-24 10:06:23 -07:00
|
|
|
|
if [ "$SOURCE_ONLY" = true ]; then
|
|
|
|
|
|
echo -e "${YELLOW}9️⃣½ Storefront (npmjs) leg SKIPPED — --source-only: v${NEW_VERSION} lives on The Source under dist-tag '${NPM_TAG}' only${NC}\n"
|
2026-08-04 08:17:02 -07:00
|
|
|
|
else
|
2026-08-24 10:06:23 -07:00
|
|
|
|
echo -e "${BLUE}9️⃣½ Publishing to npmjs (storefront, dist-tag: ${NPM_TAG})...${NC}"
|
|
|
|
|
|
# BYTE-IDENTITY LAW: the storefront republishes CI's EXACT artifact — download
|
|
|
|
|
|
# the tarball The Source serves and publish that file, never a fresh local pack
|
|
|
|
|
|
# (a local rebuild can differ byte-wise, and the fleet verifies the pair by
|
|
|
|
|
|
# shasum across registries).
|
|
|
|
|
|
STOREFRONT_TMP="$(mktemp -d)"
|
|
|
|
|
|
(cd "$STOREFRONT_TMP" && npm pack "@soulcraft/brainy@${NEW_VERSION}" "--@soulcraft:registry=${SOURCE_NPM_REG}" >/dev/null)
|
|
|
|
|
|
SOURCE_TARBALL="$(ls "$STOREFRONT_TMP"/soulcraft-brainy-*.tgz)"
|
|
|
|
|
|
echo -e "${BLUE} home artifact: $(sha256sum "$SOURCE_TARBALL" | cut -d' ' -f1)${NC}"
|
|
|
|
|
|
npm publish "$SOURCE_TARBALL" --tag "$NPM_TAG" "--@soulcraft:registry=https://registry.npmjs.org/"
|
|
|
|
|
|
rm -rf "$STOREFRONT_TMP"
|
|
|
|
|
|
# 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
|
|
|
|
|
|
# Verify the pair is byte-identical by registry-reported shasum — divergence
|
|
|
|
|
|
# here means the storefront leg must be treated as failed, loudly. RETRIED
|
|
|
|
|
|
# with raw curl: npmjs metadata propagates with a lag measured in minutes,
|
|
|
|
|
|
# and a one-shot npm-view probe fired a false DIVERGENCE on 10.0.0 while a
|
|
|
|
|
|
# raw curl of the registry document already confirmed byte-identity. The
|
|
|
|
|
|
# probe now reads the registry JSON directly (no npm cache in the path) and
|
|
|
|
|
|
# gives propagation up to 5 minutes before calling the pair divergent.
|
|
|
|
|
|
NPMJS_VERIFY_ATTEMPTS=20
|
|
|
|
|
|
NPMJS_VERIFY_INTERVAL_S=15 # 20 × 15s = 5 minutes of propagation grace
|
|
|
|
|
|
SOURCE_SHA=$(npm view "@soulcraft/brainy@${NEW_VERSION}" dist.shasum "--@soulcraft:registry=${SOURCE_NPM_REG}" 2>/dev/null || echo "source-unavailable")
|
|
|
|
|
|
PAIR_IDENTICAL=false
|
|
|
|
|
|
for ((attempt = 1; attempt <= NPMJS_VERIFY_ATTEMPTS; attempt++)); do
|
|
|
|
|
|
NPMJS_SHA=$(curl -fsSL "https://registry.npmjs.org/@soulcraft%2Fbrainy" 2>/dev/null \
|
|
|
|
|
|
| node -e "let d='';process.stdin.on('data',c=>d+=c).on('end',()=>{try{const v=JSON.parse(d).versions[process.argv[1]];console.log(v?v.dist.shasum:'')}catch{console.log('')}})" "${NEW_VERSION}" \
|
|
|
|
|
|
|| echo "")
|
|
|
|
|
|
if [ -n "$NPMJS_SHA" ] && [ "$SOURCE_SHA" = "$NPMJS_SHA" ]; then
|
|
|
|
|
|
PAIR_IDENTICAL=true
|
|
|
|
|
|
break
|
|
|
|
|
|
fi
|
|
|
|
|
|
echo -e "${YELLOW} … npmjs metadata not settled (attempt ${attempt}/${NPMJS_VERIFY_ATTEMPTS}: '${NPMJS_SHA:-absent}' vs '${SOURCE_SHA}'); retrying in ${NPMJS_VERIFY_INTERVAL_S}s${NC}"
|
|
|
|
|
|
sleep "$NPMJS_VERIFY_INTERVAL_S"
|
|
|
|
|
|
done
|
|
|
|
|
|
if [ "$PAIR_IDENTICAL" = true ]; then
|
|
|
|
|
|
echo -e "${GREEN}✅ Published to npmjs — byte-identical pair (shasum ${NPMJS_SHA})${NC}\n"
|
|
|
|
|
|
else
|
|
|
|
|
|
echo -e "${RED}❌ REGISTRY DIVERGENCE: The Source shasum ${SOURCE_SHA} != npmjs shasum ${NPMJS_SHA} after ${NPMJS_VERIFY_ATTEMPTS} attempts — investigate before announcing${NC}\n"
|
|
|
|
|
|
exit 1
|
|
|
|
|
|
fi
|
2026-08-04 08:17:02 -07:00
|
|
|
|
fi
|
2026-07-23 11:09:43 -07:00
|
|
|
|
|
2026-08-04 10:56:21 -07:00
|
|
|
|
# Step 11: Release object on The Source (presentational — the tag, CHANGELOG,
|
|
|
|
|
|
# and RELEASES.md are the record; this just gives The Source's UI a release page).
|
|
|
|
|
|
echo -e "${BLUE}🔟 Creating release page on The Source...${NC}"
|
2026-07-23 11:09:43 -07:00
|
|
|
|
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
|
2026-08-04 10:56:21 -07:00
|
|
|
|
echo -e "${GREEN}✅ Release page created on The Source${NC}\n"
|
2026-07-23 11:09:43 -07:00
|
|
|
|
else
|
2026-08-04 10:56:21 -07:00
|
|
|
|
echo -e "${RED}⚠️ Release-page API call failed — tag + CHANGELOG remain the record; create the page via The Source's UI if wanted${NC}\n"
|
2026-07-23 11:09:43 -07:00
|
|
|
|
fi
|
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
|
|
|
|
else
|
2026-07-23 11:09:43 -07:00
|
|
|
|
echo -e "${RED}⚠️ FORGEJO_RELEASE_TOKEN unset — no release page created; tag + CHANGELOG remain the record${NC}\n"
|
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
|
|
|
|
fi
|
2025-10-01 13:26:04 -07:00
|
|
|
|
|
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.
|
2026-08-24 10:06:23 -07:00
|
|
|
|
if [ "$SOURCE_ONLY" = true ]; then
|
|
|
|
|
|
echo -e "${YELLOW}1️⃣2️⃣ Docs push SKIPPED — --source-only (a home-only prerelease publishes no public docs)${NC}\n"
|
2026-07-18 14:02:23 -07:00
|
|
|
|
else
|
2026-08-24 10:06:23 -07:00
|
|
|
|
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
|
2026-07-18 14:02:23 -07:00
|
|
|
|
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 ""
|
2026-08-24 10:06:23 -07:00
|
|
|
|
if [ "$SOURCE_ONLY" = true ]; then
|
|
|
|
|
|
echo -e "📦 npmjs: ${YELLOW}not published (--source-only)${NC}"
|
|
|
|
|
|
else
|
|
|
|
|
|
echo -e "📦 npm: ${BLUE}https://www.npmjs.com/package/@soulcraft/brainy/v/${NEW_VERSION}${NC}"
|
|
|
|
|
|
fi
|
2026-08-04 10:56:21 -07:00
|
|
|
|
echo -e "🏠 The Source: ${BLUE}https://source.soulcraft.com/soulcraft/brainy/releases/tag/v${NEW_VERSION}${NC}"
|