mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-08-29 10:09:43 +02:00
Fix issue triage and PR intake workflow behavior (#49924)
## Summary - run AI issue triage only when an issue is opened or its original title/body is edited - do not run issue triage for comments or reopen events - store deterministic issue evidence in the agent-visible runner temp directory - expose structured safe-output publication through the restricted CLI proxy while keeping shell, edit, and GitHub API tools disabled - skip PR intake jobs for draft pull requests and run intake when they become ready for review - replace the `Needs-Review` lifecycle label with `Ready for review`, migrating the legacy label on subsequent intake runs Closes #49917 ## Validation - `gh aw compile issue-triage` - `python -m unittest discover .github\scripts\issue-triage\tests -v` (46 tests) - `node --test .github\scripts\pr-intake\tests\pr-intake.test.mjs` (32 tests) - `git diff --check` on committed files --------- Copilot-Session: 3067a641-aa79-4f96-8d9f-eaa1c6d9b3cf
This commit is contained in:
18
.github/scripts/issue-triage/README.md
vendored
18
.github/scripts/issue-triage/README.md
vendored
@@ -1,8 +1,8 @@
|
||||
# AI-assisted issue triage
|
||||
|
||||
The workflow in `.github/workflows/issue-triage.md` maintains one canonical
|
||||
triage comment for newly opened, edited, or reopened issues. It combines
|
||||
deterministic preprocessing with one bounded GitHub Copilot pass.
|
||||
triage comment when an issue is opened or its original title/body is edited. It
|
||||
combines deterministic preprocessing with one bounded GitHub Copilot pass.
|
||||
|
||||
## Rules
|
||||
|
||||
@@ -49,18 +49,22 @@ that retain `Needs-Author-Feedback` for seven days without activity.
|
||||
|
||||
- An author comment removes `Needs-Author-Feedback` and returns the item to
|
||||
team triage.
|
||||
- A PR push reruns PR intake, which recalculates `Needs-Author-Feedback`.
|
||||
- A push to a non-draft PR reruns PR intake, which recalculates
|
||||
`Needs-Author-Feedback`.
|
||||
- Manual label removal immediately makes the item ineligible for scheduled
|
||||
closure.
|
||||
|
||||
## Cost and safety controls
|
||||
|
||||
- The `small` model alias is limited to five turns and 10 AI credits per run.
|
||||
- A content hash skips unchanged edits and unrelated comments.
|
||||
- The workflow subscribes only to issue creation and edits to the original
|
||||
issue; comments and reopen events do not trigger it.
|
||||
- Per-user rate limits, daily AI-credit limits, and per-issue concurrency bound
|
||||
repeated execution.
|
||||
- The agent has no shell or GitHub API tools. It can only read the checked-out
|
||||
repository and call the structured safe-output tool.
|
||||
repeated issue creation or edits.
|
||||
- The agent has no general shell or GitHub API tools. Its only shell command is
|
||||
the structured safe-output CLI proxy, and Copilot's file-write tool is
|
||||
explicitly denied to work around gh-aw v0.86.2 treating `edit: false` as
|
||||
writable.
|
||||
- Threat detection fails closed; publication requires an explicit successful
|
||||
detection result.
|
||||
- The publishing job rebuilds evidence from the current issue and accepts only
|
||||
|
||||
48
.github/scripts/issue-triage/tests/test_workflow_contract.py
vendored
Normal file
48
.github/scripts/issue-triage/tests/test_workflow_contract.py
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
GITHUB_DIR = Path(__file__).resolve().parents[3]
|
||||
WORKFLOW_SOURCE = GITHUB_DIR / "workflows" / "issue-triage.md"
|
||||
WORKFLOW_LOCK = GITHUB_DIR / "workflows" / "issue-triage.lock.yml"
|
||||
|
||||
|
||||
class WorkflowContractTests(unittest.TestCase):
|
||||
def test_agent_evidence_uses_shared_runtime_directory(self):
|
||||
source = WORKFLOW_SOURCE.read_text(encoding="utf-8")
|
||||
generated = WORKFLOW_LOCK.read_text(encoding="utf-8")
|
||||
|
||||
for filename in (
|
||||
"issue-context.md",
|
||||
"triage-event.json",
|
||||
"bug-report-context.md",
|
||||
):
|
||||
expected_path = f"/tmp/gh-aw/{filename}"
|
||||
self.assertIn(expected_path, source)
|
||||
self.assertIn(expected_path, generated)
|
||||
|
||||
def test_copilot_can_publish_without_general_write_or_shell_access(self):
|
||||
generated = WORKFLOW_LOCK.read_text(encoding="utf-8")
|
||||
|
||||
self.assertIn("shell(safeoutputs:*)", generated)
|
||||
self.assertIn("--deny-tool write", generated)
|
||||
for command in (
|
||||
"cat",
|
||||
"date",
|
||||
"echo",
|
||||
"grep",
|
||||
"head",
|
||||
"ls",
|
||||
"printf",
|
||||
"pwd",
|
||||
"sort",
|
||||
"tail",
|
||||
"uniq",
|
||||
"wc",
|
||||
"yq",
|
||||
):
|
||||
self.assertIn(f"--deny-tool '\\''shell({command})'\\''", generated)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
18
.github/scripts/pr-intake/README.md
vendored
18
.github/scripts/pr-intake/README.md
vendored
@@ -1,20 +1,20 @@
|
||||
# Pull request intake
|
||||
|
||||
The workflow in `.github/workflows/pr-intake.yml` runs deterministic pull
|
||||
request intake checks on every pull request. It does not call any AI model and
|
||||
does not execute any code from the pull request head; the Node script reads all
|
||||
pull request data through the GitHub API.
|
||||
request intake checks on non-draft pull requests. It does not call any AI model
|
||||
and does not execute any code from the pull request head; the Node script reads
|
||||
all pull request data through the GitHub API.
|
||||
|
||||
## Flow
|
||||
|
||||
1. Read the current PR through the GitHub API. Mergeability is re-fetched a few
|
||||
times when GitHub still reports it as unknown so a conflicting PR is never
|
||||
treated as ready by default.
|
||||
2. Deterministically validate closing issue references, merge conflicts, draft
|
||||
state, and whether visual evidence is present.
|
||||
2. Deterministically validate closing issue references, merge conflicts, and
|
||||
whether visual evidence is present.
|
||||
3. Require visual evidence only when the changed paths touch product UI files.
|
||||
4. Keep a single canonical comment in sync and manage only the `Needs-Review`
|
||||
and `Needs-Author-Feedback` labels.
|
||||
4. Keep a single canonical comment in sync and manage only the
|
||||
`Ready for review` and `Needs-Author-Feedback` labels.
|
||||
|
||||
## Comment behavior
|
||||
|
||||
@@ -26,8 +26,8 @@ pull request data through the GitHub API.
|
||||
|
||||
Missing issue references are advisory. Explicitly invalid references, merge
|
||||
conflicts, unknown mergeability, and missing required visual evidence block
|
||||
readiness. Draft PRs do not receive `Needs-Author-Feedback` solely because they
|
||||
are drafts.
|
||||
readiness. Draft PR events skip normal intake; conversion to draft runs only
|
||||
lifecycle-label cleanup, and marking a draft ready triggers full intake.
|
||||
|
||||
The existing resource-management policy closes PRs that retain
|
||||
`Needs-Author-Feedback` for seven inactive days. PR synchronization is handled
|
||||
|
||||
24
.github/scripts/pr-intake/pr-intake.mjs
vendored
24
.github/scripts/pr-intake/pr-intake.mjs
vendored
@@ -5,8 +5,9 @@ import { fileURLToPath } from 'node:url';
|
||||
export const CANONICAL_MARKER = '<!-- powertoys-pr-intake:canonical:v1 -->';
|
||||
export const ACTIONS_BOT_LOGIN = 'github-actions[bot]';
|
||||
export const ACTIONS_BOT_ID = 41898282;
|
||||
export const READY_FOR_REVIEW_LABEL = 'Needs-Review';
|
||||
export const READY_FOR_REVIEW_LABEL = 'Ready for review';
|
||||
export const NEEDS_AUTHOR_FEEDBACK_LABEL = 'Needs-Author-Feedback';
|
||||
const LEGACY_READY_FOR_REVIEW_LABEL = 'Needs-Review';
|
||||
export const FEEDBACK_SINCE_MARKER = 'powertoys-pr-intake:feedback-since';
|
||||
export const STALE_FEEDBACK_DAYS = 7;
|
||||
|
||||
@@ -733,9 +734,27 @@ export async function runPullRequestIntake({ api, event }) {
|
||||
);
|
||||
const issueNumber = pullNumber;
|
||||
|
||||
const issue = await api.getIssue(issueNumber);
|
||||
if (pullRequest.draft === true) {
|
||||
const labelPlan = planManagedLabelChanges(
|
||||
parseIssueLabels(issue),
|
||||
[],
|
||||
[
|
||||
READY_FOR_REVIEW_LABEL,
|
||||
NEEDS_AUTHOR_FEEDBACK_LABEL,
|
||||
LEGACY_READY_FOR_REVIEW_LABEL,
|
||||
],
|
||||
);
|
||||
await syncManagedLabels(api, issueNumber, labelPlan);
|
||||
return {
|
||||
issueNumber,
|
||||
skippedDraft: true,
|
||||
labelPlan,
|
||||
};
|
||||
}
|
||||
|
||||
const fileDetails = await listAllPullRequestFileDetails(api, issueNumber);
|
||||
const changedPaths = changedPathsFromFileDetails(fileDetails);
|
||||
const issue = await api.getIssue(issueNumber);
|
||||
const closingReferenceVerification = await verifyClosingIssueReferences({
|
||||
api,
|
||||
repositoryFullName: event.repository.full_name,
|
||||
@@ -772,6 +791,7 @@ export async function runPullRequestIntake({ api, event }) {
|
||||
[
|
||||
READY_FOR_REVIEW_LABEL,
|
||||
NEEDS_AUTHOR_FEEDBACK_LABEL,
|
||||
LEGACY_READY_FOR_REVIEW_LABEL,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import test from 'node:test';
|
||||
|
||||
import {
|
||||
@@ -39,6 +40,21 @@ function botComment(id, body) {
|
||||
};
|
||||
}
|
||||
|
||||
test('workflow skips normal draft intake and handles draft transitions', () => {
|
||||
const workflow = fs.readFileSync(
|
||||
new URL('../../../workflows/pr-intake.yml', import.meta.url),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
assert.equal(READY_FOR_REVIEW_LABEL, 'Ready for review');
|
||||
assert.match(
|
||||
workflow,
|
||||
/if: \$\{\{ github\.event\.pull_request\.draft == false \|\| github\.event\.action == 'converted_to_draft' \}\}/,
|
||||
);
|
||||
assert.match(workflow, /- ready_for_review/);
|
||||
assert.match(workflow, /- converted_to_draft/);
|
||||
});
|
||||
|
||||
class MockApi {
|
||||
constructor({
|
||||
comments = [],
|
||||
@@ -59,6 +75,7 @@ class MockApi {
|
||||
this.removedLabels = [];
|
||||
this.getIssueCalls = 0;
|
||||
this.getPullRequestCalls = 0;
|
||||
this.listPullRequestFilesCalls = 0;
|
||||
this.nextCommentId = 1000;
|
||||
}
|
||||
|
||||
@@ -106,6 +123,7 @@ class MockApi {
|
||||
}
|
||||
|
||||
async listPullRequestFiles(_pullNumber, page) {
|
||||
this.listPullRequestFilesCalls += 1;
|
||||
return page === 1 ? this.files : [];
|
||||
}
|
||||
|
||||
@@ -306,6 +324,19 @@ test('label plan removes only managed lifecycle labels', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('label plan migrates the legacy review label', () => {
|
||||
const plan = planManagedLabelChanges(
|
||||
['Product-FancyZones', 'Needs-Review'],
|
||||
[READY_FOR_REVIEW_LABEL],
|
||||
[READY_FOR_REVIEW_LABEL, NEEDS_AUTHOR_FEEDBACK_LABEL, 'Needs-Review'],
|
||||
);
|
||||
|
||||
assert.deepEqual(plan, {
|
||||
add: [READY_FOR_REVIEW_LABEL],
|
||||
remove: ['Needs-Review'],
|
||||
});
|
||||
});
|
||||
|
||||
test('incomplete comment mentions the author and shows only actionable bullets', () => {
|
||||
const report = buildIntakeReport({
|
||||
changedPaths: ['src/settings-ui/Settings.UI/SettingsXAML/Views/DashboardPage.xaml'],
|
||||
@@ -612,6 +643,39 @@ test('runPullRequestIntake stays silent on a clean PR with no prior comment', as
|
||||
assert.deepEqual(result.labelPlan.add, [READY_FOR_REVIEW_LABEL]);
|
||||
});
|
||||
|
||||
test('converted draft removes lifecycle labels without running intake', async () => {
|
||||
const api = new MockApi({
|
||||
issues: [{
|
||||
number: 100,
|
||||
labels: [READY_FOR_REVIEW_LABEL, NEEDS_AUTHOR_FEEDBACK_LABEL, 'Product-FancyZones'],
|
||||
title: 'Draft PR',
|
||||
}],
|
||||
pullRequests: [{
|
||||
number: 100,
|
||||
draft: true,
|
||||
mergeable: true,
|
||||
mergeable_state: 'clean',
|
||||
body: '',
|
||||
base: { ref: 'main' },
|
||||
user: { login: 'alice' },
|
||||
}],
|
||||
});
|
||||
|
||||
const result = await runPullRequestIntake({
|
||||
api,
|
||||
event: intakeEvent({ action: 'converted_to_draft' }),
|
||||
});
|
||||
|
||||
assert.equal(result.skippedDraft, true);
|
||||
assert.deepEqual(result.labelPlan, {
|
||||
add: [],
|
||||
remove: [NEEDS_AUTHOR_FEEDBACK_LABEL, READY_FOR_REVIEW_LABEL],
|
||||
});
|
||||
assert.equal(api.listPullRequestFilesCalls, 0);
|
||||
assert.equal(api.created, 0);
|
||||
assert.equal(api.updated, 0);
|
||||
});
|
||||
|
||||
test('runPullRequestIntake replaces a stale comment with an all-clear note when the PR is clean', async () => {
|
||||
const existing = botComment(50, `${CANONICAL_MARKER}\n## 🧭 PR intake\nplease update`);
|
||||
const api = new MockApi({
|
||||
|
||||
37
.github/workflows/issue-triage.lock.yml
generated
vendored
37
.github/workflows/issue-triage.lock.yml
generated
vendored
@@ -1,4 +1,4 @@
|
||||
# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"e9a454298d120d615b774f8a43454eb1cd0fd2851f93470b6b4098a9afceed23","body_hash":"45ed48558c4efc2979f9f25a79d4c69805e91a16b47b1c9297bdccf5ca883163","compiler_version":"v0.86.2","strict":true,"agent_id":"copilot","agent_model":"small","engine_versions":{"copilot":"1.0.79"}}
|
||||
# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"c34da18b80b755b3ec6437d94fc2038fab07c4c89532e1894d3d1c2ca6199c7c","body_hash":"0b82e73d1d8939f77acf5de1cb8478fc91d567984938585dd6e032cb185ef5f3","compiler_version":"v0.86.2","strict":true,"agent_id":"copilot","agent_model":"small","engine_versions":{"copilot":"1.0.79"}}
|
||||
# gh-aw-manifest: {"version":1,"secrets":["GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/setup-python","sha":"5fda3b95a4ea91299a34e894583c3862153e4b97","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"6aab9e5b5c91c615506061f09bedd81a23babe3c","version":"v0.86.2"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.44","digest":"sha256:0d727725c737b58c7bdf51f640cffb928385ec46517e0917c7f1a02f1bada8b4","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.44@sha256:0d727725c737b58c7bdf51f640cffb928385ec46517e0917c7f1a02f1bada8b4"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.44","digest":"sha256:b50fbadba138f6e9aba94aca09711335c489bb3b15861220cb66f6092e042dc7","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.44@sha256:b50fbadba138f6e9aba94aca09711335c489bb3b15861220cb66f6092e042dc7"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.44","digest":"sha256:83e48bbe12c634be8c228a576832fe45f66c529ac3659db92bddbcf2eeb6d627","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.44@sha256:83e48bbe12c634be8c228a576832fe45f66c529ac3659db92bddbcf2eeb6d627"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.9","digest":"sha256:e5a1569aeaf41820fa7bdee3e94468cae448133cdbf00119ad24f5b74db1ab9f","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.9@sha256:e5a1569aeaf41820fa7bdee3e94468cae448133cdbf00119ad24f5b74db1ab9f"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196","pinned_image":"ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196"}]}
|
||||
# This file was automatically generated by gh-aw (v0.86.2). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md
|
||||
#
|
||||
@@ -50,14 +50,10 @@
|
||||
|
||||
name: "AI Issue Triage"
|
||||
on:
|
||||
issue_comment:
|
||||
types:
|
||||
- created
|
||||
issues:
|
||||
types:
|
||||
- opened
|
||||
- edited
|
||||
- reopened
|
||||
# roles: all # Roles processed as role check in pre-activation job
|
||||
|
||||
permissions: {}
|
||||
@@ -253,8 +249,7 @@ jobs:
|
||||
GH_AW_ACTIONS_DIR: ${{ runner.temp }}/gh-aw/actions
|
||||
GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt
|
||||
GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl
|
||||
GH_AW_PROMPT_CONFIG: "{\"items\":[{\"content_env\":\"GH_AW_PROMPT_CONTENT_0000\"},{\"file\":\"xpia.md\"},{\"file\":\"temp_folder_prompt.md\"},{\"file\":\"markdown.md\"},{\"file\":\"safe_outputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0001\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0002\"},{\"file\":\"mcp_cli_tools_prompt.md\"},{\"file\":\"pr_context_prompt.md\",\"condition_env\":\"GH_AW_INCLUDE_PR_CONTEXT\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0003\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0004\"}]}"
|
||||
GH_AW_INCLUDE_PR_CONTEXT: ${{ (github.event_name == 'issue_comment' && github.event.issue.pull_request != null) || github.event_name == 'pull_request_review_comment' || github.event_name == 'pull_request_review' }}
|
||||
GH_AW_PROMPT_CONFIG: "{\"items\":[{\"content_env\":\"GH_AW_PROMPT_CONTENT_0000\"},{\"file\":\"xpia.md\"},{\"file\":\"temp_folder_prompt.md\"},{\"file\":\"markdown.md\"},{\"file\":\"safe_outputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0001\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0002\"},{\"file\":\"mcp_cli_tools_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0003\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0004\"}]}"
|
||||
GH_AW_PROMPT_CONTENT_0000: "<system>\n"
|
||||
GH_AW_PROMPT_CONTENT_0001: "<safe-output-tools>\nTools: missing_tool, missing_data, noop, publish_triage_summary\n"
|
||||
GH_AW_PROMPT_CONTENT_0002: "</safe-output-tools>\n"
|
||||
@@ -281,7 +276,6 @@ jobs:
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
env:
|
||||
GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt
|
||||
GH_AW_INCLUDE_PR_CONTEXT: ${{ (github.event_name == 'issue_comment' && github.event.issue.pull_request != null) || github.event_name == 'pull_request_review_comment' || github.event_name == 'pull_request_review' }}
|
||||
GH_AW_MCP_CLI_SERVERS_LIST: '- `safeoutputs` — run `safeoutputs --help` to see available tools'
|
||||
GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: ${{ needs.pre_activation.outputs.activated }}
|
||||
with:
|
||||
@@ -295,7 +289,6 @@ jobs:
|
||||
return await substitutePlaceholders({
|
||||
file: process.env.GH_AW_PROMPT,
|
||||
substitutions: {
|
||||
GH_AW_INCLUDE_PR_CONTEXT: process.env.GH_AW_INCLUDE_PR_CONTEXT,
|
||||
GH_AW_MCP_CLI_SERVERS_LIST: process.env.GH_AW_MCP_CLI_SERVERS_LIST,
|
||||
GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED
|
||||
}
|
||||
@@ -419,10 +412,10 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
id: prepare
|
||||
name: Prepare deterministic issue evidence
|
||||
run: python .github/scripts/issue-triage/issue-context.py "$GITHUB_EVENT_PATH" ".github/issue-context.md" ".github/triage-event.json"
|
||||
run: python .github/scripts/issue-triage/issue-context.py "$GITHUB_EVENT_PATH" "/tmp/gh-aw/issue-context.md" "/tmp/gh-aw/triage-event.json"
|
||||
- if: steps.prepare.outputs.should_process == 'true'
|
||||
name: Prepare sanitized bug report context
|
||||
run: python .github/scripts/issue-triage/bug-report-analyzer.py ".github/triage-event.json" ".github/bug-report-context.md"
|
||||
run: python .github/scripts/issue-triage/bug-report-analyzer.py "/tmp/gh-aw/triage-event.json" "/tmp/gh-aw/bug-report-context.md"
|
||||
|
||||
- name: Configure Git credentials
|
||||
env:
|
||||
@@ -715,6 +708,7 @@ jobs:
|
||||
export DEBUG="*"
|
||||
|
||||
export GH_AW_ENGINE="copilot"
|
||||
export GH_AW_MCP_CLI_SERVERS='["safeoutputs"]'
|
||||
MCP_GATEWAY_UID=$(id -u 2>/dev/null || echo '0')
|
||||
MCP_GATEWAY_GID=$(id -g 2>/dev/null || echo '0')
|
||||
source "${RUNNER_TEMP}/gh-aw/actions/resolve_docker_socket_gid.sh"
|
||||
@@ -785,6 +779,21 @@ jobs:
|
||||
id: agentic_execution
|
||||
# Copilot CLI tool arguments (sorted):
|
||||
# --allow-tool safeoutputs
|
||||
# --allow-tool shell(cat)
|
||||
# --allow-tool shell(date)
|
||||
# --allow-tool shell(echo)
|
||||
# --allow-tool shell(grep)
|
||||
# --allow-tool shell(head)
|
||||
# --allow-tool shell(ls)
|
||||
# --allow-tool shell(printf)
|
||||
# --allow-tool shell(pwd)
|
||||
# --allow-tool shell(safeoutputs)
|
||||
# --allow-tool shell(safeoutputs:*)
|
||||
# --allow-tool shell(sort)
|
||||
# --allow-tool shell(tail)
|
||||
# --allow-tool shell(uniq)
|
||||
# --allow-tool shell(wc)
|
||||
# --allow-tool shell(yq)
|
||||
# --allow-tool write
|
||||
timeout-minutes: 20
|
||||
run: |
|
||||
@@ -832,7 +841,7 @@ jobs:
|
||||
fi
|
||||
# shellcheck disable=SC1003,SC2016,SC2086
|
||||
awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --exclude-env COPILOT_GITHUB_TOKEN --exclude-env MCP_GATEWAY_API_KEY --mount /tmp/gh-aw:/tmp/gh-aw:rw --log-level info --skip-pull \
|
||||
-- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" "${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs" "${RUNNER_TEMP}/gh-aw/bin/copilot" --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-tool safeoutputs --allow-tool write --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log
|
||||
-- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" "${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs" "${RUNNER_TEMP}/gh-aw/bin/copilot" --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-tool safeoutputs --allow-tool '\''shell(cat)'\'' --allow-tool '\''shell(date)'\'' --allow-tool '\''shell(echo)'\'' --allow-tool '\''shell(grep)'\'' --allow-tool '\''shell(head)'\'' --allow-tool '\''shell(ls)'\'' --allow-tool '\''shell(printf)'\'' --allow-tool '\''shell(pwd)'\'' --allow-tool '\''shell(safeoutputs)'\'' --allow-tool '\''shell(safeoutputs:*)'\'' --allow-tool '\''shell(sort)'\'' --allow-tool '\''shell(tail)'\'' --allow-tool '\''shell(uniq)'\'' --allow-tool '\''shell(wc)'\'' --allow-tool '\''shell(yq)'\'' --allow-tool write --deny-tool write --deny-tool '\''shell(cat)'\'' --deny-tool '\''shell(date)'\'' --deny-tool '\''shell(echo)'\'' --deny-tool '\''shell(grep)'\'' --deny-tool '\''shell(head)'\'' --deny-tool '\''shell(ls)'\'' --deny-tool '\''shell(printf)'\'' --deny-tool '\''shell(pwd)'\'' --deny-tool '\''shell(sort)'\'' --deny-tool '\''shell(tail)'\'' --deny-tool '\''shell(uniq)'\'' --deny-tool '\''shell(wc)'\'' --deny-tool '\''shell(yq)'\'' --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log
|
||||
env:
|
||||
AWF_REFLECT_ENABLED: 1
|
||||
COPILOT_AGENT_RUNNER_TYPE: STANDALONE
|
||||
@@ -1430,7 +1439,7 @@ jobs:
|
||||
fi
|
||||
# shellcheck disable=SC1003,SC2016,SC2086
|
||||
awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --exclude-env COPILOT_GITHUB_TOKEN --mount /tmp/gh-aw:/tmp/gh-aw:rw --log-level info --skip-pull \
|
||||
-- /bin/bash -c 'set +o histexpand; : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" "${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs" "${RUNNER_TEMP}/gh-aw/bin/copilot" --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log
|
||||
-- /bin/bash -c 'set +o histexpand; : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" "${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs" "${RUNNER_TEMP}/gh-aw/bin/copilot" --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --deny-tool write --deny-tool '\''shell(cat)'\'' --deny-tool '\''shell(date)'\'' --deny-tool '\''shell(echo)'\'' --deny-tool '\''shell(grep)'\'' --deny-tool '\''shell(head)'\'' --deny-tool '\''shell(ls)'\'' --deny-tool '\''shell(printf)'\'' --deny-tool '\''shell(pwd)'\'' --deny-tool '\''shell(sort)'\'' --deny-tool '\''shell(tail)'\'' --deny-tool '\''shell(uniq)'\'' --deny-tool '\''shell(wc)'\'' --deny-tool '\''shell(yq)'\'' --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log
|
||||
env:
|
||||
GITHUB_STEP_SUMMARY: /tmp/gh-aw/step-summary.md
|
||||
AWF_REFLECT_ENABLED: 1
|
||||
@@ -1560,7 +1569,7 @@ jobs:
|
||||
env:
|
||||
GH_AW_RATE_LIMIT_MAX: "5"
|
||||
GH_AW_RATE_LIMIT_WINDOW: "60"
|
||||
GH_AW_RATE_LIMIT_EVENTS: "issue_comment,issues"
|
||||
GH_AW_RATE_LIMIT_EVENTS: "issues"
|
||||
GH_AW_RATE_LIMIT_IGNORED_ROLES: "admin,maintain,write"
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
57
.github/workflows/issue-triage.md
vendored
57
.github/workflows/issue-triage.md
vendored
@@ -4,9 +4,7 @@ name: AI Issue Triage
|
||||
description: Maintain one concise issue summary with likely duplicates and missing-information guidance.
|
||||
on:
|
||||
issues:
|
||||
types: [opened, edited, reopened]
|
||||
issue_comment:
|
||||
types: [created]
|
||||
types: [opened, edited]
|
||||
roles: all
|
||||
user-rate-limit:
|
||||
max-runs-per-window: 5
|
||||
@@ -14,7 +12,37 @@ user-rate-limit:
|
||||
concurrency:
|
||||
group: issue-triage-${{ github.event.issue.number }}
|
||||
cancel-in-progress: true
|
||||
engine: copilot
|
||||
engine:
|
||||
id: copilot
|
||||
args:
|
||||
- "--deny-tool"
|
||||
- "write"
|
||||
- "--deny-tool"
|
||||
- "shell(cat)"
|
||||
- "--deny-tool"
|
||||
- "shell(date)"
|
||||
- "--deny-tool"
|
||||
- "shell(echo)"
|
||||
- "--deny-tool"
|
||||
- "shell(grep)"
|
||||
- "--deny-tool"
|
||||
- "shell(head)"
|
||||
- "--deny-tool"
|
||||
- "shell(ls)"
|
||||
- "--deny-tool"
|
||||
- "shell(printf)"
|
||||
- "--deny-tool"
|
||||
- "shell(pwd)"
|
||||
- "--deny-tool"
|
||||
- "shell(sort)"
|
||||
- "--deny-tool"
|
||||
- "shell(tail)"
|
||||
- "--deny-tool"
|
||||
- "shell(uniq)"
|
||||
- "--deny-tool"
|
||||
- "shell(wc)"
|
||||
- "--deny-tool"
|
||||
- "shell(yq)"
|
||||
model: small
|
||||
max-turns: 5
|
||||
max-ai-credits: 10
|
||||
@@ -26,9 +54,10 @@ permissions:
|
||||
issues: read
|
||||
copilot-requests: write
|
||||
tools:
|
||||
bash: false
|
||||
edit: true
|
||||
bash: [safeoutputs]
|
||||
edit: false
|
||||
github: false
|
||||
cli-proxy: true
|
||||
steps:
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v7.0.0
|
||||
@@ -41,13 +70,14 @@ steps:
|
||||
GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl
|
||||
run: >-
|
||||
python .github/scripts/issue-triage/issue-context.py "$GITHUB_EVENT_PATH"
|
||||
".github/issue-context.md" ".github/triage-event.json"
|
||||
"/tmp/gh-aw/issue-context.md"
|
||||
"/tmp/gh-aw/triage-event.json"
|
||||
- name: Prepare sanitized bug report context
|
||||
if: steps.prepare.outputs.should_process == 'true'
|
||||
run: >-
|
||||
python .github/scripts/issue-triage/bug-report-analyzer.py
|
||||
".github/triage-event.json"
|
||||
".github/bug-report-context.md"
|
||||
"/tmp/gh-aw/triage-event.json"
|
||||
"/tmp/gh-aw/bug-report-context.md"
|
||||
safe-outputs:
|
||||
report-failure-as-issue: false
|
||||
noop:
|
||||
@@ -697,9 +727,8 @@ safe-outputs:
|
||||
|
||||
## Task
|
||||
|
||||
A GitHub issue was opened, edited, reopened, received a new author bug report,
|
||||
or received `/triage refresh` from a maintainer. Read
|
||||
`.github/issue-context.md` and `.github/bug-report-context.md` exactly once.
|
||||
A GitHub issue was opened or edited. Read `/tmp/gh-aw/issue-context.md` and
|
||||
`/tmp/gh-aw/bug-report-context.md` exactly once.
|
||||
They contain deterministic, bounded issue facts, ranked duplicate candidates,
|
||||
redacted diagnostics, and a coarse language signal. Never download attachments
|
||||
or search GitHub yourself. Judge the supplied candidates, summarize the issue,
|
||||
@@ -719,7 +748,7 @@ duplicate exists, and no author action is needed.
|
||||
|
||||
## Duplicate judgment
|
||||
|
||||
- Consider only candidates supplied in `.github/issue-context.md`.
|
||||
- Consider only candidates supplied in `/tmp/gh-aw/issue-context.md`.
|
||||
- The deterministic retrieval score is not a duplicate verdict.
|
||||
- Exclude the triggering issue.
|
||||
- Return at most five candidates and only include high-confidence matches.
|
||||
@@ -779,7 +808,7 @@ the rest of triage from running.
|
||||
Call `publish_triage_summary` exactly once with:
|
||||
|
||||
- `input_sha256`: copy the exact `Input SHA-256` value from
|
||||
`.github/issue-context.md`.
|
||||
`/tmp/gh-aw/issue-context.md`.
|
||||
- `summary`: a factual one- or two-sentence summary.
|
||||
- `suggested_area`: copy `Detected area` from the deterministic evidence.
|
||||
- `product_label`: copy `Candidate product label` from the deterministic
|
||||
|
||||
3
.github/workflows/pr-intake.yml
vendored
3
.github/workflows/pr-intake.yml
vendored
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# Deterministic pull request intake. Runs on every pull request and posts a
|
||||
# single canonical comment (only when there is something for the author to act
|
||||
# on or consider), keeps the Needs-Review / Needs-Author-Feedback lifecycle
|
||||
# on or consider), keeps the Ready for review / Needs-Author-Feedback lifecycle
|
||||
# labels in sync, and nudges for visual evidence on product UI changes.
|
||||
#
|
||||
# This workflow does not run any code from the pull request head. The Node
|
||||
@@ -32,6 +32,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
pr-intake:
|
||||
if: ${{ github.event.pull_request.draft == false || github.event.action == 'converted_to_draft' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout base ref
|
||||
|
||||
Reference in New Issue
Block a user