diff --git a/.github/workflows/issue-triage-preview.yml b/.github/workflows/issue-triage-preview.yml new file mode 100644 index 00000000000..7d9a1c25f6d --- /dev/null +++ b/.github/workflows/issue-triage-preview.yml @@ -0,0 +1,75 @@ +# Preview issue triage prompt changes without modifying issues. +name: "Issue triage preview" + +on: + pull_request: + types: [opened, synchronize, reopened] + paths: + - "agents/prompts/reproduce-bug.md" + - "agents/prompts/triage-issue.md" + - "agents/schemas/issue-triage-bug.json" + - "agents/schemas/issue-triage.json" + +permissions: {} + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + collect: + if: github.repository == 'astral-sh/uv' && github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + permissions: + contents: read + issues: read + outputs: + issues: ${{ steps.issues.outputs.issues }} + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + - name: "Install uv" + uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2 + with: + version: "0.11.28" + - name: "Find recent issues" + id: issues + run: uv run --locked --no-project --script agents/scripts/issue-triage-preview.py collect + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + triage: + name: "Triage issue ${{ matrix.issue }}" + needs: collect + permissions: + contents: read + issues: read + strategy: + fail-fast: false + matrix: + issue: ${{ fromJSON(needs.collect.outputs.issues) }} + uses: ./.github/workflows/issue-triage.yml # zizmor: ignore[secrets-inherit] Required for environment secrets in reusable workflows. + with: + issue: ${{ matrix.issue }} + secrets: inherit + + report: + needs: triage + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + - name: "Install uv" + uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2 + with: + version: "0.11.28" + - name: "Create or update preview comment" + run: uv run --locked --no-project --script agents/scripts/issue-triage-preview.py comment --pull-request "$PULL_REQUEST_NUMBER" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} diff --git a/.github/workflows/issue-triage.yml b/.github/workflows/issue-triage.yml index 43d95322f74..c6bb1b34c3d 100644 --- a/.github/workflows/issue-triage.yml +++ b/.github/workflows/issue-triage.yml @@ -10,6 +10,12 @@ on: description: "An issue URL or number" required: true type: string + workflow_call: + inputs: + issue: + description: "An issue URL or number" + required: true + type: string permissions: {} @@ -62,7 +68,7 @@ jobs: if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: codex-thread-issue-triage-${{ github.run_id }}-${{ github.run_attempt }} + name: codex-thread-issue-triage-${{ inputs.issue || github.event.issue.number }}-${{ github.run_id }}-${{ github.run_attempt }} path: agents/codex/sessions if-no-files-found: warn @@ -85,6 +91,11 @@ jobs: permissions: {} steps: - name: "Write triage summary" - run: printf '```json\n%s\n```\n' "$TRIAGE_RESULT" >> "$GITHUB_STEP_SUMMARY" + run: | + { + printf '%s\n' '```json' + printf '%s\n' "$TRIAGE_RESULT" | jq . + printf '%s\n' '```' + } >> "$GITHUB_STEP_SUMMARY" env: TRIAGE_RESULT: ${{ needs.triage.outputs.result }} diff --git a/.github/workflows/reproduce-bug.yml b/.github/workflows/reproduce-bug.yml index c240aa671d4..0fd05eb9914 100644 --- a/.github/workflows/reproduce-bug.yml +++ b/.github/workflows/reproduce-bug.yml @@ -90,7 +90,7 @@ jobs: if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: codex-thread-reproduce-bug-${{ github.run_id }}-${{ github.run_attempt }} + name: codex-thread-reproduce-bug-${{ inputs.issue }}-${{ github.run_id }}-${{ github.run_attempt }} path: agents/codex/sessions if-no-files-found: warn @@ -101,6 +101,11 @@ jobs: permissions: {} steps: - name: "Write reproduction summary" - run: printf '```json\n%s\n```\n' "$REPRODUCTION_RESULT" >> "$GITHUB_STEP_SUMMARY" + run: | + { + printf '%s\n' '```json' + printf '%s\n' "$REPRODUCTION_RESULT" | jq . + printf '%s\n' '```' + } >> "$GITHUB_STEP_SUMMARY" env: REPRODUCTION_RESULT: ${{ needs.reproduce.outputs.result }} diff --git a/agents/prompts/triage-issue.md b/agents/prompts/triage-issue.md index a35aef4a4da..3601c07689c 100644 --- a/agents/prompts/triage-issue.md +++ b/agents/prompts/triage-issue.md @@ -53,5 +53,5 @@ maintainer action requested. Set `summary` to a concise overview of the closest items, or state that none were found. -Clearly distinguish source-backed findings from hypotheses. Do not draft a public reply or claim a -root cause that you have not confirmed from the repository. +Clearly distinguish source-backed findings from hypotheses in the summary and item reasons. Do not +draft a public reply or claim a root cause that you have not confirmed from the repository. diff --git a/agents/scripts/issue-triage-preview.py b/agents/scripts/issue-triage-preview.py new file mode 100644 index 00000000000..9ef84828e30 --- /dev/null +++ b/agents/scripts/issue-triage-preview.py @@ -0,0 +1,115 @@ +# /// script +# requires-python = ">=3.12" +# dependencies = ["httpx"] +# /// + +"""Collect recent issues and update the issue-triage preview comment.""" + +import argparse +import json +import os +from pathlib import Path +from typing import Any + +import httpx + +MARKER = "" + + +def request( + method: str, + path: str, + *, + query: dict[str, str | int] | None = None, + body: dict[str, str] | None = None, +) -> Any: + response = httpx.request( + method, + f"{os.environ['GITHUB_API_URL']}/{path}", + params=query, + json=body, + headers={ + "Accept": "application/vnd.github+json", + "Authorization": f"Bearer {os.environ['GH_TOKEN']}", + "X-GitHub-Api-Version": "2022-11-28", + }, + timeout=30, + ) + response.raise_for_status() + return response.json() + + +def collect(limit: int) -> None: + repository = os.environ["GITHUB_REPOSITORY"] + result = request( + "GET", + "search/issues", + query={ + "q": f"repo:{repository} is:issue", + "sort": "created", + "order": "desc", + "per_page": limit, + }, + ) + issues = [str(issue["number"]) for issue in result["items"]] + + with Path(os.environ["GITHUB_OUTPUT"]).open("a") as output: + output.write(f"issues={json.dumps(issues, separators=(',', ':'))}\n") + + +def comment(pull_request: int) -> None: + repository = os.environ["GITHUB_REPOSITORY"] + comments_path = f"repos/{repository}/issues/{pull_request}/comments" + comment_id = None + page = 1 + + while True: + comments = request("GET", comments_path, query={"per_page": 100, "page": page}) + for issue_comment in comments: + if ( + issue_comment["user"]["login"] == "github-actions[bot]" + and MARKER in issue_comment["body"] + ): + comment_id = issue_comment["id"] + if len(comments) < 100: + break + page += 1 + + run_url = ( + f"{os.environ['GITHUB_SERVER_URL']}/{repository}/actions/runs/" + f"{os.environ['GITHUB_RUN_ID']}" + ) + body = { + "body": ( + f"{MARKER}\n\n" + "## Issue triage preview\n\n" + "The issue triage preview is available in the " + f"[workflow run summary]({run_url})." + ) + } + + if comment_id is None: + request("POST", comments_path, body=body) + else: + request("PATCH", f"repos/{repository}/issues/comments/{comment_id}", body=body) + + +def main() -> None: + parser = argparse.ArgumentParser() + commands = parser.add_subparsers(dest="command", required=True) + + collect_parser = commands.add_parser("collect") + collect_parser.add_argument("--limit", type=int, default=5) + + comment_parser = commands.add_parser("comment") + comment_parser.add_argument("--pull-request", type=int, required=True) + + args = parser.parse_args() + if args.command == "collect": + collect(args.limit) + else: + comment(args.pull_request) + + +if __name__ == "__main__": + main() diff --git a/agents/scripts/issue-triage-preview.py.lock b/agents/scripts/issue-triage-preview.py.lock new file mode 100644 index 00000000000..f778ffd9e18 --- /dev/null +++ b/agents/scripts/issue-triage-preview.py.lock @@ -0,0 +1,83 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[manifest] +requirements = [{ name = "httpx" }] + +[[package]] +name = "anyio" +version = "4.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3b/72/5562aabb8dd7181e8e860622a38bea08d17842b99ecd4c91f84ac95251b0/anyio-4.14.1.tar.gz", hash = "sha256:8d648a3544c1a700e3ff78615cd679e4c5c3f149904287e73687b2596963629e", size = 254831, upload-time = "2026-06-24T20:56:06.017Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl", hash = "sha256:4e5533c5b8ff0a24f5d7a176cbe6877129cd183893f66b537f8f227d10527d72", size = 124875, upload-time = "2026-06-24T20:56:04.413Z" }, +] + +[[package]] +name = "certifi" +version = "2026.6.17" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +]