Skip to content

Release

Release #62

Workflow file for this run

name: Release
on:
workflow_run:
workflows: ["CI"]
branches: [main]
types: [completed]
workflow_dispatch:
# Prevent concurrent releases
concurrency:
group: release
cancel-in-progress: false
jobs:
check:
if: github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
outputs:
should_release: ${{ steps.check.outputs.should_release }}
next_version: ${{ steps.version.outputs.next_version }}
changelog: ${{ steps.changelog.outputs.changelog }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check if release needed
id: check
run: |
# Skip if the last commit is a release commit (avoid infinite loop)
LAST_MSG=$(git log -1 --pretty=%s)
if [[ "$LAST_MSG" == chore:\ release* ]]; then
echo "should_release=false" >> $GITHUB_OUTPUT
exit 0
fi
# Skip if no meaningful changes since last tag
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
if [ -z "$LAST_TAG" ]; then
echo "should_release=true" >> $GITHUB_OUTPUT
exit 0
fi
# Check for feat/fix/refactor commits since last tag
COMMITS=$(git log ${LAST_TAG}..HEAD --pretty=%s)
if echo "$COMMITS" | grep -qE '^(feat|fix|refactor|perf|breaking)'; then
echo "should_release=true" >> $GITHUB_OUTPUT
else
echo "should_release=false" >> $GITHUB_OUTPUT
fi
- name: Determine next version
if: steps.check.outputs.should_release == 'true'
id: version
run: |
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
echo "Last tag: $LAST_TAG"
# Parse semver
VERSION=${LAST_TAG#v}
MAJOR=$(echo $VERSION | cut -d. -f1)
MINOR=$(echo $VERSION | cut -d. -f2)
PATCH=$(echo $VERSION | cut -d. -f3)
# Determine bump type from commits
COMMITS=$(git log ${LAST_TAG}..HEAD --pretty=%s 2>/dev/null || git log --pretty=%s)
if echo "$COMMITS" | grep -qiE '^breaking|BREAKING CHANGE'; then
MAJOR=$((MAJOR + 1))
MINOR=0
PATCH=0
elif echo "$COMMITS" | grep -qE '^feat'; then
MINOR=$((MINOR + 1))
PATCH=0
else
PATCH=$((PATCH + 1))
fi
NEXT="v${MAJOR}.${MINOR}.${PATCH}"
echo "Next version: $NEXT"
echo "next_version=$NEXT" >> $GITHUB_OUTPUT
- name: Generate changelog
if: steps.check.outputs.should_release == 'true'
id: changelog
run: |
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
if [ -z "$LAST_TAG" ]; then
RANGE="HEAD"
else
RANGE="${LAST_TAG}..HEAD"
fi
{
echo 'changelog<<CHANGELOG_EOF'
# Features
FEATS=$(git log $RANGE --pretty="- %s (%h)" --grep="^feat" --extended-regexp 2>/dev/null)
if [ -n "$FEATS" ]; then
echo "### Features"
echo "$FEATS"
echo ""
fi
# Fixes
FIXES=$(git log $RANGE --pretty="- %s (%h)" --grep="^fix" --extended-regexp 2>/dev/null)
if [ -n "$FIXES" ]; then
echo "### Bug Fixes"
echo "$FIXES"
echo ""
fi
# Other changes (refactor, perf, etc.)
OTHERS=$(git log $RANGE --pretty="- %s (%h)" --grep="^refactor|^perf|^chore|^ci|^docs|^test|^build" --extended-regexp 2>/dev/null)
if [ -n "$OTHERS" ]; then
echo "### Other Changes"
echo "$OTHERS"
echo ""
fi
echo "### Platforms"
echo "- Linux (amd64, arm64, arm, 386, mips, mipsle, mips64, mips64le, ppc64le, s390x, riscv64)"
echo "- macOS (amd64, arm64)"
echo "- Windows (amd64, arm64, 386)"
echo "- FreeBSD (amd64, arm64)"
echo "- OpenBSD (amd64)"
echo "- NetBSD (amd64)"
echo 'CHANGELOG_EOF'
} >> $GITHUB_OUTPUT
build-cross:
needs: check
if: needs.check.outputs.should_release == 'true'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.24.x'
cache: false
- name: Install dependencies
run: go mod download
- name: Build all non-darwin targets
env:
CGO_ENABLED: '0'
run: |
VERSION=${{ needs.check.outputs.next_version }}
COMMIT=$(git rev-parse --short HEAD)
DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ)
LDFLAGS="-s -w -X codes/internal/commands.Version=${VERSION} -X codes/internal/commands.Commit=${COMMIT} -X codes/internal/commands.Date=${DATE}"
# Define targets: GOOS:GOARCH[:extra_env]
TARGETS=(
"linux:amd64"
"linux:arm64"
"linux:arm:GOARM=6"
"linux:386"
"linux:mips:GOMIPS=softfloat"
"linux:mipsle:GOMIPS=softfloat"
"linux:mips64"
"linux:mips64le"
"linux:ppc64le"
"linux:s390x"
"linux:riscv64"
"windows:amd64"
"windows:arm64"
"windows:386"
"freebsd:amd64"
"freebsd:arm64"
"openbsd:amd64"
"netbsd:amd64"
)
for target in "${TARGETS[@]}"; do
IFS=':' read -r os arch extra <<< "$target"
echo "==> Building ${os}/${arch} ${extra:+($extra)}"
# Set extra env vars if any
EXTRA_ENV=""
if [ -n "$extra" ]; then
export ${extra}
EXTRA_ENV="$extra"
fi
OUTPUT="codes"
if [ "$os" = "windows" ]; then
OUTPUT="codes.exe"
fi
GOOS=$os GOARCH=$arch go build -ldflags "$LDFLAGS" -o "dist/codes-${os}-${arch}/${OUTPUT}" ./cmd/codes
# Unset extra env vars
if [ -n "$EXTRA_ENV" ]; then
unset ${EXTRA_ENV%%=*}
fi
echo " OK: dist/codes-${os}-${arch}/${OUTPUT}"
done
echo ""
echo "==> All builds completed:"
ls -la dist/*/
- name: Smoke test linux-amd64
run: |
chmod +x dist/codes-linux-amd64/codes
dist/codes-linux-amd64/codes version
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: cross-binaries
path: dist/
retention-days: 7
build-darwin:
needs: check
if: needs.check.outputs.should_release == 'true'
runs-on: macos-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.24.x'
cache: false
- name: Install dependencies
run: go mod download
- name: Build darwin targets
env:
CGO_ENABLED: '0'
run: |
VERSION=${{ needs.check.outputs.next_version }}
COMMIT=$(git rev-parse --short HEAD)
DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ)
LDFLAGS="-s -w -X codes/internal/commands.Version=${VERSION} -X codes/internal/commands.Commit=${COMMIT} -X codes/internal/commands.Date=${DATE}"
for arch in amd64 arm64; do
echo "==> Building darwin/${arch}"
GOOS=darwin GOARCH=$arch go build -ldflags "$LDFLAGS" -o "dist/codes-darwin-${arch}/codes" ./cmd/codes
echo " OK"
done
- name: Sign macOS binaries
run: |
for arch in amd64 arm64; do
echo "==> Signing darwin/${arch}"
codesign --force --sign - "dist/codes-darwin-${arch}/codes"
done
- name: Smoke test native binary
run: |
# macos-latest is arm64, test that binary
chmod +x dist/codes-darwin-arm64/codes
dist/codes-darwin-arm64/codes version
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: darwin-binaries
path: dist/
retention-days: 7
release:
needs: [check, build-cross, build-darwin]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download cross-compiled binaries
uses: actions/download-artifact@v4
with:
name: cross-binaries
path: dist
- name: Download darwin binaries
uses: actions/download-artifact@v4
with:
name: darwin-binaries
path: dist
- name: Package release archives
run: |
VERSION=${{ needs.check.outputs.next_version }}
mkdir -p release
for dir in dist/codes-*; do
[ -d "$dir" ] || continue
target=$(basename "$dir" | sed 's/codes-//')
os=$(echo "$target" | cut -d- -f1)
arch=$(echo "$target" | cut -d- -f2)
if [ "$os" = "windows" ]; then
# ZIP for Windows
ARCHIVE="codes-${VERSION}-${os}-${arch}.zip"
echo "==> Packaging ${ARCHIVE}"
(cd "$dir" && zip -q "${OLDPWD}/release/${ARCHIVE}" codes.exe)
else
# tar.gz for Unix
ARCHIVE="codes-${VERSION}-${os}-${arch}.tar.gz"
echo "==> Packaging ${ARCHIVE}"
chmod +x "$dir/codes"
tar -czf "release/${ARCHIVE}" -C "$dir" codes
fi
done
echo ""
echo "==> All archives:"
ls -lh release/
- name: Generate checksums
run: |
VERSION=${{ needs.check.outputs.next_version }}
cd release
sha256sum codes-${VERSION}-* > "codes-${VERSION}-checksums.txt"
echo "==> Checksums:"
cat "codes-${VERSION}-checksums.txt"
- name: Create release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.check.outputs.next_version }}
name: ${{ needs.check.outputs.next_version }}
body: ${{ needs.check.outputs.changelog }}
files: release/*
draft: false
prerelease: false
make_latest: true