feat: Document.ToBytesAsync() — in-memory PDF output (3.0.5) #3
Workflow file for this run
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
| # Triage Bot — auto-classify unresolved review threads, resolve the | |
| # non-blocking ones, leave needs-human ones to keep the PR blocked. | |
| # | |
| # Closes the gap where native auto-merge fires the moment CI goes green, | |
| # even if Copilot / CodeRabbit later post review comments. With | |
| # branch protection `required_conversation_resolution: true` (or a | |
| # ruleset that requires it) any unresolved thread keeps the PR at | |
| # mergeStateStatus=BLOCKED until explicitly resolved. | |
| # | |
| # Decision rules (v1, deterministic — upgrade to LLM is the next iteration): | |
| # - body contains `[triage:auto-resolve]` → dismiss | |
| # - body matches /^(nit:|nitpick:|praise:)/i → dismiss | |
| # - author is Copilot, body < 200 chars → dismiss | |
| # - anything else → needs-human (default: conservative) | |
| # | |
| # Synced from github-settings-automation/templates/triage-bot.yml by the | |
| # weekly enforce-repo-settings sweep. Do not hand-edit per-repo. | |
| # | |
| # Prereqs in each consuming repo: | |
| # 1. Secret TRIAGE_PAT — classic PAT or GitHub App installation token | |
| # with `pull_requests: write`. The default GITHUB_TOKEN cannot | |
| # `resolveReviewThread` on threads it didn't author and returns | |
| # "Resource not accessible by integration". Validated in | |
| # ANcpLua/triage-bot-playground PR #1 + #2 (2026-05-18). | |
| # 2. Branch protection / ruleset on the default branch with | |
| # `required_conversation_resolution: true`. Without it the gate | |
| # doesn't exist and the workflow only adds report comments. | |
| name: Triage Bot | |
| on: | |
| pull_request_review_comment: | |
| types: [created, edited] | |
| pull_request_review: | |
| types: [submitted] | |
| workflow_dispatch: | |
| inputs: | |
| pr_number: | |
| description: PR number to triage manually | |
| required: true | |
| type: number | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| concurrency: | |
| group: triage-${{ github.event.pull_request.number || github.event.inputs.pr_number }} | |
| cancel-in-progress: false | |
| jobs: | |
| triage: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Triage unresolved review threads | |
| uses: actions/github-script@v7 | |
| with: | |
| github-token: ${{ secrets.TRIAGE_PAT }} | |
| script: | | |
| const { owner, repo } = context.repo; | |
| const pr_number = context.payload.pull_request?.number | |
| ?? Number(context.payload.inputs?.pr_number); | |
| if (!pr_number) { core.setFailed('no PR number'); return; } | |
| const data = await github.graphql(` | |
| query($owner: String!, $repo: String!, $number: Int!) { | |
| repository(owner: $owner, name: $repo) { | |
| pullRequest(number: $number) { | |
| reviewThreads(first: 100) { | |
| nodes { | |
| id isResolved viewerCanResolve | |
| comments(first: 5) { | |
| nodes { id body author { login } } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| }`, { owner, repo, number: pr_number }); | |
| const threads = data.repository.pullRequest.reviewThreads.nodes | |
| .filter(t => !t.isResolved); | |
| core.info(`PR #${pr_number}: ${threads.length} unresolved thread(s)`); | |
| const summary = []; | |
| for (const t of threads) { | |
| const first = t.comments.nodes[0] ?? {}; | |
| const body = first.body ?? ''; | |
| const author = first.author?.login ?? 'unknown'; | |
| const snippet = body.slice(0, 80).replace(/\s+/g, ' '); | |
| let decision = 'needs-human'; | |
| let reason = 'no rule matched — keeping blocked for manual review'; | |
| if (body.includes('[triage:auto-resolve]')) { | |
| decision = 'dismiss'; | |
| reason = 'explicit [triage:auto-resolve] marker present'; | |
| } else if (/^\s*(nit:|nitpick:|praise:)/i.test(body)) { | |
| decision = 'dismiss'; | |
| reason = 'classified as nit/nitpick/praise — non-blocking'; | |
| } else if (author === 'Copilot' && body.length < 200) { | |
| decision = 'dismiss'; | |
| reason = 'short Copilot comment, treated as informational'; | |
| } | |
| core.info(`thread ${t.id.slice(-8)} from @${author}: decision=${decision} viewerCanResolve=${t.viewerCanResolve}`); | |
| summary.push( | |
| `- thread ${t.id.slice(-8)} from @${author}: **${decision}** — ${reason}\n > ${snippet}`); | |
| if (decision === 'dismiss') { | |
| try { | |
| const res = await github.graphql(` | |
| mutation($id: ID!) { | |
| resolveReviewThread(input: { threadId: $id }) { | |
| thread { id isResolved } | |
| } | |
| }`, { id: t.id }); | |
| core.info(`resolved ${t.id.slice(-8)} -> isResolved=${res.resolveReviewThread.thread.isResolved}`); | |
| } catch (err) { | |
| core.warning(`resolve failed for ${t.id.slice(-8)}: ${err.message}`); | |
| } | |
| } | |
| } | |
| if (summary.length > 0) { | |
| await github.rest.issues.createComment({ | |
| owner, repo, issue_number: pr_number, | |
| body: `## Triage Bot report\n\n${summary.join('\n')}\n\n_Threads marked \`needs-human\` stay unresolved and block auto-merge._`, | |
| }); | |
| } |