Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 155 additions & 0 deletions release.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
#!/usr/bin/env bash
set -euo pipefail

VERSION_FILE="src/pyscipopt/_version.py"
SETUP_FILE="setup.py"
CHANGELOG="CHANGELOG.md"
REPO="scipopt/PySCIPOpt"

# --- Pre-flight checks ---

if ! command -v gh &>/dev/null; then
echo "Error: gh CLI is not installed. Install it from https://cli.github.com"
exit 1
fi

if ! gh auth status &>/dev/null; then
echo "Error: gh CLI is not authenticated. Run 'gh auth login' first."
exit 1
fi

if [[ -n "$(git status --porcelain)" ]]; then
echo "Error: working directory is not clean. Commit, stash, or remove changes first."
exit 1
fi

CURRENT_BRANCH=$(git branch --show-current)
if [[ "$CURRENT_BRANCH" != "master" ]]; then
echo "Error: must be on 'master' branch (currently on '${CURRENT_BRANCH}')."
exit 1
fi

git pull --ff-only

# --- Helper functions ---

validate_version() {
if [[ ! "$1" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Error: '$1' is not a valid version (expected X.Y.Z)"
exit 1
fi
}

# --- Read current version ---

CURRENT_VERSION=$(sed -n "s/^__version__.*'\(.*\)'/\1/p" "$VERSION_FILE")
validate_version "$CURRENT_VERSION"
MAJOR=$(echo "$CURRENT_VERSION" | cut -d. -f1)
MINOR=$(echo "$CURRENT_VERSION" | cut -d. -f2)
PATCH=$(echo "$CURRENT_VERSION" | cut -d. -f3)

echo "Current version: ${CURRENT_VERSION}"

# --- Prompt for bump type ---

echo ""
echo "Release type:"
echo " 1) patch -> $((MAJOR)).$((MINOR)).$((PATCH + 1))"
echo " 2) minor -> $((MAJOR)).$((MINOR + 1)).0"
echo " 3) major -> $((MAJOR + 1)).0.0"
echo ""
read -rp "Select [1/2/3]: " bump_type

case "$bump_type" in
1|patch) NEW_VERSION="$((MAJOR)).$((MINOR)).$((PATCH + 1))" ;;
2|minor) NEW_VERSION="$((MAJOR)).$((MINOR + 1)).0" ;;
3|major) NEW_VERSION="$((MAJOR + 1)).0.0" ;;
*) echo "Error: invalid selection '${bump_type}'"; exit 1 ;;
esac

# --- Check tag doesn't already exist ---

if git rev-parse "v${NEW_VERSION}" &>/dev/null; then
echo "Error: tag 'v${NEW_VERSION}' already exists locally."
exit 1
fi

if git ls-remote --tags --exit-code origin "refs/tags/v${NEW_VERSION}" &>/dev/null; then
echo "Error: tag 'v${NEW_VERSION}' already exists on origin."
exit 1
fi

# --- Show summary and confirm ---

echo ""
echo "Unreleased changelog entries:"
echo "-----------------------------"
sed -n '/^## Unreleased$/,/^## [0-9]/{/^## [0-9]/!p;}' "$CHANGELOG" | head -30
echo "-----------------------------"

TODAY=$(date +%Y.%m.%d)
echo ""
echo "This script will:"
echo " 1. Update version ${CURRENT_VERSION} -> ${NEW_VERSION} in _version.py and setup.py"
echo " 2. Update CHANGELOG.md (${NEW_VERSION} - ${TODAY})"
echo " 3. Commit, tag v${NEW_VERSION}, and push to origin"
echo " 4. Trigger the build wheels workflow (test-pypi)"
echo ""
read -rp "Proceed? [Y/n] " confirm
[[ "${confirm:-Y}" =~ ^[Nn] ]] && exit 0

# ============================================================
# From here on, everything runs without further prompts.
# ============================================================

# --- Update version files ---

sed -i.bak "s/__version__.*=.*'.*'/__version__: str = '${NEW_VERSION}'/" "$VERSION_FILE"
rm -f "${VERSION_FILE}.bak"

sed -i.bak "s/version=\"${CURRENT_VERSION}\"/version=\"${NEW_VERSION}\"/" "$SETUP_FILE"
rm -f "${SETUP_FILE}.bak"

echo "Updated version: ${CURRENT_VERSION} -> ${NEW_VERSION}"

# --- Update changelog ---

sed -i.bak "s/^## Unreleased$/## ${NEW_VERSION} - ${TODAY}/" "$CHANGELOG"
rm -f "${CHANGELOG}.bak"

sed -i.bak "/^# CHANGELOG$/a\\
\\
## Unreleased\\
### Added\\
### Fixed\\
### Changed\\
### Removed\\
" "$CHANGELOG"
rm -f "${CHANGELOG}.bak"

echo "Updated CHANGELOG.md"

# --- Commit, tag, and push ---

git add "$VERSION_FILE" "$SETUP_FILE" "$CHANGELOG"
git commit -m "release v${NEW_VERSION}"
git tag "v${NEW_VERSION}"
git push origin master
git push origin "v${NEW_VERSION}"

# --- Trigger test-pypi build ---

gh workflow run build_wheels.yml --repo "$REPO" -f upload_to_pypi=true -f test_pypi=true

echo ""
echo "Done! v${NEW_VERSION} committed, tagged, pushed, and test-pypi build triggered."
echo "Monitor at: gh run list --workflow=build_wheels.yml --repo ${REPO}"
echo ""
echo "Remaining manual steps:"
echo " 1. Test the test-pypi package:"
echo " pip install -i https://test.pypi.org/simple/ PySCIPOpt==${NEW_VERSION}"
echo " 2. Release to production pypi:"
echo " gh workflow run build_wheels.yml --repo ${REPO} -f upload_to_pypi=true -f test_pypi=false"
echo " 3. Create a GitHub release from tag v${NEW_VERSION}:"
echo " gh release create v${NEW_VERSION} --repo ${REPO} --title v${NEW_VERSION} --generate-notes"
echo " 4. Update readthedocs: Builds -> Build version (latest and stable)"
176 changes: 176 additions & 0 deletions upgrade_scip.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
#!/usr/bin/env bash
set -euo pipefail

PYPROJECT="pyproject.toml"
DEPLOY_REPO="scipopt/scipoptsuite-deploy"
REPO="scipopt/PySCIPOpt"

# --- Pre-flight checks ---

if ! command -v gh &>/dev/null; then
echo "Error: gh CLI is not installed. Install it from https://cli.github.com"
exit 1
fi

if ! gh auth status &>/dev/null; then
echo "Error: gh CLI is not authenticated. Run 'gh auth login' first."
exit 1
fi

if [[ -n "$(git status --porcelain)" ]]; then
echo "Error: working directory is not clean. Commit, stash, or remove changes first."
exit 1
fi

CURRENT_BRANCH=$(git branch --show-current)
if [[ "$CURRENT_BRANCH" != "master" ]]; then
echo "Error: must be on 'master' branch (currently on '${CURRENT_BRANCH}')."
exit 1
fi

git pull --ff-only

# --- Helper functions ---

validate_version() {
if [[ ! "$1" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Error: '$1' is not a valid version (expected X.Y.Z)"
exit 1
fi
}

prompt_version() {
local label="$1" current="$2"
read -rp "${label} [${current}]: " value
value="${value:-$current}"
validate_version "$value"
echo "$value"
}

# --- Collect all inputs ---

CURRENT_DEPLOY_VERSION=$(grep -o 'scipoptsuite-deploy/releases/download/v[0-9.]*' "$PYPROJECT" | head -1 | sed 's|.*/||')

echo "Current scipoptsuite-deploy version: ${CURRENT_DEPLOY_VERSION}"
echo ""
echo "Enter component versions (press enter to keep current):"

# Fetch current defaults from the deploy workflow
DEPLOY_WORKFLOW=$(gh api repos/${DEPLOY_REPO}/contents/.github/workflows/build_binaries.yml --jq '.content' | base64 --decode)
current_deploy_default() {
echo "$DEPLOY_WORKFLOW" | sed -n "/${1}:/,/default:/{s/.*default: \"\(.*\)\"/\1/p;}" | head -1
}

CUR_SCIP=$(current_deploy_default "scip_version")
CUR_SOPLEX=$(current_deploy_default "soplex_version")
CUR_GCG=$(current_deploy_default "gcg_version")
CUR_IPOPT=$(current_deploy_default "ipopt_version")

SCIP_VERSION=$(prompt_version "SCIP" "$CUR_SCIP")
SOPLEX_VERSION=$(prompt_version "SoPlex" "$CUR_SOPLEX")
GCG_VERSION=$(prompt_version "GCG" "$CUR_GCG")
IPOPT_VERSION=$(prompt_version "IPOPT" "$CUR_IPOPT")

# Bump deploy version (increment minor)
DEPLOY_MAJOR=$(echo "$CURRENT_DEPLOY_VERSION" | sed 's/^v//' | cut -d. -f1)
DEPLOY_MINOR=$(echo "$CURRENT_DEPLOY_VERSION" | sed 's/^v//' | cut -d. -f2)
DEPLOY_PATCH=$(echo "$CURRENT_DEPLOY_VERSION" | sed 's/^v//' | cut -d. -f3)
SUGGESTED_DEPLOY="v$((DEPLOY_MAJOR)).$((DEPLOY_MINOR + 1)).$((DEPLOY_PATCH))"

read -rp "New deploy release tag [${SUGGESTED_DEPLOY}]: " NEW_DEPLOY_VERSION
NEW_DEPLOY_VERSION="${NEW_DEPLOY_VERSION:-$SUGGESTED_DEPLOY}"

if [[ ! "$NEW_DEPLOY_VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Error: deploy tag must match vX.Y.Z"
exit 1
fi

if gh api "repos/${DEPLOY_REPO}/git/ref/tags/${NEW_DEPLOY_VERSION}" &>/dev/null; then
echo "Error: deploy tag ${NEW_DEPLOY_VERSION} already exists in ${DEPLOY_REPO}."
exit 1
fi

# --- Show summary and confirm ---

BRANCH="upgrade-scip-${SCIP_VERSION}"

echo ""
echo "This script will:"
echo " 1. Build new SCIP binaries (SCIP=${SCIP_VERSION} SoPlex=${SOPLEX_VERSION} GCG=${GCG_VERSION} IPOPT=${IPOPT_VERSION})"
echo " 2. Create scipoptsuite-deploy release ${NEW_DEPLOY_VERSION}"
echo " 3. Create branch '${BRANCH}', update pyproject.toml, and open a PR"
echo ""
read -rp "Proceed? [Y/n] " confirm
[[ "${confirm:-Y}" =~ ^[Nn] ]] && exit 0

# ============================================================
# From here on, everything runs without further prompts.
# ============================================================

# --- Build SCIP binaries ---

echo ""
echo "Triggering SCIP binary build..."
gh workflow run build_binaries.yml --repo "$DEPLOY_REPO" \
-f scip_version="$SCIP_VERSION" \
-f soplex_version="$SOPLEX_VERSION" \
-f gcg_version="$GCG_VERSION" \
-f ipopt_version="$IPOPT_VERSION"

# Wait for the run to appear
DISPATCH_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ)
for i in {1..12}; do
sleep 5
RUN_ID=$(gh run list --workflow=build_binaries.yml --repo "$DEPLOY_REPO" --limit 1 --event workflow_dispatch --json databaseId,createdAt --jq "[.[] | select(.createdAt >= \"${DISPATCH_TIME}\")] | .[0].databaseId")
[[ -n "$RUN_ID" && "$RUN_ID" != "null" ]] && break
done

if [[ -z "$RUN_ID" || "$RUN_ID" == "null" ]]; then
echo "Error: could not find the triggered workflow run."
exit 1
fi

echo "Waiting for build to complete (run ${RUN_ID})..."
echo " https://github.com/${DEPLOY_REPO}/actions/runs/${RUN_ID}"
gh run watch "$RUN_ID" --repo "$DEPLOY_REPO" --exit-status

# --- Create deploy release ---

TMPDIR=$(mktemp -d)
echo "Downloading artifacts..."
gh run download "$RUN_ID" --repo "$DEPLOY_REPO" --dir "$TMPDIR"

RELEASE_NAME="SCIP ${SCIP_VERSION} SOPLEX ${SOPLEX_VERSION} GCG ${GCG_VERSION} IPOPT ${IPOPT_VERSION}"
echo "Creating release ${NEW_DEPLOY_VERSION}..."
gh release create "$NEW_DEPLOY_VERSION" \
--repo "$DEPLOY_REPO" \
--title "$RELEASE_NAME" \
--notes "$RELEASE_NAME" \
"$TMPDIR"/linux/*.zip \
"$TMPDIR"/linux-arm/*.zip \
"$TMPDIR"/macos-arm/*.zip \
"$TMPDIR"/macos-intel/*.zip \
"$TMPDIR"/windows/*.zip

rm -rf "$TMPDIR"

# --- Create PR with updated pyproject.toml ---

git checkout -b "$BRANCH"

sed -i.bak "s|scipoptsuite-deploy/releases/download/${CURRENT_DEPLOY_VERSION}|scipoptsuite-deploy/releases/download/${NEW_DEPLOY_VERSION}|g" "$PYPROJECT"
rm -f "${PYPROJECT}.bak"

git add "$PYPROJECT"
git commit -m "Update scipoptsuite-deploy to ${NEW_DEPLOY_VERSION} (SCIP ${SCIP_VERSION})"
git push -u origin "$BRANCH"

gh pr create --repo "$REPO" \
--title "Upgrade to SCIP ${SCIP_VERSION}" \
--body "Updates scipoptsuite-deploy ${CURRENT_DEPLOY_VERSION} -> ${NEW_DEPLOY_VERSION} (SCIP ${SCIP_VERSION}, SoPlex ${SOPLEX_VERSION}, GCG ${GCG_VERSION}, IPOPT ${IPOPT_VERSION}).

Fix any API incompatibilities, get CI green, then merge and run \`./release.sh\`."

echo ""
echo "Done! PR created on branch '${BRANCH}'."
echo "Fix any API incompatibilities, get CI green, then merge and run ./release.sh"
Loading