diff --git a/skills/github-project/assets/release-labeler.yml.template b/skills/github-project/assets/release-labeler.yml.template index a65ad6f..a677ef7 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,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: 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`