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:
Niels Laute
2026-08-15 17:45:39 +02:00
committed by GitHub
parent f1548fcf8b
commit 3d0c3bdb29
8 changed files with 222 additions and 47 deletions

View File

@@ -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

View 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()

View File

@@ -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

View File

@@ -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,
],
);

View File

@@ -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({

View File

@@ -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 }}

View File

@@ -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

View File

@@ -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