From 96f17ed96e54eff4e499d13a6494c5564be9ca70 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Thu, 3 Sep 2026 11:40:14 -0700 Subject: [PATCH 01/19] Prevent duplicate workflow failure issues Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/workflow-failure-issue.yml | 22 +++++++++++++++++--- workflow-failure-issue/README.md | 5 ++++- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/.github/workflows/workflow-failure-issue.yml b/.github/workflows/workflow-failure-issue.yml index 449e8ef7768b..fc3cbc1ad314 100644 --- a/.github/workflows/workflow-failure-issue.yml +++ b/.github/workflows/workflow-failure-issue.yml @@ -10,10 +10,15 @@ on: permissions: {} +concurrency: + group: shared-workflow-failure-issue-${{ github.workflow }} + cancel-in-progress: false + queue: max + jobs: workflow-failure-issue: name: Open or close workflow failure issue - if: github.repository_owner == 'open-telemetry' + if: github.repository_owner == 'open-telemetry' && !cancelled() permissions: issues: write # needed to open, comment on, and close workflow failure issues runs-on: ubuntu-slim @@ -26,8 +31,19 @@ jobs: run: | set -euo pipefail - # TODO (trask) search doesn't support exact phrases, so it's possible that this could grab the wrong issue - number=$(gh issue list --search "in:title Workflow failed: $GITHUB_WORKFLOW" --limit 1 --json number -q .[].number) + numbers=$( + gh api --paginate \ + "repos/$GITHUB_REPOSITORY/issues?state=open&per_page=100" \ + --jq ' + .[] + | select( + .pull_request == null + and (.title | startswith("Workflow failed: " + env.GITHUB_WORKFLOW + " (#")) + ) + | .number + ' + ) + number=${numbers%%$'\n'*} echo "$number" echo "$INPUT_SUCCESS" diff --git a/workflow-failure-issue/README.md b/workflow-failure-issue/README.md index ba6232347079..399a27476b29 100644 --- a/workflow-failure-issue/README.md +++ b/workflow-failure-issue/README.md @@ -21,6 +21,9 @@ Behavior: - On a subsequent failure while an issue is already open, a comment linking to the failing run is added. - On success, any open tracking issue is closed. +- On cancellation, any open tracking issue is left unchanged. +- Updates for the same caller workflow are serialized so concurrent runs cannot + create duplicate tracking issues. ## How to use @@ -49,4 +52,4 @@ Pin `` to a commit SHA or release tag in this repository. | Name | Type | Required | Description | | ---- | ---- | -------- | ----------- | -| `success` | boolean | yes | Whether the monitored jobs succeeded. Pass `true` to close any open tracking issue, `false` to open or comment on one. | +| `success` | boolean | yes | Whether the monitored jobs succeeded. On a non-cancelled run, pass `true` to close any open tracking issue or `false` to open or comment on one. | From 5eef6df2688497e86cd08f5c8ee513b4e938f7a2 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Thu, 3 Sep 2026 11:49:01 -0700 Subject: [PATCH 02/19] Coalesce pending failure notifications Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/workflow-failure-issue.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/workflow-failure-issue.yml b/.github/workflows/workflow-failure-issue.yml index fc3cbc1ad314..e2c09b574aa8 100644 --- a/.github/workflows/workflow-failure-issue.yml +++ b/.github/workflows/workflow-failure-issue.yml @@ -13,7 +13,6 @@ permissions: {} concurrency: group: shared-workflow-failure-issue-${{ github.workflow }} cancel-in-progress: false - queue: max jobs: workflow-failure-issue: From 0977aab16538ddc38fd28e6bf15fd9dbb3398d48 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Thu, 3 Sep 2026 12:10:13 -0700 Subject: [PATCH 03/19] Address Copilot review comment: narrow issue lookup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Copilot comment: Using `--paginate` over all open issues can become expensive in repositories with large numbers of open issues (runtime + API rate-limit consumption). A more scalable approach is to narrow the REST query (e.g., apply a dedicated label to tracking issues and query with `labels=...`, or otherwise constrain the candidate set) so the workflow doesn’t have to scan every open issue on each run. Analysis: The workflow creates tracking issues through GITHUB_TOKEN, so GitHub records github-actions[bot] as their creator. Filtering the REST request by that creator keeps the immediate, exact API lookup while excluding open issues created by people and other apps. Upsides: Repositories with many open issues scan only GitHub Actions-created candidates. The exact title-prefix check and compatibility with existing tracking issues remain intact. Downsides: Repositories with many unrelated open issues created by github-actions[bot] may still need more than one page. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/workflow-failure-issue.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/workflow-failure-issue.yml b/.github/workflows/workflow-failure-issue.yml index e2c09b574aa8..c5df8bf75e08 100644 --- a/.github/workflows/workflow-failure-issue.yml +++ b/.github/workflows/workflow-failure-issue.yml @@ -32,7 +32,7 @@ jobs: numbers=$( gh api --paginate \ - "repos/$GITHUB_REPOSITORY/issues?state=open&per_page=100" \ + "repos/$GITHUB_REPOSITORY/issues?state=open&creator=github-actions%5Bbot%5D&per_page=100" \ --jq ' .[] | select( From c7d270a1544113bbbf4b9c890876336cafdc128a Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Thu, 3 Sep 2026 12:11:22 -0700 Subject: [PATCH 04/19] Address Copilot review comment: clarify required success input Copilot comment: The wording suggests callers may only need to pass `success` for non-cancelled runs, but the input is still marked `Required: yes` (so callers must pass something regardless). Consider clarifying that callers should always provide `success` (e.g., `success: ${{ success() }}` / combined expression), and that cancelled runs are ignored because the job is skipped via `if: ... && !cancelled()`. Analysis: The documentation now states that every caller must provide success. It gives success() and needs-based expressions as options, and separately explains that the reusable workflow ignores the value when cancellation skips its issue-update job. Upsides: Callers can distinguish the required input contract from the reusable workflow's cancellation behavior. Downsides: The README adds a short explanatory paragraph. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- workflow-failure-issue/README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/workflow-failure-issue/README.md b/workflow-failure-issue/README.md index 399a27476b29..1fde8b865bea 100644 --- a/workflow-failure-issue/README.md +++ b/workflow-failure-issue/README.md @@ -48,8 +48,14 @@ jobs: Pin `` to a commit SHA or release tag in this repository. +The `success` input is required, so callers must always provide it. Use +`success: ${{ success() }}` when that status covers the monitored jobs, or use +an expression over `needs` results as shown above. The reusable workflow skips +the issue update when the caller run is cancelled, so it ignores the supplied +value in that case. + ### Inputs | Name | Type | Required | Description | | ---- | ---- | -------- | ----------- | -| `success` | boolean | yes | Whether the monitored jobs succeeded. On a non-cancelled run, pass `true` to close any open tracking issue or `false` to open or comment on one. | +| `success` | boolean | yes | Whether the monitored jobs succeeded. Pass `true` to close any open tracking issue or `false` to open or comment on one. | From 49f3a388dc516f2f44054a2674593df03ebab459 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Thu, 3 Sep 2026 12:32:01 -0700 Subject: [PATCH 05/19] Address Copilot review comment: label tracking issues per workflow Copilot comment: This approach paginates through all open issues created by `github-actions[bot]` and filters client-side, which can be expensive in repos with many open bot-created issues (more API calls + more JSON processing). If possible, narrow the server-side result set (e.g., by applying a dedicated label to tracking issues and adding `labels=...` to the REST query, or otherwise constraining the listing criteria) to reduce pagination and improve reliability under rate limits. Analysis: The workflow now derives a stable label from the caller workflow name and queries open issues by that label. On the first run for a caller, it scans the older bot-created candidates once and labels an exact match. Every issue created afterward receives the label. Upsides: Steady-state lookups return only tracking issues for one caller workflow. Existing open tracking issues migrate without creating duplicates. Downsides: Each monitored workflow adds one generated label to the calling repository. Its first run still scans open issues created by github-actions[bot]. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/workflow-failure-issue.yml | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/.github/workflows/workflow-failure-issue.yml b/.github/workflows/workflow-failure-issue.yml index c5df8bf75e08..18afe4245bee 100644 --- a/.github/workflows/workflow-failure-issue.yml +++ b/.github/workflows/workflow-failure-issue.yml @@ -30,9 +30,20 @@ jobs: run: | set -euo pipefail + tracking_label="workflow-failure-$(printf '%s' "$GITHUB_WORKFLOW" | sha256sum | cut -c1-16)" + issue_endpoint="repos/$GITHUB_REPOSITORY/issues?state=open&labels=$tracking_label&per_page=100" + migrate_issue=false + if ! gh api "repos/$GITHUB_REPOSITORY/labels/$tracking_label" >/dev/null 2>&1; then + gh label create "$tracking_label" \ + --color D93F0B \ + --description "Managed by the shared workflow failure issue" + issue_endpoint="repos/$GITHUB_REPOSITORY/issues?state=open&creator=github-actions%5Bbot%5D&per_page=100" + migrate_issue=true + fi + numbers=$( gh api --paginate \ - "repos/$GITHUB_REPOSITORY/issues?state=open&creator=github-actions%5Bbot%5D&per_page=100" \ + "$issue_endpoint" \ --jq ' .[] | select( @@ -44,6 +55,10 @@ jobs: ) number=${numbers%%$'\n'*} + if [[ "$migrate_issue" == "true" && -n "$number" ]]; then + gh issue edit "$number" --add-label "$tracking_label" + fi + echo "$number" echo "$INPUT_SUCCESS" @@ -56,5 +71,6 @@ jobs: fi elif [[ "$INPUT_SUCCESS" == "false" ]]; then gh issue create --title "Workflow failed: $GITHUB_WORKFLOW (#$GITHUB_RUN_NUMBER)" \ - --body "See [$GITHUB_WORKFLOW #$GITHUB_RUN_NUMBER](https://github.com/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID)." + --body "See [$GITHUB_WORKFLOW #$GITHUB_RUN_NUMBER](https://github.com/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID)." \ + --label "$tracking_label" fi From 5af6377ee0359c744587a110f87b6188966d5f23 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Thu, 3 Sep 2026 12:52:11 -0700 Subject: [PATCH 06/19] Address Copilot review comment: converge duplicate tracking issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Copilot comment: This collects *all* matching open issue numbers but then truncates to only the first one. That contradicts the documented behavior in the README (“On success, any open tracking issue is closed.”) when duplicates already exist (historical races, manual edits, etc.). Consider iterating over all values in `numbers` for actions that should apply to every open tracking issue (at least the success/close path), and/or explicitly closing or consolidating extras so the workflow converges back to a single tracking issue. Analysis: The workflow now keeps every exact matching issue number. A successful run closes all matches. A failed run comments on the first match and closes later matches as duplicates. The one-time label migration also labels every matching legacy issue before processing it. Upsides: Historical duplicates converge to zero open issues on success or one canonical open issue on failure. The documented close behavior now covers every matching issue. Downsides: Runs with historical duplicates make one close request per extra issue. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/workflow-failure-issue.yml | 27 +++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/.github/workflows/workflow-failure-issue.yml b/.github/workflows/workflow-failure-issue.yml index 18afe4245bee..68c0af5f5005 100644 --- a/.github/workflows/workflow-failure-issue.yml +++ b/.github/workflows/workflow-failure-issue.yml @@ -53,21 +53,34 @@ jobs: | .number ' ) - number=${numbers%%$'\n'*} + if [[ -n "$numbers" ]]; then + mapfile -t issue_numbers <<< "$numbers" + else + issue_numbers=() + fi - if [[ "$migrate_issue" == "true" && -n "$number" ]]; then - gh issue edit "$number" --add-label "$tracking_label" + if [[ "$migrate_issue" == "true" ]]; then + for number in "${issue_numbers[@]}"; do + gh issue edit "$number" --add-label "$tracking_label" + done fi - echo "$number" + echo "$numbers" echo "$INPUT_SUCCESS" - if [[ -n "$number" ]]; then + if (( ${#issue_numbers[@]} > 0 )); then if [[ "$INPUT_SUCCESS" == "true" ]]; then - gh issue close "$number" + for number in "${issue_numbers[@]}"; do + gh issue close "$number" + done else - gh issue comment "$number" \ + gh issue comment "${issue_numbers[0]}" \ --body "See [$GITHUB_WORKFLOW #$GITHUB_RUN_NUMBER](https://github.com/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID)." + for number in "${issue_numbers[@]:1}"; do + gh issue close "$number" \ + --reason duplicate \ + --comment "Superseded by #${issue_numbers[0]}." + done fi elif [[ "$INPUT_SUCCESS" == "false" ]]; then gh issue create --title "Workflow failed: $GITHUB_WORKFLOW (#$GITHUB_RUN_NUMBER)" \ From 06e677d2705566f1fe9485788ebb75757203bea4 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Thu, 3 Sep 2026 13:13:33 -0700 Subject: [PATCH 07/19] Address Copilot review comments: make issue lookup robust and deterministic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Copilot comment: Migration of pre-label issues only runs when the label does not exist. If the label exists but there are still matching open tracking issues without the label (e.g., label was created manually, prior run partially migrated, or automation was interrupted), the workflow will ignore those issues and may create a new one—reintroducing duplicates. A more robust approach is to always include a fallback lookup (e.g., creator/title-prefix) and union the results with the label-based lookup, then ensure any unlabelled matches get the tracking label. Copilot comment: The canonical issue chosen for commenting/consolidation is implicitly whichever issue appears first in the GitHub API response. That ordering can be non-obvious (and may change with API defaults), which can cause the ‘primary’ tracking issue to flip over time. Consider sorting the collected issue numbers (e.g., numerically) and consistently choosing the oldest (lowest number) as the canonical issue before commenting and closing duplicates. Copilot comment: Migration of pre-label issues only runs when the label does not exist. If the label exists but there are still matching open tracking issues without the label (e.g., label was created manually, prior run partially migrated, or automation was interrupted), the workflow will ignore those issues and may create a new one—reintroducing duplicates. A more robust approach is to always include a fallback lookup (e.g., creator/title-prefix) and union the results with the label-based lookup, then ensure any unlabelled matches get the tracking label. Copilot comment: The canonical issue chosen for commenting/consolidation is implicitly whichever issue appears first in the GitHub API response. That ordering can be non-obvious (and may change with API defaults), which can cause the ‘primary’ tracking issue to flip over time. Consider sorting the collected issue numbers (e.g., numerically) and consistently choosing the oldest (lowest number) as the canonical issue before commenting and closing duplicates. Analysis: Every run now unions the label-based REST results with a narrow issue-search fallback for exact unlabelled title matches. It labels fallback matches, removes duplicate numbers, and sorts them numerically before choosing the first issue. Upsides: Manual label creation, interrupted migration, and removed labels cannot hide an existing tracking issue. The oldest issue number remains the stable canonical issue across API response order changes. Downsides: Each run makes one narrow search request in addition to the label-based REST request. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/workflow-failure-issue.yml | 32 +++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/.github/workflows/workflow-failure-issue.yml b/.github/workflows/workflow-failure-issue.yml index 68c0af5f5005..8c5669764130 100644 --- a/.github/workflows/workflow-failure-issue.yml +++ b/.github/workflows/workflow-failure-issue.yml @@ -31,19 +31,15 @@ jobs: set -euo pipefail tracking_label="workflow-failure-$(printf '%s' "$GITHUB_WORKFLOW" | sha256sum | cut -c1-16)" - issue_endpoint="repos/$GITHUB_REPOSITORY/issues?state=open&labels=$tracking_label&per_page=100" - migrate_issue=false if ! gh api "repos/$GITHUB_REPOSITORY/labels/$tracking_label" >/dev/null 2>&1; then gh label create "$tracking_label" \ --color D93F0B \ --description "Managed by the shared workflow failure issue" - issue_endpoint="repos/$GITHUB_REPOSITORY/issues?state=open&creator=github-actions%5Bbot%5D&per_page=100" - migrate_issue=true fi - numbers=$( + label_numbers=$( gh api --paginate \ - "$issue_endpoint" \ + "repos/$GITHUB_REPOSITORY/issues?state=open&labels=$tracking_label&per_page=100" \ --jq ' .[] | select( @@ -53,14 +49,34 @@ jobs: | .number ' ) + search_query="repo:$GITHUB_REPOSITORY is:issue is:open \"Workflow failed\" in:title" + unlabelled_numbers=$( + TRACKING_LABEL="$tracking_label" gh api --paginate --method GET search/issues \ + -f q="$search_query" \ + -f per_page=100 \ + --jq ' + .items[] + | select( + (.title | startswith("Workflow failed: " + env.GITHUB_WORKFLOW + " (#")) + and ((.labels | map(.name) | index(env.TRACKING_LABEL)) == null) + ) + | .number + ' + ) + numbers=$( + printf '%s\n%s\n' "$label_numbers" "$unlabelled_numbers" \ + | sed '/^$/d' \ + | sort -n -u + ) if [[ -n "$numbers" ]]; then mapfile -t issue_numbers <<< "$numbers" else issue_numbers=() fi - if [[ "$migrate_issue" == "true" ]]; then - for number in "${issue_numbers[@]}"; do + if [[ -n "$unlabelled_numbers" ]]; then + mapfile -t unlabelled_issue_numbers <<< "$unlabelled_numbers" + for number in "${unlabelled_issue_numbers[@]}"; do gh issue edit "$number" --add-label "$tracking_label" done fi From 944cbb76ba95cfd62ff31d05a22ea43929675082 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Thu, 3 Sep 2026 13:32:43 -0700 Subject: [PATCH 08/19] Address Copilot review comment: use portable duplicate close reason Copilot comment: `gh issue close --reason` does not support a `duplicate` reason for GitHub Issues (supported reasons are typically limited to values like `completed` / `not planned`). This is likely to fail at runtime and prevent consolidation. If you want to mark duplicates, either (1) close with a supported reason (or default) and leave the explanatory comment, or (2) use the GitHub API/GraphQL "mark as duplicate" capability to formally mark the issue as a duplicate of the primary issue. Analysis: Duplicate tracking issues now close with the broadly supported not planned reason. The closing comment still links each extra issue to the canonical issue. Upsides: Consolidation no longer depends on support for the newer duplicate reason value in the GitHub CLI and API versions available to the runner. Downsides: GitHub records the issue as not planned rather than as a formal duplicate, although the closing comment identifies the canonical issue. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/workflow-failure-issue.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/workflow-failure-issue.yml b/.github/workflows/workflow-failure-issue.yml index 8c5669764130..412fa8aad8eb 100644 --- a/.github/workflows/workflow-failure-issue.yml +++ b/.github/workflows/workflow-failure-issue.yml @@ -94,7 +94,7 @@ jobs: --body "See [$GITHUB_WORKFLOW #$GITHUB_RUN_NUMBER](https://github.com/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID)." for number in "${issue_numbers[@]:1}"; do gh issue close "$number" \ - --reason duplicate \ + --reason "not planned" \ --comment "Superseded by #${issue_numbers[0]}." done fi From 6590d8efb9cc87607d585cc00c2236563f317c95 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Thu, 3 Sep 2026 14:03:16 -0700 Subject: [PATCH 09/19] Address review finding: drop invalid success() guidance from README Review finding: The new guidance tells callers to use `success: ${{ success() }}`, but a status check function is not available in a reusable-workflow call's `with:` block. GitHub's context availability table lists `jobs..with.` with special functions `None`, while `always, cancelled, success, failure` are listed only for `jobs..if` and `jobs..steps.if`. A consumer who copies this line gets a workflow that fails to parse with an unrecognized-function error, so the README hands them broken YAML. Fix: drop the `success()` suggestion and keep the `needs..result` expression the README already demonstrates, or point callers at a preceding step output. Analysis: GitHub's context availability table gives the "Special functions" column the value None for jobs..with., and lists always, cancelled, success, and failure only for jobs..if and jobs..steps.if. The runner rejects a status check function outside an if condition with "Unrecognized function: 'success'", so a caller that copies the suggested line gets a workflow that never parses. The paragraph now points callers at the needs results the example above it already uses, and says in one sentence why success() is not an option, so a reader does not try it and hit the parse error. Upsides: A consumer who follows the documentation gets a workflow that runs. The note about status check functions answers the obvious next question instead of leaving the reader to discover the restriction from a parse error. Downsides: The paragraph is one line longer. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- workflow-failure-issue/README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/workflow-failure-issue/README.md b/workflow-failure-issue/README.md index 1fde8b865bea..015e207575ae 100644 --- a/workflow-failure-issue/README.md +++ b/workflow-failure-issue/README.md @@ -48,11 +48,12 @@ jobs: Pin `` to a commit SHA or release tag in this repository. -The `success` input is required, so callers must always provide it. Use -`success: ${{ success() }}` when that status covers the monitored jobs, or use -an expression over `needs` results as shown above. The reusable workflow skips -the issue update when the caller run is cancelled, so it ignores the supplied -value in that case. +The `success` input is required, so callers must always provide it. Set it from +the `needs` results of the monitored jobs, as shown above. A status check +function such as `success()` cannot be used here, because GitHub Actions allows +those functions only in an `if` condition. The reusable workflow skips the issue +update when the caller run is cancelled, so it ignores the supplied value in +that case. ### Inputs From ae2216853f429ce0fb6e109001bb915af3d6bc70 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Thu, 3 Sep 2026 14:03:55 -0700 Subject: [PATCH 10/19] Address review finding: document the tracking label in the README Review finding: The Behavior list is extended with cancellation and serialization, but it never mentions that the workflow now creates a label named `workflow-failure-<16 hex chars>` in the calling repository and applies it to every tracking issue. That label is visible in the consumer's issue list and label settings, and its name is an opaque hash of the workflow name, so a maintainer who finds it has no way to tell what created it or whether it is safe to delete. The README is the only consumer documentation for this reusable workflow. Fix: add a Behavior bullet stating that a per-workflow tracking label is created in the calling repository and applied to the tracking issues, and that deleting or renaming it breaks the lookup. Analysis: The workflow creates the label in the calling repository, not in shared-workflows, and applies it to new issues through gh issue create --label and to pre-existing tracking issues through gh issue edit --add-label. The label name ends in a truncated SHA-256 of the workflow name, so it reads as noise to anyone who meets it in the repository's label settings. This README is the only consumer documentation for the reusable workflow, and this pull request is already editing its Behavior list to record the new behavior, so the new label belongs in that same list. The added bullet says what the label is for and that the workflow creates it, which also tells a maintainer that removing it costs nothing permanent. Upsides: A consumer who finds the label knows what created it and why. The Behavior list now covers every repository resource the workflow touches. Downsides: No material downside identified. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- workflow-failure-issue/README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/workflow-failure-issue/README.md b/workflow-failure-issue/README.md index 015e207575ae..eef76777aad9 100644 --- a/workflow-failure-issue/README.md +++ b/workflow-failure-issue/README.md @@ -18,6 +18,9 @@ Behavior: - On failure, if no tracking issue is open, a new issue titled `Workflow failed: (#)` is created. +- Each tracking issue carries a `workflow-failure-` label that identifies + the monitored workflow. The workflow creates that label in the calling + repository when it is missing, and later runs use it to find the issue again. - On a subsequent failure while an issue is already open, a comment linking to the failing run is added. - On success, any open tracking issue is closed. From 64bc06d76a950110c7be526b3f5eebd726d9e704 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Thu, 3 Sep 2026 14:36:25 -0700 Subject: [PATCH 11/19] Address Copilot review comment: use caller ref as workflow identity Copilot comment: Hashing the caller's display name does not produce a unique per-workflow label because two workflow files may legally use the same `name:`. Their label and title prefix then coincide, so a success from one workflow can close the other workflow's failure issue. Derive identity from a unique caller path/ref or explicit stable key, while retaining the display name only for human-readable text. Analysis: The concurrency group and tracking label now use GITHUB_WORKFLOW_REF, which includes the caller repository, workflow path, and ref. Label-based lookup no longer checks the display-name title. The legacy fallback accepts an exact embedded workflow ref or verifies the issue's original Actions run before applying the new label. Upsides: Workflows with the same display name cannot share tracking issues. Existing unlabelled issues still migrate when their original run proves that they belong to the current caller. Display names remain in issue titles, links, and comments for readers. Downsides: Migrating an unlabelled legacy issue can require an extra Actions API request. A caller that changes refs receives a new tracking label. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/workflow-failure-issue.yml | 48 ++++++++++++++++---- workflow-failure-issue/README.md | 5 +- 2 files changed, 41 insertions(+), 12 deletions(-) diff --git a/.github/workflows/workflow-failure-issue.yml b/.github/workflows/workflow-failure-issue.yml index 412fa8aad8eb..a1a9c7826b6f 100644 --- a/.github/workflows/workflow-failure-issue.yml +++ b/.github/workflows/workflow-failure-issue.yml @@ -11,7 +11,7 @@ on: permissions: {} concurrency: - group: shared-workflow-failure-issue-${{ github.workflow }} + group: shared-workflow-failure-issue-${{ github.workflow_ref }} cancel-in-progress: false jobs: @@ -30,7 +30,7 @@ jobs: run: | set -euo pipefail - tracking_label="workflow-failure-$(printf '%s' "$GITHUB_WORKFLOW" | sha256sum | cut -c1-16)" + tracking_label="workflow-failure-$(printf '%s' "$GITHUB_WORKFLOW_REF" | sha256sum | cut -c1-16)" if ! gh api "repos/$GITHUB_REPOSITORY/labels/$tracking_label" >/dev/null 2>&1; then gh label create "$tracking_label" \ --color D93F0B \ @@ -42,16 +42,14 @@ jobs: "repos/$GITHUB_REPOSITORY/issues?state=open&labels=$tracking_label&per_page=100" \ --jq ' .[] - | select( - .pull_request == null - and (.title | startswith("Workflow failed: " + env.GITHUB_WORKFLOW + " (#")) - ) + | select(.pull_request == null) | .number ' ) search_query="repo:$GITHUB_REPOSITORY is:issue is:open \"Workflow failed\" in:title" - unlabelled_numbers=$( - TRACKING_LABEL="$tracking_label" gh api --paginate --method GET search/issues \ + unlabelled_candidates=$( + TRACKING_LABEL="$tracking_label" WORKFLOW_REF="$GITHUB_WORKFLOW_REF" \ + gh api --paginate --method GET search/issues \ -f q="$search_query" \ -f per_page=100 \ --jq ' @@ -60,9 +58,38 @@ jobs: (.title | startswith("Workflow failed: " + env.GITHUB_WORKFLOW + " (#")) and ((.labels | map(.name) | index(env.TRACKING_LABEL)) == null) ) - | .number + | [ + .number, + ( + if ((.body // "") | contains("")) + then "match" + else ((.body // "") | capture("/actions/runs/(?[0-9]+)")?.id // "unknown") + end + ) + ] + | @tsv ' ) + unlabelled_numbers=$( + while IFS=$'\t' read -r number run_id; do + if [[ -z "$number" ]]; then + continue + elif [[ "$run_id" == "match" ]]; then + echo "$number" + elif [[ "$run_id" != "unknown" ]]; then + if candidate_ref=$( + gh api "repos/$GITHUB_REPOSITORY/actions/runs/$run_id" \ + --jq '"\(.repository.full_name)/\(.path)@refs/heads/\(.head_branch)"' + ); then + if [[ "$candidate_ref" == "$GITHUB_WORKFLOW_REF" ]]; then + echo "$number" + fi + else + echo "Unable to verify workflow identity for issue #$number from run $run_id" >&2 + fi + fi + done <<< "$unlabelled_candidates" + ) numbers=$( printf '%s\n%s\n' "$label_numbers" "$unlabelled_numbers" \ | sed '/^$/d' \ @@ -100,6 +127,7 @@ jobs: fi elif [[ "$INPUT_SUCCESS" == "false" ]]; then gh issue create --title "Workflow failed: $GITHUB_WORKFLOW (#$GITHUB_RUN_NUMBER)" \ - --body "See [$GITHUB_WORKFLOW #$GITHUB_RUN_NUMBER](https://github.com/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID)." \ + --body " + See [$GITHUB_WORKFLOW #$GITHUB_RUN_NUMBER](https://github.com/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID)." \ --label "$tracking_label" fi diff --git a/workflow-failure-issue/README.md b/workflow-failure-issue/README.md index eef76777aad9..d7fc1b560e36 100644 --- a/workflow-failure-issue/README.md +++ b/workflow-failure-issue/README.md @@ -19,8 +19,9 @@ Behavior: - On failure, if no tracking issue is open, a new issue titled `Workflow failed: (#)` is created. - Each tracking issue carries a `workflow-failure-` label that identifies - the monitored workflow. The workflow creates that label in the calling - repository when it is missing, and later runs use it to find the issue again. + the monitored caller workflow file and ref. The workflow creates that label + in the calling repository when it is missing, and later runs use it to find + the issue again. Workflows that share a display name use different labels. - On a subsequent failure while an issue is already open, a comment linking to the failing run is added. - On success, any open tracking issue is closed. From 0d6ab231fc41bf52e16fd79422022360b407e08a Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Thu, 3 Sep 2026 14:57:06 -0700 Subject: [PATCH 12/19] Address Copilot review comment: expand workflow label hash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Copilot comment: The label hash is truncated to 16 hex chars (~64 bits). While collisions are unlikely, a collision would cause unrelated workflows to share a tracking label and incorrectly merge/close issues. Increasing the prefix length (e.g., 24–32 chars) materially reduces collision risk while still staying well within GitHub label length limits. Analysis: The tracking label now keeps 32 hexadecimal SHA-256 characters, which provides 128 bits of workflow identity. The resulting label is 49 characters long, within GitHub's 50-character label-name limit. Upsides: The longer hash makes an accidental label collision negligible while preserving the existing deterministic label format. Downsides: Existing 16-character labels migrate through the legacy lookup once more, and generated label names are 16 characters longer. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/workflow-failure-issue.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/workflow-failure-issue.yml b/.github/workflows/workflow-failure-issue.yml index a1a9c7826b6f..7933e0d86bd1 100644 --- a/.github/workflows/workflow-failure-issue.yml +++ b/.github/workflows/workflow-failure-issue.yml @@ -30,7 +30,7 @@ jobs: run: | set -euo pipefail - tracking_label="workflow-failure-$(printf '%s' "$GITHUB_WORKFLOW_REF" | sha256sum | cut -c1-16)" + tracking_label="workflow-failure-$(printf '%s' "$GITHUB_WORKFLOW_REF" | sha256sum | cut -c1-32)" if ! gh api "repos/$GITHUB_REPOSITORY/labels/$tracking_label" >/dev/null 2>&1; then gh label create "$tracking_label" \ --color D93F0B \ From 25bf7ae0cbec3d6479165a87724d23d23fd6b289 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Thu, 3 Sep 2026 15:18:12 -0700 Subject: [PATCH 13/19] Address Copilot review comment: preserve caller ref type Copilot comment: The workflow identity reconstruction hardcodes `@refs/heads/`. If the original run executed from a tag or other non-branch ref, this comparison can never match `$GITHUB_WORKFLOW_REF`, preventing legitimate legacy issues from being migrated/labeled. A more robust approach is to parse `$GITHUB_WORKFLOW_REF` and compare repository + workflow path + ref type/value accordingly, rather than forcing `refs/heads`. Analysis: Legacy migration now splits GITHUB_WORKFLOW_REF into the caller workflow identity and ref. It compares the candidate run's repository and path first, then matches its reported ref name as a branch or tag. Pull request refs match by the candidate run's pull request number. Upsides: Legacy issues can migrate for branch, tag, and pull request runs without treating every candidate as a branch. Downsides: The migration check reads two additional fields from the existing workflow-run API response and adds ref-specific matching branches. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/workflow-failure-issue.yml | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/.github/workflows/workflow-failure-issue.yml b/.github/workflows/workflow-failure-issue.yml index 7933e0d86bd1..ec76843004cd 100644 --- a/.github/workflows/workflow-failure-issue.yml +++ b/.github/workflows/workflow-failure-issue.yml @@ -71,17 +71,30 @@ jobs: ' ) unlabelled_numbers=$( + caller_identity="${GITHUB_WORKFLOW_REF%@*}" + caller_ref="${GITHUB_WORKFLOW_REF##*@}" while IFS=$'\t' read -r number run_id; do if [[ -z "$number" ]]; then continue elif [[ "$run_id" == "match" ]]; then echo "$number" elif [[ "$run_id" != "unknown" ]]; then - if candidate_ref=$( + if candidate_run=$( gh api "repos/$GITHUB_REPOSITORY/actions/runs/$run_id" \ - --jq '"\(.repository.full_name)/\(.path)@refs/heads/\(.head_branch)"' + --jq '[ + (.repository.full_name + "/" + .path), + (.head_branch // ""), + ((.pull_requests[0].number // "") | tostring) + ] | @tsv' ); then - if [[ "$candidate_ref" == "$GITHUB_WORKFLOW_REF" ]]; then + IFS=$'\t' read -r candidate_identity candidate_ref_name candidate_pr_number \ + <<< "$candidate_run" + if [[ "$candidate_identity" != "$caller_identity" ]]; then + continue + elif [[ "$caller_ref" == "refs/heads/$candidate_ref_name" || + "$caller_ref" == "refs/tags/$candidate_ref_name" || + (-n "$candidate_pr_number" && + "$caller_ref" == refs/pull/"$candidate_pr_number"/*) ]]; then echo "$number" fi else From 7dfe1d4b859fbd12c7947be085227c148edbae36 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Thu, 3 Sep 2026 15:19:27 -0700 Subject: [PATCH 14/19] Address Copilot review comment: build issue body with printf Copilot comment: The `--body` string includes indentation spaces on the second line (`See ...`) due to YAML indentation being inside the quoted literal. This will render with leading spaces in the issue body. Consider building the body without embedded indentation (e.g., using a heredoc or `$'...'` with `\\n`) so the `See ...` line starts at column 0. Analysis: YAML already strips the run block's common indentation, but the source layout makes that behavior easy to misread. The workflow now constructs the complete issue body with `printf -v` and an explicit newline before passing it to `gh issue create`. Upsides: The generated body makes the column-zero second line explicit and no longer relies on readers recognizing YAML literal-block indentation rules. Downsides: The shell code adds one local variable and separate format arguments for the issue body. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/workflow-failure-issue.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/workflow-failure-issue.yml b/.github/workflows/workflow-failure-issue.yml index ec76843004cd..c9ce5d2eede8 100644 --- a/.github/workflows/workflow-failure-issue.yml +++ b/.github/workflows/workflow-failure-issue.yml @@ -139,8 +139,14 @@ jobs: done fi elif [[ "$INPUT_SUCCESS" == "false" ]]; then + printf -v issue_body \ + '\nSee [%s #%s](https://github.com/%s/actions/runs/%s).' \ + "$GITHUB_WORKFLOW_REF" \ + "$GITHUB_WORKFLOW" \ + "$GITHUB_RUN_NUMBER" \ + "$GITHUB_REPOSITORY" \ + "$GITHUB_RUN_ID" gh issue create --title "Workflow failed: $GITHUB_WORKFLOW (#$GITHUB_RUN_NUMBER)" \ - --body " - See [$GITHUB_WORKFLOW #$GITHUB_RUN_NUMBER](https://github.com/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID)." \ + --body "$issue_body" \ --label "$tracking_label" fi From 8b3866769f0ae51a6b9734506f2115731a29229f Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Thu, 3 Sep 2026 15:39:20 -0700 Subject: [PATCH 15/19] Address Copilot review comment: grant Actions read permission Copilot comment: This workflow calls the Actions API to verify legacy issues, but its job grants only `issues: write`; once any explicit permissions are set, `actions` is `none`. The workflow-run endpoint requires `actions: read` for private repositories, so these lookups fail, legacy issues are skipped, and a duplicate issue can be created. Add `actions: read` here and require it in the caller job/README as well, since a reusable workflow cannot elevate permissions omitted by its caller. Analysis: The reusable job now requests actions: read for workflow-run lookups. Its in-repository caller grants the same permission, and the consumer example and guidance require callers to grant it alongside issues: write. Upsides: Legacy issue migration can verify workflow runs in private repositories. Callers receive a complete permission contract instead of a lookup that silently lacks access. Downsides: Callers adopting this version must add actions: read to jobs that currently grant only issues: write. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/pull-request-dashboard.yml | 1 + .github/workflows/workflow-failure-issue.yml | 1 + workflow-failure-issue/README.md | 7 ++++++- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pull-request-dashboard.yml b/.github/workflows/pull-request-dashboard.yml index bad2be8eb197..704ec23dcc5b 100644 --- a/.github/workflows/pull-request-dashboard.yml +++ b/.github/workflows/pull-request-dashboard.yml @@ -330,6 +330,7 @@ jobs: ) ) permissions: + actions: read # needed to identify the caller workflow for legacy issues issues: write # needed to open/close the hourly failure tracking issue uses: ./.github/workflows/workflow-failure-issue.yml with: diff --git a/.github/workflows/workflow-failure-issue.yml b/.github/workflows/workflow-failure-issue.yml index c9ce5d2eede8..d11eaaae8a7d 100644 --- a/.github/workflows/workflow-failure-issue.yml +++ b/.github/workflows/workflow-failure-issue.yml @@ -19,6 +19,7 @@ jobs: name: Open or close workflow failure issue if: github.repository_owner == 'open-telemetry' && !cancelled() permissions: + actions: read # needed to identify the caller workflow for legacy issues issues: write # needed to open, comment on, and close workflow failure issues runs-on: ubuntu-slim steps: diff --git a/workflow-failure-issue/README.md b/workflow-failure-issue/README.md index d7fc1b560e36..52c583ffd478 100644 --- a/workflow-failure-issue/README.md +++ b/workflow-failure-issue/README.md @@ -33,7 +33,7 @@ Behavior: Add a final job to the workflow you want to monitor. It must run after the jobs you care about, use `if: always()` so it also runs when they fail, and grant -`issues: write`: +`actions: read` and `issues: write`: ```yaml jobs: @@ -41,6 +41,7 @@ jobs: workflow-failure-issue: permissions: + actions: read issues: write needs: - build @@ -52,6 +53,10 @@ jobs: Pin `` to a commit SHA or release tag in this repository. +The `actions: read` permission lets the reusable workflow verify the caller of +legacy tracking issues before adding the per-workflow label. The `issues: write` +permission lets it create, update, and close those issues. + The `success` input is required, so callers must always provide it. Set it from the `needs` results of the monitored jobs, as shown above. A status check function such as `success()` cannot be used here, because GitHub Actions allows From c00fdb99c1c8048d28cd15061eee58b449bb6366 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Thu, 3 Sep 2026 15:56:04 -0700 Subject: [PATCH 16/19] Address Copilot review comment: restrict migration issue author Copilot comment: The fallback trusts issue titles/bodies from any author. In a public repository, anyone can create this title and include the public workflow-ref marker or a URL for a matching run; the workflow will then adopt that issue and suppress creation of the real failure issue. Restrict migration candidates to issues created by the GitHub Actions app, since both legacy and new issues are created with `GITHUB_TOKEN`. This issue also appears on line 95 of the same file. Analysis: The fallback search now uses the author:app/github-actions qualifier and also checks that each returned issue's login is github-actions[bot] before trusting its marker or run link. Upsides: A repository visitor cannot make the workflow adopt a crafted issue or suppress creation of the real tracking issue. The server-side qualifier also narrows the search result set. Downsides: A legacy tracking issue created manually instead of through GITHUB_TOKEN will not migrate automatically. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/workflow-failure-issue.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/workflow-failure-issue.yml b/.github/workflows/workflow-failure-issue.yml index d11eaaae8a7d..79c91c01ab7d 100644 --- a/.github/workflows/workflow-failure-issue.yml +++ b/.github/workflows/workflow-failure-issue.yml @@ -47,7 +47,7 @@ jobs: | .number ' ) - search_query="repo:$GITHUB_REPOSITORY is:issue is:open \"Workflow failed\" in:title" + search_query="repo:$GITHUB_REPOSITORY is:issue is:open author:app/github-actions \"Workflow failed\" in:title" unlabelled_candidates=$( TRACKING_LABEL="$tracking_label" WORKFLOW_REF="$GITHUB_WORKFLOW_REF" \ gh api --paginate --method GET search/issues \ @@ -56,7 +56,8 @@ jobs: --jq ' .items[] | select( - (.title | startswith("Workflow failed: " + env.GITHUB_WORKFLOW + " (#")) + .user.login == "github-actions[bot]" + and (.title | startswith("Workflow failed: " + env.GITHUB_WORKFLOW + " (#")) and ((.labels | map(.name) | index(env.TRACKING_LABEL)) == null) ) | [ From b4b72bd50b8ef4866297cee7d6f2576a98af7eb1 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Thu, 3 Sep 2026 16:14:13 -0700 Subject: [PATCH 17/19] Address review finding: document duplicate consolidation in the README Review finding: This bullet tells consumers that serialization stops new duplicates from appearing, but the Behavior list never says what the workflow now does to tracking issues that are already open. The script adds two user-visible behaviors that the list omits. First, on failure with more than one matching open issue it comments only on the lowest-numbered one and closes every other one with `--reason "not planned"` and a `Superseded by #N` comment. Second, it adopts pre-existing issues created by earlier versions of this workflow by adding the per-workflow label to them, after confirming the run linked in the issue body belongs to the same caller. A maintainer of a consumer repository sees their existing failure issues get closed as not planned and relabelled by automation, with the README as the only place that documents this workflow. The PR description already states both behaviors, so the consumer documentation is the part that is missing them. Fix: extend the Behavior list with a bullet saying that on failure any extra open tracking issues are closed as not planned and superseded by the one that receives the comment, and a bullet saying that issues opened by earlier versions of this workflow are adopted and labelled once their linked run identifies the same caller workflow. Analysis: On failure the script comments on the lowest-numbered matching issue and closes every other match with --reason "not planned" and a "Superseded by #N" comment, but the Behavior list still described the old single-issue path. It also adds the per-workflow label to issues opened by earlier versions once the run linked in the issue body identifies the same caller workflow file and ref. CONTRIBUTING.md states that a workflow's README is the contract between the workflow and the repositories that use it, and this README is the only consumer documentation for the reusable workflow. A maintainer whose repository already holds several failure issues sees automation close some of them as not planned and relabel the rest, so the contract has to say so. The subsequent-failure bullet now names which issue keeps the comment and what happens to the others, and the label bullet now records the condition under which an older issue is adopted. Upsides: The Behavior list matches what the script does to issues that are already open. A consumer can tell in advance which of their existing failure issues survives and which ones close. Downsides: No material downside identified. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- workflow-failure-issue/README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/workflow-failure-issue/README.md b/workflow-failure-issue/README.md index 52c583ffd478..750e36ae1785 100644 --- a/workflow-failure-issue/README.md +++ b/workflow-failure-issue/README.md @@ -22,8 +22,11 @@ Behavior: the monitored caller workflow file and ref. The workflow creates that label in the calling repository when it is missing, and later runs use it to find the issue again. Workflows that share a display name use different labels. -- On a subsequent failure while an issue is already open, a comment linking to - the failing run is added. + Issues created by earlier versions receive the label only when the run linked + in their body identifies the same caller workflow file and ref. +- On a subsequent failure, a comment linking to the failing run is added to + the lowest-numbered open tracking issue. Any other matching issues are + closed as not planned with a comment that points to the retained issue. - On success, any open tracking issue is closed. - On cancellation, any open tracking issue is left unchanged. - Updates for the same caller workflow are serialized so concurrent runs cannot From d56c415d3a7ab1da7e444c6f6696c7705011d028 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Thu, 3 Sep 2026 16:23:53 -0700 Subject: [PATCH 18/19] Simplify workflow failure issue tracking Remove generated labels and legacy workflow-run migration while retaining serialization and duplicate convergence. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/pull-request-dashboard.yml | 1 - .github/workflows/workflow-failure-issue.yml | 96 ++------------------ workflow-failure-issue/README.md | 26 +----- 3 files changed, 12 insertions(+), 111 deletions(-) diff --git a/.github/workflows/pull-request-dashboard.yml b/.github/workflows/pull-request-dashboard.yml index 704ec23dcc5b..bad2be8eb197 100644 --- a/.github/workflows/pull-request-dashboard.yml +++ b/.github/workflows/pull-request-dashboard.yml @@ -330,7 +330,6 @@ jobs: ) ) permissions: - actions: read # needed to identify the caller workflow for legacy issues issues: write # needed to open/close the hourly failure tracking issue uses: ./.github/workflows/workflow-failure-issue.yml with: diff --git a/.github/workflows/workflow-failure-issue.yml b/.github/workflows/workflow-failure-issue.yml index 79c91c01ab7d..42e05afbf83f 100644 --- a/.github/workflows/workflow-failure-issue.yml +++ b/.github/workflows/workflow-failure-issue.yml @@ -11,7 +11,7 @@ on: permissions: {} concurrency: - group: shared-workflow-failure-issue-${{ github.workflow_ref }} + group: shared-workflow-failure-issue-${{ github.workflow }} cancel-in-progress: false jobs: @@ -19,7 +19,6 @@ jobs: name: Open or close workflow failure issue if: github.repository_owner == 'open-telemetry' && !cancelled() permissions: - actions: read # needed to identify the caller workflow for legacy issues issues: write # needed to open, comment on, and close workflow failure issues runs-on: ubuntu-slim steps: @@ -31,83 +30,17 @@ jobs: run: | set -euo pipefail - tracking_label="workflow-failure-$(printf '%s' "$GITHUB_WORKFLOW_REF" | sha256sum | cut -c1-32)" - if ! gh api "repos/$GITHUB_REPOSITORY/labels/$tracking_label" >/dev/null 2>&1; then - gh label create "$tracking_label" \ - --color D93F0B \ - --description "Managed by the shared workflow failure issue" - fi - - label_numbers=$( + numbers=$( gh api --paginate \ - "repos/$GITHUB_REPOSITORY/issues?state=open&labels=$tracking_label&per_page=100" \ + "repos/$GITHUB_REPOSITORY/issues?state=open&creator=github-actions%5Bbot%5D&per_page=100" \ --jq ' .[] - | select(.pull_request == null) - | .number - ' - ) - search_query="repo:$GITHUB_REPOSITORY is:issue is:open author:app/github-actions \"Workflow failed\" in:title" - unlabelled_candidates=$( - TRACKING_LABEL="$tracking_label" WORKFLOW_REF="$GITHUB_WORKFLOW_REF" \ - gh api --paginate --method GET search/issues \ - -f q="$search_query" \ - -f per_page=100 \ - --jq ' - .items[] | select( - .user.login == "github-actions[bot]" + .pull_request == null and (.title | startswith("Workflow failed: " + env.GITHUB_WORKFLOW + " (#")) - and ((.labels | map(.name) | index(env.TRACKING_LABEL)) == null) ) - | [ - .number, - ( - if ((.body // "") | contains("")) - then "match" - else ((.body // "") | capture("/actions/runs/(?[0-9]+)")?.id // "unknown") - end - ) - ] - | @tsv - ' - ) - unlabelled_numbers=$( - caller_identity="${GITHUB_WORKFLOW_REF%@*}" - caller_ref="${GITHUB_WORKFLOW_REF##*@}" - while IFS=$'\t' read -r number run_id; do - if [[ -z "$number" ]]; then - continue - elif [[ "$run_id" == "match" ]]; then - echo "$number" - elif [[ "$run_id" != "unknown" ]]; then - if candidate_run=$( - gh api "repos/$GITHUB_REPOSITORY/actions/runs/$run_id" \ - --jq '[ - (.repository.full_name + "/" + .path), - (.head_branch // ""), - ((.pull_requests[0].number // "") | tostring) - ] | @tsv' - ); then - IFS=$'\t' read -r candidate_identity candidate_ref_name candidate_pr_number \ - <<< "$candidate_run" - if [[ "$candidate_identity" != "$caller_identity" ]]; then - continue - elif [[ "$caller_ref" == "refs/heads/$candidate_ref_name" || - "$caller_ref" == "refs/tags/$candidate_ref_name" || - (-n "$candidate_pr_number" && - "$caller_ref" == refs/pull/"$candidate_pr_number"/*) ]]; then - echo "$number" - fi - else - echo "Unable to verify workflow identity for issue #$number from run $run_id" >&2 - fi - fi - done <<< "$unlabelled_candidates" - ) - numbers=$( - printf '%s\n%s\n' "$label_numbers" "$unlabelled_numbers" \ - | sed '/^$/d' \ + | .number + ' \ | sort -n -u ) if [[ -n "$numbers" ]]; then @@ -116,13 +49,6 @@ jobs: issue_numbers=() fi - if [[ -n "$unlabelled_numbers" ]]; then - mapfile -t unlabelled_issue_numbers <<< "$unlabelled_numbers" - for number in "${unlabelled_issue_numbers[@]}"; do - gh issue edit "$number" --add-label "$tracking_label" - done - fi - echo "$numbers" echo "$INPUT_SUCCESS" @@ -141,14 +67,6 @@ jobs: done fi elif [[ "$INPUT_SUCCESS" == "false" ]]; then - printf -v issue_body \ - '\nSee [%s #%s](https://github.com/%s/actions/runs/%s).' \ - "$GITHUB_WORKFLOW_REF" \ - "$GITHUB_WORKFLOW" \ - "$GITHUB_RUN_NUMBER" \ - "$GITHUB_REPOSITORY" \ - "$GITHUB_RUN_ID" gh issue create --title "Workflow failed: $GITHUB_WORKFLOW (#$GITHUB_RUN_NUMBER)" \ - --body "$issue_body" \ - --label "$tracking_label" + --body "See [$GITHUB_WORKFLOW #$GITHUB_RUN_NUMBER](https://github.com/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID)." fi diff --git a/workflow-failure-issue/README.md b/workflow-failure-issue/README.md index 750e36ae1785..8d2d0734ed0e 100644 --- a/workflow-failure-issue/README.md +++ b/workflow-failure-issue/README.md @@ -18,25 +18,21 @@ Behavior: - On failure, if no tracking issue is open, a new issue titled `Workflow failed: (#)` is created. -- Each tracking issue carries a `workflow-failure-` label that identifies - the monitored caller workflow file and ref. The workflow creates that label - in the calling repository when it is missing, and later runs use it to find - the issue again. Workflows that share a display name use different labels. - Issues created by earlier versions receive the label only when the run linked - in their body identifies the same caller workflow file and ref. +- Tracking issues are matched by their workflow-name title prefix and + `github-actions[bot]` author. - On a subsequent failure, a comment linking to the failing run is added to the lowest-numbered open tracking issue. Any other matching issues are closed as not planned with a comment that points to the retained issue. - On success, any open tracking issue is closed. - On cancellation, any open tracking issue is left unchanged. -- Updates for the same caller workflow are serialized so concurrent runs cannot +- Updates for the same workflow name are serialized so concurrent runs cannot create duplicate tracking issues. ## How to use Add a final job to the workflow you want to monitor. It must run after the jobs you care about, use `if: always()` so it also runs when they fail, and grant -`actions: read` and `issues: write`: +`issues: write`: ```yaml jobs: @@ -44,7 +40,6 @@ jobs: workflow-failure-issue: permissions: - actions: read issues: write needs: - build @@ -56,19 +51,8 @@ jobs: Pin `` to a commit SHA or release tag in this repository. -The `actions: read` permission lets the reusable workflow verify the caller of -legacy tracking issues before adding the per-workflow label. The `issues: write` -permission lets it create, update, and close those issues. - -The `success` input is required, so callers must always provide it. Set it from -the `needs` results of the monitored jobs, as shown above. A status check -function such as `success()` cannot be used here, because GitHub Actions allows -those functions only in an `if` condition. The reusable workflow skips the issue -update when the caller run is cancelled, so it ignores the supplied value in -that case. - ### Inputs | Name | Type | Required | Description | | ---- | ---- | -------- | ----------- | -| `success` | boolean | yes | Whether the monitored jobs succeeded. Pass `true` to close any open tracking issue or `false` to open or comment on one. | +| `success` | boolean | yes | Whether the monitored jobs succeeded. Pass `true` to close any open tracking issue, `false` to open or comment on one. | From 2b203ce2b051c9114fe45b1555d234ea42a8747d Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Thu, 3 Sep 2026 16:55:11 -0700 Subject: [PATCH 19/19] Keep workflow failure fix concurrency-only Restore the existing issue lookup and handling so this change only serializes workflow updates. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5ed70e46-7212-49db-9233-8783a74f7d85 --- .github/workflows/workflow-failure-issue.yml | 37 ++++---------------- workflow-failure-issue/README.md | 10 ++---- 2 files changed, 9 insertions(+), 38 deletions(-) diff --git a/.github/workflows/workflow-failure-issue.yml b/.github/workflows/workflow-failure-issue.yml index 42e05afbf83f..6ca462ea720e 100644 --- a/.github/workflows/workflow-failure-issue.yml +++ b/.github/workflows/workflow-failure-issue.yml @@ -17,7 +17,7 @@ concurrency: jobs: workflow-failure-issue: name: Open or close workflow failure issue - if: github.repository_owner == 'open-telemetry' && !cancelled() + if: github.repository_owner == 'open-telemetry' permissions: issues: write # needed to open, comment on, and close workflow failure issues runs-on: ubuntu-slim @@ -30,41 +30,18 @@ jobs: run: | set -euo pipefail - numbers=$( - gh api --paginate \ - "repos/$GITHUB_REPOSITORY/issues?state=open&creator=github-actions%5Bbot%5D&per_page=100" \ - --jq ' - .[] - | select( - .pull_request == null - and (.title | startswith("Workflow failed: " + env.GITHUB_WORKFLOW + " (#")) - ) - | .number - ' \ - | sort -n -u - ) - if [[ -n "$numbers" ]]; then - mapfile -t issue_numbers <<< "$numbers" - else - issue_numbers=() - fi + # TODO (trask) search doesn't support exact phrases, so it's possible that this could grab the wrong issue + number=$(gh issue list --search "in:title Workflow failed: $GITHUB_WORKFLOW" --limit 1 --json number -q .[].number) - echo "$numbers" + echo "$number" echo "$INPUT_SUCCESS" - if (( ${#issue_numbers[@]} > 0 )); then + if [[ -n "$number" ]]; then if [[ "$INPUT_SUCCESS" == "true" ]]; then - for number in "${issue_numbers[@]}"; do - gh issue close "$number" - done + gh issue close "$number" else - gh issue comment "${issue_numbers[0]}" \ + gh issue comment "$number" \ --body "See [$GITHUB_WORKFLOW #$GITHUB_RUN_NUMBER](https://github.com/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID)." - for number in "${issue_numbers[@]:1}"; do - gh issue close "$number" \ - --reason "not planned" \ - --comment "Superseded by #${issue_numbers[0]}." - done fi elif [[ "$INPUT_SUCCESS" == "false" ]]; then gh issue create --title "Workflow failed: $GITHUB_WORKFLOW (#$GITHUB_RUN_NUMBER)" \ diff --git a/workflow-failure-issue/README.md b/workflow-failure-issue/README.md index 8d2d0734ed0e..ba6232347079 100644 --- a/workflow-failure-issue/README.md +++ b/workflow-failure-issue/README.md @@ -18,15 +18,9 @@ Behavior: - On failure, if no tracking issue is open, a new issue titled `Workflow failed: (#)` is created. -- Tracking issues are matched by their workflow-name title prefix and - `github-actions[bot]` author. -- On a subsequent failure, a comment linking to the failing run is added to - the lowest-numbered open tracking issue. Any other matching issues are - closed as not planned with a comment that points to the retained issue. +- On a subsequent failure while an issue is already open, a comment linking to + the failing run is added. - On success, any open tracking issue is closed. -- On cancellation, any open tracking issue is left unchanged. -- Updates for the same workflow name are serialized so concurrent runs cannot - create duplicate tracking issues. ## How to use