Something went wrong. Try again.
personal memory agent
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270#!/usr/bin/env bash# SPDX-License-Identifier: AGPL-3.0-only# Copyright (c) 2026 sol pbc## Multi-wheel solstone release.## Builds and uploads the seven lockstep artifacts to PyPI:# - solstone-${VERSION}.tar.gz (root sdist)# - solstone-${VERSION}-py3-none-any.whl (root any wheel)# - solstone-${VERSION}-py3-none-macosx_14_0_arm64.whl (Apple Silicon macOS 14+)# - solstone_journal-${VERSION}.tar.gz (journal CPU sdist)# - solstone_journal-${VERSION}-py3-none-any.whl (journal CPU wheel)# - solstone_journal_cuda-${VERSION}.tar.gz (journal CUDA sdist)# - solstone_journal_cuda-${VERSION}-py3-none-any.whl (journal CUDA wheel)# The independently-versioned solstone_journal_models sdist + wheel are included# only when their version is absent from the target package index.## The Linux artifacts are built locally. The macOS arm64 wheel is built on# pro5e.local with a Developer-ID-signed + notarized parakeet-helper bundled# at solstone/observe/transcribe/parakeet_helper/_bin/parakeet-helper.## Preconditions for the pro5e leg:# - pro5e.local SSH-reachable# - sol-signing.keychain-db unlocked (run `make unlock-signing` from# ~/projects/solstone-macos on pro5e once per launchd session — all build# windows in the hopper tmux session share keychain state)# - notarytool keychain-profile available; defaults to `sol-pbc-notary` per# cto/playbooks/apple-remote-dev.md § sol-signing keychain. Override with# NOTARY_KEYCHAIN_PROFILE if needed.## Tokens (set in the env before running):# PYPI_TOKEN __token__ password for production PyPI# TESTPYPI_TOKEN same shape, for --test runs
set -euo pipefail
usage() { cat <<'EOF'Usage: scripts/release.sh [--test] [--no-publish]
Options: --test Publish to TestPyPI. --no-publish Build and check artifacts only; do not upload, tag, create a GitHub release, or run pro5e. -h, --help Show this help.
Env overrides: NOTARY_KEYCHAIN_PROFILE notarytool keychain profile on pro5e (default: sol-pbc-notary) PRO5E_HOST SSH alias for the macOS build host (default: pro5e.local)EOF}
TARGET="pypi"TOKEN_VAR="PYPI_TOKEN"REPOSITORY_ARGS=()PUBLISH="yes"
while [[ $# -gt 0 ]]; do case "$1" in --test) TARGET="testpypi" TOKEN_VAR="TESTPYPI_TOKEN" REPOSITORY_ARGS=(--repository-url https://test.pypi.org/legacy/) shift ;; --no-publish) PUBLISH="no" shift ;; -h|--help) usage exit 0 ;; *) echo "unknown argument: $1" >&2 usage >&2 exit 2 ;; esacdone
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"cd "$REPO_ROOT"
if [[ "$PUBLISH" == "yes" ]]; then if [[ -z "${!TOKEN_VAR:-}" ]]; then echo "set \$${TOKEN_VAR} before re-running" >&2 exit 1 fi TOKEN="${!TOKEN_VAR}"fiPRO5E_HOST="${PRO5E_HOST:-pro5e.local}"NOTARY_PROFILE="${NOTARY_KEYCHAIN_PROFILE:-sol-pbc-notary}"
# Capture the git ref we're publishing from. pro5e checks out the same ref# so the macOS wheel's source matches the local sdist. Reject ANY tracked or# untracked (non-ignored) change — untracked files would otherwise be built# locally but absent from the ref pro5e checks out.if [[ "$PUBLISH" == "yes" ]]; then if [[ -n "$(git status --porcelain --untracked-files=normal)" ]]; then echo "working tree dirty (tracked or untracked changes); commit before releasing" >&2 exit 1 fi GIT_REF=$(git rev-parse HEAD)fi
# 1. Local lockstep artifacts: root + journal leaves + modelsecho "==> [1/5] building local lockstep artifacts"python3 scripts/render_packaging.py --checkrm -rf build/ dist/ *.egg-info/uv build --all-packagespython3 scripts/check_wheel_contents.py dist/
# Pre-flight the CHANGELOG block now — before the expensive pro5e leg and the# irreversible PyPI upload. extract_changelog.sh exits non-zero if the# `## [VERSION]` block is missing, so a forgotten changelog fails fast instead# of after publish.VERSION=$(ls dist/solstone-[0-9]*-py3-none-any.whl | head -1 | sed -E 's/.*solstone-([^-]+)-.*/\1/')bash scripts/extract_changelog.sh "$VERSION" >/dev/nullMODELS_VERSION=$(ls dist/solstone_journal_models-[0-9]*-py3-none-any.whl | head -1 | sed -E 's/.*solstone_journal_models-([^-]+)-.*/\1/')TEST_FLAG=""[[ "$TARGET" == "testpypi" ]] && TEST_FLAG="--test"MODELS_DECISION="$(python3 scripts/release_models_gate.py --version "$MODELS_VERSION" $TEST_FLAG)"echo "models gate: solstone-journal-models ${MODELS_VERSION} -> ${MODELS_DECISION}"
LOCAL_LOCKSTEP_ARTIFACTS=( "dist/solstone-${VERSION}.tar.gz" "dist/solstone-${VERSION}-py3-none-any.whl" "dist/solstone_journal-${VERSION}.tar.gz" "dist/solstone_journal-${VERSION}-py3-none-any.whl" "dist/solstone_journal_cuda-${VERSION}.tar.gz" "dist/solstone_journal_cuda-${VERSION}-py3-none-any.whl")MODELS_ARTIFACTS=( "dist/solstone_journal_models-${MODELS_VERSION}.tar.gz" "dist/solstone_journal_models-${MODELS_VERSION}-py3-none-any.whl")
if [[ "$PUBLISH" == "no" ]]; then echo echo "release artifacts:" ls -la dist/
echo echo "==> twine check (no publish)" uvx twine check "${LOCAL_LOCKSTEP_ARTIFACTS[@]}" "${MODELS_ARTIFACTS[@]}"
echo echo "build/check complete (--no-publish); skipped pro5e, upload, git tag, git push, and GitHub release" echo " root sdist: dist/solstone-${VERSION}.tar.gz" echo " root any: dist/solstone-${VERSION}-py3-none-any.whl" echo " journal sdist: dist/solstone_journal-${VERSION}.tar.gz" echo " journal any: dist/solstone_journal-${VERSION}-py3-none-any.whl" echo " cuda sdist: dist/solstone_journal_cuda-${VERSION}.tar.gz" echo " cuda any: dist/solstone_journal_cuda-${VERSION}-py3-none-any.whl" echo " models sdist: dist/solstone_journal_models-${MODELS_VERSION}.tar.gz" echo " models any: dist/solstone_journal_models-${MODELS_VERSION}-py3-none-any.whl" echo " models gate: ${MODELS_DECISION}" echo " macos: pro5e/publish-time-only dist/solstone-${VERSION}-py3-none-macosx_14_0_arm64.whl" exit 0fi
# 2. macOS arm64 wheel: build helper + sign + notarize + bundle on pro5eecho "==> [2/5] pro5e: building macosx_14_0_arm64 wheel from $GIT_REF"if ! ssh -o ConnectTimeout=5 "$PRO5E_HOST" true 2>/dev/null; then echo "error: $PRO5E_HOST not reachable; skip with --no-macos to publish only Linux artifacts (not implemented)" >&2 exit 1fi
# tmux-run is required: codesign + notarytool need the sol-signing keychain# unlocked, and that unlock state lives in the hopper tmux session's launchd# session — fresh raw SSH connections don't inherit it. ensure-build-windows# is idempotent and re-applies the unlock.ssh "$PRO5E_HOST" "ensure-build-windows >/dev/null"ssh "$PRO5E_HOST" "tmux-run hopper ~/projects/solstone 'set -e; \ git fetch origin && \ git checkout $GIT_REF && \ rm -rf build/ dist/ *.egg-info/ solstone/observe/transcribe/parakeet_helper/_bin && \ NOTARY_KEYCHAIN_PROFILE=$NOTARY_PROFILE make wheel-macos'"
# 3. Pull the macOS wheel back into local dist/echo "==> [3/5] rsyncing macOS wheel back"rsync -av --include='*macosx_14_0_arm64.whl' --exclude='*' \ "$PRO5E_HOST:projects/solstone/dist/" ./dist/
echoecho "release artifacts:"ls -la dist/
ARTIFACTS=( "dist/solstone-${VERSION}.tar.gz" dist/solstone-${VERSION}-*.whl "dist/solstone_journal-${VERSION}.tar.gz" "dist/solstone_journal-${VERSION}-py3-none-any.whl" "dist/solstone_journal_cuda-${VERSION}.tar.gz" "dist/solstone_journal_cuda-${VERSION}-py3-none-any.whl")GH_RELEASE_ARTIFACT_HINT="dist/solstone-${VERSION}.tar.gz dist/solstone-${VERSION}-*.whl dist/solstone_journal-${VERSION}.tar.gz dist/solstone_journal-${VERSION}-*.whl dist/solstone_journal_cuda-${VERSION}.tar.gz dist/solstone_journal_cuda-${VERSION}-*.whl"if [[ "$MODELS_DECISION" == "publish" ]]; then ARTIFACTS+=( "dist/solstone_journal_models-${MODELS_VERSION}.tar.gz" "dist/solstone_journal_models-${MODELS_VERSION}-py3-none-any.whl" ) GH_RELEASE_ARTIFACT_HINT="${GH_RELEASE_ARTIFACT_HINT} dist/solstone_journal_models-${MODELS_VERSION}.tar.gz dist/solstone_journal_models-${MODELS_VERSION}-*.whl"fi
# 4. twine check + uploadechoecho "==> [4/5] twine check + upload to $TARGET"uvx twine check "${ARTIFACTS[@]}"TWINE_USERNAME=__token__ TWINE_PASSWORD="$TOKEN" \ uvx twine upload "${REPOSITORY_ARGS[@]}" "${ARTIFACTS[@]}"
echoecho "published solstone ${VERSION} to ${TARGET}:"echo " root sdist: dist/solstone-${VERSION}.tar.gz"echo " root any: dist/solstone-${VERSION}-py3-none-any.whl"echo " journal sdist: dist/solstone_journal-${VERSION}.tar.gz"echo " journal any: dist/solstone_journal-${VERSION}-py3-none-any.whl"echo " cuda sdist: dist/solstone_journal_cuda-${VERSION}.tar.gz"echo " cuda any: dist/solstone_journal_cuda-${VERSION}-py3-none-any.whl"echo " macos: dist/solstone-${VERSION}-py3-none-macosx_14_0_arm64.whl"if [[ "$MODELS_DECISION" == "publish" ]]; then echo " models sdist: dist/solstone_journal_models-${MODELS_VERSION}.tar.gz" echo " models any: dist/solstone_journal_models-${MODELS_VERSION}-py3-none-any.whl"else echo " models: skipped; solstone-journal-models ${MODELS_VERSION} already exists on ${TARGET}"fi
# 5. tag the commit + cut a GitHub Release. Production only — a TestPyPI dry-run# should not leave a git tag or a public release behind. Mirrors the# solstone-linux release.sh tail so all product repos share one shape, with# release notes pulled from the shared scripts/extract_changelog.sh.if [[ "$TARGET" != "pypi" ]]; then echo echo "skipping git tag + GitHub release (TestPyPI run)" exit 0fi
TAG="v${VERSION}"echoecho "==> [5/5] tagging ${TAG} + creating GitHub release"git tag -a "$TAG" -m "solstone ${VERSION}"if ! git push origin "$TAG"; then echo "error: git push origin ${TAG} failed; the tag was created locally but not pushed." >&2 echo " PyPI is published and immutable. Resolve the push and create the release manually:" >&2 echo " gh release create ${TAG} ${GH_RELEASE_ARTIFACT_HINT} --title 'solstone ${VERSION}' --notes-file <(scripts/extract_changelog.sh ${VERSION})" >&2 exit 1fi
NOTES_FILE=$(mktemp)trap 'rm -f "$NOTES_FILE"' EXITscripts/extract_changelog.sh "$VERSION" > "$NOTES_FILE"
if ! gh release create "$TAG" \ "${ARTIFACTS[@]}" \ --title "solstone ${VERSION}" \ --notes-file "$NOTES_FILE"; then echo "error: gh release create failed." >&2 echo " PyPI is published and immutable; the git tag ${TAG} is pushed." >&2 echo " Re-run manually:" >&2 echo " gh release create ${TAG} ${GH_RELEASE_ARTIFACT_HINT} --title 'solstone ${VERSION}' --notes-file <(scripts/extract_changelog.sh ${VERSION})" >&2 exit 1fi
echoecho "✓ tagged ${TAG} and created GitHub release with sdist + wheels attached"