diff --git a/.github/workflows/issue-triage-agent.lock.yml b/.github/workflows/issue-triage-agent.lock.yml index 68f074279c1d..bd333dada3da 100644 --- a/.github/workflows/issue-triage-agent.lock.yml +++ b/.github/workflows/issue-triage-agent.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"5fdfd6a25aeb5104b54e6e86f1e56cbe50c3eeea0293cb7b11a1192840ddbb6b","body_hash":"19919bfd6cc21def311f85ac5d709372b1ccbebc0aad764a4adb86ed6f55136e","compiler_version":"v0.87.10","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.80"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"6692a8a9e92f28552219d65e6c332294ff60bec705904b949d6d075e15e28871","body_hash":"efcd4a2f6148fb143c80349dcd1ce2a6fcbffc8d4181f3866049569aa7c1ddad","compiler_version":"v0.87.10","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.80"}} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_PAT_0","COPILOT_PAT_1","COPILOT_PAT_2","COPILOT_PAT_3","COPILOT_PAT_4","COPILOT_PAT_5","COPILOT_PAT_6","COPILOT_PAT_7","COPILOT_PAT_8","COPILOT_PAT_9","GH_AW_DEFAULT_OTLP_HEADERS","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/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"bc8c008a419c5b7a29df6f5641edd35fd1c6ea85","version":"v0.87.10"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.28.10","digest":"sha256:c01e6d16d11ea4f2a46cc023a9f402224a3b3861b026818eec0dc586d7e6918e","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.28.10@sha256:c01e6d16d11ea4f2a46cc023a9f402224a3b3861b026818eec0dc586d7e6918e"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.10","digest":"sha256:c3a18aebb8251339117ea998296315de17bada366f8d03919b3348ea71112e64","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.10@sha256:c3a18aebb8251339117ea998296315de17bada366f8d03919b3348ea71112e64"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.28.10","digest":"sha256:c06076f7aca95df713e0748c44d80c0a3c2538fad67bfdd04296d45158e083e6","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.28.10@sha256:c06076f7aca95df713e0748c44d80c0a3c2538fad67bfdd04296d45158e083e6"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.14","digest":"sha256:b2f0c2b2f17b5fbe809e5bb99dc185b6ddd70df25295dc63a6d526350334eff5","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.14@sha256:b2f0c2b2f17b5fbe809e5bb99dc185b6ddd70df25295dc63a6d526350334eff5"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e","pinned_image":"ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e"},{"image":"ghcr.io/github/github-mcp-server:v1.11.0","digest":"sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699","pinned_image":"ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699"}],"mcp_servers":[{"name":"github","tools":["get_commit","get_file_contents","get_latest_release","get_me","get_pull_request","get_pull_request_comments","get_pull_request_diff","get_pull_request_files","get_pull_request_review_comments","get_pull_request_reviews","get_pull_request_status","get_release_by_tag","get_tag","issue_read","list_branches","list_commits","list_issue_types","list_issues","list_pull_requests","list_releases","list_starred_repositories","list_tags","pull_request_read","search_code","search_issues","search_pull_requests","search_repositories"]},{"name":"safeoutputs","tools":["add_comment","add_labels","missing_data","missing_tool","noop","remove_labels","set_issue_type"]}]} # This file was automatically generated by gh-aw (v0.87.10). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -106,6 +106,7 @@ env: jobs: activation: needs: + - issue_context - pat_pool - pre_activation if: > @@ -317,6 +318,8 @@ jobs: GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + GH_AW_NEEDS_ISSUE_CONTEXT_OUTPUTS_ISSUE_TYPE: ${{ needs.issue_context.outputs.issue_type }} + GH_AW_NEEDS_ISSUE_CONTEXT_OUTPUTS_LOOKUP_SUCCEEDED: ${{ needs.issue_context.outputs.lookup_succeeded }} GH_AW_STEPS_SANITIZED_OUTPUTS_BODY: ${{ steps.sanitized.outputs.body }} GH_AW_STEPS_SANITIZED_OUTPUTS_TITLE: ${{ steps.sanitized.outputs.title }} GH_AW_PROMPT_CONTENT_0000: "\n" @@ -340,6 +343,8 @@ jobs: GH_AW_GITHUB_EVENT_INPUTS_ISSUE_NUMBER: ${{ github.event.inputs.issue_number }} GH_AW_EXPR_726AD1D2: ${{ github.event.issue.number || github.event.inputs.issue_number }} GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} + GH_AW_NEEDS_ISSUE_CONTEXT_OUTPUTS_ISSUE_TYPE: ${{ needs.issue_context.outputs.issue_type }} + GH_AW_NEEDS_ISSUE_CONTEXT_OUTPUTS_LOOKUP_SUCCEEDED: ${{ needs.issue_context.outputs.lookup_succeeded }} GH_AW_STEPS_SANITIZED_OUTPUTS_BODY: ${{ steps.sanitized.outputs.body }} GH_AW_STEPS_SANITIZED_OUTPUTS_TITLE: ${{ steps.sanitized.outputs.title }} with: @@ -367,6 +372,8 @@ jobs: GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} GH_AW_MCP_CLI_SERVERS_LIST: "- `github` — run `github --help` to see available tools\n- `safeoutputs` — run `safeoutputs --help` to see available tools" + GH_AW_NEEDS_ISSUE_CONTEXT_OUTPUTS_ISSUE_TYPE: ${{ needs.issue_context.outputs.issue_type }} + GH_AW_NEEDS_ISSUE_CONTEXT_OUTPUTS_LOOKUP_SUCCEEDED: ${{ needs.issue_context.outputs.lookup_succeeded }} GH_AW_NEEDS_PAT_POOL_OUTPUTS_PAT_NUMBER: ${{ needs.pat_pool.outputs.pat_number }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: ${{ needs.pre_activation.outputs.activated }} GH_AW_STEPS_SANITIZED_OUTPUTS_BODY: ${{ steps.sanitized.outputs.body }} @@ -397,6 +404,8 @@ jobs: GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID, GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE, GH_AW_MCP_CLI_SERVERS_LIST: process.env.GH_AW_MCP_CLI_SERVERS_LIST, + GH_AW_NEEDS_ISSUE_CONTEXT_OUTPUTS_ISSUE_TYPE: process.env.GH_AW_NEEDS_ISSUE_CONTEXT_OUTPUTS_ISSUE_TYPE, + GH_AW_NEEDS_ISSUE_CONTEXT_OUTPUTS_LOOKUP_SUCCEEDED: process.env.GH_AW_NEEDS_ISSUE_CONTEXT_OUTPUTS_LOOKUP_SUCCEEDED, GH_AW_NEEDS_PAT_POOL_OUTPUTS_PAT_NUMBER: process.env.GH_AW_NEEDS_PAT_POOL_OUTPUTS_PAT_NUMBER, GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED, GH_AW_STEPS_SANITIZED_OUTPUTS_BODY: process.env.GH_AW_STEPS_SANITIZED_OUTPUTS_BODY, @@ -439,6 +448,7 @@ jobs: agent: needs: - activation + - issue_context - pat_pool if: needs.activation.outputs.daily_ai_credits_exceeded != 'true' runs-on: ubuntu-latest @@ -607,7 +617,7 @@ jobs: env: GH_AW_FILE_ROOT: "${{ runner.temp }}/gh-aw" GH_AW_FILE_CONFIG: "{\"files\":[{\"path\":\"safeoutputs/config.json\",\"content_env\":\"GH_AW_SAFE_OUTPUTS_CONFIG\"}]}" - GH_AW_SAFE_OUTPUTS_CONFIG: "{\"add_comment\":{\"hide_older_comments\":true,\"max\":1,\"target\":\"*\"},\"add_labels\":{\"allowed\":[\"area-auth\",\"area-blazor\",\"area-commandlinetools\",\"area-dataprotection\",\"area-grpc\",\"area-healthchecks\",\"area-hosting\",\"area-identity\",\"area-infrastructure\",\"area-middleware\",\"area-minimal\",\"area-mvc\",\"area-networking\",\"area-perf\",\"area-routing\",\"area-security\",\"area-signalr\",\"area-ui-rendering\",\"area-unified-build\",\"by-design\",\"question\",\"external\",\"docs\",\"api-proposal\",\"test-failure\",\"performance\"],\"max\":3},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"remove_labels\":{\"allowed\":[\"needs-area-label\"],\"max\":1},\"report_incomplete\":{},\"set_issue_type\":{\"allowed\":[\"Bug\",\"Feature\",\"Task\",\"Epic\"],\"max\":1}}" + GH_AW_SAFE_OUTPUTS_CONFIG: "{\"add_comment\":{\"hide_older_comments\":true,\"max\":1,\"target\":\"*\"},\"add_labels\":{\"allowed\":[\"area-auth\",\"area-blazor\",\"area-commandlinetools\",\"area-dataprotection\",\"area-grpc\",\"area-healthchecks\",\"area-hosting\",\"area-identity\",\"area-infrastructure\",\"area-middleware\",\"area-minimal\",\"area-mvc\",\"area-networking\",\"area-perf\",\"area-routing\",\"area-security\",\"area-signalr\",\"area-ui-rendering\",\"area-unified-build\",\"by-design\",\"question\",\"external\",\"docs\",\"api-proposal\",\"test-failure\",\"performance\"],\"max\":3,\"staged\":\"${{ github.event.inputs.dry_run == 'true' }}\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"remove_labels\":{\"allowed\":[\"needs-area-label\"],\"max\":1,\"staged\":\"${{ github.event.inputs.dry_run == 'true' }}\"},\"report_incomplete\":{},\"set_issue_type\":{\"allowed\":[\"Bug\",\"Feature\",\"Task\"],\"max\":1,\"staged\":\"\"}}" with: script: | const path = require('path'); @@ -1281,6 +1291,7 @@ jobs: - activation - agent - detection + - issue_context - pat_pool - safe_outputs if: > @@ -1820,6 +1831,45 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/conclude_threat_detection.sh" /tmp/gh-aw/threat-detection/detection_result.json + issue_context: + name: Read trusted issue metadata + runs-on: ubuntu-latest + permissions: + issues: read + outputs: + issue_type: ${{ steps.issue.outputs.issue_type }} + lookup_succeeded: ${{ steps.issue.outputs.lookup_succeeded }} + steps: + - name: Configure GH_HOST for enterprise compatibility + id: ghes-host-config + shell: bash + run: | # zizmor: ignore[github-env] - GITHUB_SERVER_URL is set by GitHub Actions, not user input. + # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct + # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. + GH_HOST="${GITHUB_SERVER_URL#https://}" + GH_HOST="${GH_HOST#http://}" + echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" + - name: Read current issue type + id: issue + run: | + # A failed or impossible lookup must never be reported as "this issue has no + # type". It is reported as lookup_succeeded=false with an empty issue_type so + # that type mutation fails closed while the rest of triage stays available. + issue_type="" + lookup_succeeded="false" + if [ -n "${ISSUE_NUMBER}" ] && issue_type="$(gh api "repos/${GITHUB_REPOSITORY}/issues/${ISSUE_NUMBER}" --jq '.type.name // ""')"; then + lookup_succeeded="true" + else + issue_type="" + fi + # Keep the value single-line so it cannot forge additional step outputs. + issue_type="$(printf '%s' "${issue_type}" | tr -d '\r\n')" + echo "issue_type=${issue_type}" >> "$GITHUB_OUTPUT" + echo "lookup_succeeded=${lookup_succeeded}" >> "$GITHUB_OUTPUT" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ISSUE_NUMBER: ${{ github.event.issue.number || github.event.inputs.issue_number }} + pat_pool: needs: pre_activation runs-on: ubuntu-slim @@ -1941,6 +1991,7 @@ jobs: - activation - agent - detection + - issue_context if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success' runs-on: ubuntu-slim environment: copilot-pat-pool @@ -2032,7 +2083,7 @@ jobs: GH_AW_ALLOWED_DOMAINS: "api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,s.symcb.com,s.symcd.com,security.ubuntu.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"hide_older_comments\":true,\"max\":1,\"target\":\"*\"},\"add_labels\":{\"allowed\":[\"area-auth\",\"area-blazor\",\"area-commandlinetools\",\"area-dataprotection\",\"area-grpc\",\"area-healthchecks\",\"area-hosting\",\"area-identity\",\"area-infrastructure\",\"area-middleware\",\"area-minimal\",\"area-mvc\",\"area-networking\",\"area-perf\",\"area-routing\",\"area-security\",\"area-signalr\",\"area-ui-rendering\",\"area-unified-build\",\"by-design\",\"question\",\"external\",\"docs\",\"api-proposal\",\"test-failure\",\"performance\"],\"max\":3},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"remove_labels\":{\"allowed\":[\"needs-area-label\"],\"max\":1},\"report_incomplete\":{},\"set_issue_type\":{\"allowed\":[\"Bug\",\"Feature\",\"Task\",\"Epic\"],\"max\":1}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"hide_older_comments\":true,\"max\":1,\"target\":\"*\"},\"add_labels\":{\"allowed\":[\"area-auth\",\"area-blazor\",\"area-commandlinetools\",\"area-dataprotection\",\"area-grpc\",\"area-healthchecks\",\"area-hosting\",\"area-identity\",\"area-infrastructure\",\"area-middleware\",\"area-minimal\",\"area-mvc\",\"area-networking\",\"area-perf\",\"area-routing\",\"area-security\",\"area-signalr\",\"area-ui-rendering\",\"area-unified-build\",\"by-design\",\"question\",\"external\",\"docs\",\"api-proposal\",\"test-failure\",\"performance\"],\"max\":3,\"staged\":\"${{ github.event.inputs.dry_run == 'true' }}\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"remove_labels\":{\"allowed\":[\"needs-area-label\"],\"max\":1,\"staged\":\"${{ github.event.inputs.dry_run == 'true' }}\"},\"report_incomplete\":{},\"set_issue_type\":{\"allowed\":[\"Bug\",\"Feature\",\"Task\"],\"max\":1,\"staged\":\"${{ needs.issue_context.outputs.lookup_succeeded != 'true' || needs.issue_context.outputs.issue_type != '' || github.event.inputs.dry_run == 'true' }}\"}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/issue-triage-agent.md b/.github/workflows/issue-triage-agent.md index e3a932d225a2..fcf7cc735ee0 100644 --- a/.github/workflows/issue-triage-agent.md +++ b/.github/workflows/issue-triage-agent.md @@ -39,6 +39,37 @@ concurrency: job-discriminator: ${{ github.event.issue.number || github.event.inputs.issue_number || github.run_id }} queue: max +jobs: + issue_context: + name: Read trusted issue metadata + runs-on: ubuntu-latest + permissions: + issues: read + outputs: + issue_type: ${{ steps.issue.outputs.issue_type }} + lookup_succeeded: ${{ steps.issue.outputs.lookup_succeeded }} + steps: + - name: Read current issue type + id: issue + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ISSUE_NUMBER: ${{ github.event.issue.number || github.event.inputs.issue_number }} + run: | + # A failed or impossible lookup must never be reported as "this issue has no + # type". It is reported as lookup_succeeded=false with an empty issue_type so + # that type mutation fails closed while the rest of triage stays available. + issue_type="" + lookup_succeeded="false" + if [ -n "${ISSUE_NUMBER}" ] && issue_type="$(gh api "repos/${GITHUB_REPOSITORY}/issues/${ISSUE_NUMBER}" --jq '.type.name // ""')"; then + lookup_succeeded="true" + else + issue_type="" + fi + # Keep the value single-line so it cannot forge additional step outputs. + issue_type="$(printf '%s' "${issue_type}" | tr -d '\r\n')" + echo "issue_type=${issue_type}" >> "$GITHUB_OUTPUT" + echo "lookup_succeeded=${lookup_succeeded}" >> "$GITHUB_OUTPUT" + tools: bash: ["cat", "head", "tail", "grep", "wc", "jq"] github: @@ -46,11 +77,13 @@ tools: safe-outputs: report-failure-as-issue: false + needs: [issue_context] noop: report-as-issue: false set-issue-type: - allowed: ["Bug", "Feature", "Task", "Epic"] + allowed: ["Bug", "Feature", "Task"] max: 1 + staged: ${{ needs.issue_context.outputs.lookup_succeeded != 'true' || needs.issue_context.outputs.issue_type != '' || github.event.inputs.dry_run == 'true' }} add-labels: allowed: - area-auth @@ -80,9 +113,11 @@ safe-outputs: - test-failure - performance max: 3 + staged: ${{ github.event.inputs.dry_run == 'true' }} remove-labels: allowed: [needs-area-label] max: 1 + staged: ${{ github.event.inputs.dry_run == 'true' }} add-comment: max: 1 target: "*" @@ -114,7 +149,7 @@ You are an issue-triage agent for the **dotnet/aspnetcore** repository. Your job is to analyze a newly opened issue and perform four tasks: 1. **Area classification** - assign the correct `area-*` label -2. **Type classification** - assign an issue type (not a label) (Bug, Feature, Task, or Epic) +2. **Type classification** - preserve an existing issue type, or assign Bug, Feature, or Task 3. **Duplicate detection** - search for similar existing issues 4. **Triage comment** - post a single summary comment on the issue (unless the vulnerability gate below suppresses it) @@ -127,13 +162,30 @@ You **must** obtain the real issue title and body before doing anything else. Tw sources are available — use whichever is populated: - **Number:** #${{ github.event.issue.number || github.event.inputs.issue_number }} +- **Current issue type (trusted metadata):** ${{ needs.issue_context.outputs.issue_type }} +- **Current issue type lookup status:** ${{ needs.issue_context.outputs.lookup_succeeded }} - **Title (from payload):** ${{ steps.sanitized.outputs.title }} - **Body (from payload):** ${{ steps.sanitized.outputs.body }} +**Read the lookup status before you read the current issue type.** The current +issue type field above is only meaningful when the lookup status is exactly +`true`. + +- Lookup status `true` and a non-empty current type: the issue is typed, and that + type is authoritative. +- Lookup status `true` and an empty current type: the issue is genuinely untyped. +- Lookup status anything other than `true` (including `false` or blank): the + trusted lookup **failed**. The current type is **unknown**, not empty. Never + treat this as proof that the issue is untyped, and never use it to justify + assigning a type. Fall back to the `issue_read` MCP tool described below to + learn the real current type, and follow the unknown-type rules in Step 2. + **If both the title and body above are populated**, use them directly as the source -of truth and **skip the MCP fetch entirely.** +of truth, treat the current issue type above as trusted workflow metadata when the +lookup status is `true`, and **skip the MCP fetch entirely** unless the lookup +status is not `true`. A non-empty current issue type is authoritative. **If the title or body above is empty, that is normal — not an error.** The payload is intentionally blank in two common cases: (a) `workflow_dispatch` runs, which do @@ -145,6 +197,10 @@ with the **github** MCP server's `issue_read` tool before proceeding: - Call `issue_read` with owner `dotnet`, repo `aspnetcore`, and issue number `${{ github.event.issue.number || github.event.inputs.issue_number }}`. +- Capture the issue's current type returned by `issue_read` along with its title, + body, and labels. Treat any non-empty current type as authoritative. This is + also the fallback source of truth whenever the trusted lookup status above is + not `true`. - This `issue_read` call is **required, not optional.** An empty payload is never a reason to stop: do **not** report missing data, do **not** call `noop`, and do **not** give up before you have successfully called `issue_read`. @@ -437,12 +493,37 @@ Explain why in the comment instead. ## Step 2: Type Classification -Classify the issue into one of these types: +First inspect the trusted current issue type lookup status collected above, then +the trusted current issue type itself. + +- If the lookup status is not exactly `true`, the current type is **unknown**. + Use the type reported by `issue_read` if you obtained one: if that is + non-empty, preserve it. If you have no reliable current type at all, do not + assert that the issue is untyped, do not claim a type was applied, and report + the type as `unknown (type lookup unavailable)` in your comment. The workflow + independently blocks type mutation in this state, so any `set-issue-type` + call you make will only be staged, never applied. +- If the lookup status is `true` and the current type is non-empty, preserve it + exactly and do not recommend or apply a replacement type. This includes + maintainer-created `Epic` issues and template- or automation-assigned `Bug`, + `Feature`, or `Task` issues. +- If the lookup status is `true` and the current type is empty, the issue is + genuinely untyped: classify it into exactly one of these types: | Type | When to use | |-----------|-------------| -| `Bug` | The report clearly identifies a behavior as a bug and it can be reproduced. Something is broken or behaving unexpectedly compared to its intended design. | +| `Bug` | The report clearly identifies a behavior as a bug and it can be reproduced. Something is broken or behaving unexpectedly compared to its intended design. A small or mechanical fix to broken shipped behavior is still a Bug — the deciding factor is that current behavior is broken, not the size of the fix. | | `Feature` | The report asks for a behavior that is not currently implemented. This may be a brand-new feature or an addition/enhancement to an existing feature. | +| `Task` | Bounded maintenance, documentation, test, infrastructure, or refactoring work where current behavior is not broken. A docs-only deliverable gets the existing `docs` sub-type label alongside this type. | + +`Epic` remains valid maintainer-managed planning metadata in the dotnet +organization, but this automated intake workflow must never assign it. A broad +or large single feature request remains a `Feature`; implementation size alone +does not change its type. + +Preserving an existing type changes only the type mutation decision. Continue +the complete area, subtype, duplicate, vulnerability-gate, comment, and no-op +analysis normally. ## Step 3: Additional Labels @@ -512,7 +593,7 @@ structure — no additional sections beyond what is listed below: ### Triage Summary **Area:** `area-xyz` (brief reason) -**Type:** `Bug` | `Feature` (brief reason) +**Type:** `` (preserved) | `unknown (type lookup unavailable)` | `Bug` | `Feature` | `Task` (brief reason) #### Regression Info - **Previously working version:** .NET x.y / ASP.NET Core x.y @@ -650,7 +731,9 @@ no Notes section. Order of operations matters. Do these in this exact order: -1. **Decide the labels and issue type** you will apply, based on Steps 1–5. +1. **Decide the labels and type action** based on Steps 1–5. A preserved + existing type is not a reason to skip area, sub-type, duplicate, comment, + vulnerability-gate, removal, or no-op analysis. 2. **Apply the area label** and (if applicable from Step 3) one **additional sub-type label** using the `add-labels` safe output. The `add-labels` @@ -661,10 +744,22 @@ Order of operations matters. Do these in this exact order: step 3 below. Pass `item_number` explicitly, using `${{ github.event.issue.number || github.event.inputs.issue_number }}`. -3. **Apply the issue type** using `set-issue-type` with one of `Bug`, - `Feature`, `Task`, or `Epic` based on your Step 2 classification. Call - `set-issue-type` exactly once and pass `issue_number` explicitly, using - `${{ github.event.issue.number || github.event.inputs.issue_number }}`. +3. **Handle the issue type** based on the trusted current value: + - If the current issue type lookup status is not exactly `true`, the current + type is unknown. Do **not** call `set-issue-type` at all. Type mutation is + blocked by the workflow in this state regardless of what you emit, so a + call would be staged and silently discarded. Report the type as preserved + if `issue_read` gave you one, otherwise as + `unknown (type lookup unavailable)`. + - If the current issue type is non-empty, report it as preserved and do + **not** call `set-issue-type`. + - If the current issue type is empty and the lookup was reliable, apply + exactly one of `Bug`, `Feature`, or `Task` using `set-issue-type`. Call + `set-issue-type` exactly once and pass `issue_number` explicitly, using + `${{ github.event.issue.number || github.event.inputs.issue_number }}`. + - Area labels, sub-type labels, `needs-area-label` removal, and the triage + comment are never blocked by an unavailable type lookup. Continue with them + normally. 4. If the issue currently has `needs-area-label` and you assigned an area, **remove `needs-area-label`** using `remove-labels`. Pass `item_number`