Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
284 changes: 284 additions & 0 deletions .github/scripts/create-release.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@
#!/usr/bin/env bash
#
# create-release.sh - Create a new release with semantic versioning
#
# Usage: ./create-release.sh [--dry-run] <version>
#
# Examples:
# ./create-release.sh v0.1.0 # Create release v0.1.0
# ./create-release.sh --dry-run v1.0.0 # Show what would happen
#
# This script:
# 1. Validates the version follows semver (vX.Y.Z)
# 2. Creates the version tag (v0.1.0)
# 3. Updates/creates floating tags (v0.1, v0) for stable releases only
# 4. Pushes all tags to origin
# 5. Outputs a release notes template

set -euo pipefail

# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color

DRY_RUN=false
VERSION=""

usage() {
echo "Usage: $0 [--dry-run] <version>"
echo ""
echo "Options:"
echo " --dry-run Show what would happen without making changes"
echo ""
echo "Arguments:"
echo " version Semantic version (e.g., v0.1.0, v1.0.0-beta)"
echo ""
echo "Examples:"
echo " $0 v0.1.0"
echo " $0 --dry-run v1.0.0"
exit 1
}

log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}

log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}

log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}

log_error() {
echo -e "${RED}[ERROR]${NC} $1" >&2
}

run_cmd() {
if [[ "$DRY_RUN" == "true" ]]; then
echo -e "${YELLOW}[DRY-RUN]${NC} Would run: $*"
else
"$@"
fi
}

# Parse arguments
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run)
DRY_RUN=true
shift
;;
-h|--help)
usage
;;
-*)
log_error "Unknown option: $1"
usage
;;
*)
if [[ -z "$VERSION" ]]; then
VERSION="$1"
else
log_error "Unexpected argument: $1"
usage
fi
shift
;;
esac
done

if [[ -z "$VERSION" ]]; then
log_error "Version is required"
usage
fi

# Validate semver format
if [[ ! "$VERSION" =~ ^v([0-9]+)\.([0-9]+)\.([0-9]+)(-[a-zA-Z0-9.]+)?$ ]]; then
log_error "Invalid version format: $VERSION"
echo "Version must match semver: vX.Y.Z or vX.Y.Z-prerelease"
echo "Examples: v0.1.0, v1.0.0, v2.0.0-beta, v1.0.0-rc.1"
exit 1
fi

MAJOR="${BASH_REMATCH[1]}"
MINOR="${BASH_REMATCH[2]}"
PRERELEASE="${BASH_REMATCH[4]}"

MAJOR_TAG="v${MAJOR}"
MINOR_TAG="v${MAJOR}.${MINOR}"

IS_PRERELEASE=false
if [[ -n "$PRERELEASE" ]]; then
IS_PRERELEASE=true
fi

# Check we're in a git repo
if ! git rev-parse --git-dir > /dev/null 2>&1; then
log_error "Not in a git repository"
exit 1
fi

# Check for uncommitted changes
if ! git diff-index --quiet HEAD -- 2>/dev/null; then
log_error "There are uncommitted changes. Please commit or stash them first."
exit 1
fi

# Warn if not on main branch
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
if [[ "$CURRENT_BRANCH" != "main" ]]; then
log_warn "Creating release from branch '$CURRENT_BRANCH' (not main)"
read -p "Continue? [y/N] " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
exit 1
fi
fi

# Fetch latest tags
log_info "Fetching latest tags from origin..."
if [[ "$DRY_RUN" != "true" ]]; then
git fetch --tags origin
fi

# Check if version tag already exists
if git rev-parse "$VERSION" > /dev/null 2>&1; then
log_error "Tag $VERSION already exists"
exit 1
fi

# Get current commit
CURRENT_SHA=$(git rev-parse HEAD)
CURRENT_SHA_SHORT=$(git rev-parse --short HEAD)

echo ""
log_info "Release Configuration:"
echo " Version: $VERSION"
echo " Major tag: $MAJOR_TAG"
echo " Minor tag: $MINOR_TAG"
echo " Commit: $CURRENT_SHA_SHORT"
echo " Branch: $CURRENT_BRANCH"
echo " Pre-release: $IS_PRERELEASE"
echo ""

if [[ "$DRY_RUN" == "true" ]]; then
log_warn "DRY-RUN MODE - No changes will be made"
echo ""
fi

# Create the version tag
log_info "Creating tag $VERSION..."
run_cmd git tag -a "$VERSION" -m "Release $VERSION"

# Update floating tags only for stable releases
if [[ "$IS_PRERELEASE" == "false" ]]; then
log_info "Updating floating tag $MINOR_TAG..."
run_cmd git tag -f "$MINOR_TAG" -m "Floating tag for ${MAJOR}.${MINOR}.x releases"

log_info "Updating floating tag $MAJOR_TAG..."
run_cmd git tag -f "$MAJOR_TAG" -m "Floating tag for ${MAJOR}.x.x releases"
else
log_info "Skipping floating tag updates for pre-release"
fi

# Push tags
log_info "Pushing tags to origin..."
run_cmd git push origin "$VERSION"
if [[ "$IS_PRERELEASE" == "false" ]]; then
run_cmd git push -f origin "$MINOR_TAG"
run_cmd git push -f origin "$MAJOR_TAG"
fi

echo ""
if [[ "$DRY_RUN" == "true" ]]; then
log_success "Dry run complete. Run without --dry-run to create the release."
else
log_success "Tags created and pushed successfully!"
fi

# Generate release notes template
echo ""
echo "=========================================="
echo "Release Notes Template"
echo "=========================================="
echo ""

# Get commits since last tag (or all if no tags)
PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")

# Validate PREV_TAG only contains safe git ref characters
# Valid: alphanumeric, dots, underscores, hyphens, and forward slashes (for hierarchical refs)
if [[ -n "$PREV_TAG" && ! "$PREV_TAG" =~ ^[a-zA-Z0-9._/-]+$ ]]; then
log_warn "Previous tag '$PREV_TAG' contains unexpected characters, skipping commit list"
PREV_TAG=""
fi

cat << EOF
## $VERSION

### Highlights

<!-- Add 2-3 bullet points summarizing the key changes -->

### Changes

EOF

if [[ -n "$PREV_TAG" ]]; then
echo "<!-- Commits since $PREV_TAG -->"
if [[ "$DRY_RUN" != "true" ]]; then
git log --oneline "${PREV_TAG}..HEAD" | sed 's/^/- /'
else
echo "<!-- (dry-run: commit list would appear here) -->"
fi
else
echo "<!-- This is the first release -->"
if [[ "$DRY_RUN" != "true" ]]; then
git log --oneline -10 | sed 's/^/- /'
echo "<!-- ... and more -->"
fi
fi

cat << EOF

### Docker Images

\`\`\`bash
docker pull ghcr.io/jwbron/egg-sandbox:$VERSION
docker pull ghcr.io/jwbron/egg-gateway:$VERSION
\`\`\`

### Versioned References

For stability, pin to the major version:
\`\`\`yaml
uses: jwbron/egg/action@$MAJOR_TAG
\`\`\`

For full reproducibility:
\`\`\`yaml
uses: jwbron/egg/action@$VERSION
\`\`\`
EOF

if [[ "$IS_PRERELEASE" == "true" ]]; then
echo ""
echo "---"
echo "**Note:** This is a pre-release version ($PRERELEASE)."
fi

echo ""
echo "=========================================="

if [[ "$DRY_RUN" != "true" ]]; then
echo ""
log_info "Next steps:"
echo " 1. Go to: https://github.com/jwbron/egg/releases/new?tag=$VERSION"
echo " 2. Copy the release notes template above"
echo " 3. Edit and publish the release"
fi
74 changes: 66 additions & 8 deletions .github/workflows/release-images.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,77 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Set image tag
id: tag
- name: Validate and parse version tag
id: version
run: |
TAG=""
IS_RELEASE="false"
IS_PRERELEASE="false"

if [[ "${{ github.event_name }}" == "release" ]]; then
echo "tag=${{ github.event.release.tag_name }}" >> "$GITHUB_OUTPUT"
TAG="${{ github.event.release.tag_name }}"
IS_RELEASE="true"
elif [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
echo "tag=${{ inputs.tag }}" >> "$GITHUB_OUTPUT"
TAG="${{ inputs.tag }}"
if [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+ ]]; then
IS_RELEASE="true"
fi
else
TAG="latest"
fi

# Validate and parse semver for releases
if [[ "$IS_RELEASE" == "true" ]]; then
if [[ ! "$TAG" =~ ^v([0-9]+)\.([0-9]+)\.([0-9]+)(-[a-zA-Z0-9.]+)?$ ]]; then
echo "::error::Invalid version tag '$TAG'. Must match semver format: vX.Y.Z or vX.Y.Z-prerelease"
exit 1
fi

# Extract version components from the validated regex
MAJOR="${BASH_REMATCH[1]}"
MINOR="${BASH_REMATCH[2]}"
PRERELEASE="${BASH_REMATCH[4]}"

echo "major_tag=v${MAJOR}" >> "$GITHUB_OUTPUT"
echo "minor_tag=v${MAJOR}.${MINOR}" >> "$GITHUB_OUTPUT"

if [[ -n "$PRERELEASE" ]]; then
IS_PRERELEASE="true"
fi
fi

echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "is_release=$IS_RELEASE" >> "$GITHUB_OUTPUT"
echo "is_prerelease=$IS_PRERELEASE" >> "$GITHUB_OUTPUT"

- name: Generate image tags
id: tags
run: |
IMAGE="${{ matrix.image }}"
TAG="${{ steps.version.outputs.tag }}"
IS_RELEASE="${{ steps.version.outputs.is_release }}"
IS_PRERELEASE="${{ steps.version.outputs.is_prerelease }}"
MAJOR_TAG="${{ steps.version.outputs.major_tag }}"
MINOR_TAG="${{ steps.version.outputs.minor_tag }}"

TAGS="${IMAGE}:${TAG}"

if [[ "$IS_RELEASE" == "true" ]]; then
if [[ "$IS_PRERELEASE" == "false" ]]; then
# Stable release: add floating tags and latest
TAGS="${TAGS},${IMAGE}:${MINOR_TAG},${IMAGE}:${MAJOR_TAG}"
TAGS="${TAGS},${IMAGE}:latest"
fi
# Pre-releases only get the exact version tag (e.g., v1.0.0-alpha)
else
echo "tag=latest" >> "$GITHUB_OUTPUT"
# Non-release (push to main or workflow_dispatch without semver):
# TAG is already set (e.g., "latest"), no additional tags needed
:
fi

echo "tags=$TAGS" >> "$GITHUB_OUTPUT"
echo "Generated tags: $TAGS"

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

Expand All @@ -64,6 +124,4 @@ jobs:
file: ${{ matrix.dockerfile }}
push: true
platforms: linux/amd64
tags: |
${{ matrix.image }}:${{ steps.tag.outputs.tag }}
${{ matrix.image }}:latest
tags: ${{ steps.tags.outputs.tags }}
Loading
Loading