diff --git a/scripts/release.sh b/scripts/release.sh index 03d60ac2..5d03e66b 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -15,6 +15,12 @@ NC='\033[0m' # No Color RELEASE_TYPE="${1:-patch}" # patch, minor, or major SKIP_TESTS=false DRY_RUN=false +# --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 for arg in "$@"; do case $arg in @@ -24,6 +30,9 @@ for arg in "$@"; do --dry-run) DRY_RUN=true ;; + --source-only) + SOURCE_ONLY=true + ;; esac done @@ -100,7 +109,7 @@ else ;; *) echo -e "${RED}❌ Invalid release type: ${RELEASE_TYPE}${NC}" - echo "Usage: ./scripts/release.sh [patch|minor|major|] [--dry-run]" + echo "Usage: ./scripts/release.sh [patch|minor|major|] [--dry-run] [--source-only (prereleases only)]" exit 1 ;; esac @@ -119,6 +128,13 @@ 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 +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 echo "" if [ "$DRY_RUN" = true ]; then @@ -221,46 +237,50 @@ else exit 1 fi -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" +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" 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 + 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 fi # Step 11: Release object on The Source (presentational — the tag, CHANGELOG, @@ -283,16 +303,24 @@ fi # 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" +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" else - echo -e "${RED}❌ Docs push FAILED — soulcraft.com/docs trails npm until re-run or interim sync${NC}\n" + 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 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}" +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 echo -e "🏠 The Source: ${BLUE}https://source.soulcraft.com/soulcraft/brainy/releases/tag/v${NEW_VERSION}${NC}"