-
Notifications
You must be signed in to change notification settings - Fork 138
ci: enrich scheduled-failure issues with an LLM triage pass #2663
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
41325bb
2710672
b51bb2e
5b0534e
2a4ea10
82f2df5
e16680b
239f8fc
4d958b7
29a2f84
d99b81d
f0ce3b7
4f8e490
8a36b96
69c9248
504a7fa
2cc1f11
6ea53a7
8155246
b53cdc7
6d602a6
9b3ad1b
824e9ba
a68f1b6
5d543a9
f15bcf1
f7e20fd
351192b
ae5f5b0
203e440
b7fb74b
52c63f6
2e79c77
e101469
880d523
d54fb7d
f43d91a
f41d471
304b6ae
8df2f14
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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: | ||
| 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 | ||
| 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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The enricher is now I'd like to keep the coarse match as bash in its own job rather than folding it into the script:
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.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The current split and permissions rationale makes sense to me, thanks.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. WDYT about If we do want |
||
|
|
||
| enrich: | ||
| needs: [notify] | ||
| permissions: | ||
| issues: write | ||
| actions: read | ||
|
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 | ||
Uh oh!
There was an error while loading. Please reload this page.