-
Notifications
You must be signed in to change notification settings - Fork 281
Add release automation script #1201
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Joao-Dionisio
wants to merge
5
commits into
master
Choose a base branch
from
release-automation
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+331
−0
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
5a5189f
Add release automation script
Joao-Dionisio a061adc
Add scipoptsuite-deploy integration to release script
Joao-Dionisio b137b33
Merge branch 'master' into release-automation
Joao-Dionisio 8501bec
Address review: clean dir check, version validation, remote tag check
Joao-Dionisio 73d63cf
Split deploy into upgrade_scip.sh, simplify release.sh
Joao-Dionisio File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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)" | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.