Skip to content

Latest commit

 

History

History
657 lines (552 loc) · 25.8 KB

File metadata and controls

657 lines (552 loc) · 25.8 KB
name Release
description Build, test, and release gh-aw extension, then generate and prepend release highlights
true
roles workflow_dispatch
admin
maintainer
inputs
release_type
description required type default options
Release type (patch, minor, or major)
true
choice
patch
patch
minor
major
permissions
contents pull-requests actions issues
read
read
read
read
engine copilot
timeout-minutes 20
network
allowed
defaults
node
github.github.com
safe-outputs
update-release
imports
shared/community-attribution.md
jobs
config push_tag sync_actions release
needs runs-on outputs steps
pre_activation
activation
ubuntu-latest
release_tag
${{ steps.compute_config.outputs.release_tag }}
name uses with
Checkout repository
actions/checkout@v6.0.2
fetch-depth persist-credentials
0
false
name id uses with
Compute release configuration
compute_config
actions/github-script@v8
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}`);
needs runs-on permissions steps
pre_activation
activation
config
ubuntu-latest
contents
write
name uses with
Checkout repository
actions/checkout@v6.0.2
fetch-depth persist-credentials
0
true
name env run
Create or update tag
RELEASE_TAG
${{ needs.config.outputs.release_tag }}
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 uses with
Setup Go
actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c
go-version-file cache
go.mod
false
name env run
Build binaries
RELEASE_TAG
${{ needs.config.outputs.release_tag }}
echo "Building binaries for release: $RELEASE_TAG" bash scripts/build-release.sh "$RELEASE_TAG" echo "✓ Binaries built successfully"
name uses
Setup Docker Buildx (pre-validation)
docker/setup-buildx-action@v4
name uses with
Build Docker image (validation only)
docker/build-push-action@v7
context platforms push load build-args cache-from
.
linux/amd64
false
false
BINARY=dist/linux-amd64
type=gha
name uses with
Upload release binaries
actions/upload-artifact@v7
name path retention-days
release-binaries-${{ needs.config.outputs.release_tag }}
dist/
1
name env run
Notify - run sync actions and merge PR
RELEASE_TAG
${{ needs.config.outputs.release_tag }}
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."
needs runs-on environment steps
pre_activation
activation
config
push_tag
ubuntu-latest
gh-aw-actions-release
name run
Await manual approval
echo "Manual approval received. Continuing release."
needs runs-on permissions outputs steps
pre_activation
activation
config
sync_actions
ubuntu-latest
contents packages id-token attestations
write
write
write
write
release_id
${{ steps.get_release.outputs.release_id }}
name env run
Verify tag exists in gh-aw-actions
RELEASE_TAG GH_TOKEN
${{ needs.config.outputs.release_tag }}
${{ secrets.GITHUB_TOKEN }}
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 uses with
Checkout repository
actions/checkout@v6.0.2
fetch-depth persist-credentials
0
true
name uses with
Download release binaries
actions/download-artifact@v8.0.1
name path
release-binaries-${{ needs.config.outputs.release_tag }}
dist/
name id env run
Create GitHub release
get_release
GH_TOKEN RELEASE_TAG
${{ secrets.GITHUB_TOKEN }}
${{ needs.config.outputs.release_tag }}
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 run
Download Go modules
go mod download
name uses with
Generate SBOM (SPDX format)
anchore/sbom-action@v0.24.0
artifact-name output-file format
sbom.spdx.json
sbom.spdx.json
spdx-json
name uses with
Generate SBOM (CycloneDX format)
anchore/sbom-action@v0.24.0
artifact-name output-file format
sbom.cdx.json
sbom.cdx.json
cyclonedx-json
name run
Audit SBOM files for secrets
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 uses with
Upload SBOM artifacts
actions/upload-artifact@v7
name path retention-days
sbom-artifacts
sbom.spdx.json sbom.cdx.json
7
name env run
Upload SBOM files to release
GH_TOKEN RELEASE_TAG
${{ secrets.GITHUB_TOKEN }}
${{ needs.config.outputs.release_tag }}
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 uses
Setup Docker Buildx
docker/setup-buildx-action@v4
name uses with
Log in to GitHub Container Registry
docker/login-action@v4.1.0
registry username password
ghcr.io
${{ github.actor }}
${{ secrets.GITHUB_TOKEN }}
name id uses with
Extract metadata for Docker
meta
docker/metadata-action@v6
images tags
ghcr.io/${{ github.repository }}
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 id uses with
Build and push Docker image (amd64)
build
docker/build-push-action@v7
context platforms push tags labels build-args cache-from cache-to sbom provenance
.
linux/amd64
true
${{ steps.meta.outputs.tags }}
${{ steps.meta.outputs.labels }}
BINARY=dist/linux-amd64
type=gha
type=gha,mode=max
true
mode=max
steps
name env run
Setup release environment
RELEASE_ID RELEASE_TAG GH_TOKEN
${{ needs.release.outputs.release_id }}
${{ needs.config.outputs.release_tag }}
${{ secrets.GITHUB_TOKEN }}
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&gt;/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 \ &gt; /tmp/gh-aw/release-data/community_issues_closed_in_window.json 2&gt;/dev/null \ || echo "[]" &gt; /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

# 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:

## 🌟 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}):

## 🎉 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):

## 🔧 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.

{"noop": {"message": "No action needed: [brief explanation of what was analyzed and why]"}}