Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
41325bb
ci: enrich scheduled-failure issues with an LLM triage pass
tonyandrewmeyer Jul 25, 2026
2710672
fix: append the Workflow footer, and report fallbacks to the log
tonyandrewmeyer Jul 25, 2026
b51bb2e
fix: stop rejecting every envelope that carries `also`
tonyandrewmeyer Jul 25, 2026
5b0534e
fix: keep the matched issue as a candidate, and tolerate null fields
tonyandrewmeyer Jul 25, 2026
2a4ea10
fix: ignore fields that do not apply, rather than rejecting the response
tonyandrewmeyer Jul 25, 2026
82f2df5
fix: find the notifier's marker without waiting on the search index
tonyandrewmeyer Jul 25, 2026
e16680b
test: keep the notifier test fixtures under the 99-column limit
tonyandrewmeyer Jul 25, 2026
239f8fc
test: apply ruff --preview formatting to the notifier tests
tonyandrewmeyer Jul 25, 2026
4d958b7
fix: stop the workflow-level fallback opening a duplicate issue
tonyandrewmeyer Jul 26, 2026
29a2f84
test: address review feedback on the notifier tests
tonyandrewmeyer Aug 18, 2026
d99b81d
refactor: call the enricher via workflow_call instead of workflow_run
tonyandrewmeyer Aug 18, 2026
f0ce3b7
refactor: narrow the workflow-level fallback to the case that needs it
tonyandrewmeyer Aug 18, 2026
4f8e490
Revert "call the enricher via workflow_call", keep what it uncovered
tonyandrewmeyer Aug 18, 2026
8a36b96
refactor: give repository scripts a tested home in scripts/
tonyandrewmeyer Aug 23, 2026
69c9248
fix: report the status gh gave when a job log cannot be fetched
tonyandrewmeyer Aug 23, 2026
504a7fa
fix: pass --allow-escape-sequences, so the log fetch returns the log
tonyandrewmeyer Aug 23, 2026
2cc1f11
refactor: call the enricher via workflow_call instead of workflow_run
tonyandrewmeyer Aug 23, 2026
6ea53a7
fix: parse a step's output, not the script the runner echoed
tonyandrewmeyer Aug 24, 2026
8155246
refactor: pass the OpenRouter key by name, not by inheriting everything
tonyandrewmeyer Aug 24, 2026
b53cdc7
refactor: drop the workflow-level fallback, as James suggested
tonyandrewmeyer Aug 24, 2026
6d602a6
refactor: rename the notifier's job to `notify`
tonyandrewmeyer Aug 24, 2026
9b3ad1b
Merge branch 'main' into ai-failure-notifications-step-5
tonyandrewmeyer Aug 24, 2026
824e9ba
fix: drop the script's 3.11 pin, which was never true
tonyandrewmeyer Aug 24, 2026
a68f1b6
docs: drop the fork-run detail from the echoed-script comment
tonyandrewmeyer Aug 24, 2026
5d543a9
docs: drop the fork-run detail from the test comment too
tonyandrewmeyer Aug 24, 2026
f15bcf1
Apply suggestions from code review
tonyandrewmeyer Aug 24, 2026
f7e20fd
ci: pass the notifier's issue number through to the enricher
tonyandrewmeyer Aug 26, 2026
351192b
ci: don't enrich when the notification itself failed
tonyandrewmeyer Aug 26, 2026
ae5f5b0
ci: drop the comments restating GitHub's permission model
tonyandrewmeyer Aug 26, 2026
203e440
ci: document the secret mechanics once, where the environment is
tonyandrewmeyer Aug 26, 2026
b7fb74b
ci: trim the caller boilerplate to the part a caller needs
tonyandrewmeyer Aug 26, 2026
52c63f6
ci: shorten the workflow header to what a caller has to do
tonyandrewmeyer Aug 26, 2026
2e79c77
ci: run the notifier from its own package, not a copy in this repo
tonyandrewmeyer Aug 31, 2026
e101469
Merge remote-tracking branch 'upstream-https/main' into ai-failure-no…
tonyandrewmeyer Sep 10, 2026
880d523
ci: pin the notifier package to its merged commit
tonyandrewmeyer Sep 10, 2026
d54fb7d
docs: say what the workflows do, not how they got here
tonyandrewmeyer Sep 10, 2026
f43d91a
ci: pin the notifier package to the issue-type fix
tonyandrewmeyer Sep 10, 2026
f41d471
ci: pin the notifier package to the target-issue fix
tonyandrewmeyer Sep 10, 2026
304b6ae
ci: pin the notifier package to the OpenRouter error-detail fix
tonyandrewmeyer Sep 10, 2026
8df2f14
Merge branch 'main' into ai-failure-notifications-step-5
tonyandrewmeyer Sep 14, 2026
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
70 changes: 70 additions & 0 deletions .github/workflows/ai-failure-enrich.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
name: AI-enrich scheduled-failure notification

# Called by notify-scheduled-failure.yaml, which is called by scheduled workflows on failure.
# - notify-scheduled-failure.yaml creates or comments on an issue before calling this workflow.
# - github.workflow and github.run_id refer to the scheduled workflow that failed.

on:
workflow_call:
inputs:
# What the notifier did, when it got that far. Optional: an empty value
# means the script looks the issue up itself, which is what happens when
# `notify` fails before it can create one.
issue:
description: The issue the notifier created or commented on.
required: false
type: string
origin:
description: Either "new" or "comment", matching what the notifier did.
required: false
type: string
secrets:
OPENROUTER_API_KEY:
required: false

permissions: {}

jobs:
enrich:
runs-on: ubuntu-latest
environment: ai-failure-triage # Required to access secrets.OPENROUTER_API_KEY
permissions:
Comment thread
james-garner-canonical marked this conversation as resolved.
issues: write
actions: read
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
- name: Extract signature, dedupe, ask the LLM, apply
id: enrich
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
RUN_ID: ${{ github.run_id }}
WORKFLOW_NAME: ${{ github.workflow }} # The workflow that failed.
NOTIFY_ISSUE: ${{ inputs.issue }}
NOTIFY_ORIGIN: ${{ inputs.origin }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
# It takes two things to get this key here, and it's tempting to
# remove the second one, so please don't:
#
# - `environment:` on this job holds the real value. A caller
# can't see it, because a job without an `environment:` gets
# an empty string for `secrets.OPENROUTER_API_KEY`.
# - every call site in the chain names the secret, and it's
# declared above. That only ever passes the empty string, which
# is why it looks like it does nothing.
#
# Take the second one out and the key arrives empty, and nothing
# complains: we just get a plain notice instead of an enriched one.
# If enrichment stops happening for no obvious reason, look here.
#
# Each repo that adopts this provisions its own key. Without one,
# the script writes the plain fallback body and never calls
# OpenRouter (see the package's cli.main()).
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
OPENROUTER_MODEL: ${{ vars.OPENROUTER_MODEL }}
run: >-
uvx --from "git+https://github.com/canonical/charm-tech-code@3702a80516503509a0472f62dcffbcf022654925#subdirectory=ai-failure-notifier"
ai-failure-notifier
4 changes: 4 additions & 0 deletions .github/workflows/example-charm-charmcraft-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,8 @@ jobs:
needs: [integration]
permissions:
issues: write
actions: read # for ai-failure-enrich.yaml
secrets:
# For ai-failure-enrich.yaml -- can only be read in the ai-failure-triage environment.
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
uses: ./.github/workflows/notify-scheduled-failure.yaml
4 changes: 4 additions & 0 deletions .github/workflows/example-charm-integration-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,8 @@ jobs:
needs: [machine-examples-integration, k8s-examples-integration]
permissions:
issues: write
actions: read # for ai-failure-enrich.yaml
secrets:
# For ai-failure-enrich.yaml -- can only be read in the ai-failure-triage environment.
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
uses: ./.github/workflows/notify-scheduled-failure.yaml
4 changes: 4 additions & 0 deletions .github/workflows/integration.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,8 @@ jobs:
needs: [integration]
permissions:
issues: write
actions: read # for ai-failure-enrich.yaml
secrets:
# For ai-failure-enrich.yaml -- can only be read in the ai-failure-triage environment.
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
uses: ./.github/workflows/notify-scheduled-failure.yaml
76 changes: 67 additions & 9 deletions .github/workflows/notify-scheduled-failure.yaml
Original file line number Diff line number Diff line change
@@ -1,26 +1,84 @@
name: Notify on scheduled failure

# Reusable workflow: opens an issue when a scheduled workflow fails.
# Callers gate the invocation with `if: failure() && github.event_name == 'schedule'`
# and pass `permissions: issues: write`.
# Reusable workflow: opens (or comments on) an issue when a scheduled
# workflow fails. Callers gate the invocation with
# `if: failure() && github.event_name == 'schedule'` and pass
# `permissions: issues: write` plus `actions: read`, and grant access to
# `secrets.OPENROUTER_API_KEY`.
#
# 1. `notify` opens or comments on an issue. No LLM and no secrets: once it
# has run, a notification exists.
# 2. `enrich` (ai-failure-enrich.yaml) rewrites that issue into something
# worth reading. If its key or environment is missing, we still have 1.

on:
workflow_call:
secrets:
# For ai-failure-enrich.yaml -- can only be read in the ai-failure-triage environment.
OPENROUTER_API_KEY:
required: false

jobs:
open-issue:
notify:
runs-on: ubuntu-latest
permissions:
issues: write
# Handed to `enrich` below, so that it's told which issue to rewrite
# rather than searching for the marker this job has just stamped.
# GitHub's issue search index doesn't necessarily show you what you have
# only just written, so that search can miss a marker from seconds ago
# and open a second issue for a run that already has one.
outputs:
issue: ${{ steps.notify.outputs.issue }}
origin: ${{ steps.notify.outputs.origin }}
steps:
- name: Create issue on failure
- name: Create or comment on issue, deduping coarsely by workflow name
id: notify
env:
GH_TOKEN: ${{ github.token }}
WORKFLOW_NAME: ${{ github.workflow }} # The workflow that called this one.
REPO: ${{ github.repository }}
RUN_ID: ${{ github.run_id }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
gh issue create \
--repo "$REPO" \
--title "Scheduled workflow '$WORKFLOW_NAME' failed" \
--body "Scheduled workflow '$WORKFLOW_NAME' failed: $RUN_URL"
set -euo pipefail
marker="<!-- ai-failure-notifications:run=$RUN_ID:origin="

# Search for the workflow name in issue bodies or titles.
match=$(gh issue list --repo "$REPO" --state open \
--search "\"$WORKFLOW_NAME\"" \
--json number --limit 1 -q '.[0].number // empty')

if [[ -n "$match" ]]; then
comment_body="Scheduled workflow '$WORKFLOW_NAME' failed again: $RUN_URL"$'\n\n'"${marker}comment -->"
gh issue comment "$match" --repo "$REPO" --body "$comment_body"
echo "issue=$match" >> "$GITHUB_OUTPUT"
echo "origin=comment" >> "$GITHUB_OUTPUT"
else
issue_body="Scheduled workflow '$WORKFLOW_NAME' failed: $RUN_URL"$'\n\n'"${marker}new -->"
# `gh issue create` prints the new issue's URL; the number is its
# last path segment.
issue_url=$(gh issue create --repo "$REPO" \
--title "Scheduled workflow '$WORKFLOW_NAME' failed" \
--body "$issue_body")
echo "issue=${issue_url##*/}" >> "$GITHUB_OUTPUT"
echo "origin=new" >> "$GITHUB_OUTPUT"
fi

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like it would be simpler to fold the dumb match and comment logic into the fallback branch of the enrich script and just call the enrich workflow here ... or even just fold the workflow in here and call the script directly. If the concern is making the AI parts easy to tear out or disable later, I think we could do that with clearly commented sections of input parameters and in the script itself; or perhaps factor the script into two separate modules.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The enricher is now on: workflow_call and this workflow calls it after open-issue, so the workflow_run trigger, the hardcoded list of seven caller workflow names, and the zizmor: ignore[dangerous-triggers] suppression are all gone. I had tried something along those lines when experimenting with this on my fork but hadn't found this path, which doesn't seem better, thanks! (I'm double-checking it on the fork at the moment.)

I'd like to keep the coarse match as bash in its own job rather than folding it into the script:

  • open-issue currently runs with nothing but gh and github.token. Folding it in adds a checkout, setup-uv, and the environment as prerequisites for a notification happening at all.
  • enrich needs environment: ai-failure-triage for the OpenRouter key, and environments can carry protection rules including required reviewers. One job means the notification inherits that, so a misconfigured environment or an approval gate doesn't degrade the notification, it blocks it pending a human for a workflow whose whole purpose is telling people something broke overnight. Admittedly, we would presumably configure that in canonical-repo-automation so hopefully notice it, but it could happen.

The split is down to about 30 lines of bash now that it isn't also carrying the workflow_run argument, which feels ok for a notification path with no checkout, no uv and no environment on it. Happy to revisit if you still think it's not worth it, particularly once all the other changes are verified to be working.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current split and permissions rationale makes sense to me, thanks.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WDYT about notify outputing the issue number for us (commented on or created), which could simplify the ai_failure_notifier.py script since we wouldn't need to duplicate the issue lookup logic and can just treat it as an input?

If we do want enrich to run even if notify fails, this also lets us cleanly distinguish "we were passed a specific issue" from "we have to track down an issue on our own", so I think outputting the issue number could be helpful even if we end up needing to retain issue lookup logic in the script.


enrich:
needs: [notify]
permissions:
issues: write
actions: read
Comment thread
tonyandrewmeyer marked this conversation as resolved.
# Passes an empty string, and is still required -- see the note on
# OPENROUTER_API_KEY in ai-failure-enrich.yaml.
secrets:
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
# `notify` has to have succeeded to get here, so these are always set for
# this repo. The script can still look the issue up if they're empty,
# which is what a repo that adopts the enricher before it passes these
# will get.
with:
issue: ${{ needs.notify.outputs.issue }}
origin: ${{ needs.notify.outputs.origin }}
uses: ./.github/workflows/ai-failure-enrich.yaml
4 changes: 4 additions & 0 deletions .github/workflows/smoke.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,8 @@ jobs:
needs: [test]
permissions:
issues: write
actions: read # for ai-failure-enrich.yaml
secrets:
# For ai-failure-enrich.yaml -- can only be read in the ai-failure-triage environment.
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
Comment thread
tonyandrewmeyer marked this conversation as resolved.
uses: ./.github/workflows/notify-scheduled-failure.yaml
4 changes: 4 additions & 0 deletions .github/workflows/tiobe.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,8 @@ jobs:
needs: [TICS]
permissions:
issues: write
actions: read # for ai-failure-enrich.yaml
secrets:
# For ai-failure-enrich.yaml -- can only be read in the ai-failure-triage environment.
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
uses: ./.github/workflows/notify-scheduled-failure.yaml
4 changes: 4 additions & 0 deletions .github/workflows/update-best-practice-doc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,8 @@ jobs:
needs: [update-docs]
permissions:
issues: write
actions: read # for ai-failure-enrich.yaml
secrets:
# For ai-failure-enrich.yaml -- can only be read in the ai-failure-triage environment.
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
uses: ./.github/workflows/notify-scheduled-failure.yaml
4 changes: 4 additions & 0 deletions .github/workflows/update-charm-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,8 @@ jobs:
needs: [update-pins]
permissions:
issues: write
actions: read # for ai-failure-enrich.yaml
secrets:
# For ai-failure-enrich.yaml -- can only be read in the ai-failure-triage environment.
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
uses: ./.github/workflows/notify-scheduled-failure.yaml