name: Promote to Production on: workflow_dispatch: inputs: commit_sha: description: 'Specific commit SHA to cherry-pick (leave empty to promote everything in main)' type: string required: false default: '' worker_container_increment: description: 'Worker container version increment. Defaults to patch.' type: choice options: - major - minor - patch required: false default: 'patch' jobs: promote: runs-on: ubuntu-latest permissions: contents: write pull-requests: write steps: - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 token: ${{ secrets.PROMOTE_PAT_TOKEN }} - name: Configure Git run: | git config --global user.name "github-actions[bot]" git config --global user.email "github-actions[bot]@users.noreply.github.com" - name: Setup branches run: | # Ensure we have latest changes git fetch origin # Create production branch if it doesn't exist if ! git show-ref --verify --quiet refs/remotes/origin/production; then echo "Creating production branch from main" git checkout -b production origin/main git push origin production else echo "Production branch already exists" git checkout production git pull origin production fi # Ensure main is up to date git checkout main git pull origin main - name: Cherry-pick specific commit if: ${{ inputs.commit_sha != '' }} run: | echo "Cherry-picking specific commit: ${{ inputs.commit_sha }}" # Validate commit exists in main if ! git cat-file -e "${{ inputs.commit_sha }}^{commit}"; then echo "Error: Commit ${{ inputs.commit_sha }} does not exist" exit 1 fi # Check if commit is in main branch if ! git merge-base --is-ancestor "${{ inputs.commit_sha }}" main; then echo "Error: Commit ${{ inputs.commit_sha }} is not in main branch" exit 1 fi # Switch to production and cherry-pick git checkout production # Check if commit is already in production if git merge-base --is-ancestor "${{ inputs.commit_sha }}" production; then echo "Commit ${{ inputs.commit_sha }} is already in production branch" exit 0 fi # Cherry-pick the commit COMMIT_MSG=$(git log -1 --pretty=format:"%s" "${{ inputs.commit_sha }}") echo "Cherry-picking: $COMMIT_MSG" if git cherry-pick "${{ inputs.commit_sha }}"; then echo "✅ Successfully cherry-picked commit" git push origin production echo "commit_promoted=true" >> $GITHUB_ENV else echo "❌ Cherry-pick failed - conflicts may need resolution" git cherry-pick --abort exit 1 fi - name: Recreate production from main if: ${{ inputs.commit_sha == '' }} run: | echo "Recreating production branch from main" # Get current commit on main for summary MAIN_COMMIT=$(git rev-parse main) MAIN_COMMIT_MSG=$(git log -1 --pretty=format:"%s" main) echo "Main branch is at: $MAIN_COMMIT ($MAIN_COMMIT_MSG)" # Check if production branch exists remotely if git show-ref --verify --quiet refs/remotes/origin/production; then PROD_COMMIT=$(git rev-parse origin/production) PROD_COMMIT_MSG=$(git log -1 --pretty=format:"%s" origin/production) echo "Current production is at: $PROD_COMMIT ($PROD_COMMIT_MSG)" # Check if they're already the same if [ "$MAIN_COMMIT" = "$PROD_COMMIT" ]; then echo "Production is already up to date with main" echo "production_updated=false" >> $GITHUB_ENV exit 0 fi echo "Deleting existing production branch" git push origin --delete production # Also delete local production branch if it exists if git show-ref --verify --quiet refs/heads/production; then echo "Deleting local production branch" git branch -D production fi else echo "Production branch doesn't exist, will create new one" fi # Create new production branch from main echo "Creating new production branch from main" git checkout main git checkout -b production git push origin production echo "✅ Successfully recreated production branch from main" echo "production_updated=true" >> $GITHUB_ENV echo "main_commit=$MAIN_COMMIT" >> $GITHUB_ENV - name: Create summary if: always() run: | echo "## Promotion Summary" >> $GITHUB_STEP_SUMMARY if [ "${{ inputs.commit_sha }}" != "" ]; then echo "### Single Commit Promotion" >> $GITHUB_STEP_SUMMARY echo "**Target Commit:** \`${{ inputs.commit_sha }}\`" >> $GITHUB_STEP_SUMMARY if [ "${commit_promoted:-false}" = "true" ]; then echo "✅ **Status:** Successfully promoted to production" >> $GITHUB_STEP_SUMMARY else echo "❌ **Status:** Promotion failed or commit already existed" >> $GITHUB_STEP_SUMMARY fi else echo "### Bulk Promotion" >> $GITHUB_STEP_SUMMARY if [ "${production_updated:-false}" = "true" ]; then echo "✅ **Status:** Successfully updated production branch" >> $GITHUB_STEP_SUMMARY else echo "ℹ️ **Status:** No new commits to promote or production already up to date" >> $GITHUB_STEP_SUMMARY fi fi echo "" >> $GITHUB_STEP_SUMMARY echo "### Branches" >> $GITHUB_STEP_SUMMARY echo "- **Source:** main" >> $GITHUB_STEP_SUMMARY echo "- **Target:** production" >> $GITHUB_STEP_SUMMARY calculate-version: runs-on: ubuntu-latest needs: promote outputs: version: ${{ steps.calculate_version.outputs.version }} previous_version: ${{ steps.calculate_version.outputs.previous_version }} steps: - name: Checkout production branch uses: actions/checkout@v4 with: ref: production fetch-depth: 0 - name: Get latest release and calculate next version id: calculate_version run: | # Get the latest release version from GitHub LATEST_RELEASE=$(gh release view --json tagName --jq .tagName 2>/dev/null || echo "") if [ -z "$LATEST_RELEASE" ]; then echo "No previous releases found, starting at 0.0.0" CURRENT_VERSION="0.0.0" else # Remove 'v' prefix if present CURRENT_VERSION=${LATEST_RELEASE#v} echo "Latest release: $LATEST_RELEASE (version: $CURRENT_VERSION)" fi # Split version into components IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT_VERSION" # Increment based on input case "${{ inputs.worker_container_increment }}" in major) MAJOR=$((MAJOR + 1)) MINOR=0 PATCH=0 ;; minor) MINOR=$((MINOR + 1)) PATCH=0 ;; patch) PATCH=$((PATCH + 1)) ;; esac NEW_VERSION="$MAJOR.$MINOR.$PATCH" echo "New version: $NEW_VERSION" echo "version=$NEW_VERSION" >> $GITHUB_OUTPUT echo "previous_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} build-and-push: needs: calculate-version strategy: matrix: include: - arch: amd64 os: ubuntu-latest - arch: arm64 os: linux-arm64 runs-on: ${{ matrix.os }} permissions: contents: write packages: write env: VERSION: ${{ needs.calculate-version.outputs.version }} REGISTRY: ghcr.io steps: - name: Checkout production branch uses: actions/checkout@v4 with: ref: production fetch-depth: 0 - name: Install uv uses: astral-sh/setup-uv@v5 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 - name: Log in to GitHub Container Registry uses: docker/login-action@v2 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build distributions run: | echo "Building full distribution for production release..." make full-dist - name: Build and push Worker images run: | # Build both worker and worker-base images make docker VERSION=${{ env.VERSION }} ARCH=${{ matrix.arch }} make docker-base VERSION=${{ env.VERSION }} ARCH=${{ matrix.arch }} # Push to GHCR only make publish-ghcr VERSION=${{ env.VERSION }} ARCH=${{ matrix.arch }} push-manifest: runs-on: ubuntu-latest needs: [calculate-version, build-and-push] permissions: contents: write packages: write env: VERSION: ${{ needs.calculate-version.outputs.version }} REGISTRY: ghcr.io steps: - name: Checkout production branch uses: actions/checkout@v4 with: ref: production fetch-depth: 0 - name: Log in to GitHub Container Registry uses: docker/login-action@v2 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Push manifest to GHCR working-directory: ./docker run: | make ghcr-manifest VERSION=${{ env.VERSION }} make ghcr-manifest INSTALL_TOOLKITS=false VERSION=${{ env.VERSION }} create-release: runs-on: ubuntu-latest needs: [calculate-version, push-manifest] permissions: contents: write env: VERSION: ${{ needs.calculate-version.outputs.version }} PREVIOUS_VERSION: ${{ needs.calculate-version.outputs.previous_version }} steps: - name: Checkout production branch uses: actions/checkout@v4 with: ref: production fetch-depth: 0 token: ${{ secrets.PROMOTE_PAT_TOKEN }} - name: Generate release notes id: generate_release_notes run: | echo "# Release ${{ env.VERSION }}" > release_notes.md echo "" >> release_notes.md echo "## Container Images" >> release_notes.md echo "" >> release_notes.md echo "### GitHub Container Registry (GHCR)" >> release_notes.md echo "\`\`\`bash" >> release_notes.md echo "docker pull ghcr.io/arcadeai/worker:${{ env.VERSION }}" >> release_notes.md echo "docker pull ghcr.io/arcadeai/worker-base:${{ env.VERSION }}" >> release_notes.md echo "\`\`\`" >> release_notes.md echo "" >> release_notes.md echo "## What's Changed" >> release_notes.md echo "" >> release_notes.md if [ "${{ inputs.commit_sha }}" != "" ]; then # Case 1: Specific commit SHA to cherry-pick was provided echo "### Cherry-picked commit:" >> release_notes.md echo "" >> release_notes.md git log -1 --pretty=format:"- **%h** %s (%an)" "${{ inputs.commit_sha }}" >> release_notes.md echo "" >> release_notes.md echo "_This release contains a single cherry-picked commit from main._" >> release_notes.md else # Case 2: Bulk promotion (production == main) echo "### Commits in this release:" >> release_notes.md echo "" >> release_notes.md # Get the list of previous releases to find the last release PREV_RELEASE_DATE=$(gh release list --limit 5 --json publishedAt,tagName --jq '.[1].publishedAt // empty' 2>/dev/null || echo "") if [ -z "$PREV_RELEASE_DATE" ]; then # First promotion ever echo "**Initial release** - Recent commits from main branch:" >> release_notes.md echo "" >> release_notes.md git log -20 --pretty=format:"- **%h** %s (%an)" --reverse >> release_notes.md else # Show commits since the previous promotion echo "Changes since previous release ($(echo $PREV_RELEASE_DATE | cut -d'T' -f1)):" >> release_notes.md echo "" >> release_notes.md git log --since="$PREV_RELEASE_DATE" --pretty=format:"- **%h** %s (%an)" --reverse >> release_notes.md echo "" >> release_notes.md # Fallback if no commits since last release (shouldn't happen, but just in case) if [ $(git log --since="$PREV_RELEASE_DATE" --oneline | wc -l) -eq 0 ]; then echo "_No commits found since previous release. Showing recent commits:_" >> release_notes.md echo "" >> release_notes.md git log -10 --pretty=format:"- **%h** %s (%an)" --reverse >> release_notes.md fi fi echo "" >> release_notes.md echo "_This release includes all changes from the main branch._" >> release_notes.md fi env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Create GitHub Release uses: actions/create-release@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: tag_name: v${{ env.VERSION }} release_name: Release v${{ env.VERSION }} body_path: release_notes.md draft: false prerelease: false - name: Update workflow summary run: | echo "" >> $GITHUB_STEP_SUMMARY echo "## 🚀 Container Release" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "### Version Information" >> $GITHUB_STEP_SUMMARY echo "- **Previous Version:** v${{ env.PREVIOUS_VERSION }}" >> $GITHUB_STEP_SUMMARY echo "- **New Version:** v${{ env.VERSION }}" >> $GITHUB_STEP_SUMMARY echo "- **Increment Type:** ${{ inputs.worker_container_increment }}" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "### Container Images Published to GHCR" >> $GITHUB_STEP_SUMMARY echo "- ✅ ghcr.io/arcadeai/worker:${{ env.VERSION }}" >> $GITHUB_STEP_SUMMARY echo "- ✅ ghcr.io/arcadeai/worker-base:${{ env.VERSION }}" >> $GITHUB_STEP_SUMMARY