diff --git a/.github/scripts/create-release.sh b/.github/scripts/create-release.sh new file mode 100755 index 0000000000..cb515801e2 --- /dev/null +++ b/.github/scripts/create-release.sh @@ -0,0 +1,284 @@ +#!/usr/bin/env bash +# +# create-release.sh - Create a new release with semantic versioning +# +# Usage: ./create-release.sh [--dry-run] +# +# 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] " + 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 + + + +### Changes + +EOF + +if [[ -n "$PREV_TAG" ]]; then + echo "" + if [[ "$DRY_RUN" != "true" ]]; then + git log --oneline "${PREV_TAG}..HEAD" | sed 's/^/- /' + else + echo "" + fi +else + echo "" + if [[ "$DRY_RUN" != "true" ]]; then + git log --oneline -10 | sed 's/^/- /' + echo "" + 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 diff --git a/.github/workflows/release-images.yml b/.github/workflows/release-images.yml index d42bd5e837..e7a5864d39 100644 --- a/.github/workflows/release-images.yml +++ b/.github/workflows/release-images.yml @@ -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 @@ -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 }} diff --git a/README.md b/README.md index 0650f80a6c..1fe37c85f6 100644 --- a/README.md +++ b/README.md @@ -263,6 +263,44 @@ See [GitHub Action documentation](action/README.md) for details. - [Contributing](CONTRIBUTING.md) — Development setup and workflow - [Why egg Works](docs/collaboration-effectiveness.md) — Safety, quality, and collaboration +## Versioning + +egg uses [semantic versioning](https://semver.org/) for both Docker images and GitHub Action references. + +> **Note:** Use `@main` until the first release (v0.1.0) is published, which will create the `@v0` tag. + +### Version Pinning + +For stability, pin to a major version: +```yaml +uses: jwbron/egg/action@v0 # Receives all v0.x.y updates +``` + +For full reproducibility: +```yaml +uses: jwbron/egg/action@v0.1.0 # Exact version +``` + +### Docker Images + +```bash +# Latest stable (updated on every release) +docker pull ghcr.io/jwbron/egg-sandbox:latest + +# Major version (updated on v0.x.y releases) +docker pull ghcr.io/jwbron/egg-sandbox:v0 + +# Exact version +docker pull ghcr.io/jwbron/egg-sandbox:v0.1.0 +``` + +### Breaking Changes + +- **v0.x.y**: Pre-stable releases. Minor versions may contain breaking changes. +- **v1.x.y and later**: Stable releases. Breaking changes only in major version bumps. + +See [RELEASING.md](RELEASING.md) for the release process. + ## Development ```bash diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 0000000000..150af32975 --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,143 @@ +# Releasing + +This document describes the release process for egg. + +## Version Scheme + +egg uses [semantic versioning](https://semver.org/): + +- **Major version (vX.0.0)**: Breaking changes to workflows, action inputs/outputs, or gateway API +- **Minor version (v0.X.0)**: New features, backward-compatible enhancements +- **Patch version (v0.0.X)**: Bug fixes, documentation updates + +### Pre-1.0 Releases + +During the v0.x.y phase, minor versions may contain breaking changes. Pin to exact versions for maximum stability. + +### Pre-release Versions + +Use suffixes for pre-release versions: +- `-alpha.N`: Early development, may be unstable +- `-beta.N`: Feature complete, seeking feedback +- `-rc.N`: Release candidate, final testing + +Pre-release versions do not update floating tags (`vX`, `vX.Y`) or the `latest` Docker tag. + +## Release Artifacts + +Each release produces: + +| Artifact | Tags | +|----------|------| +| Docker images | `vX.Y.Z`, `vX.Y`, `vX`, `latest` (stable only) | +| Git tags | `vX.Y.Z`, `vX.Y`, `vX` (floating, stable only) | +| GitHub Release | `vX.Y.Z` with changelog | + +Pre-release versions only produce the exact version tag (`vX.Y.Z-suffix`). + +## Creating a Release + +### Prerequisites + +- All tests passing on main +- No critical open issues +- CHANGELOG.md updated (if applicable) + +### Using the Release Script + +```bash +# Dry run first +.github/scripts/create-release.sh --dry-run v0.2.0 + +# Create the release +.github/scripts/create-release.sh v0.2.0 +``` + +The script will: +1. Validate the version format +2. Warn if not running from the `main` branch +3. Create the version tag (v0.2.0) +4. Update floating tags (v0.2, v0) — skipped for pre-releases +5. Push all tags to origin +6. Output a release notes template + +### Creating the GitHub Release + +After running the script: + +1. Go to https://github.com/jwbron/egg/releases/new?tag=vX.Y.Z +2. Copy the release notes template from the script output +3. Edit the highlights and changelog sections +4. For pre-release versions, check "Set as a pre-release" +5. Publish the release + +The `release-images.yml` workflow will automatically build and push Docker images with all version tags. + +## Release Checklist + +Before releasing: + +- [ ] All CI checks passing on main +- [ ] Version number follows semver +- [ ] Breaking changes documented (if any) +- [ ] Migration notes included for breaking changes + +During release: + +- [ ] Run `create-release.sh --dry-run` to verify +- [ ] Run `create-release.sh` to create and push tags +- [ ] Create GitHub release with changelog +- [ ] Verify Docker images are pushed + +After release: + +- [ ] Verify `docker pull ghcr.io/jwbron/egg-sandbox:vX.Y.Z` works +- [ ] Verify `@vX` floating tag is updated +- [ ] Notify users of breaking changes (if any) + +## Rollback + +### Bad Release + +If a release is broken: + +```bash +# Delete the bad tags (example: rolling back v0.2.0) +git push --delete origin v0.2.0 v0.2 v0 +git tag -d v0.2.0 v0.2 v0 + +# Point floating tags to the last good release (e.g., v0.1.3) +git tag -f v0.1 v0.1.3 +git tag -f v0 v0.1.3 +git push -f origin v0.1 v0 +``` + +Note: The rollback target should be the last known-good release in the +previous minor series. Adjust `v0.1.3` to whatever your actual last good +release was. + +### Emergency Hotfix + +For critical bugs in a released version: + +1. Create a hotfix branch from the release tag +2. Fix the issue +3. Release as vX.Y.Z+1 patch version +4. Floating tags will update automatically + +## Dependabot + +External consumers using Dependabot will receive automatic PRs when: +- New major versions are released (requires manual merge) +- New minor/patch versions are released (can auto-merge if configured) + +Configure in consumer repos: +```yaml +# .github/dependabot.yml +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" +``` diff --git a/action/README.md b/action/README.md index d2ca054da6..33f01e4044 100644 --- a/action/README.md +++ b/action/README.md @@ -26,12 +26,26 @@ This action runs the egg autonomous coding agent within GitHub Actions. It sets ## Quick Start ```yaml -- uses: jwbron/egg@main +- uses: jwbron/egg/action@v0 with: prompt: "Fix the failing tests" anthropic-oauth-token: ${{ secrets.ANTHROPIC_OAUTH_TOKEN }} ``` +> **Note:** Use `@main` until the first release (v0.1.0) creates the `@v0` tag. + +### Version Pinning + +For stability, pin to a major version (receives all patch and minor updates): +```yaml +uses: jwbron/egg/action@v0 +``` + +For full reproducibility, pin to an exact version: +```yaml +uses: jwbron/egg/action@v0.1.0 +``` + ## Documentation For design details, inputs, outputs, and implementation notes, see the [GitHub Actions Support ADR](../docs/adr/in-progress/ADR-GitHub-Actions-Support.md). diff --git a/docs/guides/reusable-workflows.md b/docs/guides/reusable-workflows.md index a2b469f690..3be76b19cf 100644 --- a/docs/guides/reusable-workflows.md +++ b/docs/guides/reusable-workflows.md @@ -6,6 +6,25 @@ This guide explains how to use egg's SDLC workflows in your own repositories. The egg project provides a set of reusable GitHub Actions workflows for AI-powered code review, autofix, conflict resolution, and SDLC pipeline management. These workflows can be called from any repository that has the required secrets configured. +## Version Pinning + +All workflow examples below use `@main`. After the first release (v0.1.0) creates the `@v0` tag, switch to `@v0` for stability. + +**For stability** (recommended after first release), pin to a major version: +```yaml +uses: jwbron/egg/.github/workflows/reusable-review.yml@v0 +``` + +**For full reproducibility**, pin to an exact version: +```yaml +uses: jwbron/egg/.github/workflows/reusable-review.yml@v0.1.0 +``` + +**For latest development** (not recommended for production): +```yaml +uses: jwbron/egg/.github/workflows/reusable-review.yml@main +``` + ## Available Workflows ### Core Review Workflow