From af4b408df94cbc60e6cbeeee7fa96d262a5fc59e Mon Sep 17 00:00:00 2001 From: Dana Wensberg Date: Fri, 24 Apr 2026 11:50:12 -0400 Subject: [PATCH] publish command line utility --- publish_package.sh | 153 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100755 publish_package.sh diff --git a/publish_package.sh b/publish_package.sh new file mode 100755 index 0000000..7c297bb --- /dev/null +++ b/publish_package.sh @@ -0,0 +1,153 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ── Colors ─────────────────────────────────────────────────────────────── +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +CYAN='\033[0;36m' +BOLD='\033[1m' +DIM='\033[2m' +RESET='\033[0m' + +info() { echo -e " ${CYAN}ℹ${RESET} $*"; } +ok() { echo -e " ${GREEN}✓${RESET} $*"; } +warn() { echo -e " ${YELLOW}⚠${RESET} $*"; } +fail() { echo -e " ${RED}✗${RESET} $*"; } +header(){ echo -e "\n ${BOLD}$*${RESET}\n"; } + +PKG_NAME="subconscious-sdk" +REGISTRY="PyPI" +VERSION_FILE="pyproject.toml" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +header "Subconscious Python SDK Release Helper" + +# ── Step 0: Git state checks ───────────────────────────────────────────── +header "Step 0: Git state checks" + +BRANCH=$(git rev-parse --abbrev-ref HEAD) +if [ "$BRANCH" != "main" ]; then + fail "You are on branch ${BOLD}$BRANCH${RESET}, must be on ${BOLD}main${RESET}." + exit 1 +fi +ok "On branch main" + +if ! git diff --quiet || ! git diff --cached --quiet; then + fail "You have uncommitted local changes. Commit or stash them first." + exit 1 +fi +ok "No uncommitted changes" + +git fetch origin main --quiet +LOCAL_SHA=$(git rev-parse HEAD) +REMOTE_SHA=$(git rev-parse origin/main) +if [ "$LOCAL_SHA" != "$REMOTE_SHA" ]; then + fail "Local main (${DIM}${LOCAL_SHA:0:8}${RESET}) is not up to date with origin/main (${DIM}${REMOTE_SHA:0:8}${RESET})." + info "Run: git pull origin main" + exit 1 +fi +ok "Up to date with origin/main" + +# ── Step 1: Current published version ──────────────────────────────────── +header "Step 1: Current published version on $REGISTRY" + +PUBLISHED=$(pip index versions "$PKG_NAME" 2>/dev/null | head -1 | sed 's/.*(\(.*\))/\1/' || echo "") +if [ -z "$PUBLISHED" ]; then + warn "Could not query PyPI for $PKG_NAME. Is pip available?" + warn "Continuing with published version unknown." + PUBLISHED="0.0.0" +fi + +PUBLISHED_TAG="v$PUBLISHED" +info "Published version: ${BOLD}$PUBLISHED${RESET} (tag: $PUBLISHED_TAG)" + +if git rev-parse "$PUBLISHED_TAG" >/dev/null 2>&1; then + TAG_COMMIT=$(git rev-parse "$PUBLISHED_TAG") + TAG_DATE=$(git log -1 --format='%ci' "$PUBLISHED_TAG") + info "Tag ${BOLD}$PUBLISHED_TAG${RESET} -> commit ${DIM}${TAG_COMMIT:0:10}${RESET} (${TAG_DATE})" +else + warn "Tag $PUBLISHED_TAG not found locally. It may have been created from another machine." + warn "Skipping diff check — will continue with version checks." + TAG_COMMIT="" +fi + +# ── Step 2: Changes since last release ─────────────────────────────────── +header "Step 2: Changes since last release" + +if [ -n "$TAG_COMMIT" ]; then + CHANGES=$(git diff --stat "$PUBLISHED_TAG"..HEAD -- .) + if [ -z "$CHANGES" ]; then + fail "No changes since $PUBLISHED_TAG. Nothing to release." + exit 0 + fi + ok "Changes found since $PUBLISHED_TAG:" + echo "" + echo "$CHANGES" | sed 's/^/ /' + echo "" +else + warn "Cannot diff (tag not found locally). Proceeding based on version check." +fi + +# ── Step 3: Version bump check ─────────────────────────────────────────── +header "Step 3: Version bump check" + +LOCAL_VERSION=$(grep -m1 '^version' "$VERSION_FILE" | sed 's/.*"\(.*\)".*/\1/') + +info "Published: ${BOLD}$PUBLISHED${RESET} → Local ($VERSION_FILE): ${BOLD}$LOCAL_VERSION${RESET}" + +HIGHER=$(printf '%s\n%s\n' "$PUBLISHED" "$LOCAL_VERSION" | sort -V | tail -1) +if [ "$LOCAL_VERSION" = "$PUBLISHED" ]; then + fail "Local version ($LOCAL_VERSION) is the same as the published version." + fail "Bump the version in ${BOLD}$VERSION_FILE${RESET} first." + exit 1 +fi +if [ "$HIGHER" != "$LOCAL_VERSION" ]; then + fail "Local version ($LOCAL_VERSION) is lower than published ($PUBLISHED)." + fail "Bump the version in ${BOLD}$VERSION_FILE${RESET} to something higher than $PUBLISHED." + exit 1 +fi +ok "Version $LOCAL_VERSION is higher than published $PUBLISHED" + +# ── Step 4: Tag collision check ────────────────────────────────────────── +header "Step 4: Tag collision check" + +NEW_TAG="v$LOCAL_VERSION" +REMOTE_TAG_EXISTS=$(git ls-remote --tags origin "$NEW_TAG" 2>/dev/null || true) + +if [ -n "$REMOTE_TAG_EXISTS" ]; then + fail "Tag ${BOLD}$NEW_TAG${RESET} already exists on the remote." + info "If the tag is orphaned and you need to delete it:" + echo -e " ${DIM}git push origin :refs/tags/$NEW_TAG${RESET}" + exit 1 +fi +ok "Tag $NEW_TAG does not exist on remote — safe to create" + +# ── Step 5: Pre-release checklist ──────────────────────────────────────── +header "Step 5: Ready to release" + +echo -e " ${GREEN}All checks passed.${RESET} Here's what will happen:" +echo "" +echo -e " 1. A git tag ${BOLD}$NEW_TAG${RESET} will be created at HEAD (${DIM}${LOCAL_SHA:0:10}${RESET})" +echo -e " 2. The tag will be pushed to origin" +echo -e " 3. GitHub Actions (trusted publisher / OIDC) will build and publish" +echo -e " ${BOLD}$PKG_NAME $LOCAL_VERSION${RESET} to PyPI" +echo "" + +echo -e " ${YELLOW}${BOLD}Pre-release checklist:${RESET}" +echo "" +echo -e " ${YELLOW}□${RESET} Backwards compatibility — have you considered breaking changes?" +echo -e " ${YELLOW}□${RESET} Migration guide — if breaking, is MIGRATION.md updated?" +echo -e " ${YELLOW}□${RESET} API version — is the corresponding API version live on main?" +echo -e " ${YELLOW}□${RESET} Docs — are subconscious-docs updated for any new/changed features?" +echo "" + +echo -e " ${BOLD}Run this command when ready:${RESET}" +echo "" +echo -e " ${GREEN}git tag $NEW_TAG && git push origin $NEW_TAG${RESET}" +echo "" +echo -e " Then track the build:" +echo -e " ${CYAN}https://github.com/subconscious-systems/subconscious-python/actions${RESET}" +echo ""