Simplify bump-go.sh toolchain logic

Address review feedback: always set both go and toolchain
directives via go mod edit, then let go mod tidy normalize.
This eliminates complex conditional toolchain handling.

Additional fixes:
- Add go mod tidy after edits to reconcile dependencies
- Commit go.sum alongside go.mod
- Filter PR search to open PRs only (--state open)
- Use GITHUB_REPOSITORY for repo instead of hardcoding
- Use git diff to detect no-op bumps post-tidy
- Read go.mod state via go mod edit -json instead of grep

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
William Martin 2026-05-13 13:25:39 +02:00
parent d2cc91bdd4
commit e5f5427b97

View file

@ -1,15 +1,15 @@
#!/usr/bin/env bash
#
# bump-go.sh Update go.mod `go` directive and toolchain to latest stable Go release.
# bump-go.sh -- Update go.mod `go` directive and toolchain to latest stable Go release.
#
# Usage:
# ./bump-go.sh [--apply|-a] <path/to/go.mod>
#
# By default the script runs in *dryrun* mode: it creates a local branch,
# By default the script runs in *dry-run* mode: it creates a local branch,
# commits the version bump, shows the exact patch, **checks for an existing PR**
# with the same title, and exits. Nothing is pushed. The temporary branch is
# deleted automatically on exit, so your working tree stays clean. Pass
# --apply (or -a) to push the branch and open a new PR *only if one doesnt
# --apply (or -a) to push the branch and open a new PR *only if one doesn't
# already exist*.
# -----------------------------------------------------------------------------
set -euo pipefail
@ -35,29 +35,32 @@ done
[[ -z "$GO_MOD" ]] && usage
[[ -f "$GO_MOD" ]] || { echo "Error: '$GO_MOD' not found" >&2; exit 1; }
REPO="cli/cli"
MODULE_DIR=$(dirname "$GO_MOD")
GO_SUM="$MODULE_DIR/go.sum"
# ---- Discover latest stable Go release --------------------------------------
echo "Fetching latest stable Go version…"
echo "Fetching latest stable Go version..."
LATEST_JSON=$(curl -fsSL https://go.dev/dl/?mode=json | jq -c '[.[] | select(.stable==true)][0]')
FULL_VERSION=$(jq -r '.version' <<< "$LATEST_JSON") # e.g. go1.23.4
TOOLCHAIN_VERSION="${FULL_VERSION#go}" # e.g. 1.23.4
GO_DIRECTIVE_VERSION="$(cut -d. -f1-2 <<< "$TOOLCHAIN_VERSION").0"
# The go directive can be either X.Y.0 (minor version) or X.Y.Z (patch version)
# We accept both forms as "latest" if they match the toolchain's major.minor
LATEST_MAJOR_MINOR="$(cut -d. -f1-2 <<< "$TOOLCHAIN_VERSION")"
echo " → latest toolchain : $TOOLCHAIN_VERSION"
echo " → go directive : $GO_DIRECTIVE_VERSION"
echo " → toolchain : go$TOOLCHAIN_VERSION"
# ---- Read current go.mod state using go mod edit ----------------------------
GO_MOD_JSON=$(go mod edit -json "$GO_MOD")
CURRENT_GO_DIRECTIVE=$(jq -r '.Go // ""' <<< "$GO_MOD_JSON")
CURRENT_TOOLCHAIN_DIRECTIVE=$(jq -r '.Toolchain // ""' <<< "$GO_MOD_JSON")
CURRENT_TOOLCHAIN=$(jq -r '.Toolchain // ""' <<< "$GO_MOD_JSON")
CURRENT_MAJOR_MINOR="$(cut -d. -f1-2 <<< "$CURRENT_GO_DIRECTIVE")"
echo " → current go : $CURRENT_GO_DIRECTIVE"
echo " → current tc : ${CURRENT_TOOLCHAIN:-(none)}"
# ---- Prepare Git branch -----------------------------------------------------
BRANCH="bump-go-$TOOLCHAIN_VERSION"
BRANCH_CREATED=0
# Set up cleanup trap early (before any potential exits)
cleanup() {
if [[ $BRANCH_CREATED -eq 1 ]]; then
git checkout - >/dev/null 2>&1 || true
@ -66,58 +69,32 @@ cleanup() {
}
trap cleanup EXIT
# Check if we're already up to date
# Note: toolchain directive may be missing when go directive matches latest toolchain.
# This is expected behavior - `go mod tidy` removes the toolchain line when
# the minimum Go version matches the latest toolchain, as it's redundant.
if [[ "$CURRENT_MAJOR_MINOR" = "$LATEST_MAJOR_MINOR" ]]; then
# Current go directive is at the same major.minor as latest
if [[ -z "$CURRENT_TOOLCHAIN_DIRECTIVE" ]]; then
# No toolchain directive present - this is expected when go version matches latest
echo "Already on latest Go version: $CURRENT_GO_DIRECTIVE (latest toolchain: $TOOLCHAIN_VERSION)"
echo " → Note: No toolchain directive (expected when go version matches latest toolchain)"
exit 0
elif [[ "$CURRENT_TOOLCHAIN_DIRECTIVE" = "go$TOOLCHAIN_VERSION" ]]; then
echo "Already on latest Go version: $CURRENT_GO_DIRECTIVE (toolchain: $CURRENT_TOOLCHAIN_DIRECTIVE)"
exit 0
fi
# Current go directive is latest but toolchain is outdated - continue to update toolchain
fi
echo "Creating branch $BRANCH"
git switch -c "$BRANCH" >/dev/null 2>&1
BRANCH_CREATED=1
# ---- Patch go.mod using go mod edit -----------------------------------------
# Only update go directive if we're not already at the latest major.minor version
if [[ "$CURRENT_MAJOR_MINOR" != "$LATEST_MAJOR_MINOR" ]]; then
# Bump to the latest major.minor.0 (preserves the convention of X.Y.0 for go directive)
NEW_GO_DIRECTIVE="$LATEST_MAJOR_MINOR.0"
go mod edit -go="$NEW_GO_DIRECTIVE" "$GO_MOD"
echo " • go directive $CURRENT_GO_DIRECTIVE$NEW_GO_DIRECTIVE"
# After updating, the current go directive is now the new one for toolchain logic
CURRENT_GO_DIRECTIVE="$NEW_GO_DIRECTIVE"
fi
# ---- Patch go.mod -----------------------------------------------------------
# Always set both directives and let `go mod tidy` normalize.
# When the go directive version matches the toolchain version, tidy will remove
# the toolchain line because it is redundant -- this is expected Go behavior.
go mod edit -go="$GO_DIRECTIVE_VERSION" -toolchain="go$TOOLCHAIN_VERSION" "$GO_MOD"
echo " • set go directive → $GO_DIRECTIVE_VERSION"
echo " • set toolchain → go$TOOLCHAIN_VERSION"
# Handle toolchain directive - may need to add, update, or skip
if [[ -z "$CURRENT_TOOLCHAIN_DIRECTIVE" ]]; then
# No toolchain directive exists
CURRENT_MAJOR_MINOR="$(cut -d. -f1-2 <<< "$CURRENT_GO_DIRECTIVE")"
if [[ "$CURRENT_MAJOR_MINOR" = "$LATEST_MAJOR_MINOR" ]]; then
# go directive is at latest major.minor - toolchain line is redundant
echo " • toolchain directive not needed (go version matches latest toolchain)"
else
# go directive is older than latest toolchain - add toolchain directive
go mod edit -toolchain="go$TOOLCHAIN_VERSION" "$GO_MOD"
echo " • toolchain directive added: go$TOOLCHAIN_VERSION"
fi
elif [[ "$CURRENT_TOOLCHAIN_DIRECTIVE" != "go$TOOLCHAIN_VERSION" ]]; then
# Toolchain directive exists but needs updating
go mod edit -toolchain="go$TOOLCHAIN_VERSION" "$GO_MOD"
echo " • toolchain $CURRENT_TOOLCHAIN_DIRECTIVE → go$TOOLCHAIN_VERSION"
# Let go mod tidy reconcile dependencies and normalize directives.
echo " • running go mod tidy..."
pushd "$MODULE_DIR" > /dev/null
go mod tidy
popd > /dev/null
# ---- Check if anything actually changed -------------------------------------
if git diff --quiet -- "$GO_MOD" "$GO_SUM" 2>/dev/null; then
echo "Already on latest Go version -- no changes needed."
exit 0
fi
git add "$GO_MOD"
[[ -f "$GO_SUM" ]] && git add "$GO_SUM"
# ---- Commit -----------------------------------------------------------------
COMMIT_MSG="Bump Go to $TOOLCHAIN_VERSION"
@ -127,48 +104,45 @@ COMMIT_HASH=$(git rev-parse --short HEAD)
PR_TITLE="$COMMIT_MSG"
# ---- Check for existing PR --------------------------------------------------
existing_pr=$(gh search prs --repo cli/cli --match title "$PR_TITLE" --json title --jq "map(select(.title == \"$PR_TITLE\") | .title) | length > 0")
existing_pr=$(gh search prs --repo "$REPO" --state open --match title "$PR_TITLE" \
--json title --jq "map(select(.title == \"$PR_TITLE\") | .title) | length > 0")
if [[ "$existing_pr" == "true" ]]; then
echo "Found an existing open PR titled '$PR_TITLE'. Skipping push/PR creation."
if [[ $APPLY -eq 0 ]]; then
echo -e "\n=== DRYRUN DIFF (commit $COMMIT_HASH):\n"
echo -e "\n=== DRY-RUN DIFF (commit $COMMIT_HASH):\n"
git --no-pager show --color "$COMMIT_HASH"
fi
exit 0
fi
# ---- Dryrun handling -------------------------------------------------------
# ---- Dry-run handling -------------------------------------------------------
if [[ $APPLY -eq 0 ]]; then
echo -e "\n=== DRYRUN DIFF (commit $COMMIT_HASH):\n"
echo -e "\n=== DRY-RUN DIFF (commit $COMMIT_HASH):\n"
git --no-pager show --color "$COMMIT_HASH"
echo -e "\nIf --apply were provided, script would continue with:\n git push -u origin $BRANCH\n gh pr create --title \"$PR_TITLE\" --body <body>\n"
exit 0
fi
# ---- Push & PR --------------------------------------------------------------
# Get the actual go directive from the updated go.mod using go mod edit
FINAL_GO_MOD_JSON=$(go mod edit -json "$GO_MOD")
FINAL_GO_DIRECTIVE=$(jq -r '.Go // ""' <<< "$FINAL_GO_MOD_JSON")
FINAL_TOOLCHAIN_DIRECTIVE=$(jq -r '.Toolchain // ""' <<< "$FINAL_GO_MOD_JSON")
FINAL_GO=$(jq -r '.Go // ""' <<< "$FINAL_GO_MOD_JSON")
FINAL_TC=$(jq -r '.Toolchain // ""' <<< "$FINAL_GO_MOD_JSON")
if [[ -n "$FINAL_TOOLCHAIN_DIRECTIVE" ]]; then
PR_BODY=$(cat <<EOF
This PR updates Go to the latest stable release.
* **go directive:** \`$FINAL_GO_DIRECTIVE\`
* **toolchain:** \`$FINAL_TOOLCHAIN_DIRECTIVE\`
EOF
)
# Build PR body reflecting final state after tidy
if [[ -n "$FINAL_TC" ]]; then
TC_LINE="* **toolchain:** \`$FINAL_TC\`"
else
PR_BODY=$(cat <<EOF
TC_LINE="* **toolchain:** _(none -- \`go mod tidy\` removed it because the go directive already implies go$TOOLCHAIN_VERSION)_"
fi
PR_BODY=$(cat <<EOF
This PR updates Go to the latest stable release.
* **go directive:** \`$FINAL_GO_DIRECTIVE\`
* **toolchain:** (not specified - go version matches latest toolchain \`$TOOLCHAIN_VERSION\`)
* **go directive:** \`$FINAL_GO\`
$TC_LINE
EOF
)
fi
git push -u origin "$BRANCH"