|
| 1 | +#!/usr/bin/env bash |
| 2 | +set -euo pipefail |
| 3 | + |
| 4 | +VERSION_FILE="src/pyscipopt/_version.py" |
| 5 | +SETUP_FILE="setup.py" |
| 6 | +CHANGELOG="CHANGELOG.md" |
| 7 | +REPO="scipopt/PySCIPOpt" |
| 8 | + |
| 9 | +# --- Pre-flight checks --- |
| 10 | + |
| 11 | +if ! command -v gh &>/dev/null; then |
| 12 | + echo "Error: gh CLI is not installed. Install it from https://cli.github.com" |
| 13 | + exit 1 |
| 14 | +fi |
| 15 | + |
| 16 | +if ! gh auth status &>/dev/null; then |
| 17 | + echo "Error: gh CLI is not authenticated. Run 'gh auth login' first." |
| 18 | + exit 1 |
| 19 | +fi |
| 20 | + |
| 21 | +if ! git diff --quiet || ! git diff --cached --quiet; then |
| 22 | + echo "Error: working directory has uncommitted changes. Commit or stash them first." |
| 23 | + exit 1 |
| 24 | +fi |
| 25 | + |
| 26 | +CURRENT_BRANCH=$(git branch --show-current) |
| 27 | +if [[ "$CURRENT_BRANCH" != "master" ]]; then |
| 28 | + echo "Error: must be on 'master' branch (currently on '${CURRENT_BRANCH}')." |
| 29 | + exit 1 |
| 30 | +fi |
| 31 | + |
| 32 | +git pull --ff-only |
| 33 | + |
| 34 | +# --- Read current version --- |
| 35 | + |
| 36 | +CURRENT_VERSION=$(sed -n "s/^__version__.*'\(.*\)'/\1/p" "$VERSION_FILE") |
| 37 | +MAJOR=$(echo "$CURRENT_VERSION" | cut -d. -f1) |
| 38 | +MINOR=$(echo "$CURRENT_VERSION" | cut -d. -f2) |
| 39 | +PATCH=$(echo "$CURRENT_VERSION" | cut -d. -f3) |
| 40 | + |
| 41 | +echo "Current version: ${CURRENT_VERSION}" |
| 42 | + |
| 43 | +# --- Prompt for bump type --- |
| 44 | + |
| 45 | +echo "" |
| 46 | +echo "Release type:" |
| 47 | +echo " 1) patch -> $((MAJOR)).$((MINOR)).$((PATCH + 1))" |
| 48 | +echo " 2) minor -> $((MAJOR)).$((MINOR + 1)).0" |
| 49 | +echo " 3) major -> $((MAJOR + 1)).0.0" |
| 50 | +echo "" |
| 51 | +read -rp "Select [1/2/3]: " bump_type |
| 52 | + |
| 53 | +case "$bump_type" in |
| 54 | + 1|patch) NEW_VERSION="$((MAJOR)).$((MINOR)).$((PATCH + 1))" ;; |
| 55 | + 2|minor) NEW_VERSION="$((MAJOR)).$((MINOR + 1)).0" ;; |
| 56 | + 3|major) NEW_VERSION="$((MAJOR + 1)).0.0" ;; |
| 57 | + *) echo "Error: invalid selection '${bump_type}'"; exit 1 ;; |
| 58 | +esac |
| 59 | + |
| 60 | +# --- Check tag doesn't already exist --- |
| 61 | + |
| 62 | +if git rev-parse "v${NEW_VERSION}" &>/dev/null; then |
| 63 | + echo "Error: tag 'v${NEW_VERSION}' already exists." |
| 64 | + exit 1 |
| 65 | +fi |
| 66 | + |
| 67 | +# --- Show changelog preview --- |
| 68 | + |
| 69 | +echo "" |
| 70 | +echo "Unreleased changelog entries:" |
| 71 | +echo "-----------------------------" |
| 72 | +# Print lines between "## Unreleased" and the next "## " header |
| 73 | +sed -n '/^## Unreleased$/,/^## [0-9]/{/^## [0-9]/!p;}' "$CHANGELOG" | head -30 |
| 74 | +echo "-----------------------------" |
| 75 | +echo "" |
| 76 | + |
| 77 | +TODAY=$(date +%Y.%m.%d) |
| 78 | +echo "" |
| 79 | +echo "This script will:" |
| 80 | +echo " 1. Update version ${CURRENT_VERSION} -> ${NEW_VERSION} in _version.py and setup.py" |
| 81 | +echo " 2. Update CHANGELOG.md (${NEW_VERSION} - ${TODAY})" |
| 82 | +echo " 3. Commit, tag v${NEW_VERSION}, and push to origin" |
| 83 | +echo " 4. Trigger the build wheels workflow (test-pypi)" |
| 84 | +echo "" |
| 85 | +read -rp "Proceed? [Y/n] " confirm |
| 86 | +[[ "${confirm:-Y}" =~ ^[Nn] ]] && exit 0 |
| 87 | + |
| 88 | +# --- Update version files --- |
| 89 | + |
| 90 | +sed -i.bak "s/__version__.*=.*'.*'/__version__: str = '${NEW_VERSION}'/" "$VERSION_FILE" |
| 91 | +rm -f "${VERSION_FILE}.bak" |
| 92 | + |
| 93 | +sed -i.bak "s/version=\"${CURRENT_VERSION}\"/version=\"${NEW_VERSION}\"/" "$SETUP_FILE" |
| 94 | +rm -f "${SETUP_FILE}.bak" |
| 95 | + |
| 96 | +echo "Updated version: ${CURRENT_VERSION} -> ${NEW_VERSION}" |
| 97 | + |
| 98 | +# --- Update changelog --- |
| 99 | + |
| 100 | +UNRELEASED_HEADER="## Unreleased" |
| 101 | +NEW_HEADER="## ${NEW_VERSION} - ${TODAY}" |
| 102 | +EMPTY_UNRELEASED="## Unreleased\n### Added\n### Fixed\n### Changed\n### Removed\n" |
| 103 | + |
| 104 | +sed -i.bak "s/^${UNRELEASED_HEADER}$/${NEW_HEADER}/" "$CHANGELOG" |
| 105 | +rm -f "${CHANGELOG}.bak" |
| 106 | + |
| 107 | +# Add empty Unreleased section at the top (after "# CHANGELOG" line) |
| 108 | +sed -i.bak "/^# CHANGELOG$/a\\ |
| 109 | +\\ |
| 110 | +${EMPTY_UNRELEASED}" "$CHANGELOG" |
| 111 | +rm -f "${CHANGELOG}.bak" |
| 112 | + |
| 113 | +echo "Updated CHANGELOG.md" |
| 114 | + |
| 115 | +# --- Commit, tag, and push --- |
| 116 | + |
| 117 | +git add "$VERSION_FILE" "$SETUP_FILE" "$CHANGELOG" |
| 118 | +git commit -m "release v${NEW_VERSION}" |
| 119 | +git tag "v${NEW_VERSION}" |
| 120 | +git push origin master |
| 121 | +git push origin "v${NEW_VERSION}" |
| 122 | + |
| 123 | +# --- Trigger test-pypi build --- |
| 124 | + |
| 125 | +gh workflow run build_wheels.yml --repo "$REPO" -f upload_to_pypi=true -f test_pypi=true |
| 126 | + |
| 127 | +echo "" |
| 128 | +echo "Done! v${NEW_VERSION} committed, tagged, pushed, and test-pypi build triggered." |
| 129 | +echo "Monitor at: gh run list --workflow=build_wheels.yml --repo ${REPO}" |
| 130 | +echo "" |
| 131 | +echo "Remaining manual steps:" |
| 132 | +echo " 1. Test the test-pypi package:" |
| 133 | +echo " pip install -i https://test.pypi.org/simple/ PySCIPOpt==${NEW_VERSION}" |
| 134 | +echo " 2. Release to production pypi:" |
| 135 | +echo " gh workflow run build_wheels.yml --repo ${REPO} -f upload_to_pypi=true -f test_pypi=false" |
| 136 | +echo " 3. Create a GitHub release from tag v${NEW_VERSION}:" |
| 137 | +echo " gh release create v${NEW_VERSION} --repo ${REPO} --title v${NEW_VERSION} --generate-notes" |
| 138 | +echo " 4. Update readthedocs: Builds -> Build version (latest and stable)" |
0 commit comments