From fa76ea2fe1855a4988fcb47a09926526ce44db16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20de=20la=20Pe=C3=B1a?= Date: Fri, 8 May 2026 17:47:20 +0200 Subject: [PATCH 1/2] fix(release): consistency when releasing modules that update in-repo modules --- .github/scripts/check-pre-release.sh | 82 ----------- .github/scripts/common.sh | 67 ++++++++- .github/scripts/prepare-release-pr.sh | 190 ++++++++++++++++---------- .github/scripts/release.sh | 168 ----------------------- .github/workflows/release.yml | 53 ++----- Makefile | 15 -- RELEASING.md | 46 ++++--- commons-test.mk | 26 ---- 8 files changed, 214 insertions(+), 433 deletions(-) delete mode 100755 .github/scripts/check-pre-release.sh delete mode 100755 .github/scripts/release.sh diff --git a/.github/scripts/check-pre-release.sh b/.github/scripts/check-pre-release.sh deleted file mode 100755 index 00724a4b..00000000 --- a/.github/scripts/check-pre-release.sh +++ /dev/null @@ -1,82 +0,0 @@ -#!/bin/bash - -# ============================================================================= -# Pre-Release Check Script -# ============================================================================= -# Description: Verifies that pre-release was completed successfully for a module -# by checking if the next-tag file exists and matches version.go -# -# Usage: ./.github/scripts/check-pre-release.sh -# -# Arguments: -# module - Name of the module to check (required) -# Examples: client, container, config, context, image, network -# -# Exit Codes: -# 0 - Check passed -# 1 - Check failed (missing files or version mismatch) -# -# Examples: -# ./.github/scripts/check-pre-release.sh client -# ./.github/scripts/check-pre-release.sh container -# -# Dependencies: -# - grep (for parsing version.go) -# -# Files Checked: -# - .github/scripts/.build/-next-tag - Pre-release version file -# - /version.go - Current version file -# -# ============================================================================= - -set -e - -# Source common functions -readonly SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -source "${SCRIPT_DIR}/common.sh" - -# Get module name from argument and lowercase it -readonly MODULE=$(echo "${1:-}" | tr '[:upper:]' '[:lower:]') - -if [[ -z "$MODULE" ]]; then - echo "Error: Module name is required" - echo "Usage: $0 " - echo "Example: $0 client" - exit 1 -fi - -echo "Checking if pre-release was completed for module: ${MODULE}" - -# Check if next-tag file exists -readonly BUILD_FILE="${BUILD_DIR}/${MODULE}-next-tag" -if [[ ! -f "${BUILD_FILE}" ]]; then - echo "Error: Missing build file for module '${MODULE}' at ${BUILD_FILE}" - echo "Please run 'make pre-release-all' or 'make pre-release' first (with DRY_RUN=false)" - exit 1 -fi - -# Read next version from build file -readonly NEXT_VERSION=$(cat "${BUILD_FILE}" | tr -d '\n') -readonly NEXT_VERSION_NO_V="${NEXT_VERSION#v}" - -# Check if version.go exists -readonly VERSION_FILE="${ROOT_DIR}/${MODULE}/version.go" -if [[ ! -f "${VERSION_FILE}" ]]; then - echo "Error: version.go not found at ${VERSION_FILE}" - exit 1 -fi - -# Read current version from version.go -readonly CURRENT_VERSION=$(get_version_from_file "${VERSION_FILE}") - -# Compare versions -if [[ "${CURRENT_VERSION}" != "${NEXT_VERSION_NO_V}" ]]; then - echo "Error: Version mismatch for module '${MODULE}'" - echo " Expected (from ${BUILD_FILE}): ${NEXT_VERSION_NO_V}" - echo " Actual (from ${VERSION_FILE}): ${CURRENT_VERSION}" - echo "Please run 'make pre-release-all' or 'make pre-release' again (with DRY_RUN=false)" - exit 1 -fi - -echo "✅ Pre-release check passed for module: ${MODULE} (version: ${NEXT_VERSION_NO_V})" -exit 0 diff --git a/.github/scripts/common.sh b/.github/scripts/common.sh index c296199b..d71f0c2e 100755 --- a/.github/scripts/common.sh +++ b/.github/scripts/common.sh @@ -10,12 +10,14 @@ # When true, commands are echoed instead of executed # # Functions: -# curlGolangProxy - Trigger Go proxy to fetch module (for publishing) -# execute_or_echo - Execute command or echo based on DRY_RUN setting -# find_latest_tag - Find latest tag for a given module -# get_modules - Get list of modules from go.work file -# get_script_dir - Get directory of the calling script -# portable_sed - Portable in-place sed editing +# curlGolangProxy - Trigger Go proxy to fetch module (for publishing) +# execute_or_echo - Execute command or echo based on DRY_RUN setting +# find_latest_tag - Find latest tag for a given module +# get_modules - Get list of modules from go.work file +# get_modules_to_release - Expand a module to itself + transitive in-repo consumers +# get_script_dir - Get directory of the calling script +# portable_sed - Portable in-place sed editing +# validate_git_remote - Verify origin points to docker/go-sdk # # Constants: # ROOT_DIR - Root directory of the repository @@ -41,7 +43,18 @@ readonly BUILD_DIR="${ROOT_DIR}/.github/scripts/.build" readonly GITHUB_REPO="github.com/docker/go-sdk" readonly EXPECTED_ORIGIN_SSH="git@github.com:docker/go-sdk.git" readonly EXPECTED_ORIGIN_HTTPS="https://${GITHUB_REPO}.git" -readonly DRY_RUN="${DRY_RUN:-true}" + +# Normalize DRY_RUN: only the literal string "false" (any case) opts into a +# real run. Everything else — typos like "True", "FALSE", "no", or unset — +# stays in dry-run. This biases the safety default toward not making changes, +# so a typo in the OFF case can't accidentally trigger a real release. +# Export so the canonical value propagates to subprocess invocations. +case "$(echo "${DRY_RUN:-true}" | tr '[:upper:]' '[:lower:]')" in + false) DRY_RUN="false" ;; + *) DRY_RUN="true" ;; +esac +export DRY_RUN +readonly DRY_RUN # This function is used to trigger the Go proxy to fetch the module. # See https://pkg.go.dev/about#adding-a-package for more details. @@ -111,6 +124,46 @@ get_modules() { go work edit -json | jq -r '.Use[] | "\(.DiskPath | ltrimstr("./"))"' | tr '\n' ' ' && echo } +# Compute the set of modules that must be released together. +# +# When invoked without arguments, returns every module in go.work. +# +# When invoked with a single module name, returns that module plus every +# in-repo module that requires it (transitively). This is required because +# pre-release.sh rewrites the go.mod of every module that depends on the +# released one, but only bumps version.go for the released module itself. +# Without this expansion, consumer modules end up with rewritten go.mod +# content under main while their existing tag still references the old +# dependency version — leaving "main" inconsistent with the latest tag. +get_modules_to_release() { + local requested="${1:-}" + local all_modules + all_modules=$(get_modules) + + if [[ -z "${requested}" ]]; then + echo "${all_modules}" + return + fi + + local to_release="${requested}" + local added=1 + while [[ ${added} -eq 1 ]]; do + added=0 + for m in ${all_modules}; do + case " ${to_release} " in *" ${m} "*) continue ;; esac + for dep in ${to_release}; do + if grep -qE "${GITHUB_REPO}/${dep} v" "${ROOT_DIR}/${m}/go.mod" 2>/dev/null; then + to_release="${to_release} ${m}" + added=1 + break + fi + done + done + done + + echo "${to_release}" +} + # Function to find latest tag for a module find_latest_tag() { local module="$1" diff --git a/.github/scripts/prepare-release-pr.sh b/.github/scripts/prepare-release-pr.sh index 89137169..438e8164 100755 --- a/.github/scripts/prepare-release-pr.sh +++ b/.github/scripts/prepare-release-pr.sh @@ -15,11 +15,15 @@ # # Environment Variables: # BUMP_TYPE - Type of version bump (default: prerelease) +# DRY_RUN - Enable dry run mode (default: true) +# When true, expansion + version preview run but no branch +# is created and no commit/push/PR happens. Set to "false" +# to actually create the release PR. # # Dependencies: # - git (configured with push permissions, origin must point to docker/go-sdk) # - go (for go.work parsing and go mod tidy) -# - gh (GitHub CLI, for creating PRs) +# - gh (GitHub CLI, for creating PRs; only required when DRY_RUN=false) # - jq (for parsing go.work) # - Docker (for semver-tool, used by pre-release.sh) # @@ -27,109 +31,153 @@ set -eo pipefail -# Source common functions readonly SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" source "${SCRIPT_DIR}/common.sh" -# Validate git remote before doing anything -validate_git_remote - MODULE=$(echo "${1:-}" | tr '[:upper:]' '[:lower:]') BUMP_TYPE="${BUMP_TYPE:-prerelease}" TIMESTAMP="$(date +%Y%m%d%H%M%S)" -# Determine branch name and commit title +# Always validate the module name (cheap, surfaces typos in both run modes). +# Two-step validation: +# 1. Format check: must be a single token of [a-z0-9-]+ — guards against +# whitespace (grep -F treats embedded newlines in the pattern as alternation, +# which would silently make a newline-separated MODULE match any of its parts) +# and against regex/glob metacharacters. +# 2. Existence check: grep -Fxq for exact-line, fixed-string matching against +# the modules listed in go.work. if [[ -n "${MODULE}" ]]; then - BRANCH_NAME="release/bump-${MODULE}-${TIMESTAMP}" - COMMIT_TITLE="chore(${MODULE}): bump version" -else - BRANCH_NAME="release/bump-versions-${TIMESTAMP}" - COMMIT_TITLE="chore(release): bump module versions" -fi + if ! [[ "${MODULE}" =~ ^[a-z][a-z0-9-]*$ ]]; then + echo "❌ Error: Module name must match [a-z][a-z0-9-]* — got '${MODULE}'" + exit 1 + fi -# Ensure we start from a clean, up-to-date main branch -CURRENT_BRANCH=$(git -C "${ROOT_DIR}" rev-parse --abbrev-ref HEAD) -if [[ "${CURRENT_BRANCH}" != "main" ]]; then - echo "❌ Error: Must be on the 'main' branch to create a release PR" - echo " Current branch: ${CURRENT_BRANCH}" - echo "" - echo "Switch to main first:" - echo " git checkout main" - exit 1 + ALL_MODS=$(get_modules) + if ! printf '%s\n' ${ALL_MODS} | grep -Fxq -- "${MODULE}"; then + echo "❌ Error: Module '${MODULE}' not found in go.work" + echo "" + echo "Available modules:" + for m in ${ALL_MODS}; do echo " - ${m}"; done + exit 1 + fi fi -if [[ -n "$(git -C "${ROOT_DIR}" status --porcelain)" ]]; then - echo "❌ Error: Working tree is not clean" - echo " Commit or stash your changes before running a release." - exit 1 +# Real-run pre-flight: origin must be docker/go-sdk, working tree must be a +# clean main that's in sync with origin. Dry runs skip these so the preview +# works from any branch, fork, or detached state — handy for contributors +# who haven't set their origin to docker/go-sdk. +if [[ "${DRY_RUN}" != "true" ]]; then + validate_git_remote + + CURRENT_BRANCH=$(git -C "${ROOT_DIR}" rev-parse --abbrev-ref HEAD) + if [[ "${CURRENT_BRANCH}" != "main" ]]; then + echo "❌ Error: Must be on the 'main' branch to create a release PR" + echo " Current branch: ${CURRENT_BRANCH}" + echo "" + echo "Switch to main first:" + echo " git checkout main" + exit 1 + fi + + if [[ -n "$(git -C "${ROOT_DIR}" status --porcelain)" ]]; then + echo "❌ Error: Working tree is not clean" + echo " Commit or stash your changes before running a release." + exit 1 + fi + + echo "Fetching latest from origin..." + git -C "${ROOT_DIR}" fetch origin main + LOCAL_SHA=$(git -C "${ROOT_DIR}" rev-parse HEAD) + REMOTE_SHA=$(git -C "${ROOT_DIR}" rev-parse origin/main) + if [[ "${LOCAL_SHA}" != "${REMOTE_SHA}" ]]; then + echo "❌ Error: Local main is not up to date with origin/main" + echo " Local: ${LOCAL_SHA}" + echo " Remote: ${REMOTE_SHA}" + echo "" + echo "Update your local main first:" + echo " git pull origin main" + exit 1 + fi fi -echo "Fetching latest from origin..." -git -C "${ROOT_DIR}" fetch origin main -LOCAL_SHA=$(git -C "${ROOT_DIR}" rev-parse HEAD) -REMOTE_SHA=$(git -C "${ROOT_DIR}" rev-parse origin/main) -if [[ "${LOCAL_SHA}" != "${REMOTE_SHA}" ]]; then - echo "❌ Error: Local main is not up to date with origin/main" - echo " Local: ${LOCAL_SHA}" - echo " Remote: ${REMOTE_SHA}" - echo "" - echo "Update your local main first:" - echo " git pull origin main" - exit 1 +# Compute the modules to release. When releasing a single module, this also +# includes any in-repo module that requires it (transitively) — pre-release.sh +# already rewrites their go.mod, but we must also bump their version.go and +# tag them so main never drifts from the latest published tag. +MODULES_TO_RELEASE=$(get_modules_to_release "${MODULE}") +NUM_MODULES_TO_RELEASE=$(echo "${MODULES_TO_RELEASE}" | wc -w | tr -d ' ') + +# Determine branch name and commit title. +# A single-module input that fans out to multiple modules switches to the +# release-wide title so Phase 2's commit-message check still matches. +if [[ -n "${MODULE}" && "${NUM_MODULES_TO_RELEASE}" -eq 1 ]]; then + BRANCH_NAME="release/bump-${MODULE}-${TIMESTAMP}" + COMMIT_TITLE="chore(${MODULE}): bump version" +else + BRANCH_NAME="release/bump-versions-${TIMESTAMP}" + COMMIT_TITLE="chore(release): bump module versions" fi echo "=== Phase 1: Prepare Release PR ===" echo " Module: ${MODULE:-all}" echo " Bump type: ${BUMP_TYPE}" echo " Branch: ${BRANCH_NAME}" +echo " Modules to release: ${MODULES_TO_RELEASE}" +echo " Dry run: ${DRY_RUN}" echo "" -# Create release branch from up-to-date main -git checkout -b "${BRANCH_NAME}" +# Real run creates the release branch up front; dry run stays on main and +# never writes to the working tree (pre-release.sh is invoked with DRY_RUN=true). +if [[ "${DRY_RUN}" != "true" ]]; then + git checkout -b "${BRANCH_NAME}" +fi -# Clean build directory +# Clean build directory so .build/-next-tag reflects this run only rm -rf "${BUILD_DIR}" mkdir -p "${BUILD_DIR}" -# Run pre-release for target module(s) -if [[ -n "${MODULE}" ]]; then - echo "Running pre-release for module: ${MODULE}" - env DRY_RUN=false BUMP_TYPE="${BUMP_TYPE}" "${SCRIPT_DIR}/pre-release.sh" "${MODULE}" -else - echo "Running pre-release for all modules..." - MODULES=$(get_modules) - for m in $MODULES; do - echo "" - echo "--- Pre-releasing module: ${m} ---" - env DRY_RUN=false BUMP_TYPE="${BUMP_TYPE}" "${SCRIPT_DIR}/pre-release.sh" "${m}" - done -fi - -# Get all modules for staging -ALL_MODULES=$(get_modules) - -# Determine which modules to include in version summary -if [[ -n "${MODULE}" ]]; then - MODULES_TO_TAG="${MODULE}" -else - MODULES_TO_TAG="${ALL_MODULES}" -fi +# Run pre-release for each module in the release set, propagating DRY_RUN +for m in ${MODULES_TO_RELEASE}; do + echo "" + echo "--- Pre-releasing module: ${m} ---" + env DRY_RUN="${DRY_RUN}" BUMP_TYPE="${BUMP_TYPE}" "${SCRIPT_DIR}/pre-release.sh" "${m}" +done -# Stage version.go files for released modules and build commit body +# Build the version-summary commit body from the next-tag files that +# pre-release.sh wrote (these are produced in both dry and real runs). commit_body="" -for m in $MODULES_TO_TAG; do +for m in ${MODULES_TO_RELEASE}; do next_tag_path=$(get_next_tag "${m}") if [[ ! -f "${next_tag_path}" ]]; then echo "Skipping ${m} because the pre-release script did not run" continue fi - - git add "${ROOT_DIR}/${m}/version.go" nextTag=$(cat "${next_tag_path}") commit_body="${commit_body}\n - ${m}: ${nextTag}" done -# Stage go.mod and go.sum for ALL modules +if [[ "${DRY_RUN}" == "true" ]]; then + echo "" + echo "=== Dry Run Summary ===" + echo -e "${commit_body}" + echo "" + echo "✅ Dry run completed. No git commits, pushes, or pull requests were created." + echo "To create the release PR, re-run with DRY_RUN=false." + exit 0 +fi + +# Real run: stage files, commit, push, open PR. + +# Stage version.go for each released module +for m in ${MODULES_TO_RELEASE}; do + next_tag_path=$(get_next_tag "${m}") + if [[ -f "${next_tag_path}" ]]; then + git add "${ROOT_DIR}/${m}/version.go" + fi +done + +# Stage go.mod and go.sum for ALL modules (pre-release.sh may have rewritten them) +ALL_MODULES=$(get_modules) for m in $ALL_MODULES; do git add "${ROOT_DIR}/${m}/go.mod" if [[ -f "${ROOT_DIR}/${m}/go.sum" ]]; then @@ -137,19 +185,14 @@ for m in $ALL_MODULES; do fi done -# Verify there are staged changes if [[ -z "$(git diff --cached)" ]]; then echo "No changes detected. Aborting." exit 1 fi -# Commit git commit -m "${COMMIT_TITLE}" -m "$(echo -e "${commit_body}")" - -# Push the branch git push origin "${BRANCH_NAME}" -# Build PR body PR_BODY="## Release Version Bump **Bump type**: \`${BUMP_TYPE}\` @@ -161,7 +204,6 @@ $(echo -e "${commit_body}") This PR was created automatically by the release workflow. Merging this PR will trigger Phase 2 (automatic tagging and Go proxy update)." -# Create PR with gh PR_URL=$(gh pr create \ --title "${COMMIT_TITLE}" \ --body "${PR_BODY}" \ diff --git a/.github/scripts/release.sh b/.github/scripts/release.sh deleted file mode 100755 index d80eeec2..00000000 --- a/.github/scripts/release.sh +++ /dev/null @@ -1,168 +0,0 @@ -#!/bin/bash - -# ============================================================================= -# Release Committer -# ============================================================================= -# Description: Stages and commits version changes for modules. -# This script is typically run after pre-release.sh has -# updated module versions. It creates a local commit only — -# pushing, tagging, and Go proxy notification are handled by -# prepare-release-pr.sh (Phase 1) and tag-release.sh (Phase 2). -# -# Usage: ./.github/scripts/release.sh [module] -# -# Arguments: -# module - Name of specific module to release (optional) -# If not provided, releases all modules with prepared versions -# -# Environment Variables: -# DRY_RUN - Enable dry run mode (default: true) -# When true, shows what would be committed without actually doing it -# -# Examples: -# ./.github/scripts/release.sh -# ./.github/scripts/release.sh container -# DRY_RUN=false ./.github/scripts/release.sh -# DRY_RUN=false ./.github/scripts/release.sh container -# -# Dependencies: -# - git (configured with push permissions) -# - go (for go.work parsing via 'go work edit -json') -# - jq (for parsing go.work) -# -# Git Operations: -# - Adds all modified version.go and go.mod files -# - Creates commit with version bump message (e.g. chore(client): bump version) -# -# Note: This script no longer pushes to main, creates tags, or triggers the -# Go proxy. Those operations are handled by the two-phase release process: -# - Phase 1: prepare-release-pr.sh (creates a PR) -# - Phase 2: tag-release.sh (tags after PR merge) -# -# ============================================================================= - -set -e - -# Source common functions -readonly SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -source "${SCRIPT_DIR}/common.sh" - -# Validate git remote before doing anything -validate_git_remote - -MODULE="${1:-}" - -# Collect and stage changes across modules, then create a single commit -if [[ -n "${MODULE}" ]]; then - # Single module release - echo "Releasing single module: ${MODULE}" - commit_title="chore(${MODULE}): bump version" -else - # All modules release - echo "Releasing all modules with prepared versions" - commit_title="chore(release): bump module versions" -fi - -# Get all modules for staging go.mod changes -ALL_MODULES=$(get_modules) - -commit_body="" - -# Determine which modules to process -if [[ -n "${MODULE}" ]]; then - MODULES_TO_TAG="${MODULE}" -else - MODULES_TO_TAG="${ALL_MODULES}" -fi - -# Stage version.go and collect tag information only for modules being released. -# Note: Only version.go files for modules being released are staged here. -# go.mod and go.sum files for all modules are staged separately below. -for m in $MODULES_TO_TAG; do - next_tag_path=$(get_next_tag "${m}") - # if the module version file does not exist, skip it - if [[ ! -f "${next_tag_path}" ]]; then - echo "Skipping ${m} because the pre-release script did not run" - continue - fi - - execute_or_echo git add "${ROOT_DIR}/${m}/version.go" - - nextTag=$(cat "${next_tag_path}") - echo "Next tag for ${m}: ${nextTag}" - commit_body="${commit_body}\n - ${m}: ${nextTag}" -done - -# Stage go.mod and go.sum for ALL modules (they all need to reference the new version) -for m in $ALL_MODULES; do - execute_or_echo git add "${ROOT_DIR}/${m}/go.mod" - if [[ -f "${ROOT_DIR}/${m}/go.sum" ]]; then - execute_or_echo git add "${ROOT_DIR}/${m}/go.sum" - fi -done - -if [[ "${DRY_RUN}" == "true" ]]; then - echo "" - echo "==========================================" - echo "DRY RUN MODE - No git changes will be made" - echo "==========================================" - echo "" - echo "Would create commit (local only, no push):" - echo " Title: ${commit_title}" - echo " Body: $(echo -e "${commit_body}")" - echo "" - echo "Files that would be committed:" - for m in $MODULES_TO_TAG; do - next_tag_path=$(get_next_tag "${m}") - if [[ -f "${next_tag_path}" ]]; then - echo " ${m}/version.go" - fi - done - for m in $ALL_MODULES; do - echo " ${m}/go.mod" - if [[ -f "${ROOT_DIR}/${m}/go.sum" ]]; then - echo " ${m}/go.sum" - fi - done - echo "" - echo "Changes in module files:" - for m in $ALL_MODULES; do - echo "" - echo "--- ${m}/... ---" - git --no-pager diff "${ROOT_DIR}/${m}" || echo " (new file)" - done - echo "" - echo "==========================================" - echo "NOTE: This script only creates a local commit." - echo "Tags and pushing are handled by the two-phase release process." - echo "See RELEASING.md for details." - echo "" - echo "To perform the actual commit, run:" - echo " DRY_RUN=false $0 $@" - echo "==========================================" - exit 0 -fi - -# Create a single commit if there are staged changes -if [[ -n "$(git diff --cached)" ]]; then - execute_or_echo git commit -m "${commit_title}" -m "$(echo -e "${commit_body}")" -else - echo "No changes detected in modules. Release process aborted." - exit 1 # exit with error code 1 to not proceed with the release -fi - -echo "" -echo "✅ Created commit successfully" -echo "Last commit:" -git_log_format='%C(auto)%h%C(reset) %s%nAuthor: %an <%ae>%nDate: %ad' -execute_or_echo git -C "${ROOT_DIR}" --no-pager log -1 --pretty=format:"${git_log_format}" --date=iso-local -echo "" - -echo "" -echo "==========================================" -echo "NOTE: This script no longer pushes directly to main or creates tags." -echo "Use the two-phase release process instead:" -echo " Phase 1: ./.github/scripts/prepare-release-pr.sh — creates a release PR" -echo " Phase 2: ./.github/scripts/tag-release.sh — auto-tags after PR merge" -echo "See RELEASING.md for details." -echo "==========================================" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b82e89bd..ae0bf122 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -61,60 +61,29 @@ jobs: git config --global user.email "github-actions[bot]@users.noreply.github.com" - name: Display run configuration + env: + # Route the freeform string input through env to prevent expression + # injection. inputs.dry_run (boolean) and inputs.bump_type (choice) + # are constrained types and safe to interpolate inline. + INPUT_MODULE: ${{ inputs.module }} run: | echo "🚀 Release Configuration:" - echo " - Module: ${{ inputs.module || 'all' }}" + echo " - Module: ${INPUT_MODULE:-all}" echo " - Dry Run: ${{ inputs.dry_run }}" echo " - Bump Type: ${{ inputs.bump_type }}" echo " - Repository: ${{ github.repository }}" echo " - Branch: ${{ github.ref_name }}" - - name: Normalize and validate module - id: module - run: | - # Normalize module name to lowercase - MODULE=$(echo "${{ inputs.module }}" | tr '[:upper:]' '[:lower:]') - echo "name=${MODULE}" >> "$GITHUB_OUTPUT" - - if [[ -n "${MODULE}" ]]; then - AVAILABLE_MODULES=$(go work edit -json | jq -r '.Use[].DiskPath' | sed 's|^\./||') - if ! echo "$AVAILABLE_MODULES" | grep -Fxq "${MODULE}"; then - echo "❌ Error: Module '${MODULE}' not found in go.work" - echo "" - echo "Available modules:" - echo "$AVAILABLE_MODULES" | sed 's/^/ - /' - exit 1 - fi - echo "✅ Module '${MODULE}' is valid" - fi - - - name: Dry run preview - if: ${{ inputs.dry_run }} - env: - DRY_RUN: "true" - BUMP_TYPE: ${{ inputs.bump_type }} - run: | - echo "=== Dry Run Preview ===" - if [[ -n "${{ steps.module.outputs.name }}" ]]; then - ./.github/scripts/pre-release.sh "${{ steps.module.outputs.name }}" - else - make pre-release-all - fi - echo "" - echo "✅ Dry run completed. No git commits, pushes, or pull requests were created." - echo "To create a release PR, re-run with dry_run: false" - - name: Prepare release PR - if: ${{ !inputs.dry_run }} env: + DRY_RUN: ${{ inputs.dry_run }} BUMP_TYPE: ${{ inputs.bump_type }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + INPUT_MODULE: ${{ inputs.module }} run: | - if [[ -n "${{ steps.module.outputs.name }}" ]]; then - ./.github/scripts/prepare-release-pr.sh "${{ steps.module.outputs.name }}" - else - ./.github/scripts/prepare-release-pr.sh - fi + # The script handles lowercasing and module validation itself, and treats + # an empty argument the same as no argument (release all modules). + ./.github/scripts/prepare-release-pr.sh "${INPUT_MODULE}" # Phase 2: Auto-tag after release PR is merged tag-release: diff --git a/Makefile b/Makefile index e35dc5af..c5285e45 100644 --- a/Makefile +++ b/Makefile @@ -15,21 +15,6 @@ tidy-all: @echo "Running tidy in all modules..." $(call for-all-modules,go mod tidy) -clean-build-dir: - @echo "Cleaning build directory..." - @rm -rf .github/scripts/.build - @mkdir -p .github/scripts/.build - -# Pre-release version for all modules -pre-release-all: clean-build-dir - @echo "Preparing releasing versions for all modules..." - $(call for-all-modules,make pre-release) - -# Release version for all modules. It must be run after pre-release-all. -release-all: - $(call for-all-modules,make check-pre-release) - @./.github/scripts/release.sh - # Tag release for all modules (Phase 2 of release process) tag-release: @./.github/scripts/tag-release.sh diff --git a/RELEASING.md b/RELEASING.md index 51d2b9bf..bb7a8336 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -50,33 +50,41 @@ Dry Run: false The module name is validated against the modules in `go.work`. -### Local Dry Run Preview +#### In-repo consumers are bumped together -Always start with a dry run. This does not require origin to point to `docker/go-sdk` — it only previews version changes without any git operations: +When the requested module is a dependency of other modules in this repository, **those consumer modules are bumped in the same PR** (transitively). For example, requesting a release of `image` will also bump and tag `container`, because `container/go.mod` requires `image`. -```bash -# Preview version changes for all modules -DRY_RUN=true make pre-release-all +This is required for consistency: `pre-release.sh` rewrites the `go.mod` of every consumer to reference the new dependency version. If those consumers were not also bumped, `main` would carry rewritten `go.mod` content while their existing tag still pointed at the old dependency — producing two different module contents under the same version string. -# Preview for a specific module -cd container -DRY_RUN=true make pre-release -``` +When the expansion pulls in additional modules, the PR title switches from `chore(): bump version` to `chore(release): bump module versions` so Phase 2's commit-message check still recognizes it as a release commit. The PR body lists every bumped module. + +Modules with no in-repo consumers (e.g., `container`, `volume`, `legacyadapters`) release as a single-module bump with no fan-out. -### Running Phase 1 Locally +### Running Phase 1 locally -After reviewing the dry run output, you can run the release PR script directly from your machine. Your `origin` remote **must** point to `docker/go-sdk`: +`prepare-release-pr.sh` is the single entry point for both previewing and creating a release. It defaults to `DRY_RUN=true` — opt in with `DRY_RUN=false` to actually create the PR. ```bash -BUMP_TYPE=prerelease ./.github/scripts/prepare-release-pr.sh # all modules -BUMP_TYPE=prerelease ./.github/scripts/prepare-release-pr.sh client # single module +# Preview (default) — works on any branch, any fork, no origin setup required. +./.github/scripts/prepare-release-pr.sh client # one module + its consumers +./.github/scripts/prepare-release-pr.sh # all modules + +# Real run — requires origin to point to docker/go-sdk and a clean main. +DRY_RUN=false ./.github/scripts/prepare-release-pr.sh client +DRY_RUN=false ./.github/scripts/prepare-release-pr.sh + +# Different bump types: +BUMP_TYPE=patch DRY_RUN=false ./.github/scripts/prepare-release-pr.sh client ``` -The script will: -1. Validate that `origin` points to `docker/go-sdk` (fails with instructions if not) -2. Verify you're on `main` with a clean working tree -3. Fetch `origin/main` and verify your local branch is up to date -4. Create a release branch, bump versions, commit, push, and open a PR +The script: + +1. Validates the requested module exists in `go.work`. +2. **Real run only** — validates `origin` points to `docker/go-sdk`, verifies you're on `main` with a clean working tree, and fetches `origin/main` to confirm you're up to date. +3. Computes the modules to release (the requested module plus its in-repo consumers). +4. Runs `pre-release.sh` for each module. +5. **Dry run** — prints a version summary and exits. +6. **Real run** — creates a release branch, commits, pushes, and opens a PR. ## Phase 2: Automatic Tagging @@ -171,7 +179,7 @@ If tags were pushed but `main` doesn't contain the version bump commit: ### Origin Remote Points to a Fork -Both `prepare-release-pr.sh` and `tag-release.sh` validate that `origin` points to `docker/go-sdk`. If you see: +`tag-release.sh` always — and `prepare-release-pr.sh` when run with `DRY_RUN=false` — validate that `origin` points to `docker/go-sdk`. If you see: ``` ❌ Error: Git remote 'origin' points to the wrong repository diff --git a/commons-test.mk b/commons-test.mk index 748da176..5d760168 100644 --- a/commons-test.mk +++ b/commons-test.mk @@ -77,32 +77,6 @@ dependencies-scan: # ------------------------------------------------------------------------------ # Release # ------------------------------------------------------------------------------ -.PHONY: pre-release -pre-release: - @if [ -z "$(MODULE_DIR)" ]; then \ - echo "Usage: make pre-release, from one of the module directories (e.g. make pre-release from client/ directory)"; \ - exit 1; \ - fi - @echo "Releasing version for module: $(MODULE_DIR)" - @$(ROOT_DIR)/.github/scripts/pre-release.sh "$(MODULE_DIR)" - -.PHONY: check-pre-release -check-pre-release: - @if [ -z "$(MODULE_DIR)" ]; then \ - echo "Usage: make check-pre-release, from one of the module directories (e.g. make check-pre-release from client/ directory)"; \ - exit 1; \ - fi - @$(ROOT_DIR)/.github/scripts/check-pre-release.sh "$(MODULE_DIR)" - -.PHONY: release -release: - @if [ -z "$(MODULE_DIR)" ]; then \ - echo "Usage: make release, from one of the module directories (e.g. make release from client/ directory)"; \ - exit 1; \ - fi - @echo "Finalizing release for module: $(MODULE_DIR)" - @$(ROOT_DIR)/.github/scripts/release.sh "$(MODULE_DIR)" - .PHONY: tag-release tag-release: @if [ -z "$(MODULE_DIR)" ]; then \ From 7e63317a08fcf9dc6861d60beefd6bcaf488b7ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20de=20la=20Pe=C3=B1a?= Date: Fri, 8 May 2026 18:06:42 +0200 Subject: [PATCH 2/2] fix(release): skip go mod tidy in pre-release.sh dry-run go mod tidy was running unconditionally in the per-module loop, which could mutate go.mod/go.sum (checksums, unused requires, new imports) even though portable_sed was a no-op in dry-run. Once prepare-release-pr.sh advertised dry-run as side-effect free, this became a real regression. Guard tidy with the same DRY_RUN check the version.go write already uses, so the dry-run path leaves the working tree untouched. Co-Authored-By: Claude Opus 4.7 --- .github/scripts/pre-release.sh | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/scripts/pre-release.sh b/.github/scripts/pre-release.sh index 3c59870e..a9cbc108 100755 --- a/.github/scripts/pre-release.sh +++ b/.github/scripts/pre-release.sh @@ -146,6 +146,11 @@ echo "${NEXT_vVERSION}" > "$(get_next_tag "${MODULE}")" for m in $MODULES; do portable_sed "s|${GITHUB_REPO}/${MODULE} v[^[:space:]]*|${GITHUB_REPO}/${MODULE} v${NEXT_VERSION}|g" "${ROOT_DIR}/${m}/go.mod" - # Update the go.sum file - (cd "${ROOT_DIR}/${m}" && go mod tidy) + # Update the go.sum file. Skip in dry-run: portable_sed was a no-op, so + # go.mod is unchanged and there's nothing for tidy to reconcile — and tidy + # itself can mutate go.mod/go.sum (checksums, unused requires, new imports), + # which would break the dry-run "no working-tree changes" guarantee. + if [[ "${DRY_RUN}" != "true" ]]; then + (cd "${ROOT_DIR}/${m}" && go mod tidy) + fi done