Skip to content

Commit 9bd6504

Browse files
dcramercodex
andcommitted
fix(flue): Harden issue triage workflow contracts
Validate the reusable workflow against GitHub Actions and action input contracts. Keep the workflow scoped to getsentry, validate required secrets before token creation, and configure pnpm setup to read automation/package.json. Also reject cross-repository duplicate candidates before automatic closure so only same-repo duplicates can be closed without human review. Co-Authored-By: GPT-5 Codex <noreply@openai.com>
1 parent 592995c commit 9bd6504

6 files changed

Lines changed: 82 additions & 9 deletions

File tree

.agents/skills/issue-triage/SKILL.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ Goal: determine whether the new issue is a confirmed duplicate.
5151
- Search open and closed issues in the same repository with `gh search issues --repo <repository>`.
5252
- Add `--limit 10` to every `gh search issues` command.
5353
- Exclude the current issue number from candidates.
54+
- Only mark same-repository issues as duplicates. A cross-repository issue can be related context, but it must not be returned as `duplicate`.
5455
3. Keep search terms specific.
5556
- Do not search generic language, stack, or repo terms by themselves, such as `typescript`, `javascript`, `python`, `rust`, `language`, `rewrite`, `error`, or `timeout`.
5657
- For low-signal rewrite requests like "rewrite in Rust" with body "because Rust is good", search only the exact title and exact distinctive body phrase. Do not fan out to generic terms.

.agents/skills/issue-triage/SOURCES.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
| --- | --- |
77
| User request in this session | Defines required behavior: duplicate search and closure, repository checkout, diagnosis, validation, concise issue rewrites, issue-triage-bot identity in the first comment sentence, casually professional comment voice, and inheriting `not planned` closure from canonical duplicate issues. |
88
| Flue README issue triage example | Confirms GitHub Actions + CLI-only Flue agent pattern, `sandbox: "local"`, staged skill calls, command grants, and structured Valibot results. |
9+
| GitHub Actions reusable workflow documentation | Confirms `workflow_call`, caller job `uses`, inherited secrets, caller-owned `github` context, and `GITHUB_TOKEN` permission downgrading behavior. |
10+
| GitHub Actions events and workflow template documentation | Confirms issue events run from workflows in the event repository and org `.github` templates are for creating local workflow files, not global event subscription. |
11+
| `actions/create-github-app-token`, `actions/checkout`, `actions/setup-node`, and `pnpm/action-setup` documentation | Confirms GitHub App token inputs, scoped repository tokens, checkout inputs, pnpm cache setup, and non-root `package_json_file` behavior. |
12+
| OpenAI API authentication documentation | Confirms the model provider key should stay secret and be loaded from server-side environment variables. |
913
| `gh issue --help`, `gh issue view --help`, `gh issue edit --help`, `gh issue close --help`, `gh search issues --help`, `gh label list --help` | Confirms available GitHub CLI commands and flags for reading issues, searching duplicates, editing bodies, closing issues, and listing labels. |
1014
| Repository `AGENTS.md` | Supplies project workflow constraints, security expectations, and quality gate expectations. |
1115

@@ -26,5 +30,5 @@
2630

2731
## Open Gaps
2832

29-
- The first implementation does not run an end-to-end dry run against a real issue to confirm GitHub token permissions.
33+
- The first implementation does not run an end-to-end smoke test against a real issue to confirm GitHub token permissions.
3034
- Duplicate detection is agent-assisted and conservative; it may require follow-up tuning after observing real triage outcomes.

.flue/agents/issue-triage.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,24 @@ function normalizeStateReason(value: unknown) {
298298
return value.toLowerCase().replace(/[\s-]+/g, "_");
299299
}
300300

301+
export function issueRepositoryFromUrl(url: string) {
302+
try {
303+
const parsed = new URL(url);
304+
if (parsed.hostname !== "github.com") {
305+
return null;
306+
}
307+
308+
const [owner, name, type] = parsed.pathname.split("/").filter(Boolean);
309+
if (!owner || !name || type !== "issues") {
310+
return null;
311+
}
312+
313+
return `${owner}/${name}`;
314+
} catch {
315+
return null;
316+
}
317+
}
318+
301319
export function wasClosedAsNotPlanned(issue: unknown) {
302320
if (!isRecord(issue)) {
303321
return false;
@@ -887,6 +905,31 @@ export default async function ({ init, payload }: FlueContext) {
887905
);
888906
}
889907

908+
const duplicateRepository = issueRepositoryFromUrl(
909+
duplicateSearch.duplicate.url,
910+
);
911+
if (repository && duplicateRepository !== repository) {
912+
return {
913+
outcome: "needs_human_review",
914+
steps: [
915+
{ name: "search-duplicates", result: duplicateSearch.status },
916+
{
917+
name: "validate-duplicate",
918+
result: duplicateRepository
919+
? `cross-repo candidate from ${duplicateRepository}`
920+
: "candidate URL did not identify a same-repo GitHub issue",
921+
},
922+
],
923+
duplicate: duplicateSearch.duplicate,
924+
labels_applied: [],
925+
comment_posted: false,
926+
needs_human_review: true,
927+
summary: duplicateRepository
928+
? `Found duplicate candidate #${duplicateSearch.duplicate.number} in ${duplicateRepository}, but automatic closure only supports same-repo duplicates.`
929+
: `Found duplicate candidate #${duplicateSearch.duplicate.number}, but its URL could not be validated as a same-repo issue.`,
930+
};
931+
}
932+
890933
const closureContext = await readIssueContext(
891934
session,
892935
issueNumber,

.flue/tests/issue-triage.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
buildDuplicateClosureComment,
55
hasDuplicateOfFlag,
66
hasIssueTriageBotIntro,
7+
issueRepositoryFromUrl,
78
wasClosedAsNotPlanned,
89
withIssueTriageBotIntro,
910
} from "../agents/issue-triage";
@@ -82,4 +83,13 @@ describe("duplicate closure", () => {
8283
);
8384
expect(hasDuplicateOfFlag(" --reason string Reason")).toBe(false);
8485
});
86+
87+
it("extracts the repository from GitHub issue URLs", () => {
88+
expect(
89+
issueRepositoryFromUrl(
90+
"https://github.com/getsentry/sentry-mcp/issues/950",
91+
),
92+
).toBe("getsentry/sentry-mcp");
93+
expect(issueRepositoryFromUrl("https://example.com/issues/950")).toBeNull();
94+
});
8595
});

.github/flue/README.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,10 @@ jobs:
4242
Repositories are still centrally gated by `features.json`. If a caller workflow
4343
is added to a repository that is not listed there, the reusable workflow exits
4444
before creating a Sentry Intern app token or checking out the target repository.
45-
For `workflow_call` runs, the requested repository must also match the caller
46-
repository. Manual dispatch from `.github` is the only path that can point the
47-
workflow at a different allowlisted repository for smoke testing.
45+
For `workflow_call` runs, the requested repository must also belong to
46+
`getsentry` and match the caller repository. Manual dispatch from `.github` is
47+
the only path that can point the workflow at a different allowlisted repository
48+
for smoke testing.
4849

4950
## Configuration
5051

@@ -55,8 +56,11 @@ Required organization configuration:
5556
- `FLUE_OPENAI_API_KEY` secret for the model provider.
5657

5758
Sentry Intern only needs the GitHub App `Issues: read and write` repository
58-
permission for triage comments, labels, issue edits, and issue closure. Source
59-
checkout uses the caller workflow's `GITHUB_TOKEN` with `contents: read`.
59+
permission for triage comments, labels, issue edits, and issue closure. GitHub
60+
Apps also receive read-only metadata access. Source checkout uses the caller
61+
workflow's `GITHUB_TOKEN` with `contents: read`; the current enabled
62+
repositories are public, so manual smoke-test checkouts from `.github` work
63+
without granting the app contents access.
6064

6165
## Testing
6266

.github/workflows/issue-triage.yml

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ jobs:
6060
TARGET_ISSUE_NUMBER: ${{ inputs['issue-number'] }}
6161
TARGET_REF: ${{ inputs['target-ref'] }}
6262
AUTOMATION_REF: ${{ inputs['automation-ref'] }}
63-
EXPECTED_OWNER: ${{ github.repository_owner }}
63+
EXPECTED_OWNER: getsentry
6464
CALLER_REPOSITORY: ${{ github.repository }}
6565
EVENT_NAME: ${{ github.event_name }}
6666
WORKFLOW_REF_NAME: ${{ github.ref_name }}
@@ -109,7 +109,7 @@ jobs:
109109
- name: Checkout org automation
110110
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
111111
with:
112-
repository: ${{ github.repository_owner }}/.github
112+
repository: getsentry/.github
113113
ref: ${{ steps.target.outputs['automation-ref'] }}
114114
path: automation
115115
persist-credentials: false
@@ -126,19 +126,29 @@ jobs:
126126
- name: Validate Flue configuration
127127
env:
128128
FLUE_CLIENT_ID: ${{ vars.FLUE_CLIENT_ID }}
129+
FLUE_PRIVATE_KEY: ${{ secrets.FLUE_PRIVATE_KEY }}
130+
FLUE_OPENAI_API_KEY: ${{ secrets.FLUE_OPENAI_API_KEY }}
129131
run: |
130132
if [ -z "$FLUE_CLIENT_ID" ]; then
131133
echo "Missing required FLUE_CLIENT_ID organization variable" >&2
132134
exit 2
133135
fi
136+
if [ -z "$FLUE_PRIVATE_KEY" ]; then
137+
echo "Missing required FLUE_PRIVATE_KEY organization secret" >&2
138+
exit 2
139+
fi
140+
if [ -z "$FLUE_OPENAI_API_KEY" ]; then
141+
echo "Missing required FLUE_OPENAI_API_KEY organization secret" >&2
142+
exit 2
143+
fi
134144
135145
- name: Create issue triage bot token
136146
id: issue-triage-app-token
137147
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
138148
with:
139149
client-id: ${{ vars.FLUE_CLIENT_ID }}
140150
private-key: ${{ secrets.FLUE_PRIVATE_KEY }}
141-
owner: ${{ github.repository_owner }}
151+
owner: getsentry
142152
repositories: ${{ steps.target.outputs.name }}
143153
permission-issues: write
144154

@@ -164,6 +174,7 @@ jobs:
164174
- name: Install pnpm
165175
uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0
166176
with:
177+
package_json_file: automation/package.json
167178
run_install: false
168179

169180
- name: Setup Node.js

0 commit comments

Comments
 (0)