From 11155c865c2904922579e375fb994c84d4024fec Mon Sep 17 00:00:00 2001 From: Sebastian Mendel Date: Sun, 22 Feb 2026 12:35:03 +0100 Subject: [PATCH 1/2] feat: add release announcement discussion to release-labeler Add announce-release job that creates a GitHub Discussion in the Announcements category when a release is published. Category ID is resolved dynamically via GraphQL (portable across repos), duplicates are checked against the first 100 discussions, and the body is passed via file to avoid shell expansion issues with release notes. Top-level permissions reduced to contents:read only, with job-level permissions for each job (discussions:write for announce-release, issues:write + pull-requests:write for label-release). Signed-off-by: Sebastian Mendel --- .../assets/release-labeler.yml.template | 103 +++++++++++++++++- .../references/release-labeling.md | 36 +++++- 2 files changed, 130 insertions(+), 9 deletions(-) diff --git a/skills/github-project/assets/release-labeler.yml.template b/skills/github-project/assets/release-labeler.yml.template index a65ad6f..dd4bfbc 100644 --- a/skills/github-project/assets/release-labeler.yml.template +++ b/skills/github-project/assets/release-labeler.yml.template @@ -1,7 +1,9 @@ # 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 @@ -9,7 +11,8 @@ # # 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 @@ -19,13 +22,107 @@ 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 }} + 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="${{ github.repository_owner }}" \ + -f name="${{ github.event.repository.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: Get release info id: release diff --git a/skills/github-project/references/release-labeling.md b/skills/github-project/references/release-labeling.md index 07030cc..eaaf77d 100644 --- a/skills/github-project/references/release-labeling.md +++ b/skills/github-project/references/release-labeling.md @@ -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 @@ -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` From 8edd5cb67186d0c9a122cdc204c656b7c3d8a4c1 Mon Sep 17 00:00:00 2001 From: Sebastian Mendel Date: Sun, 22 Feb 2026 13:37:20 +0100 Subject: [PATCH 2/2] fix: address review feedback for announcement workflow - Use env vars instead of GitHub expressions in duplicate check query - Add -- to grep to handle tags starting with hyphen - Add harden-runner to label-release job for consistent security Signed-off-by: Sebastian Mendel --- .../assets/release-labeler.yml.template | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/skills/github-project/assets/release-labeler.yml.template b/skills/github-project/assets/release-labeler.yml.template index dd4bfbc..a677ef7 100644 --- a/skills/github-project/assets/release-labeler.yml.template +++ b/skills/github-project/assets/release-labeler.yml.template @@ -68,6 +68,8 @@ jobs: 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 }} @@ -90,10 +92,10 @@ jobs: } } }' \ - -f owner="${{ github.repository_owner }}" \ - -f name="${{ github.event.repository.name }}" \ + -f owner="$REPO_OWNER" \ + -f name="$REPO_NAME" \ -f categoryId="$CATEGORY_ID" \ - --jq '.data.repository.discussions.nodes[].title' | grep -Fx "$RELEASE_TAG" || true) + --jq '.data.repository.discussions.nodes[].title' | grep -Fx -- "$RELEASE_TAG" || true) if [[ -n "$EXISTING" ]]; then echo "Discussion for $RELEASE_TAG already exists, skipping" @@ -124,6 +126,11 @@ jobs: 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: