triage-issue-1306 #66
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: issue-triage | |
| run-name: triage-issue-${{ github.event.issue.number || inputs.issue_number }} | |
| # Triages every incoming issue on open and edit: closes issues that skip every template (free, no | |
| # LLM call), then runs an LLM based substantiation, template-completeness, and noise check on the | |
| # rest. | |
| # Both jobs apply immediately; a manual run stays dry only if dry_run is set to true. | |
| on: # yamllint disable-line rule:truthy | |
| issues: | |
| types: [opened, edited] | |
| workflow_dispatch: | |
| inputs: | |
| issue_number: | |
| description: 'Number of an existing issue to triage.' | |
| required: true | |
| type: string | |
| dry_run: | |
| description: 'Report the decision only, do not touch the issue.' | |
| required: false | |
| type: boolean | |
| default: false | |
| # One run per issue, so a burst of edits cannot race on the same comment or label. | |
| concurrency: | |
| group: issue-triage-${{ github.event.issue.number || inputs.issue_number }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read | |
| issues: write | |
| env: | |
| MARKER: '<!-- gdunit4-issue-triage -->' | |
| UNVERIFIABLE_LABEL: 'triage:failed' | |
| PASSED_LABEL: 'triage:passed' | |
| LLM_MODEL: 'gemini-3.5-flash' | |
| MIN_CONFIDENCE: '0.8' | |
| jobs: | |
| check-template-usage: | |
| name: Check Template Usage | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 5 | |
| outputs: | |
| bypass: ${{ steps.verdict.outputs.bypass }} | |
| number: ${{ steps.resolve.outputs.number }} | |
| state: ${{ steps.resolve.outputs.state }} | |
| template-name: ${{ steps.resolve.outputs.template-name }} | |
| required-ids: ${{ steps.resolve.outputs.required-ids }} | |
| steps: | |
| - name: Checkout GdUnit Repository | |
| uses: actions/checkout@v7 | |
| - name: Resolve Issue Template | |
| id: resolve | |
| uses: ./.github/actions/resolve-issue-template | |
| with: | |
| issue-number: ${{ github.event.issue.number || inputs.issue_number }} | |
| - name: Determine Triage Verdict | |
| id: verdict | |
| shell: bash | |
| env: | |
| TEMPLATE_NAME: ${{ steps.resolve.outputs.template-name }} | |
| run: | | |
| set -euo pipefail | |
| if [ -z "$TEMPLATE_NAME" ]; then | |
| echo "bypass=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "bypass=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Reject Bypassed Issue | |
| if: steps.verdict.outputs.bypass == 'true' | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| NUMBER: ${{ steps.resolve.outputs.number }} | |
| ISSUE_STATE: ${{ steps.resolve.outputs.state }} | |
| EVENT_NAME: ${{ github.event_name }} | |
| INPUT_DRY_RUN: ${{ inputs.dry_run }} | |
| run: | | |
| set -euo pipefail | |
| # A manual run follows its own dry_run input. Event driven runs always apply. | |
| DRY_RUN='false' | |
| if [ "$EVENT_NAME" = 'workflow_dispatch' ]; then | |
| DRY_RUN="$INPUT_DRY_RUN" | |
| fi | |
| close='false' | |
| if [ "$ISSUE_STATE" = 'open' ]; then | |
| close='true' | |
| fi | |
| { | |
| echo "$MARKER" | |
| echo | |
| echo '## This issue was closed by automated triage' | |
| echo | |
| echo 'This issue was created without going through one of the provided issue templates, most' | |
| echo 'commonly by filing it directly through the API rather than the "New issue" page. The' | |
| echo 'templates ask for exactly the information a maintainer needs to act on a report.' | |
| echo | |
| echo 'Please open a new issue using one of the templates offered on the "New issue" page.' | |
| } > triage/comment.md | |
| { | |
| echo "## Issue triage for #$NUMBER" | |
| echo | |
| if [ "$DRY_RUN" = 'true' ]; then | |
| echo 'Mode: **DRY RUN, nothing was written**' | |
| else | |
| echo 'Mode: **applied**' | |
| fi | |
| echo | |
| echo 'Decision: the issue was not filed through one of the provided issue templates' | |
| echo | |
| echo "- issue closed: $close" | |
| echo | |
| echo '<details><summary>Triage comment</summary>' | |
| echo | |
| cat triage/comment.md | |
| echo | |
| echo '</details>' | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| if [ "$DRY_RUN" = 'true' ]; then | |
| exit 0 | |
| fi | |
| # One sticky comment per issue, updated in place, so repeated runs never flood the thread. | |
| comment_id=$(gh api --paginate "repos/$GITHUB_REPOSITORY/issues/$NUMBER/comments" \ | |
| --jq "[.[] | select(.body | contains(\"$MARKER\")) | .id] | first // empty") | |
| jq -n --rawfile body triage/comment.md '{body: $body}' > triage/comment.json | |
| if [ -z "$comment_id" ]; then | |
| gh api --method POST "repos/$GITHUB_REPOSITORY/issues/$NUMBER/comments" --input triage/comment.json | |
| else | |
| gh api --method PATCH "repos/$GITHUB_REPOSITORY/issues/comments/$comment_id" --input triage/comment.json | |
| fi | |
| if [ "$close" = 'true' ]; then | |
| gh issue close "$NUMBER" --repo "$GITHUB_REPOSITORY" --reason 'not planned' | |
| fi | |
| llm-review: | |
| name: LLM Review | |
| needs: check-template-usage | |
| if: needs.check-template-usage.outputs.bypass != 'true' | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Checkout GdUnit Repository | |
| uses: actions/checkout@v7 | |
| - name: Run LLM Issue Review | |
| uses: ./.github/actions/llm-issue-review | |
| with: | |
| issue-number: ${{ needs.check-template-usage.outputs.number }} | |
| issue-state: ${{ needs.check-template-usage.outputs.state }} | |
| template-name: ${{ needs.check-template-usage.outputs.template-name }} | |
| required-ids: ${{ needs.check-template-usage.outputs.required-ids }} | |
| dry-run: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run || 'false' }} | |
| llm-api-key: ${{ secrets.LLM_API_KEY }} | |
| llm-model: ${{ env.LLM_MODEL }} | |
| min-confidence: ${{ env.MIN_CONFIDENCE }} | |
| marker: ${{ env.MARKER }} | |
| unverifiable-label: ${{ env.UNVERIFIABLE_LABEL }} | |
| passed-label: ${{ env.PASSED_LABEL }} |