From 9c01205257a58cfc4d5ff2415ee84d8315b23239 Mon Sep 17 00:00:00 2001 From: Reynier Ortiz Vega Date: Wed, 10 Jun 2026 15:46:28 -0400 Subject: [PATCH 1/3] Trigger release notes via workflow_run not release event The Generate Release Notes workflow ran anthropics/claude-code-action on the `release: published` event, but that action only accepts a fixed set of event types and rejects `release` with "Unsupported event type: release", failing the step before the skill could run. Switch the trigger to workflow_run on "Create Release Tag" (the workflow that creates the tag and GitHub Release). workflow_run is a supported event and is not subject to the GITHUB_TOKEN downstream-trigger limit. Resolve the tag from the VERSION file at the run's head commit and keep workflow_dispatch for manual runs. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/release-notes.yml | 56 ++++++++++++++++++++--------- 1 file changed, 40 insertions(+), 16 deletions(-) diff --git a/.github/workflows/release-notes.yml b/.github/workflows/release-notes.yml index 8179a568d3..ab148c509e 100644 --- a/.github/workflows/release-notes.yml +++ b/.github/workflows/release-notes.yml @@ -1,12 +1,19 @@ # Generate Release Notes workflow # -# When a GitHub Release is published (by create-release-tag.yml after a release -# PR merges), this workflow runs the `release-notes` Claude skill to produce -# polished, copy-pasteable release notes: +# Runs after "Create Release Tag" completes (that workflow creates the git tag +# and GitHub Release once a release PR merges). It then runs the `release-notes` +# Claude skill to produce polished, copy-pasteable release notes: # - analyzes every merged PR between the previous tag and this one # - cross-references linked issues # - dispatches expert subagents to assess breaking changes # +# Why workflow_run and not the `release` event: anthropics/claude-code-action +# only accepts a fixed set of event types (issues, pull_request*, comments, +# workflow_dispatch, repository_dispatch, schedule, workflow_run). A `release` +# trigger makes the action fail immediately with "Unsupported event type: +# release". workflow_run is supported and is not subject to the GITHUB_TOKEN +# downstream-trigger limitation, so it fires reliably after the tag workflow. +# # Delivery is REVIEW-THEN-PUBLISH: the public release notes are NOT overwritten. # The generated markdown is uploaded as a build artifact, written to the job # summary, and posted as a comment on the release PR for a maintainer to review @@ -17,8 +24,9 @@ name: Generate Release Notes on: - release: - types: [published] + workflow_run: + workflows: ["Create Release Tag"] + types: [completed] workflow_dispatch: inputs: tag: @@ -36,32 +44,48 @@ jobs: name: Generate Release Notes runs-on: ubuntu-latest timeout-minutes: 30 + # On workflow_run, only proceed if the tag workflow actually succeeded. + # Manual dispatch always proceeds. + if: >- + github.event_name == 'workflow_dispatch' || + github.event.workflow_run.conclusion == 'success' env: # Mapped to a job-level env var so the Slack step can gate on it: the # `secrets` context is not available in step-level `if:` conditions. SLACK_WEBHOOK: ${{ secrets.SLACK_TOOLHIVE_RELEASE_WEBHOOK_URL }} steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + # For workflow_run, check out the exact commit the tag workflow ran + # against so the VERSION file matches the release being announced. For + # workflow_dispatch this is empty and the default branch is used. + ref: ${{ github.event.workflow_run.head_sha }} + # Full history + tags are required: the skill lists/sorts tags and + # compares ranges between the current and previous release. + fetch-depth: 0 + - name: Resolve release tag id: tag env: - EVENT_TAG: ${{ github.event.release.tag_name }} INPUT_TAG: ${{ inputs.tag }} run: | - TAG="${EVENT_TAG:-$INPUT_TAG}" - if [ -z "$TAG" ]; then - echo "::error::No release tag resolved from event or input" + # Manual runs pass the tag explicitly; workflow_run derives it from the + # VERSION file at the checked-out commit (same source the tag workflow + # uses to create the tag), prefixed with "v". + if [ -n "$INPUT_TAG" ]; then + TAG="$INPUT_TAG" + else + VERSION=$(tr -d '[:space:]' < VERSION) + TAG="v$VERSION" + fi + if [ -z "$TAG" ] || [ "$TAG" = "v" ]; then + echo "::error::No release tag resolved from input or VERSION file" exit 1 fi echo "tag=$TAG" >> "$GITHUB_OUTPUT" echo "Generating release notes for: $TAG" - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - # Full history + tags are required: the skill lists/sorts tags and - # compares ranges between the current and previous release. - fetch-depth: 0 - - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6 with: go-version: 'stable' From 406f3ef2cc027b4ecfec344ac387951c760bad4c Mon Sep 17 00:00:00 2001 From: Reynier Ortiz Vega Date: Wed, 10 Jun 2026 15:53:29 -0400 Subject: [PATCH 2/3] Pass github_token to claude-code-action to skip OIDC Without a github_token input, the action mints its own token via OIDC (the Claude GitHub App flow), which fails with "Could not fetch an OIDC token" unless the workflow has id-token: write and the app installed. We only need the API key for Anthropic auth and GH_TOKEN for the skill's read-only gh calls, so pass github.token directly to bypass OIDC. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/release-notes.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/release-notes.yml b/.github/workflows/release-notes.yml index ab148c509e..6c91e5d155 100644 --- a/.github/workflows/release-notes.yml +++ b/.github/workflows/release-notes.yml @@ -103,6 +103,11 @@ jobs: GH_TOKEN: ${{ github.token }} with: anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + # Hand the action a token explicitly so it does not try to mint one via + # OIDC (the Claude GitHub App flow), which would require `id-token: write` + # and the app installed. We only need the API key for Anthropic auth and + # GH_TOKEN for the skill's read-only gh calls. + github_token: ${{ github.token }} prompt: | You are generating GitHub release notes for the ToolHive release ${{ steps.tag.outputs.tag }}. From dc574cd3efa728d69cc76135e81fcb94fed12dc0 Mon Sep 17 00:00:00 2001 From: Reynier Ortiz Vega Date: Wed, 10 Jun 2026 16:03:04 -0400 Subject: [PATCH 3/3] Check out default branch, not workflow_run head ref Checking out github.event.workflow_run.head_sha in a workflow that has access to secrets is the untrusted-checkout anti-pattern (flagged by CodeQL actions/untrusted-checkout, high). The head ref was only used to read VERSION; since "Create Release Tag" only runs on a VERSION push to main, main HEAD already is the release commit, so checking out the default branch yields the same VERSION without trusting an event ref. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/release-notes.yml | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release-notes.yml b/.github/workflows/release-notes.yml index 6c91e5d155..056ff4ab2e 100644 --- a/.github/workflows/release-notes.yml +++ b/.github/workflows/release-notes.yml @@ -57,10 +57,13 @@ jobs: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: - # For workflow_run, check out the exact commit the tag workflow ran - # against so the VERSION file matches the release being announced. For - # workflow_dispatch this is empty and the default branch is used. - ref: ${{ github.event.workflow_run.head_sha }} + # Always check out the default branch, never the workflow_run head ref. + # Checking out an event-supplied ref in a secrets-bearing workflow_run + # is the "untrusted checkout" anti-pattern (CodeQL actions/untrusted- + # checkout). It is safe here too: "Create Release Tag" only runs on a + # VERSION push to main, so main HEAD already is the release commit and + # its VERSION matches the tag being announced. + # # Full history + tags are required: the skill lists/sorts tags and # compares ranges between the current and previous release. fetch-depth: 0 @@ -71,8 +74,8 @@ jobs: INPUT_TAG: ${{ inputs.tag }} run: | # Manual runs pass the tag explicitly; workflow_run derives it from the - # VERSION file at the checked-out commit (same source the tag workflow - # uses to create the tag), prefixed with "v". + # VERSION file on the default branch (the release commit that triggered + # the tag workflow), prefixed with "v". if [ -n "$INPUT_TAG" ]; then TAG="$INPUT_TAG" else