Skip to content

Fix go.sum handling to avoid errors when file isn't modified #18

Fix go.sum handling to avoid errors when file isn't modified

Fix go.sum handling to avoid errors when file isn't modified #18

Workflow file for this run

name: Module Release
run-name: Module Release for ${{ inputs.module || github.event.inputs.module }} - ${{ inputs.releaseType || github.event.inputs.releaseType }}
on:
workflow_dispatch:
inputs:
module:
description: 'Module to release (select from dropdown)'
required: true
type: choice
options:
- auth
- cache
- chimux
- database
- eventbus
- httpclient
- httpserver
- jsonschema
- letsencrypt
- logmasker
- reverseproxy
- scheduler
version:
description: 'Version to release (leave blank for auto-increment)'
required: false
type: string
releaseType:
description: 'Release type'
required: true
type: choice
options:
- patch
- minor
- major
default: 'patch'
workflow_call:
inputs:
module:
description: 'Module to release'
required: true
type: string
version:
description: 'Version to release (leave blank for auto-increment)'
required: false
type: string
releaseType:
description: 'Release type'
required: true
type: string
jobs:
prepare-release:
runs-on: ubuntu-latest
outputs:
modules: ${{ steps.get-modules.outputs.modules }}
steps:
- name: Checkout code
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Detect module changes since last tag
id: detect
shell: bash
run: |
set -euo pipefail
MODULE="${{ inputs.module || github.event.inputs.module }}"
if [ -z "$MODULE" ]; then
echo "No module input provided (prepare phase); skipping detection output."; echo 'changed=false' >> $GITHUB_OUTPUT; exit 0; fi
LATEST_TAG=$(git tag -l "modules/${MODULE}/v*" | sort -V | tail -n1 || echo '')
echo "Latest tag for ${MODULE}: ${LATEST_TAG:-<none>}"
CHANGED=false
if [ -z "$LATEST_TAG" ]; then
# First release: treat as changed if any go or go.mod/go.sum files exist
FILE=$(find "modules/${MODULE}" -type f \( -name '*.go' -o -name 'go.mod' -o -name 'go.sum' \) | head -n1 || true)
[ -n "$FILE" ] && CHANGED=true || CHANGED=false
echo "No previous tag; initial existence implies changed? $CHANGED (sample: ${FILE:-none})"
else
DIFF=$(git diff --name-only ${LATEST_TAG}..HEAD -- "modules/${MODULE}/" || true)
echo "Raw changed files: ${DIFF:-<none>}"
RELEVANT=""
if [ -n "$DIFF" ]; then
while IFS= read -r f; do
[[ $f == *_test.go ]] && continue
[[ $f == *.md ]] && continue
if [[ $f == *.go ]] || [[ $f == */go.mod ]] || [[ $f == */go.sum ]]; then
RELEVANT+="$f "
fi
done <<< "$DIFF"
fi
[ -n "$RELEVANT" ] && CHANGED=true || CHANGED=false
echo "Relevant: ${RELEVANT:-<none>} => changed? $CHANGED"
fi
echo "changed=$CHANGED" >> $GITHUB_OUTPUT
- name: Get available modules
id: get-modules
run: |
MODULES=$(find modules -maxdepth 1 -mindepth 1 -type d -exec basename {} \; | jq -R . | jq -s .)
{
echo "modules<<EOF"
echo "$MODULES"
echo "EOF"
} >> $GITHUB_OUTPUT
echo "Available modules: $MODULES"
release-module:
needs: prepare-release
runs-on: ubuntu-latest
if: needs.prepare-release.outputs.modules && needs.prepare-release.result == 'success'
steps:
- name: Checkout code
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Skip if no changes
id: skipcheck
shell: bash
run: |
set -euo pipefail
# Replicate detection using prepare-release step output (can't directly access its step outputs except via job outputs; we didn't expose changed there to avoid altering callers). Re-run quick detection.
MODULE="${{ inputs.module || github.event.inputs.module }}"
LATEST_TAG=$(git tag -l "modules/${MODULE}/v*" | sort -V | tail -n1 || echo '')
CHANGED=false
if [ -z "$LATEST_TAG" ]; then
FILE=$(find "modules/${MODULE}" -type f \( -name '*.go' -o -name 'go.mod' -o -name 'go.sum' \) | head -n1 || true)
[ -n "$FILE" ] && CHANGED=true || CHANGED=false
else
DIFF=$(git diff --name-only ${LATEST_TAG}..HEAD -- "modules/${MODULE}/" || true)
RELEVANT=$(echo "$DIFF" | grep -Ev '(_test.go$|\.md$)' | grep -E '(\.go$|/go.mod$|/go.sum$)' || true)
[ -n "$RELEVANT" ] && CHANGED=true || CHANGED=false
fi
echo "changed=$CHANGED" >> $GITHUB_OUTPUT
if [ "$CHANGED" != true ]; then
echo "No relevant changes for module ${MODULE}; creating no-op output markers and exiting."; exit 0; fi
- name: Set up Go
if: steps.skipcheck.outputs.changed == 'true'
uses: actions/setup-go@v6
with:
go-version: '^1.25'
check-latest: true
- name: Build modcli
if: steps.skipcheck.outputs.changed == 'true'
run: |
cd cmd/modcli
go build -o modcli
- name: Determine release version (contract-aware)
if: steps.skipcheck.outputs.changed == 'true'
id: version
run: |
set -euo pipefail
MODULE="${{ inputs.module || github.event.inputs.module }}"
INPUT_RELEASE_TYPE='${{ inputs.releaseType || github.event.inputs.releaseType }}'
INPUT_MANUAL_VERSION='${{ inputs.version || github.event.inputs.version }}'
echo "Selected module: $MODULE"
echo "Requested releaseType: $INPUT_RELEASE_TYPE"
[ -n "$INPUT_MANUAL_VERSION" ] && echo "Manual version override: $INPUT_MANUAL_VERSION"
LATEST_TAG=$(git tag -l "modules/${MODULE}/v*" | sort -V | tail -n1 || echo "")
if [ -z "$LATEST_TAG" ]; then
BASE_VERSION="v0.0.0"; PREV_CONTRACT_REF=""; echo "No previous version found";
else
BASE_VERSION=${LATEST_TAG#modules/${MODULE}/}; PREV_CONTRACT_REF="$LATEST_TAG"; echo "Latest tag: $LATEST_TAG (base $BASE_VERSION)";
fi
mkdir -p artifacts/contracts/prev artifacts/contracts/current artifacts/diffs
# Extract current contract (must succeed)
echo "Extracting current contract for ${MODULE}..."
if ! ./cmd/modcli/modcli contract extract modules/${MODULE} -o artifacts/contracts/current/${MODULE}.json 2>&1; then
echo "ERROR: Failed to extract current contract for ${MODULE}"
exit 1
fi
echo "✓ Current contract extracted successfully"
# Extract previous contract if tag exists
if [ -n "$PREV_CONTRACT_REF" ]; then
echo "Extracting previous contract from ${PREV_CONTRACT_REF}..."
TMPDIR=$(mktemp -d)
trap "rm -rf '$TMPDIR'" EXIT
# Extract source from previous tag
git archive $PREV_CONTRACT_REF | tar -x -C "$TMPDIR"
# Copy current modcli binary to temp dir
mkdir -p "$TMPDIR/cmd/modcli" && cp cmd/modcli/modcli "$TMPDIR/cmd/modcli/modcli"
# Extract contract from previous version using fixed modcli
if (cd "$TMPDIR" && ./cmd/modcli/modcli contract extract modules/${MODULE} -o prev-contract.json 2>&1); then
mv "$TMPDIR/prev-contract.json" artifacts/contracts/prev/${MODULE}.json
echo "✓ Previous contract extracted from $PREV_CONTRACT_REF"
else
echo "⚠ WARNING: Failed to extract contract from $PREV_CONTRACT_REF - treating as new module"
fi
fi
CHANGE_CLASS="none"
DIFF_MD_PATH="artifacts/diffs/${MODULE}.md"
if [ -f artifacts/contracts/prev/${MODULE}.json ] && [ -f artifacts/contracts/current/${MODULE}.json ]; then
echo "Comparing contracts..."
if ./cmd/modcli/modcli contract compare artifacts/contracts/prev/${MODULE}.json artifacts/contracts/current/${MODULE}.json -o artifacts/diffs/${MODULE}.json --format=markdown > "$DIFF_MD_PATH" 2>&1; then
if [ -s "$DIFF_MD_PATH" ]; then
echo "✓ Additive changes detected (non-breaking)"
CHANGE_CLASS="minor"
else
echo "✓ No API changes detected"
CHANGE_CLASS="none"
fi
else
echo "⚠ Breaking changes detected!"
echo "Diff output:"
cat "$DIFF_MD_PATH" || true
CHANGE_CLASS="major"
[ -s "$DIFF_MD_PATH" ] || echo "⚠️ Breaking changes detected but diff unavailable" > "$DIFF_MD_PATH"
fi
else
# No previous contract - check if current has content
if [ -f artifacts/contracts/current/${MODULE}.json ] && [ $(wc -c < artifacts/contracts/current/${MODULE}.json) -gt 20 ]; then
echo "✓ No previous contract found - treating as initial public API"
CHANGE_CLASS="minor"
else
echo "⚠ No public API detected"
CHANGE_CLASS="none"
fi
fi
echo "Contract change classification: $CHANGE_CLASS"
CUR=${BASE_VERSION#v}; MAJOR=${CUR%%.*}; REST=${CUR#*.}; MINOR=${REST%%.*}; PATCH=${CUR##*.}
if [ -n "$INPUT_MANUAL_VERSION" ]; then V="$INPUT_MANUAL_VERSION"; [[ $V == v* ]] || V="v$V"; NEXT_VERSION="$V"; REASON="manual override"; else
case "$CHANGE_CLASS" in
major) NEXT_VERSION="v$((MAJOR + 1)).0.0"; REASON="contract breaking change" ;;
minor) if [ "$INPUT_RELEASE_TYPE" = "major" ]; then NEXT_VERSION="v$((MAJOR + 1)).0.0"; REASON="user requested major"; else NEXT_VERSION="v${MAJOR}.$((MINOR + 1)).0"; REASON="contract additive change"; fi ;;
none) if [ "$INPUT_RELEASE_TYPE" = "major" ]; then NEXT_VERSION="v$((MAJOR + 1)).0.0"; REASON="user requested major (no contract change)"; elif [ "$INPUT_RELEASE_TYPE" = "minor" ]; then NEXT_VERSION="v${MAJOR}.$((MINOR + 1)).0"; REASON="user requested minor (no contract change)"; else NEXT_VERSION="v${MAJOR}.${MINOR}.$((PATCH + 1))"; REASON="patch (no contract change)"; fi ;;
esac
fi
echo "next_version=$NEXT_VERSION" >> $GITHUB_OUTPUT
echo "tag=modules/${MODULE}/${NEXT_VERSION}" >> $GITHUB_OUTPUT
echo "module=${MODULE}" >> $GITHUB_OUTPUT
echo "change_class=$CHANGE_CLASS" >> $GITHUB_OUTPUT
echo "reason=$REASON" >> $GITHUB_OUTPUT
echo "Next version: ${NEXT_VERSION}, tag will be: modules/${MODULE}/${NEXT_VERSION} ($REASON)"
- name: Check go.mod for major version v2+
if: steps.skipcheck.outputs.changed == 'true'
id: check_module_path
run: |
set -euo pipefail
MODULE="${{ steps.version.outputs.module }}"
VERSION="${{ steps.version.outputs.next_version }}"
# Extract major version from VERSION (e.g., v2.0.0 -> 2)
MAJOR_VERSION="${VERSION#v}"
MAJOR_VERSION="${MAJOR_VERSION%%.*}"
echo "Major version: $MAJOR_VERSION"
echo "major_version=$MAJOR_VERSION" >> $GITHUB_OUTPUT
# Only check go.mod if major version is 2 or greater
if [ "$MAJOR_VERSION" -ge 2 ]; then
GO_MOD_PATH="modules/${MODULE}/go.mod"
CURRENT_MODULE_PATH=$(grep "^module " "$GO_MOD_PATH" | awk '{print $2}')
echo "Current module path: $CURRENT_MODULE_PATH"
echo "current_module_path=$CURRENT_MODULE_PATH" >> $GITHUB_OUTPUT
# Check if module path already has version suffix
if [[ "$CURRENT_MODULE_PATH" =~ /v[0-9]+$ ]]; then
# Extract current major version from module path
CURRENT_MAJOR="${CURRENT_MODULE_PATH##*/v}"
echo "Current module path has version suffix: v$CURRENT_MAJOR"
if [ "$CURRENT_MAJOR" -ne "$MAJOR_VERSION" ]; then
echo "ERROR: Module path has /v${CURRENT_MAJOR} but releasing v${MAJOR_VERSION}"
echo "Please manually update module path in ${GO_MOD_PATH} to include /v${MAJOR_VERSION}"
exit 1
fi
echo "Module path already correct for v${MAJOR_VERSION}"
echo "needs_update=false" >> $GITHUB_OUTPUT
else
# No version suffix, need to add it
echo "Module path needs v${MAJOR_VERSION} suffix"
echo "needs_update=true" >> $GITHUB_OUTPUT
NEW_MODULE_PATH="${CURRENT_MODULE_PATH}/v${MAJOR_VERSION}"
echo "new_module_path=$NEW_MODULE_PATH" >> $GITHUB_OUTPUT
fi
else
echo "Major version is $MAJOR_VERSION (< 2), no module path update needed"
echo "needs_update=false" >> $GITHUB_OUTPUT
fi
- name: Create PR for module path update
if: steps.skipcheck.outputs.changed == 'true' && steps.check_module_path.outputs.needs_update == 'true'
run: |
set -euo pipefail
MODULE="${{ steps.version.outputs.module }}"
VERSION="${{ steps.version.outputs.next_version }}"
MAJOR_VERSION="${{ steps.check_module_path.outputs.major_version }}"
CURRENT_MODULE_PATH="${{ steps.check_module_path.outputs.current_module_path }}"
NEW_MODULE_PATH="${{ steps.check_module_path.outputs.new_module_path }}"
# Create a new branch for the module path update
BRANCH_NAME="chore/${MODULE}-update-module-path-v${MAJOR_VERSION}"
git checkout -b "$BRANCH_NAME"
# Update the module path in go.mod
GO_MOD_PATH="modules/${MODULE}/go.mod"
sed -i "s|^module ${CURRENT_MODULE_PATH}|module ${NEW_MODULE_PATH}|" "$GO_MOD_PATH"
# Run go mod tidy to ensure consistency
cd "modules/${MODULE}"
go mod tidy
cd ../..
# Commit the change
git config user.name 'github-actions[bot]'
git config user.email 'github-actions[bot]@users.noreply.github.com'
git add "modules/${MODULE}/go.mod"
# Add go.sum if it was modified
git add "modules/${MODULE}/go.sum" 2>/dev/null || true
git commit -m "chore(${MODULE}): update module path for v${MAJOR_VERSION}
This updates the ${MODULE} module path from:
${CURRENT_MODULE_PATH}
to:
${NEW_MODULE_PATH}
This is required for releasing version ${VERSION} according to Go module versioning requirements.

Check failure on line 331 in .github/workflows/module-release.yml

View workflow run for this annotation

GitHub Actions / .github/workflows/module-release.yml

Invalid workflow file

You have an error in your yaml syntax on line 331
See https://go.dev/blog/v2-go-modules for more information."
# Push the branch
git push origin "$BRANCH_NAME"
# Create the PR
gh pr create \
--title "chore(${MODULE}): update module path for v${MAJOR_VERSION}" \
--body "## Module Path Update for ${MODULE} ${VERSION}
This PR updates the module path in \`modules/${MODULE}/go.mod\` to include the \`/v${MAJOR_VERSION}\` suffix as required by Go module versioning for major versions 2 and above.
### Changes:
- **Module**: ${MODULE}
- **Old module path**: \`${CURRENT_MODULE_PATH}\`
- **New module path**: \`${NEW_MODULE_PATH}\`
### Why This Change Is Needed:
According to Go module versioning requirements, when releasing a major version v2 or higher, the module path must include a version suffix. This ensures proper dependency resolution and prevents conflicts with v0/v1 versions.
Reference: https://go.dev/blog/v2-go-modules
### Next Steps:
1. Review and merge this PR
2. After merging, re-run the module release workflow for **${MODULE}** to complete the ${VERSION} release
This PR was automatically created by the module release workflow." \
--base "${{ github.ref_name }}" \
--head "$BRANCH_NAME"
echo "✓ Created PR for module path update"
echo ""
echo "============================================"
echo "⚠️ RELEASE PAUSED"
echo "============================================"
echo ""
echo "A PR has been created to update the module path for ${MODULE} v${MAJOR_VERSION}."
echo "Please merge the PR, then re-run this release workflow to complete the release."
echo ""
exit 1
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Generate changelog
if: steps.skipcheck.outputs.changed == 'true' && steps.check_module_path.outputs.needs_update != 'true'
run: |
MODULE=${{ steps.version.outputs.module }}
TAG=${{ steps.version.outputs.tag }}
CHANGE_CLASS=${{ steps.version.outputs.change_class }}
# Find the previous tag for this module to use as starting point for changelog
PREV_TAG=$(git tag -l "modules/${MODULE}/v*" | sort -V | tail -n1 || echo "")
# Generate changelog by looking at commits that touched the module's directory
if [ -z "$PREV_TAG" ]; then
echo "No previous tag found, including all history for the module"
CHANGELOG=$(git log --pretty=format:"- %s (%h)" -- "modules/${MODULE}")
else
echo "Generating changelog from $PREV_TAG to HEAD"
CHANGELOG=$(git log --pretty=format:"- %s (%h)" ${PREV_TAG}..HEAD -- "modules/${MODULE}")
fi
# If no specific changes found for this module
if [ -z "$CHANGELOG" ]; then
CHANGELOG="- No specific changes to this module since last release"
fi
# Save changelog to a file with module & version info
echo "# ${MODULE} ${TAG}" > changelog.md
echo "" >> changelog.md
echo "## Changes" >> changelog.md
echo "" >> changelog.md
echo "$CHANGELOG" >> changelog.md
echo "" >> changelog.md
echo "## API Contract Changes" >> changelog.md
echo "" >> changelog.md
if [ -f artifacts/diffs/${MODULE}.md ] && [ -s artifacts/diffs/${MODULE}.md ]; then
case "$CHANGE_CLASS" in
major) echo "⚠️ Breaking changes detected (major bump)." >> changelog.md; echo "" >> changelog.md ;;
minor) echo "✅ Additive, backward-compatible changes (minor bump)." >> changelog.md; echo "" >> changelog.md ;;
none) echo "ℹ️ No public API surface changes detected." >> changelog.md; echo "" >> changelog.md ;;
esac
echo '```diff' >> changelog.md
cat artifacts/diffs/${MODULE}.md >> changelog.md
echo '```' >> changelog.md
if [ -f artifacts/diffs/${MODULE}.json ] && [ -s artifacts/diffs/${MODULE}.json ]; then
echo "" >> changelog.md
echo "<details><summary>Raw Contract JSON Diff</summary>" >> changelog.md
echo "" >> changelog.md
echo '```json' >> changelog.md
if command -v jq >/dev/null 2>&1; then jq . artifacts/diffs/${MODULE}.json >> changelog.md || cat artifacts/diffs/${MODULE}.json >> changelog.md; else cat artifacts/diffs/${MODULE}.json >> changelog.md; fi
echo '```' >> changelog.md
echo "" >> changelog.md
echo '</details>' >> changelog.md
fi
else
echo "No API contract differences compared to previous release." >> changelog.md
fi
echo "Generated changelog for $MODULE"
- name: Create release
if: steps.skipcheck.outputs.changed == 'true' && steps.check_module_path.outputs.needs_update != 'true'
id: create_release
run: |
gh release create ${{ steps.version.outputs.tag }} \
--title "${{ steps.version.outputs.module }} ${{ steps.version.outputs.next_version }}" \
--notes-file changelog.md \
--repo ${{ github.repository }} \
--latest=false
# Attach module-only source archive
MOD=${{ steps.version.outputs.module }}
VERSION=${{ steps.version.outputs.next_version }}
ARCHIVE=${MOD}-${VERSION}.tar.gz
tar -czf "$ARCHIVE" modules/${MOD}
gh release upload ${{ steps.version.outputs.tag }} "$ARCHIVE" --repo ${{ github.repository }} --clobber
git tag ${{ steps.version.outputs.tag }}
git push origin ${{ steps.version.outputs.tag }}
# Get all assets of the release and delete each one
gh release view ${{ steps.version.outputs.tag }} --json assets --jq '.assets[].name' | while read asset; do
echo "Deleting asset: $asset"
gh release delete-asset ${{ steps.version.outputs.tag }} "$asset" -y
done
# Capture URL for display step
RELEASE_URL=$(gh release view ${{ steps.version.outputs.tag }} --json url --jq .url)
echo "html_url=$RELEASE_URL" >> $GITHUB_OUTPUT
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Announce to Go proxy
if: steps.skipcheck.outputs.changed == 'true' && steps.check_module_path.outputs.needs_update != 'true'
run: |
set -euo pipefail
VERSION=${{ steps.version.outputs.next_version }}
MODULE="${{ steps.version.outputs.module }}"
# Extract major version from VERSION
MAJOR_VERSION="${VERSION#v}"
MAJOR_VERSION="${MAJOR_VERSION%%.*}"
# Construct correct module path with version suffix for v2+
if [ "$MAJOR_VERSION" -ge 2 ]; then
MODULE_NAME="github.com/CrisisTextLine/modular/modules/${MODULE}/v${MAJOR_VERSION}"
else
MODULE_NAME="github.com/CrisisTextLine/modular/modules/${MODULE}"
fi
echo "Announcing ${MODULE_NAME}@${VERSION} to Go proxy..."
GOPROXY=proxy.golang.org go list -m ${MODULE_NAME}@${VERSION}
echo "✓ Announced version ${MODULE}@${VERSION} to Go proxy"