Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 107 additions & 3 deletions skills/github-project/assets/release-labeler.yml.template
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
# Release Labeler Workflow
# Automatically labels PRs and issues with the release version they shipped in
# and creates an announcement discussion for each release.
#
# Features:
# - Creates a discussion in Announcements category for each release
# - Creates `released:vX.Y.Z` label on release publish
# - Labels all PRs merged between previous and current release
# - Labels issues that were closed by those PRs
# - Adds comment linking to the release
#
# Usage:
# 1. Copy to .github/workflows/release-labeler.yml
# 2. Ensure GITHUB_TOKEN has issues:write and pull-requests:write permissions
# 2. Ensure GITHUB_TOKEN has issues:write, pull-requests:write, and discussions:write permissions
# 3. Enable Discussions on the repository (Settings > General > Features > Discussions)

name: Release Labeler

Expand All @@ -19,14 +22,115 @@ on:

permissions:
contents: read
issues: write
pull-requests: write

jobs:
announce-release:
name: Create Discussion Announcement
runs-on: ubuntu-latest

permissions:
contents: read
discussions: write

steps:
- name: Harden Runner
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit

- name: Resolve announcements category ID
id: category
env:
GH_TOKEN: ${{ github.token }}
REPO_OWNER: ${{ github.repository_owner }}
REPO_NAME: ${{ github.event.repository.name }}
run: |
CATEGORY_ID=$(gh api graphql -f query='
query($owner: String!, $name: String!) {
repository(owner: $owner, name: $name) {
discussionCategories(first: 20) {
nodes { id name }
}
}
}' -f owner="$REPO_OWNER" -f name="$REPO_NAME" \
--jq '.data.repository.discussionCategories.nodes[] | select(.name == "Announcements") | .id')

if [[ -z "$CATEGORY_ID" ]]; then
echo "::warning::No 'Announcements' discussion category found, skipping"
echo "found=false" >> "$GITHUB_OUTPUT"
exit 0
fi

echo "id=$CATEGORY_ID" >> "$GITHUB_OUTPUT"
echo "found=true" >> "$GITHUB_OUTPUT"

- name: Create announcement discussion
if: steps.category.outputs.found == 'true'
env:
GH_TOKEN: ${{ github.token }}
REPO_OWNER: ${{ github.repository_owner }}
REPO_NAME: ${{ github.event.repository.name }}
RELEASE_TAG: ${{ github.event.release.tag_name }}
RELEASE_URL: ${{ github.event.release.html_url }}
RELEASE_BODY: ${{ github.event.release.body }}
REPO_ID: ${{ github.event.repository.node_id }}
CATEGORY_ID: ${{ steps.category.outputs.id }}
run: |
# Build discussion body safely (no shell expansion of release body)
{
printf '## [%s](%s)\n\n' "$RELEASE_TAG" "$RELEASE_URL"
printf '%s\n\n' "$RELEASE_BODY"
printf '---\n*Automatically created from [GitHub Release](%s).*\n' "$RELEASE_URL"
} > /tmp/discussion-body.md

# Check if discussion already exists (search all announcements by title)
EXISTING=$(gh api graphql -f query='
query($owner: String!, $name: String!, $categoryId: ID!) {
repository(owner: $owner, name: $name) {
discussions(first: 100, categoryId: $categoryId, orderBy: {field: CREATED_AT, direction: DESC}) {
nodes { title }
}
}
}' \
-f owner="$REPO_OWNER" \
-f name="$REPO_NAME" \
-f categoryId="$CATEGORY_ID" \
--jq '.data.repository.discussions.nodes[].title' | grep -Fx -- "$RELEASE_TAG" || true)

if [[ -n "$EXISTING" ]]; then
echo "Discussion for $RELEASE_TAG already exists, skipping"
exit 0
fi

gh api graphql \
-f query='mutation($repoId: ID!, $categoryId: ID!, $title: String!, $body: String!) {
createDiscussion(input: {repositoryId: $repoId, categoryId: $categoryId, title: $title, body: $body}) {
discussion { url }
}
}' \
-f repoId="$REPO_ID" \
-f categoryId="$CATEGORY_ID" \
-f title="$RELEASE_TAG" \
-F body=@/tmp/discussion-body.md

echo "## Announcement Created" >> $GITHUB_STEP_SUMMARY
echo "Discussion created for $RELEASE_TAG" >> $GITHUB_STEP_SUMMARY

label-release:
name: Label PRs and Issues
runs-on: ubuntu-latest

permissions:
contents: read
issues: write
pull-requests: write

steps:
- name: Harden Runner
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit

- name: Get release info
id: release
env:
Expand Down
36 changes: 30 additions & 6 deletions skills/github-project/references/release-labeling.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,29 @@
## Overview

When a release is published, the release-labeler workflow:
1. Creates a label `released:vX.Y.Z`
2. Finds all PRs merged since the previous release
3. Labels those PRs and their linked issues
4. Adds comments linking to the release
1. Creates a discussion announcement in the Announcements category
2. Creates a label `released:vX.Y.Z`
3. Finds all PRs merged since the previous release
4. Labels those PRs and their linked issues
5. Adds comments linking to the release

## Release Announcements

The `announce-release` job automatically creates a discussion in the repository's **Discussions > Announcements** category whenever a release is published.

### How it works

- **Dynamic category resolution:** The Announcements category ID is resolved at runtime via a GraphQL query by name, making the workflow portable across any repository with Discussions enabled.
- **Duplicate detection:** Before creating a discussion, the job checks the first 100 existing discussions in the Announcements category for a matching title (the release tag). If one already exists, creation is skipped.
- **Safe body construction:** The discussion body is built using `printf` to a temporary file and passed to the GraphQL mutation via `-F body=@file`. This avoids shell expansion issues with special characters in release notes (backticks, quotes, dollar signs, etc.).
- **Conditional execution:** The creation step only runs when `steps.category.outputs.found == 'true'`. If the repository has no Announcements category, a warning annotation is emitted and the job exits cleanly.
- **Permissions:** Requires `discussions: write` at the job level. The top-level workflow permissions remain minimal (`contents: read` only).

### Setup

1. **Enable Discussions** on the repository: Settings > General > Features > Discussions
2. Ensure an **Announcements** category exists (GitHub creates this by default when Discussions is enabled)
3. The workflow template already includes the `announce-release` job -- no additional configuration needed

## Benefits

Expand Down Expand Up @@ -40,14 +59,19 @@ curl -o .github/workflows/release-labeler.yml \
https://raw.githubusercontent.com/netresearch/github-project-skill/main/skills/github-project/assets/release-labeler.yml.template
```

### 2. Ensure permissions
### 2. Enable Discussions (for announcements)

Go to Settings > General > Features and enable **Discussions**. Ensure an **Announcements** category exists (created by default).

### 3. Ensure permissions

The workflow needs:
- `issues: write` - To label issues and add comments
- `pull-requests: write` - To label PRs and add comments
- `contents: read` - To compare releases
- `discussions: write` - To create announcement discussions

### 3. Link issues to PRs
### 4. Link issues to PRs

For automatic issue labeling, PRs must reference issues using:
- `Fixes #123`
Expand Down