Skip to content

Commit e12210e

Browse files
authored
Refactor AI Agent workflow for app token usage (#3689)
Updated the AI Agent GitHub workflow to use app tokens instead of PATs for authentication and modified agent mention handling.
1 parent f9e1fcc commit e12210e

1 file changed

Lines changed: 84 additions & 18 deletions

File tree

‎.github/workflows/ai-agent.yml‎

Lines changed: 84 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,13 @@
33
# Listens for @<agent-name> please <request> mentions from authorized users
44
# and runs Claude Code to respond.
55
#
6+
# Authenticates as the `ai4c-agent` GitHub App (installation token minted per
7+
# job), not as a machine account PAT. Same app and secret names already used by
8+
# claude-code-review.yml.
9+
#
610
# Required secrets:
7-
# - PAT_FOR_PR
11+
# - AI4C_AGENT_APP_ID
12+
# - AI4C_AGENT_PRIVATE_KEY
813
# - CLAUDE_CODE_OAUTH_TOKEN
914
#
1015
# Configuration:
@@ -13,7 +18,19 @@
1318
name: AI Agent GitHub Mentions
1419

1520
env:
16-
AGENT_NAME: dragon-ai-agent
21+
# The handle controllers type to invoke the agent. NOT a real GitHub account:
22+
# the app's login is AGENT_LOGIN (`<name>[bot]`), and apps cannot be
23+
# @-mentioned, so "@ai4c-agent" renders as plain text.
24+
AGENT_MENTION: ai4c-agent
25+
# Deprecated handles still honoured as triggers (comma-separated). Also the
26+
# only logins that can work as assignment triggers -- see ASSIGNMENT below.
27+
AGENT_MENTION_LEGACY: dragon-ai-agent
28+
# GitHub App bot identity, for commit attribution. AGENT_USER_ID is the
29+
# numeric id of the ai4c-agent[bot] *account*
30+
# (`gh api /users/ai4c-agent%5Bbot%5D`), NOT the app id in
31+
# AI4C_AGENT_APP_ID -- different numbers. Public, hence plain env.
32+
AGENT_LOGIN: ai4c-agent[bot]
33+
AGENT_USER_ID: 242316268
1734
MODEL: claude-opus-4-7
1835
TOOLS_DIR: ${{ github.workspace }}/tools
1936
TIMEOUT_MINUTES: 30
@@ -34,8 +51,11 @@ on:
3451
prompt:
3552
description: "The request/prompt for the agent"
3653
required: true
37-
# `assigned` lets authorized controllers dispatch work by assigning
38-
# `dragon-ai-agent` directly.
54+
# ASSIGNMENT: `assigned` lets authorized controllers dispatch work by
55+
# assigning the agent directly. NOTE this cannot work with the GitHub App --
56+
# `ai4c-agent[bot]` is not an assignable user, so only the legacy
57+
# `dragon-ai-agent` machine account can still trigger this path, and only for
58+
# as long as that account exists. See `agentAssignees` in the check script.
3959
issues:
4060
types: [opened, edited, assigned]
4161
issue_comment:
@@ -55,17 +75,27 @@ jobs:
5575
outputs:
5676
result: ${{ steps.check.outputs.result }}
5777
steps:
78+
# Minted per-job: installation tokens are short-lived (1h) and must not be
79+
# passed between jobs, since GitHub's log masking does not follow job
80+
# outputs.
81+
- name: Mint app token
82+
id: app-token
83+
uses: actions/create-github-app-token@v3
84+
with:
85+
app-id: ${{ secrets.AI4C_AGENT_APP_ID }}
86+
private-key: ${{ secrets.AI4C_AGENT_PRIVATE_KEY }}
87+
5888
- name: Checkout repository
5989
uses: actions/checkout@v6
6090
with:
6191
fetch-depth: 1
62-
token: ${{ secrets.PAT_FOR_PR }}
92+
token: ${{ steps.app-token.outputs.token }}
6393

6494
- name: Check for qualifying mention
6595
id: check
6696
uses: actions/github-script@v8
6797
with:
68-
github-token: ${{ secrets.PAT_FOR_PR }}
98+
github-token: ${{ steps.app-token.outputs.token }}
6999
script: |
70100
const fs = require("fs");
71101
@@ -78,7 +108,18 @@ jobs:
78108
allowedUsers = ["cmungall"];
79109
}
80110
81-
const agentName = process.env.AGENT_NAME || "dragon-ai-agent";
111+
// Trigger on the canonical handle plus any legacy aliases, with an
112+
// optional [bot] suffix (people paste back what they see the bot
113+
// signing as).
114+
const agentName = process.env.AGENT_MENTION;
115+
const legacyNames = (process.env.AGENT_MENTION_LEGACY || "")
116+
.split(",").map((s) => s.trim()).filter(Boolean);
117+
const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
118+
const mentionAlternation = [agentName, ...legacyNames].map(escapeRe).join("|");
119+
// Assignment dispatch can only ever match a real, assignable user
120+
// account. The app's `ai4c-agent[bot]` login is not assignable, so
121+
// this is the legacy names only.
122+
const agentAssignees = legacyNames;
82123
83124
if (context.eventName === "workflow_dispatch") {
84125
const inputs = context.payload.inputs;
@@ -139,8 +180,16 @@ jobs:
139180
reactionTarget = "pull_request_review_comment";
140181
}
141182
183+
// Never act on our own output. Unlike GITHUB_TOKEN, events authored
184+
// with an app installation token do retrigger workflows.
185+
if (userLogin.endsWith("[bot]")) {
186+
console.log(`Ignoring bot author: ${userLogin}`);
187+
return { qualifiedMention: false };
188+
}
189+
142190
const isAllowed = allowedUsers.includes(userLogin);
143-
const mentionRegex = new RegExp(`@${agentName}\\s+please\\s+([\\s\\S]*)`, "i");
191+
const mentionRegex = new RegExp(
192+
`@(${mentionAlternation})(?:\\[bot\\])?\\s+please\\s+([\\s\\S]*)`, "i");
144193
const mentionMatch = content.match(mentionRegex);
145194
// On synchronize we want this workflow to revalidate on push without
146195
// re-dispatching the agent from a stale PR body mention.
@@ -152,19 +201,23 @@ jobs:
152201
const isAgentAssignment =
153202
(context.eventName === "issues" || context.eventName === "pull_request") &&
154203
context.payload.action === "assigned" &&
155-
context.payload.assignee?.login === agentName;
204+
agentAssignees.includes(context.payload.assignee?.login);
156205
const hasQualifiedBodyMention =
157206
isAllowed && shouldHonorBodyMention && mentionMatch !== null;
158207
const qualifiedMention =
159208
hasQualifiedBodyMention || (isAllowed && isAgentAssignment);
160209
const prompt = hasQualifiedBodyMention
161-
? mentionMatch[1].trim()
210+
? mentionMatch[2].trim()
162211
: (isAgentAssignment
163212
? "You were assigned to this item. Read the full GitHub context, identify the next concrete action, and carry it through."
164213
: "");
214+
const mentionUsed = mentionMatch ? mentionMatch[1] : "";
215+
const usedLegacyMention =
216+
hasQualifiedBodyMention && mentionUsed.toLowerCase() !== agentName.toLowerCase();
165217
166218
console.log(
167219
`User: ${userLogin}, Allowed: ${isAllowed}, Has mention: ${mentionMatch !== null}, ` +
220+
`Handle: ${mentionUsed}, ` +
168221
`Honor body mention: ${shouldHonorBodyMention}, ` +
169222
`Agent assignment: ${isAgentAssignment}`
170223
);
@@ -202,7 +255,10 @@ jobs:
202255
203256
try {
204257
const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
205-
const commentBody = `🤖 Working on it...\n\nFollow along: [View workflow run](${runUrl})\n\n*— @${agentName}*`;
258+
const deprecationNote = usedLegacyMention
259+
? `\n\n> [!NOTE]\n> \`@${mentionUsed}\` is deprecated and will stop working. Please use \`@${agentName} please ...\` from now on.`
260+
: "";
261+
const commentBody = `🤖 Working on it...\n\nFollow along: [View workflow run](${runUrl})${deprecationNote}\n\n*— @${agentName}*`;
206262
207263
await github.rest.issues.createComment({
208264
owner: context.repo.owner,
@@ -245,16 +301,26 @@ jobs:
245301
runs-on: ubuntu-latest
246302
container: ${{ fromJSON(needs.check-mention.outputs.result).useOdkContainer && 'obolibrary/odkfull:v1.6' || null }}
247303
steps:
304+
# Must precede checkout: the token is persisted as the git credential and
305+
# is what later `git push` calls authenticate with. Valid for 1h, which
306+
# bounds how far timeout-minutes above can safely be raised.
307+
- name: Mint app token
308+
id: app-token
309+
uses: actions/create-github-app-token@v3
310+
with:
311+
app-id: ${{ secrets.AI4C_AGENT_APP_ID }}
312+
private-key: ${{ secrets.AI4C_AGENT_PRIVATE_KEY }}
313+
248314
- name: Checkout repository
249315
uses: actions/checkout@v6
250316
with:
251317
fetch-depth: 1
252-
token: ${{ secrets.PAT_FOR_PR }}
318+
token: ${{ steps.app-token.outputs.token }}
253319

254320
- name: Configure Git
255321
run: |
256-
git config --global user.name "${{ env.AGENT_NAME }}[bot]"
257-
git config --global user.email "${{ env.AGENT_NAME }}[bot]@users.noreply.github.com"
322+
git config --global user.name "${{ env.AGENT_LOGIN }}"
323+
git config --global user.email "${{ env.AGENT_USER_ID }}+${{ env.AGENT_LOGIN }}@users.noreply.github.com"
258324
259325
- name: Create tools directory
260326
run: mkdir -p "${{ env.TOOLS_DIR }}"
@@ -278,20 +344,20 @@ jobs:
278344
echo "${{ github.workspace }}/.venv/bin" >> "$GITHUB_PATH"
279345
280346
- name: Export GitHub token for gh CLI
281-
run: echo "GH_TOKEN=${{ secrets.PAT_FOR_PR }}" >> "$GITHUB_ENV"
347+
run: echo "GH_TOKEN=${{ steps.app-token.outputs.token }}" >> "$GITHUB_ENV"
282348

283349
- name: Run Claude Code
284350
uses: anthropics/claude-code-action@v1
285351
with:
286352
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
287-
github_token: ${{ secrets.PAT_FOR_PR }}
353+
github_token: ${{ steps.app-token.outputs.token }}
288354
allowed_bots: "claude,github-actions"
289355
show_full_output: true
290356
claude_args: |
291357
--allowedTools "Bash,Read,Write,Edit,Glob,Grep,LS,MultiEdit,NotebookEdit,TodoRead,TodoWrite,WebFetch,WebSearch"
292358
--model "${{ env.MODEL }}"
293359
prompt: |
294-
You are @${{ env.AGENT_NAME }}, responding to a request from @${{ fromJSON(needs.check-mention.outputs.result).user }} on GitHub ${{ fromJSON(needs.check-mention.outputs.result).itemType }} #${{ fromJSON(needs.check-mention.outputs.result).itemNumber }}.
360+
You are @${{ env.AGENT_MENTION }}, responding to a request from @${{ fromJSON(needs.check-mention.outputs.result).user }} on GitHub ${{ fromJSON(needs.check-mention.outputs.result).itemType }} #${{ fromJSON(needs.check-mention.outputs.result).itemNumber }}.
295361
296362
Follow the repository instructions in `CLAUDE.md`.
297363
@@ -327,7 +393,7 @@ jobs:
327393
Always include the following signature block in commit messages and GitHub comments:
328394
```
329395
---
330-
🤖 **Generated by @${{ env.AGENT_NAME }}**
396+
🤖 **Generated by @${{ env.AGENT_MENTION }}**
331397
- Model: `${{ env.MODEL }}`
332398
- Agent harness: claude-code
333399
- Triggered by: @${{ fromJSON(needs.check-mention.outputs.result).user }}

0 commit comments

Comments
 (0)