From 3d0c3bdb294f96e2e2907dd9fcd7ba363ed0f8e8 Mon Sep 17 00:00:00 2001 From: Niels Laute Date: Sat, 15 Aug 2026 17:45:39 +0200 Subject: [PATCH] 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 --- .github/scripts/issue-triage/README.md | 18 ++++-- .../tests/test_workflow_contract.py | 48 ++++++++++++++ .github/scripts/pr-intake/README.md | 18 +++--- .github/scripts/pr-intake/pr-intake.mjs | 24 ++++++- .../pr-intake/tests/pr-intake.test.mjs | 64 +++++++++++++++++++ .github/workflows/issue-triage.lock.yml | 37 +++++++---- .github/workflows/issue-triage.md | 57 +++++++++++++---- .github/workflows/pr-intake.yml | 3 +- 8 files changed, 222 insertions(+), 47 deletions(-) create mode 100644 .github/scripts/issue-triage/tests/test_workflow_contract.py diff --git a/.github/scripts/issue-triage/README.md b/.github/scripts/issue-triage/README.md index d5488fb248..4912dddd8e 100644 --- a/.github/scripts/issue-triage/README.md +++ b/.github/scripts/issue-triage/README.md @@ -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 diff --git a/.github/scripts/issue-triage/tests/test_workflow_contract.py b/.github/scripts/issue-triage/tests/test_workflow_contract.py new file mode 100644 index 0000000000..0908ec736d --- /dev/null +++ b/.github/scripts/issue-triage/tests/test_workflow_contract.py @@ -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() diff --git a/.github/scripts/pr-intake/README.md b/.github/scripts/pr-intake/README.md index 539210124b..f55fdad0c8 100644 --- a/.github/scripts/pr-intake/README.md +++ b/.github/scripts/pr-intake/README.md @@ -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 diff --git a/.github/scripts/pr-intake/pr-intake.mjs b/.github/scripts/pr-intake/pr-intake.mjs index d30c57da92..d8c8da20d5 100644 --- a/.github/scripts/pr-intake/pr-intake.mjs +++ b/.github/scripts/pr-intake/pr-intake.mjs @@ -5,8 +5,9 @@ import { fileURLToPath } from 'node:url'; export const CANONICAL_MARKER = ''; 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, ], ); diff --git a/.github/scripts/pr-intake/tests/pr-intake.test.mjs b/.github/scripts/pr-intake/tests/pr-intake.test.mjs index b4ca5467e0..3e05f0e4e3 100644 --- a/.github/scripts/pr-intake/tests/pr-intake.test.mjs +++ b/.github/scripts/pr-intake/tests/pr-intake.test.mjs @@ -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({ diff --git a/.github/workflows/issue-triage.lock.yml b/.github/workflows/issue-triage.lock.yml index ba967cc5ed..84200ace9a 100644 --- a/.github/workflows/issue-triage.lock.yml +++ b/.github/workflows/issue-triage.lock.yml @@ -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: "\n" GH_AW_PROMPT_CONTENT_0001: "\nTools: missing_tool, missing_data, noop, publish_triage_summary\n" GH_AW_PROMPT_CONTENT_0002: "\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 }} diff --git a/.github/workflows/issue-triage.md b/.github/workflows/issue-triage.md index e48f1f1a34..8f0ea1d1c0 100644 --- a/.github/workflows/issue-triage.md +++ b/.github/workflows/issue-triage.md @@ -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 diff --git a/.github/workflows/pr-intake.yml b/.github/workflows/pr-intake.yml index 0f14a39204..157278549a 100644 --- a/.github/workflows/pr-intake.yml +++ b/.github/workflows/pr-intake.yml @@ -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