Release #30
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| version_bump: | |
| description: "Version bump type" | |
| required: true | |
| default: "patch" | |
| type: choice | |
| options: | |
| - patch | |
| - minor | |
| - major | |
| concurrency: | |
| group: release | |
| cancel-in-progress: true | |
| permissions: | |
| contents: write | |
| jobs: | |
| release: | |
| name: Create release | |
| runs-on: ubuntu-latest | |
| outputs: | |
| version: ${{ steps.version.outputs.version }} | |
| sha: ${{ steps.release-commit.outputs.sha }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | |
| with: | |
| fetch-depth: 0 | |
| token: ${{ secrets.RELEASE_TOKEN }} | |
| - name: Configure git | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| - name: Calculate new version | |
| id: version | |
| env: | |
| BUMP_TYPE: ${{ inputs.version_bump }} | |
| run: | | |
| # Get the latest semver tag | |
| LATEST_TAG=$(git tag -l 'v*.*.*' --sort=-v:refname | head -n1) | |
| if [ -z "$LATEST_TAG" ]; then | |
| echo "No existing version tags found, starting at v0.0.0" | |
| MAJOR=0; MINOR=0; PATCH=0 | |
| else | |
| VERSION="${LATEST_TAG#v}" | |
| MAJOR=$(echo "$VERSION" | cut -d. -f1) | |
| MINOR=$(echo "$VERSION" | cut -d. -f2) | |
| PATCH=$(echo "$VERSION" | cut -d. -f3) | |
| echo "Current version: v${MAJOR}.${MINOR}.${PATCH}" | |
| fi | |
| # Bump the chosen segment | |
| case "$BUMP_TYPE" in | |
| major) MAJOR=$((MAJOR + 1)); MINOR=0; PATCH=0 ;; | |
| minor) MINOR=$((MINOR + 1)); PATCH=0 ;; | |
| patch) PATCH=$((PATCH + 1)) ;; | |
| esac | |
| NEW_VERSION="v${MAJOR}.${MINOR}.${PATCH}" | |
| # Collision avoidance: if tag already exists, bump patch until unique | |
| while git rev-parse "$NEW_VERSION" >/dev/null 2>&1; do | |
| echo "Tag $NEW_VERSION already exists, bumping patch..." | |
| PATCH=$((PATCH + 1)) | |
| NEW_VERSION="v${MAJOR}.${MINOR}.${PATCH}" | |
| done | |
| echo "version=$NEW_VERSION" >> $GITHUB_OUTPUT | |
| echo "previous=${LATEST_TAG}" >> $GITHUB_OUTPUT | |
| echo "New version: $NEW_VERSION (previous: ${LATEST_TAG:-none})" | |
| # CI cannot push commits to main (branch protection). Instead, we create | |
| # a detached release commit with pinned refs, reachable only via tags. | |
| # main keeps @latest refs for development; tagged commits are self-contained. | |
| - name: Create release commit with pinned refs | |
| id: release-commit | |
| env: | |
| VERSION: ${{ steps.version.outputs.version }} | |
| run: | | |
| set -e | |
| echo "Pinning @latest refs to ${VERSION}..." | |
| # Pin all docker/cagent-action*@latest refs -> @vX.Y.Z | |
| # Uses a capture group so any sub-path (e.g., /review-pr, /review-pr/reply) is preserved. | |
| # Automatically covers new sub-actions without needing to update this workflow. | |
| # Only targets `uses:` lines to avoid pinning refs in comments or documentation. | |
| PIN_PATTERN='s|^\([^#]*uses: *docker/cagent-action\)\([^@]*\)@latest|\1\2@'"${VERSION}"'|g' | |
| PINNED_FILES=() | |
| while IFS= read -r file; do | |
| sed -i "$PIN_PATTERN" "$file" | |
| PINNED_FILES+=("$file") | |
| echo " Pinned: $file" | |
| done < <(grep -rl 'uses: *docker/cagent-action[^@]*@latest' --include='*.yml' --include='*.yaml' \ | |
| --exclude-dir=.git \ | |
| review-pr/ .github/workflows/review-pr.yml) | |
| if [ ${#PINNED_FILES[@]} -eq 0 ]; then | |
| echo "::error::No @latest refs found to pin — expected at least one. Check that review-pr/ actions still reference @latest." | |
| exit 1 | |
| fi | |
| # Verify no @latest uses: refs remain in pinned files | |
| REMAINING=$(grep -n '^[^#]*uses: *docker/cagent-action[^@]*@latest' "${PINNED_FILES[@]}" 2>/dev/null || true) | |
| if [ -n "$REMAINING" ]; then | |
| echo "::error::Unpinned @latest refs remain after release:" | |
| echo "$REMAINING" | |
| exit 1 | |
| fi | |
| echo "Pinned refs:" | |
| grep -rn "cagent-action@" "${PINNED_FILES[@]}" | |
| # Create a detached commit (not on main) with the pinned refs | |
| # Note: write-tree captures the full index (all files from HEAD), | |
| # but we explicitly add DOCKER_AGENT_VERSION for clarity. | |
| git add "${PINNED_FILES[@]}" DOCKER_AGENT_VERSION | |
| TREE=$(git write-tree) | |
| RELEASE_SHA=$(git commit-tree "$TREE" -p HEAD -m "release: ${VERSION}") | |
| echo "sha=$RELEASE_SHA" >> $GITHUB_OUTPUT | |
| echo "Release commit: $RELEASE_SHA" | |
| - name: Push version tag | |
| env: | |
| VERSION: ${{ steps.version.outputs.version }} | |
| RELEASE_SHA: ${{ steps.release-commit.outputs.sha }} | |
| run: | | |
| git tag "$VERSION" "$RELEASE_SHA" | |
| git push origin "$VERSION" | |
| - name: Create GitHub Release | |
| env: | |
| VERSION: ${{ steps.version.outputs.version }} | |
| PREVIOUS: ${{ steps.version.outputs.previous }} | |
| GH_TOKEN: ${{ secrets.RELEASE_TOKEN }} | |
| run: | | |
| ARGS=(--generate-notes --latest) | |
| if [ -n "$PREVIOUS" ]; then | |
| ARGS+=(--notes-start-tag "$PREVIOUS") | |
| fi | |
| gh release create "$VERSION" "${ARGS[@]}" | |
| - name: Update latest tag | |
| env: | |
| VERSION: ${{ steps.version.outputs.version }} | |
| RELEASE_SHA: ${{ steps.release-commit.outputs.sha }} | |
| run: | | |
| # Delete existing latest tag (local + remote) | |
| git tag -d latest 2>/dev/null || true | |
| git push origin :refs/tags/latest 2>/dev/null || true | |
| # Create new latest tag pointing to the release commit | |
| git tag latest "$RELEASE_SHA" | |
| git push origin latest | |
| echo "Updated 'latest' tag to point to ${VERSION} ($RELEASE_SHA)" | |
| update-pinata: | |
| name: Update pinata pr-review workflow | |
| needs: release | |
| if: success() | |
| runs-on: ubuntu-latest | |
| concurrency: | |
| group: update-pinata | |
| cancel-in-progress: false | |
| steps: | |
| - name: Checkout pinata | |
| uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | |
| with: | |
| repository: docker/pinata | |
| token: ${{ secrets.RELEASE_TOKEN }} | |
| - name: Update cagent-action reference | |
| id: update | |
| env: | |
| SHA: ${{ needs.release.outputs.sha }} | |
| VERSION: ${{ needs.release.outputs.version }} | |
| run: | | |
| FILE=".github/workflows/pr-review.yml" | |
| if [ ! -f "$FILE" ]; then | |
| echo "::error::$FILE not found in pinata" | |
| exit 1 | |
| fi | |
| if [ -z "$SHA" ] || [ -z "$VERSION" ]; then | |
| echo "::error::SHA or VERSION is empty (SHA='$SHA', VERSION='$VERSION')" | |
| exit 1 | |
| fi | |
| PATTERN='cagent-action/\.github/workflows/review-pr\.yml@[[:xdigit:]]\{40\} # v[0-9.]*' | |
| if ! grep -q "$PATTERN" "$FILE"; then | |
| echo "::error::Expected cagent-action reference pattern not found in $FILE — format may have changed" | |
| exit 1 | |
| fi | |
| sed -i "s|${PATTERN}|cagent-action/.github/workflows/review-pr.yml@${SHA} # ${VERSION}|" "$FILE" | |
| if git diff --quiet "$FILE"; then | |
| echo "File already up to date, skipping." | |
| echo "skip=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "Updated reference to ${SHA} # ${VERSION}" | |
| echo "skip=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Create or update PR | |
| if: steps.update.outputs.skip != 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.RELEASE_TOKEN }} | |
| VERSION: ${{ needs.release.outputs.version }} | |
| SHA: ${{ needs.release.outputs.sha }} | |
| run: | | |
| BRANCH="auto/update-cagent-action" | |
| RELEASE_URL="https://github.com/docker/cagent-action/releases/tag/$VERSION" | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git checkout -B "$BRANCH" | |
| git add .github/workflows/pr-review.yml | |
| git commit -m "chore: update cagent-action to $VERSION" | |
| # Force-push to handle both new and existing branches. | |
| # This branch is exclusively managed by this workflow, so --force is safe. | |
| git push --force origin "$BRANCH" | |
| EXISTING_PR=$(gh pr list --head "$BRANCH" --state open --json number --jq '.[0].number') | |
| PR_BODY="$(cat <<EOF | |
| ## Summary | |
| Updates \`cagent-action\` reference in \`pr-review.yml\` to [$VERSION]($RELEASE_URL). | |
| - **Commit**: \`${SHA}\` | |
| - **Version**: \`${VERSION}\` | |
| > Auto-generated by the [release](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) workflow. | |
| /skip-builds | |
| /skip-tests | |
| EOF | |
| )" | |
| if [ -n "$EXISTING_PR" ]; then | |
| echo "Updating existing PR #$EXISTING_PR" | |
| gh pr edit "$EXISTING_PR" \ | |
| --title "chore: update cagent-action to $VERSION" \ | |
| --body "$PR_BODY" \ | |
| --add-reviewer "derekmisler" | |
| else | |
| echo "Creating new PR" | |
| gh pr create \ | |
| --title "chore: update cagent-action to $VERSION" \ | |
| --body "$PR_BODY" \ | |
| --label "team/gordon" \ | |
| --label "merge/auto" \ | |
| --reviewer "derekmisler" | |
| fi | |
| publish-agent: | |
| name: Push review-pr agent to Docker Hub | |
| needs: release | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | |
| with: | |
| ref: ${{ needs.release.outputs.version }} | |
| - name: Install Docker Agent | |
| run: | | |
| set -e | |
| DOCKER_AGENT_VERSION=$(tr -d '[:space:]' < DOCKER_AGENT_VERSION) | |
| if [ -z "$DOCKER_AGENT_VERSION" ]; then | |
| echo "::error::Could not extract Docker Agent version from DOCKER_AGENT_VERSION" | |
| exit 1 | |
| fi | |
| echo "Using Docker Agent version from DOCKER_AGENT_VERSION: ${DOCKER_AGENT_VERSION}" | |
| curl -fL -o docker-agent \ | |
| "https://github.com/docker/docker-agent/releases/download/${DOCKER_AGENT_VERSION}/docker-agent-linux-amd64" | |
| chmod +x docker-agent | |
| sudo mv docker-agent /usr/local/bin/ | |
| - name: Docker Hub login | |
| env: | |
| HUB_USER: ${{ secrets.HUB_USER }} | |
| HUB_PAT: ${{ secrets.HUB_PAT }} | |
| run: | | |
| set -e | |
| if [ -z "${HUB_USER}" ]; then echo "::error::HUB_USER secret is not set"; exit 1; fi | |
| if [ -z "${HUB_PAT}" ]; then echo "::error::HUB_PAT secret is not set"; exit 1; fi | |
| echo "${HUB_PAT}" | docker login --username "${HUB_USER}" --password-stdin | |
| - name: Push agent | |
| env: | |
| HUB_ORG: ${{ secrets.HUB_ORG }} | |
| run: | | |
| set -e | |
| if [ -z "${HUB_ORG}" ]; then echo "::error::HUB_ORG secret is not set"; exit 1; fi | |
| cd review-pr/agents | |
| TELEMETRY_ENABLED=false docker agent share push pr-review.yaml "${HUB_ORG}/review-pr" | |
| - name: Upload README to Docker Hub | |
| env: | |
| HUB_USER: ${{ secrets.HUB_USER }} | |
| HUB_PAT: ${{ secrets.HUB_PAT }} | |
| HUB_ORG: ${{ secrets.HUB_ORG }} | |
| run: | | |
| set -e | |
| TOKEN=$(curl -sSf -X POST https://hub.docker.com/v2/users/login/ \ | |
| -H "Content-Type: application/json" \ | |
| -d "{\"username\":\"${HUB_USER}\",\"password\":\"${HUB_PAT}\"}" | jq -r .token) | |
| if [ -z "${TOKEN}" ] || [ "${TOKEN}" = "null" ]; then | |
| echo "::error::Failed to get Docker Hub API token" | |
| exit 1 | |
| fi | |
| curl -sSf -X PATCH "https://hub.docker.com/v2/namespaces/${HUB_ORG}/repositories/review-pr" \ | |
| -H "Authorization: Bearer ${TOKEN}" \ | |
| -H "Content-Type: application/json" \ | |
| -d "$(jq -n --arg desc "Docker Agent-powered PR review team. Analyzes code changes, posts reviews, and learns from feedback." \ | |
| --rawfile readme review-pr/README.md \ | |
| '{description: $desc, full_description: $readme}')" | |
| notify: | |
| name: Notify Slack | |
| needs: [release, publish-agent] | |
| if: success() | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Fetch release notes from GitHub | |
| id: release-notes | |
| env: | |
| GH_TOKEN: ${{ secrets.RELEASE_TOKEN }} | |
| VERSION: ${{ needs.release.outputs.version }} | |
| run: | | |
| if ! NOTES=$(gh release view "$VERSION" --repo docker/cagent-action --json body --jq '.body'); then | |
| echo "::warning::Failed to fetch release notes, using fallback" | |
| NOTES="Release ${VERSION} — see GitHub for details." | |
| fi | |
| echo "notes<<EOF" >> $GITHUB_OUTPUT | |
| echo "$NOTES" >> $GITHUB_OUTPUT | |
| echo "EOF" >> $GITHUB_OUTPUT | |
| - name: Generate Slack summary | |
| id: slack-summary | |
| # @latest resolves to the previous release here, not the one just created. | |
| # This is fine — the release-notes agent doesn't change between versions. | |
| # GitHub Actions requires static `uses:` values, so we can't pin dynamically. | |
| uses: docker/cagent-action@latest | |
| with: | |
| agent: agentcatalog/github-action-release-notes | |
| prompt: | | |
| Convert these release notes to a SHORT plain text Slack message. | |
| VERSION: ${{ needs.release.outputs.version }} | |
| RELEASE URL: https://github.com/docker/cagent-action/releases/tag/${{ needs.release.outputs.version }} | |
| ORIGINAL RELEASE NOTES: | |
| ${{ steps.release-notes.outputs.notes }} | |
| CRITICAL - PLAIN TEXT ONLY: | |
| This goes through Slack Workflow Builder which does NOT support formatting. | |
| - DO NOT use *asterisks* - they show literally | |
| - DO NOT use <url|text> links - use plain URLs | |
| - DO use :emoji: codes - those work | |
| - DO use bullet points with • | |
| - Use CAPS for section headers instead of bold | |
| OUTPUT REQUIREMENTS: | |
| - Keep it SHORT - max 5-7 bullet points total | |
| - Include the release URL at the end (plain URL, not link syntax) | |
| - Output ONLY the Slack message, nothing else | |
| - Skip the "Platforms" line — this is a GitHub Action, not a binary | |
| EXAMPLE FORMAT: | |
| :tada: cagent-action v1.3.0 Released | |
| :package: WHAT'S NEW | |
| • New feature one | |
| • New feature two | |
| :wrench: IMPROVEMENTS | |
| • Enhancement description | |
| :bug: BUG FIXES | |
| • Fix description | |
| :inbox_tray: Release: https://github.com/docker/cagent-action/releases/tag/v1.3.0 | |
| anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }} | |
| - name: Send Slack notification | |
| env: | |
| SLACK_WEBHOOK_URL: ${{ secrets.SLACK_RELEASE_WEBHOOK }} | |
| VERSION: ${{ needs.release.outputs.version }} | |
| SLACK_SUMMARY_FILE: ${{ steps.slack-summary.outputs.output-file }} | |
| run: | | |
| if [ -z "$SLACK_WEBHOOK_URL" ]; then | |
| echo "⚠️ SLACK_RELEASE_WEBHOOK not configured, skipping notification" | |
| exit 0 | |
| fi | |
| RELEASE_URL="https://github.com/docker/cagent-action/releases/tag/${VERSION}" | |
| # Use the AI-generated Slack summary | |
| if [ -f "$SLACK_SUMMARY_FILE" ] && [ -s "$SLACK_SUMMARY_FILE" ]; then | |
| MESSAGE_TEXT=$(cat "$SLACK_SUMMARY_FILE") | |
| else | |
| # Fallback message if summary generation failed | |
| MESSAGE_TEXT=$(printf ':tada: cagent-action %s Released\n\n:package: See the full release notes for details.\n\n:inbox_tray: Release: %s' "$VERSION" "$RELEASE_URL") | |
| fi | |
| # Create payload with text and release_url fields (matching Slack Workflow Builder webhook) | |
| PAYLOAD=$(jq -n \ | |
| --arg text "$MESSAGE_TEXT" \ | |
| --arg release_url "$RELEASE_URL" \ | |
| '{ | |
| text: $text, | |
| release_url: $release_url | |
| }') | |
| if ! HTTP_CODE=$(curl -s -w "%{http_code}" -o /tmp/slack_response.txt -X POST "$SLACK_WEBHOOK_URL" \ | |
| -H "Content-Type: application/json" \ | |
| -d "$PAYLOAD"); then | |
| echo "⚠️ Slack notification failed (curl error)" | |
| elif [ "$HTTP_CODE" -eq 200 ]; then | |
| echo "✅ Slack notification sent" | |
| else | |
| echo "⚠️ Slack notification failed (HTTP $HTTP_CODE)" | |
| cat /tmp/slack_response.txt | |
| fi | |
| # Don't fail the workflow if Slack fails |