You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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
threat-detection
imports
shared/community-attribution.md
shared/otlp.md
jobs
validate_container_pins
config
push_tag
defender
sync_actions
release
needs
runs-on
steps
pre_activation
activation
ubuntu-latest
name
uses
with
Checkout repository
actions/checkout@v7.0.0
persist-credentials
name
run
Validate container SHA pins in actions-lock.json files
echo "Validating container SHA pins in actions-lock.json files..."
LOCK_FILES=()
while IFS= read -r -d '' f; do
LOCK_FILES+=("$f")
done < <(find . -name "actions-lock.json" -not -path "*/node_modules/*" -print0)
if [ ${#LOCK_FILES[@]} -eq 0 ]; then
echo "❌ No actions-lock.json files found in the repository."
echo " Run 'gh aw compile' to generate lock files with resolved container SHA pins."
exit 1
fi
FAILED=0
for lock_file in "${LOCK_FILES[@]}"; do
echo "Checking: $lock_file"
container_count=$(jq '.containers | length // 0' "$lock_file")
echo " Found $container_count container entries"
if [ "$container_count" -eq 0 ]; then
echo " ✓ No containers to validate"
continue
fi
missing_pins=$(jq -r '
.containers // {} |
to_entries[] |
select(
((.value.digest // "") | test("^sha256:[a-f0-9]{64}$") | not) or
((.value.pinned_image // "") | test("@sha256:[a-f0-9]{64}$") | not)
) |
.key
' "$lock_file")
if [ -n "$missing_pins" ]; then
echo " ❌ Missing or invalid SHA pins for:"
echo "$missing_pins" | while read -r image; do
echo " - $image"
done
FAILED=1
else
echo " ✓ All $container_count containers have valid SHA pins"
fi
done
if [ "$FAILED" -eq 1 ]; then
echo ""
echo "❌ Validation failed: Some container images are missing SHA pins."
echo " Run 'gh aw compile' to resolve and cache container SHA pins before releasing."
exit 1
fi
echo ""
echo "✓ All container SHA pins are resolved and cached in actions-lock.json files."
needs
runs-on
outputs
steps
pre_activation
activation
validate_container_pins
ubuntu-latest
release_tag
${{ steps.compute_config.outputs.release_tag }}
name
uses
with
Checkout repository
actions/checkout@v7.0.0
fetch-depth
persist-credentials
0
false
name
id
uses
with
Compute Release Config
compute_config
actions/github-script@v9.0.0
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;
}
// Helper: check whether a given tag already exists (as a release or git ref)
const tagExists = async (tagName) => {
const releaseExists = releases.some(r => r.tag_name === tagName);
if (releaseExists) return true;
try {
await github.rest.git.getRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: `tags/${tagName}`
});
return true; // tag ref exists
} catch (error) {
if (error.status === 404) return false;
throw new Error(`Failed to check if tag ${tagName} exists: ${error.message}`);
}
};
// Find the first available tag, bumping the minor/patch if the computed one is taken.
// This handles the case where a release failed half-way and left a tag behind.
const MAX_ATTEMPTS = 10;
let releaseTag;
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
const candidate = `v${major}.${minor}.${patch}`;
if (!(await tagExists(candidate))) {
releaseTag = candidate;
break;
}
console.log(`Tag ${candidate} already exists – bumping version and retrying…`);
// For patch releases keep bumping the patch number.
// For minor/major releases bump the minor number (patch is already 0).
switch (releaseType) {
case 'patch':
patch += 1;
break;
case 'minor':
minor += 1;
break;
case 'major':
minor += 1;
break;
}
}
if (!releaseTag) {
core.setFailed(`Could not find an available release tag after ${MAX_ATTEMPTS} attempts. Please check existing tags and releases.`);
return;
}
console.log(`Computed release tag: ${releaseTag}`);
core.setOutput('release_tag', releaseTag);
console.log(`✓ Release tag: ${releaseTag}`);
{
echo "## Manual Sync Actions Required"
echo ""
echo "The following manual steps must be completed in **github/gh-aw-actions** before this release continues:"
echo ""
echo "1. Trigger the **sync-actions** workflow in github/gh-aw-actions:"
echo " https://github.com/github/gh-aw-actions/actions/workflows/sync-actions.yml"
echo "2. Merge the PR created by the sync-actions workflow in **github/gh-aw-actions**"
echo "3. Verify that tag **\`${RELEASE_TAG}\`** exists in github/gh-aw-actions"
echo ""
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."
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
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 (retry to handle eventual consistency)
MAX_ATTEMPTS=5
RELEASE_ID=""
for attempt in $(seq 1 "$MAX_ATTEMPTS"); do
set +e
release_view_output=$(gh release view "$RELEASE_TAG" --json databaseId --jq '.databaseId' 2>&1)
release_view_status=$?
set -e
if [ "$release_view_status" -eq 0 ] && [ -n "$release_view_output" ]; then
RELEASE_ID="$release_view_output"
break
fi
if ! echo "$release_view_output" | grep -qiE "not found|404"; then
echo "Error: Failed to resolve release ID for $RELEASE_TAG"
echo "$release_view_output"
exit 1
fi
if [ "$attempt" -lt "$MAX_ATTEMPTS" ]; then
echo "Release ID not available yet (attempt $attempt/$MAX_ATTEMPTS); retrying..."
sleep $((2 ** attempt))
fi
done
if [ -z "$RELEASE_ID" ]; then
echo "Error: Failed to resolve release ID for $RELEASE_TAG after $MAX_ATTEMPTS attempts"
exit 1
fi
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"
set -e
mkdir -p /tmp/gh-aw/agent/release-data
mkdir -p /tmp/gh-aw/agent/community-data
# Copy community issues from the agent/community-data path (written by community-attribution import step)
cp /tmp/gh-aw/agent/community-data/community_issues.json /tmp/gh-aw/agent/community-data/community_issues.json 2>/dev/null || echo "[]" > /tmp/gh-aw/agent/community-data/community_issues.json
# 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/agent/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/agent/release-data/pull_requests.json
echo "[]" > /tmp/gh-aw/agent/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/agent/release-data/pull_requests.json
PR_COUNT=$(jq length "/tmp/gh-aw/agent/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/agent/release-data/pull_requests.json \
> /tmp/gh-aw/agent/release-data/closing_refs_by_issue.json 2>/dev/null \
|| echo "{}" > /tmp/gh-aw/agent/release-data/closing_refs_by_issue.json
# Also expose to community-data dir so shared attribution strategy can reference it
cp /tmp/gh-aw/agent/release-data/closing_refs_by_issue.json /tmp/gh-aw/agent/community-data/closing_refs_by_issue.json
cp /tmp/gh-aw/agent/release-data/pull_requests.json /tmp/gh-aw/agent/community-data/pull_requests.json
DIRECT_CLOSE_COUNT=$(jq 'keys | length' /tmp/gh-aw/agent/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/agent/community-data/community_issues.json \
> /tmp/gh-aw/agent/release-data/community_issues_closed_in_window.json 2>/dev/null \
|| echo "[]" > /tmp/gh-aw/agent/release-data/community_issues_closed_in_window.json
CLOSED_IN_WINDOW=$(jq length /tmp/gh-aw/agent/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/agent/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/agent/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/agent/release-data/docs_files.txt || echo "No docs directory found"
echo "✓ Setup complete."
echo " Release data: /tmp/gh-aw/agent/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/agent/community-data/ (community_issues.json,"
echo " closing_refs_by_issue.json, pull_requests.json)"
tools
cli-proxy
bash
true
jq
awk
sed
Release Highlights Generator
Generate an engaging release highlights summary for $GITHUB_REPOSITORY release ${RELEASE_TAG}.
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/agent/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)
Focuses on user impact (why changes matter, not just what changed)
Workflow
1. Load Data
# View release metadata
cat /tmp/gh-aw/agent/release-data/current_release.json | jq
# List PRs (empty if first release)
cat /tmp/gh-aw/agent/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/agent/community-data/community_issues.json | jq -r '.[] | "- #\(.number): \(.title) by @\(.author.login)"'# View GitHub-native closing references (issue -> [PRs])
cat /tmp/gh-aw/agent/release-data/closing_refs_by_issue.json | jq
# List community issues closed in this release window (attribution candidates)
cat /tmp/gh-aw/agent/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/agent/release-data/CHANGELOG.md 2>/dev/null ||echo"No CHANGELOG"# View available docs
cat /tmp/gh-aw/agent/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/agent/release-data/community_issues_closed_in_window.json
as the set of candidates and /tmp/gh-aw/agent/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.]
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.
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!