diff --git a/.forgejo/workflows/publish-forge.yml b/.forgejo/workflows/publish-forge.yml new file mode 100644 index 00000000..fb7428bf --- /dev/null +++ b/.forgejo/workflows/publish-forge.yml @@ -0,0 +1,67 @@ +name: Publish (forge) + +# Datacenter-side forge publish, moved off the laptop: an 87MB tarball PUT +# over the laptop's WAN times out; the forge's own runner does it in seconds. +# scripts/release.sh tags + pushes, then polls this workflow's result (npm +# view against the forge registry) before it ever touches the npmjs leg — +# see the "delegation contract" in scripts/release.sh's forge-publish step. + +on: + push: + tags: + - 'v*' + +jobs: + publish: + name: Publish to the forge registry + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '22' + cache: npm + - run: npm ci + - run: npm run build + - name: Publish + readback-verify on the forge registry + env: + FORGE_NPM_TOKEN: ${{ secrets.FORGE_NPM_TOKEN }} + run: | + set -eo pipefail + + FORGE_NPM_REG="https://source.soulcraft.com/api/packages/soulcraft/npm/" + VERSION="$(node -p "require('./package.json').version")" + echo "Publishing @soulcraft/brainy@${VERSION} to the forge registry..." + + TMPRC="$(mktemp)" + chmod 600 "$TMPRC" + { + echo "@soulcraft:registry=${FORGE_NPM_REG}" + echo "//source.soulcraft.com/api/packages/soulcraft/npm/:_authToken=${FORGE_NPM_TOKEN}" + } > "$TMPRC" + + # The release script bumps package.json's version before it tags, so + # this tag's checkout already carries the version being published — + # nothing here re-derives it from the tag name. + PUBLISH_OK=true + if ! npm publish --tag latest --userconfig "$TMPRC"; then + PUBLISH_OK=false + fi + + # Readback verify is the source of truth, run regardless of the publish + # exit code: a benign duplicate publish (a prior run, or a mirror, already + # landed this exact version) reports failure even though the registry + # already holds the right content. + LANDED_VERSION="$(npm view "@soulcraft/brainy@${VERSION}" version --userconfig "$TMPRC" 2>/dev/null || echo "")" + rm -f "$TMPRC" + + if [ "$LANDED_VERSION" != "$VERSION" ]; then + echo "::error::Readback verify FAILED — the forge registry reports version '${LANDED_VERSION:-}', expected '${VERSION}'. This is a genuine publish failure, not a benign duplicate." + exit 1 + fi + + if [ "$PUBLISH_OK" = true ]; then + echo "Published and verified @soulcraft/brainy@${VERSION} on the forge registry." + else + echo "::warning::npm publish reported failure, but readback confirms @soulcraft/brainy@${VERSION} is already live on the forge (a prior run or mirror landed it) — treating this run as successful, since the registry content is correct. Any OTHER failure mode would have failed the readback check above instead." + fi diff --git a/RELEASES.md b/RELEASES.md index dee17a44..da8fa134 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -61,6 +61,9 @@ to the caller today. Migration-audit evidence, not a repair: nonzero drift is reported loudly (`console.warn` with the counts) and nothing is auto-healed — run `brain.repairIndex()` to reconcile the metadata index once drift is confirmed. +- **Ops note (consumer-invisible): the release pipeline's forge-registry publish now runs + on CI**, triggered by the release tag, instead of PUTting the tarball from the laptop + over WAN — no change to what gets published or how a consumer installs it. ## v8.10.1 — 2026-07-24 (the no-hot-retry contract + warm()'s metadata surface under native providers) diff --git a/scripts/release.sh b/scripts/release.sh index 43fa50bd..c64d6c5e 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -181,30 +181,33 @@ 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. +# Step 10: Forge publish is CI's job now, not the laptop's — a tag push (just +# above) triggers .forgejo/workflows/publish-forge.yml, which builds and +# publishes on the forge's own runner (datacenter-side: seconds, not the +# laptop's WAN timing out on an 87MB tarball PUT). The laptop holds no forge +# publish credential anymore; it only waits for CI's result before trusting +# the forge/npmjs pair enough to publish the storefront leg. 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 +FORGE_POLL_INTERVAL_S=15 +FORGE_POLL_MAX_ATTEMPTS=40 # 40 × 15s = 10 minutes +echo -e "${BLUE}9️⃣ Waiting for CI to publish v${NEW_VERSION} to the forge registry (home)...${NC}" +FORGE_LANDED=false +for ((attempt = 1; attempt <= FORGE_POLL_MAX_ATTEMPTS; attempt++)); do + LANDED_VERSION=$(npm view "@soulcraft/brainy@${NEW_VERSION}" version "--@soulcraft:registry=${FORGE_NPM_REG}" 2>/dev/null || echo "") + if [ "$LANDED_VERSION" = "$NEW_VERSION" ]; then + FORGE_LANDED=true + break fi - rm -f "$TMPRC" + echo -e "${YELLOW} … not yet on the forge (attempt ${attempt}/${FORGE_POLL_MAX_ATTEMPTS}); retrying in ${FORGE_POLL_INTERVAL_S}s${NC}" + sleep "$FORGE_POLL_INTERVAL_S" +done + +if [ "$FORGE_LANDED" = true ]; then + echo -e "${GREEN}✅ CI published v${NEW_VERSION} to the forge${NC}\n" 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}" + echo -e "${RED}❌ CI forge publish did not land — check the workflow run on The Source; the pair must not diverge.${NC}" + echo -e "${RED} v${NEW_VERSION} was tagged and pushed, but @soulcraft/brainy@${NEW_VERSION} never became visible on the${NC}" + echo -e "${RED} forge registry after ${FORGE_POLL_MAX_ATTEMPTS} attempts, ${FORGE_POLL_INTERVAL_S}s apart. Aborting before npmjs.${NC}" exit 1 fi