Release #331
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 | |
| description: Build, test, and release gh-aw extension, then generate and prepend release highlights | |
| on: | |
| roles: | |
| - admin | |
| - maintainer | |
| workflow_dispatch: | |
| inputs: | |
| release_type: | |
| description: 'Release type (patch, minor, or major)' | |
| required: true | |
| type: choice | |
| default: patch | |
| options: | |
| - patch | |
| - minor | |
| - major | |
| permissions: | |
| contents: read | |
| pull-requests: read | |
| actions: read | |
| issues: read | |
| engine: copilot | |
| timeout-minutes: 20 | |
| network: | |
| allowed: | |
| - defaults | |
| - node | |
| - "github.github.com" | |
| safe-outputs: | |
| update-release: | |
| imports: | |
| - shared/community-attribution.md | |
| jobs: | |
| config: | |
| needs: ["pre_activation", "activation"] | |
| runs-on: ubuntu-latest | |
| outputs: | |
| release_tag: ${{ steps.compute_config.outputs.release_tag }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6.0.2 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: Compute release configuration | |
| id: compute_config | |
| uses: actions/github-script@v8 | |
| with: | |
| script: | | |
| const releaseType = context.payload.inputs.release_type; | |
| console.log(`Computing next version for release type: ${releaseType}`); | |
| // Get all releases and sort by semver to find the actual latest version | |
| const { data: releases } = await github.rest.repos.listReleases({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| per_page: 100 | |
| }); | |
| // Parse semver and sort releases by version (newest first) | |
| const parseSemver = (tag) => { | |
| const match = tag.match(/^v?(\d+)\.(\d+)\.(\d+)/); | |
| if (!match) return null; | |
| return { | |
| tag, | |
| major: parseInt(match[1], 10), | |
| minor: parseInt(match[2], 10), | |
| patch: parseInt(match[3], 10) | |
| }; | |
| }; | |
| const sortedReleases = releases | |
| .map(r => parseSemver(r.tag_name)) | |
| .filter(v => v !== null) | |
| .sort((a, b) => { | |
| if (a.major !== b.major) return b.major - a.major; | |
| if (a.minor !== b.minor) return b.minor - a.minor; | |
| return b.patch - a.patch; | |
| }); | |
| if (sortedReleases.length === 0) { | |
| core.setFailed('No existing releases found. Cannot determine base version for incrementing. Please create an initial release manually (e.g., v0.1.0).'); | |
| return; | |
| } | |
| const latestTag = sortedReleases[0].tag; | |
| console.log(`Latest release tag (semver-sorted): ${latestTag}`); | |
| // Parse version components (strip 'v' prefix) | |
| const version = latestTag.replace(/^v/, ''); | |
| let [major, minor, patch] = version.split('.').map(Number); | |
| // Increment based on release type | |
| switch (releaseType) { | |
| case 'major': | |
| major += 1; | |
| minor = 0; | |
| patch = 0; | |
| break; | |
| case 'minor': | |
| minor += 1; | |
| patch = 0; | |
| break; | |
| case 'patch': | |
| patch += 1; | |
| break; | |
| } | |
| const releaseTag = `v${major}.${minor}.${patch}`; | |
| console.log(`Computed release tag: ${releaseTag}`); | |
| // Sanity check: Verify the computed tag doesn't already exist | |
| const existingRelease = releases.find(r => r.tag_name === releaseTag); | |
| if (existingRelease) { | |
| core.setFailed(`Release tag ${releaseTag} already exists (created ${existingRelease.created_at}). Cannot create duplicate release. Please check existing releases.`); | |
| return; | |
| } | |
| // Also check if tag exists in git (in case release was deleted but tag remains) | |
| try { | |
| await github.rest.git.getRef({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| ref: `tags/${releaseTag}` | |
| }); | |
| // If we get here, the tag exists | |
| core.setFailed(`Git tag ${releaseTag} already exists in the repository. Cannot create duplicate tag. Please delete the existing tag or use a different version.`); | |
| return; | |
| } catch (error) { | |
| // 404 means tag doesn't exist, which is what we want | |
| if (error.status !== 404) { | |
| throw error; // Re-throw unexpected errors | |
| } | |
| } | |
| core.setOutput('release_tag', releaseTag); | |
| console.log(`✓ Release tag: ${releaseTag}`); | |
| push_tag: | |
| needs: ["pre_activation", "activation", "config"] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6.0.2 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: true | |
| - name: Create or update tag | |
| env: | |
| RELEASE_TAG: ${{ needs.config.outputs.release_tag }} | |
| run: | | |
| echo "Creating tag: $RELEASE_TAG" | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git tag "$RELEASE_TAG" | |
| git push origin "$RELEASE_TAG" | |
| echo "✓ Tag created: $RELEASE_TAG" | |
| - name: Setup Go | |
| uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 | |
| with: | |
| go-version-file: go.mod | |
| cache: false # Disabled for release security - prevent cache poisoning attacks | |
| - name: Build binaries | |
| env: | |
| RELEASE_TAG: ${{ needs.config.outputs.release_tag }} | |
| run: | | |
| echo "Building binaries for release: $RELEASE_TAG" | |
| bash scripts/build-release.sh "$RELEASE_TAG" | |
| echo "✓ Binaries built successfully" | |
| - name: Setup Docker Buildx (pre-validation) | |
| uses: docker/setup-buildx-action@v4 | |
| - name: Build Docker image (validation only) | |
| uses: docker/build-push-action@v7 | |
| with: | |
| context: . | |
| platforms: linux/amd64 | |
| push: false | |
| load: false | |
| build-args: | | |
| BINARY=dist/linux-amd64 | |
| cache-from: type=gha | |
| - name: Upload release binaries | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: release-binaries-${{ needs.config.outputs.release_tag }} | |
| path: dist/ | |
| retention-days: 1 | |
| - name: Notify - run sync actions and merge PR | |
| env: | |
| RELEASE_TAG: ${{ needs.config.outputs.release_tag }} | |
| run: | | |
| echo "## Manual Sync Actions Required" >> "$GITHUB_STEP_SUMMARY" | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| echo "The following manual steps must be completed in **github/gh-aw-actions** before this release continues:" >> "$GITHUB_STEP_SUMMARY" | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| echo "1. Trigger the **sync-actions** workflow in github/gh-aw-actions:" >> "$GITHUB_STEP_SUMMARY" | |
| echo " https://github.com/github/gh-aw-actions/actions/workflows/sync-actions.yml" >> "$GITHUB_STEP_SUMMARY" | |
| echo "2. Merge the PR created by the sync-actions workflow in **github/gh-aw-actions**" >> "$GITHUB_STEP_SUMMARY" | |
| echo "3. Verify that tag **\`${RELEASE_TAG}\`** exists in github/gh-aw-actions" >> "$GITHUB_STEP_SUMMARY" | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| echo "Once the above steps are complete, approve the **gh-aw-actions-release** environment gate to continue the release." >> "$GITHUB_STEP_SUMMARY" | |
| echo "Sync actions instructions written for release: $RELEASE_TAG" | |
| echo "Ensure the sync-actions job has been run and the PR merged in github/gh-aw-actions before approving." | |
| sync_actions: | |
| needs: ["pre_activation", "activation", "config", "push_tag"] | |
| runs-on: ubuntu-latest | |
| environment: gh-aw-actions-release | |
| steps: | |
| - name: Await manual approval | |
| run: echo "Manual approval received. Continuing release." | |
| release: | |
| needs: ["pre_activation", "activation", "config", "sync_actions"] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| packages: write | |
| id-token: write | |
| attestations: write | |
| outputs: | |
| release_id: ${{ steps.get_release.outputs.release_id }} | |
| steps: | |
| - name: Verify tag exists in gh-aw-actions | |
| env: | |
| RELEASE_TAG: ${{ needs.config.outputs.release_tag }} | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| echo "Verifying tag $RELEASE_TAG exists in github/gh-aw-actions..." | |
| if gh api "repos/github/gh-aw-actions/git/refs/tags/$RELEASE_TAG" --jq '.ref' > /dev/null 2>&1; then | |
| echo "✓ Tag $RELEASE_TAG exists in github/gh-aw-actions" | |
| else | |
| echo "Error: Tag $RELEASE_TAG not found in github/gh-aw-actions after sync" | |
| exit 1 | |
| fi | |
| - name: Checkout repository | |
| uses: actions/checkout@v6.0.2 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: true | |
| - name: Download release binaries | |
| uses: actions/download-artifact@v8.0.1 | |
| with: | |
| name: release-binaries-${{ needs.config.outputs.release_tag }} | |
| path: dist/ | |
| - name: Create GitHub release | |
| id: get_release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| RELEASE_TAG: ${{ needs.config.outputs.release_tag }} | |
| run: | | |
| echo "Creating GitHub release: $RELEASE_TAG" | |
| # Create release with binaries (SBOM files will be added later) | |
| gh release create "$RELEASE_TAG" \ | |
| dist/* \ | |
| --title "$RELEASE_TAG" \ | |
| --generate-notes \ | |
| --prerelease \ | |
| --latest=false | |
| # Get release ID | |
| RELEASE_ID=$(gh release view "$RELEASE_TAG" --json databaseId --jq '.databaseId') | |
| echo "release_id=$RELEASE_ID" >> "$GITHUB_OUTPUT" | |
| echo "✓ Release created: $RELEASE_TAG" | |
| echo "✓ Release ID: $RELEASE_ID" | |
| - name: Download Go modules | |
| run: go mod download | |
| - name: Generate SBOM (SPDX format) | |
| uses: anchore/sbom-action@v0.24.0 | |
| with: | |
| artifact-name: sbom.spdx.json | |
| output-file: sbom.spdx.json | |
| format: spdx-json | |
| - name: Generate SBOM (CycloneDX format) | |
| uses: anchore/sbom-action@v0.24.0 | |
| with: | |
| artifact-name: sbom.cdx.json | |
| output-file: sbom.cdx.json | |
| format: cyclonedx-json | |
| - name: Audit SBOM files for secrets | |
| run: | | |
| echo "Auditing SBOM files for potential secrets..." | |
| if grep -rE "GITHUB_TOKEN|SECRET|PASSWORD|API_KEY|PRIVATE_KEY" sbom.*.json; then | |
| echo "Error: Potential secrets found in SBOM files" | |
| exit 1 | |
| fi | |
| echo "✓ No secrets detected in SBOM files" | |
| - name: Upload SBOM artifacts | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: sbom-artifacts | |
| path: | | |
| sbom.spdx.json | |
| sbom.cdx.json | |
| retention-days: 7 # Minimize exposure window | |
| - name: Upload SBOM files to release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| RELEASE_TAG: ${{ needs.config.outputs.release_tag }} | |
| run: | | |
| echo "Uploading SBOM files to release: $RELEASE_TAG" | |
| gh release upload "$RELEASE_TAG" \ | |
| sbom.spdx.json \ | |
| sbom.cdx.json | |
| echo "✓ SBOM files uploaded to release" | |
| - name: Setup Docker Buildx | |
| uses: docker/setup-buildx-action@v4 | |
| - name: Log in to GitHub Container Registry | |
| uses: docker/login-action@v4.1.0 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Extract metadata for Docker | |
| id: meta | |
| uses: docker/metadata-action@v6 | |
| with: | |
| images: ghcr.io/${{ github.repository }} | |
| tags: | | |
| type=semver,pattern={{version}} | |
| type=semver,pattern={{major}}.{{minor}} | |
| type=semver,pattern={{major}} | |
| type=sha,format=long | |
| type=raw,value=latest,enable={{is_default_branch}} | |
| - name: Build and push Docker image (amd64) | |
| id: build | |
| uses: docker/build-push-action@v7 | |
| with: | |
| context: . | |
| platforms: linux/amd64 | |
| push: true | |
| tags: ${{ steps.meta.outputs.tags }} | |
| labels: ${{ steps.meta.outputs.labels }} | |
| build-args: | | |
| BINARY=dist/linux-amd64 | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| sbom: true | |
| provenance: mode=max | |
| steps: | |
| - name: Setup release environment | |
| env: | |
| RELEASE_ID: ${{ needs.release.outputs.release_id }} | |
| RELEASE_TAG: ${{ needs.config.outputs.release_tag }} | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -e | |
| mkdir -p /tmp/gh-aw/release-data | |
| # Use the release ID and tag from the release job | |
| echo "Release ID from release job: $RELEASE_ID" | |
| echo "Release tag from release job: $RELEASE_TAG" | |
| echo "Processing release: $RELEASE_TAG" | |
| echo "RELEASE_TAG=$RELEASE_TAG" >> "$GITHUB_ENV" | |
| # Get the current release information | |
| # Use release ID to fetch release data | |
| gh api "/repos/${{ github.repository }}/releases/$RELEASE_ID" > /tmp/gh-aw/release-data/current_release.json | |
| echo "✓ Fetched current release information" | |
| # Get the previous release to determine the range | |
| PREV_RELEASE_TAG=$(gh release list --limit 2 --json tagName --jq '.[1].tagName // empty') | |
| if [ -z "$PREV_RELEASE_TAG" ]; then | |
| echo "No previous release found. This appears to be the first release." | |
| echo "PREV_RELEASE_TAG=" >> "$GITHUB_ENV" | |
| touch /tmp/gh-aw/release-data/pull_requests.json | |
| echo "[]" > /tmp/gh-aw/release-data/pull_requests.json | |
| else | |
| echo "Previous release: $PREV_RELEASE_TAG" | |
| echo "PREV_RELEASE_TAG=$PREV_RELEASE_TAG" >> "$GITHUB_ENV" | |
| # Get commits between releases | |
| echo "Fetching commits between $PREV_RELEASE_TAG and $RELEASE_TAG..." | |
| git fetch --unshallow 2>/dev/null || git fetch --depth=1000 | |
| # Get all merged PRs between the two releases (include closingIssuesReferences for attribution) | |
| echo "Fetching pull requests merged between releases..." | |
| PREV_PUBLISHED_AT=$(gh release view "$PREV_RELEASE_TAG" --json publishedAt --jq .publishedAt) | |
| CURR_PUBLISHED_AT=$(gh release view "$RELEASE_TAG" --json publishedAt --jq .publishedAt) | |
| gh pr list \ | |
| --state merged \ | |
| --limit 1000 \ | |
| --json number,title,author,labels,mergedAt,url,body,closingIssuesReferences \ | |
| --jq "[.[] | select(.mergedAt >= \"$PREV_PUBLISHED_AT\" and .mergedAt <= \"$CURR_PUBLISHED_AT\")]" \ | |
| > /tmp/gh-aw/release-data/pull_requests.json | |
| PR_COUNT=$(jq length "/tmp/gh-aw/release-data/pull_requests.json") | |
| echo "✓ Fetched $PR_COUNT pull requests" | |
| fi | |
| # Build closing references index from GitHub-native closingIssuesReferences | |
| # Maps each closed issue number -> list of PR numbers that directly close it | |
| echo "Building closing references index from GitHub-native PR links..." | |
| # Use a nested reduce so the outer body always returns the accumulator, | |
| # even when closingIssuesReferences is empty (avoids jq setting acc to null). | |
| jq ' | |
| reduce .[] as $pr ( | |
| {}; | |
| reduce ($pr.closingIssuesReferences // [])[] as $issue ( | |
| .; | |
| ($issue.number | tostring) as $key | | |
| .[$key] = (.[$key] // []) + [$pr.number] | |
| ) | |
| ) | |
| ' /tmp/gh-aw/release-data/pull_requests.json \ | |
| > /tmp/gh-aw/release-data/closing_refs_by_issue.json 2>/dev/null \ | |
| || echo "{}" > /tmp/gh-aw/release-data/closing_refs_by_issue.json | |
| # Also expose to community-data dir so shared attribution strategy can reference it | |
| cp /tmp/gh-aw/release-data/closing_refs_by_issue.json /tmp/gh-aw/community-data/closing_refs_by_issue.json | |
| cp /tmp/gh-aw/release-data/pull_requests.json /tmp/gh-aw/community-data/pull_requests.json | |
| DIRECT_CLOSE_COUNT=$(jq 'keys | length' /tmp/gh-aw/release-data/closing_refs_by_issue.json) | |
| echo "✓ Found $DIRECT_CLOSE_COUNT issues with GitHub-native closing PR references" | |
| # Find community issues closed during this release window (candidates for attribution review) | |
| if [ -n "$PREV_PUBLISHED_AT" ]; then | |
| jq --arg prev "$PREV_PUBLISHED_AT" --arg curr "$CURR_PUBLISHED_AT" \ | |
| '[.[] | select(.closedAt != null and .closedAt >= $prev and .closedAt <= $curr)]' \ | |
| /tmp/gh-aw/community-data/community_issues.json \ | |
| > /tmp/gh-aw/release-data/community_issues_closed_in_window.json 2>/dev/null \ | |
| || echo "[]" > /tmp/gh-aw/release-data/community_issues_closed_in_window.json | |
| CLOSED_IN_WINDOW=$(jq length /tmp/gh-aw/release-data/community_issues_closed_in_window.json) | |
| echo "✓ Found $CLOSED_IN_WINDOW community issues closed in this release window" | |
| else | |
| echo "[]" > /tmp/gh-aw/release-data/community_issues_closed_in_window.json | |
| fi | |
| # Get the CHANGELOG.md content around this version | |
| if [ -f "CHANGELOG.md" ]; then | |
| cp CHANGELOG.md /tmp/gh-aw/release-data/CHANGELOG.md | |
| echo "✓ Copied CHANGELOG.md for reference" | |
| fi | |
| # List documentation files for linking | |
| find docs -type f -name "*.md" 2>/dev/null > /tmp/gh-aw/release-data/docs_files.txt || echo "No docs directory found" | |
| echo "✓ Setup complete." | |
| echo " Release data: /tmp/gh-aw/release-data/ (current_release.json, pull_requests.json," | |
| echo " closing_refs_by_issue.json, community_issues_closed_in_window.json," | |
| echo " CHANGELOG.md (if exists), docs_files.txt)" | |
| echo " Community data: /tmp/gh-aw/community-data/ (community_issues.json," | |
| echo " closing_refs_by_issue.json, pull_requests.json)" | |
| --- | |
| # Release Highlights Generator | |
| Generate an engaging release highlights summary for **${{ github.repository }}** release `${RELEASE_TAG}`. | |
| **Release ID**: ${{ needs.release.outputs.release_id }} | |
| ## Data Available | |
| Release-specific data is pre-fetched in `/tmp/gh-aw/release-data/`: | |
| - `current_release.json` - Release metadata (tag, name, dates, existing body) | |
| - `pull_requests.json` - PRs merged between `${PREV_RELEASE_TAG}` and `${RELEASE_TAG}` (includes `closingIssuesReferences` for each PR; empty array if first release) | |
| - `closing_refs_by_issue.json` - Map of `{issue_number: [pr_numbers]}` built from GitHub-native closing references in merged PRs | |
| - `community_issues_closed_in_window.json` - Community issues whose `closedAt` falls within this release window (attribution candidates) | |
| - `CHANGELOG.md` - Full changelog for context (if exists) | |
| - `docs_files.txt` - Available documentation files for linking | |
| Community data is pre-fetched in `/tmp/gh-aw/community-data/` (by the shared community-attribution step): | |
| - `community_issues.json` - All issues labeled `community` (issue number, title, author, closedAt, createdAt, url) | |
| - `closing_refs_by_issue.json` - Same closing references index, mirrored for the shared attribution strategy | |
| - `pull_requests.json` - Same PR list, mirrored for the shared attribution strategy | |
| ## Output Requirements | |
| Create a **"🌟 Release Highlights"** section that: | |
| - Is concise and scannable (users grasp key changes in 30 seconds) | |
| - Uses professional, enthusiastic tone (not overly casual) | |
| - Categorizes changes logically (features, fixes, docs, breaking changes) | |
| - Links to relevant documentation where helpful | |
| - Focuses on user impact (why changes matter, not just what changed) | |
| ## Workflow | |
| ### 1. Load Data | |
| ```bash | |
| # View release metadata | |
| cat /tmp/gh-aw/release-data/current_release.json | jq | |
| # List PRs (empty if first release) | |
| cat /tmp/gh-aw/release-data/pull_requests.json | jq -r '.[] | "- #\(.number): \(.title) by @\(.author.login)"' | |
| # List community issues (fetched by shared community-attribution step) | |
| cat /tmp/gh-aw/community-data/community_issues.json | jq -r '.[] | "- #\(.number): \(.title) by @\(.author.login)"' | |
| # View GitHub-native closing references (issue -> [PRs]) | |
| cat /tmp/gh-aw/release-data/closing_refs_by_issue.json | jq | |
| # List community issues closed in this release window (attribution candidates) | |
| cat /tmp/gh-aw/release-data/community_issues_closed_in_window.json | jq -r '.[] | "- #\(.number): \(.title) by @\(.author.login) (closed: \(.closedAt))"' | |
| # Check CHANGELOG context | |
| head -100 /tmp/gh-aw/release-data/CHANGELOG.md 2>/dev/null || echo "No CHANGELOG" | |
| # View available docs | |
| cat /tmp/gh-aw/release-data/docs_files.txt | |
| ``` | |
| ### 2. Identify Community Contributions | |
| The `community` label is the **primary attribution signal** — apply the | |
| four-tier Community Attribution Strategy from the imported shared component | |
| (`shared/community-attribution.md`) to attribute all community-labeled issues | |
| that were closed in this release window. Use `/tmp/gh-aw/release-data/community_issues_closed_in_window.json` | |
| as the set of candidates and `/tmp/gh-aw/release-data/closing_refs_by_issue.json` | |
| as the attribution index. | |
| ### 3. Categorize & Prioritize | |
| Group PRs by category (omit categories with no items): | |
| - **🐛 Bug Fixes** - Issue resolutions | |
| - **⚡ Performance** - Speed/efficiency improvements | |
| - **📚 Documentation** - Guide/reference updates | |
| - **⚠️ Breaking Changes** - Requires user action (ALWAYS list first if present) | |
| - **🔧 Internal** - Refactoring, dependencies (usually omit from highlights) | |
| ### 4. Write Highlights | |
| Structure: | |
| ```markdown | |
| ## 🌟 Release Highlights | |
| [1-2 sentence summary of the release theme/focus] | |
| ### ⚠️ Breaking Changes | |
| [If any - list FIRST with migration guidance] | |
| ### ✨ What's New | |
| [Top 3-5 features with user benefit, link docs when relevant] | |
| ### 🐛 Bug Fixes & Improvements | |
| [Notable fixes - focus on user impact] | |
| ### 📚 Documentation | |
| [Only if significant doc additions/improvements] | |
| ### 🌍 Community Contributions | |
| [Only if any community-labeled issues are resolved in this release] | |
| A huge thank you to the community members who reported issues that were resolved in this release: | |
| - **@[author]** for [issue title] ([#number](url)) | |
| - _(via follow-up #N)_ — include only when attribution was confirmed through a follow-up issue chain | |
| [One entry per community issue author. Omit this section entirely if no community issues are resolved.] | |
| ### ⚠️ Attribution Candidates Need Review | |
| [Only if Tier 4 found community issues closed in this release window with no confirmed linkage] | |
| The following community issues were closed during this release window but could not be automatically linked to a specific merged PR. Please verify whether they should be credited: | |
| - **@[author]** for [issue title] ([#number](url)) — closed [date], no confirmed PR linkage found | |
| [Omit this section entirely if all closed community issues have confirmed attribution.] | |
| --- | |
| For complete details, see [CHANGELOG](https://github.com/github/gh-aw/blob/main/CHANGELOG.md). | |
| ``` | |
| **Writing Guidelines:** | |
| - Lead with benefits: "GitHub MCP now supports remote mode" not "Added remote mode" | |
| - Be specific: "Reduced compilation time by 40%" not "Faster compilation" | |
| - Skip internal changes unless they have user impact | |
| - Use docs links: `[Learn more](https://github.github.com/gh-aw/path/)` | |
| - Celebrate community contributors: thank each issue author by name with a link to their issue | |
| ### 5. Handle Special Cases | |
| **First Release** (no `${PREV_RELEASE_TAG}`): | |
| ```markdown | |
| ## 🎉 First Release | |
| Welcome to the inaugural release! This version includes [core capabilities]. | |
| ### Key Features | |
| [List primary features with brief descriptions] | |
| ``` | |
| **Maintenance Release** (no user-facing changes): | |
| ```markdown | |
| ## 🔧 Maintenance Release | |
| Dependency updates and internal improvements to keep things running smoothly. | |
| ``` | |
| ## Output Format | |
| **CRITICAL**: You MUST call the `update_release` MCP tool to update the release with the generated highlights. | |
| **HOW TO CALL THE TOOL:** | |
| The `update_release` tool is an **MCP (Model Context Protocol) tool**, not a bash command or file operation. | |
| **✅ CORRECT - Call the MCP tool directly:** | |
| ``` | |
| safeoutputs/update_release( | |
| tag="v0.38.1", | |
| operation="prepend", | |
| body="## 🌟 Release Highlights\n\n[Your complete markdown highlights here]" | |
| ) | |
| ``` | |
| **❌ INCORRECT - DO NOT:** | |
| - Write JSON files manually (e.g., `/tmp/gh-aw/safeoutputs/update_release_001.json`) | |
| - Use bash to simulate tool calls | |
| - Create scripts that write to outputs.jsonl | |
| - Use any file operations - the MCP tool handles everything | |
| **Required Parameters:** | |
| - `tag` - Release tag from `${RELEASE_TAG}` environment variable (e.g., "v0.38.1") | |
| - `operation` - Must be `"prepend"` to add before existing notes | |
| - `body` - Complete markdown content (include all formatting, emojis, links) | |
| **IMPORTANT**: The tool is accessed via the MCP gateway as `safeoutputs/update_release`. When you call this tool, the MCP server automatically writes to `/opt/gh-aw/safeoutputs/outputs.jsonl`. | |
| **WARNING**: If you don't call the MCP tool properly, the release notes will NOT be updated! | |
| **Documentation Base URLs:** | |
| - User docs: `https://github.github.com/gh-aw/` | |
| - Reference: `https://github.github.com/gh-aw/reference/` | |
| - Setup: `https://github.github.com/gh-aw/setup/` | |
| Verify paths exist in `docs_files.txt` before linking. | |
| **Important**: If no action is needed after completing your analysis, you **MUST** call the `noop` safe-output tool with a brief explanation. Failing to call any safe-output tool is the most common cause of safe-output workflow failures. | |
| ```json | |
| {"noop": {"message": "No action needed: [brief explanation of what was analyzed and why]"}} | |
| ``` |