ci: add DeepSeek-powered issue triage bot - #415
Conversation
Adds an Issue Triage workflow that drafts a label suggestion, a one-line summary, and possible-duplicate candidates for every opened issue using DeepSeek V4-Flash, then holds it in the run summary until a maintainer approves by adding the triage-ok label. Only the approval-gated apply job holds the GitHub App key and posts publicly, so the job that runs on arbitrary user-opened issues has read-only access and never posts.
📝 WalkthroughWalkthroughAdds a Node.js script that generates and renders DeepSeek issue-triage results. Adds a GitHub Actions workflow that displays suggestions and applies approved labels and comments. ChangesIssue triage automation
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Issue as GitHub Issue
participant Actions as GitHub Actions
participant Triage as triage.mjs
participant DeepSeek as DeepSeek API
participant GitHub as GitHub API
Issue->>Actions: Trigger issue triage
Actions->>Triage: Read issue and comparison data
Triage->>DeepSeek: Request structured triage
DeepSeek-->>Triage: Return triage response
Triage-->>Actions: Emit normalized JSON and Markdown
Actions->>GitHub: Write summary or apply approved labels and comment
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
✨ Simplify code
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/scripts/triage.mjs:
- Around line 106-120: Update the model request flow around raw and
parseModelJson so empty choices[0].message.content responses are retried a
bounded number of times before failing. Preserve the existing fenced-JSON
parsing and parse error handling, and only retry empty content rather than
malformed non-empty JSON.
- Around line 123-142: Validate and normalize the result from parseModelJson
before accessing triage.category, triage.dup_candidates, or triage.summary,
ensuring the root is a non-array object. In the dup_candidates pipeline, filter
out null and non-object entries before reading d.number, d.confidence, or
d.reason, while preserving the existing open-issue and allowed-label filtering.
In @.github/workflows/triage.yml:
- Line 34: Update both actions/checkout@v4 steps in .github/workflows/triage.yml
at lines 34-34 and 77-77 to set the checkout input persist-credentials to false;
no other workflow behavior should change.
- Around line 95-98: Persist the triage draft generated by the suggest job and
make the apply job consume that exact draft after approval instead of rerunning
triage.mjs. Update the workflow around the “Re-run triage with DeepSeek” step
and the apply job’s gather/post steps to transfer and retrieve the approved
run’s triage.json, preserving the existing posting and label application against
the persisted content.
- Around line 51-54: Add a finite timeout-minutes setting to both the triage job
containing “Draft triage with DeepSeek” and the apply job, using a value
appropriate for the DeepSeek call so stalled requests release the runner and
concurrency group promptly.
- Around line 81-86: Update the “Mint spotatui[bot] token” step using a pinned
create-github-app-token action version, and configure its permission inputs to
grant only issue-level access needed for comments and label updates. Keep the
existing app credentials and app-token step wiring unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 4487dc5c-e699-4bc0-a4d1-d7e9816090bf
📒 Files selected for processing (2)
.github/scripts/triage.mjs.github/workflows/triage.yml
| const raw = (await resp.json()).choices?.[0]?.message?.content ?? ""; | ||
|
|
||
| // Flash models occasionally wrap JSON in a ```json fence even with | ||
| // response_format set; strip it before parsing. | ||
| function parseModelJson(s) { | ||
| const fenced = s.match(/```(?:json)?\s*([\s\S]*?)```/i); | ||
| return JSON.parse((fenced ? fenced[1] : s).trim()); | ||
| } | ||
|
|
||
| let triage; | ||
| try { | ||
| triage = parseModelJson(raw); | ||
| } catch (e) { | ||
| console.error(`Could not parse model output as JSON: ${e.message}\n---\n${raw}`); | ||
| process.exit(1); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== file excerpt =="
sed -n '1,180p' .github/scripts/triage.mjs
echo
echo "== retry-related references in scripts =="
rg -n "retry|empty|content|choices|message|raw|parseModelJson|process.exit" .github/scripts/triage.mjs .github/workflows 2>/dev/null || true
echo
echo "== relevant workflow triggers/calls =="
rg -n "triage\.mjs|GITHUB_EVENT_NAME|INPUT|ON|jobs:" .github/workflows .github/scripts/triage.mjs 2>/dev/null || trueRepository: LargeModGames/spotatui
Length of output: 13593
🌐 Web query:
DeepSeek Chat Completions JSON Output empty content API documentation
💡 Result:
When using the DeepSeek API to generate JSON output, encountering an empty content response (or a JSONDecodeError) is a known edge case that developers should account for in their application logic [1][2]. To enable and optimize JSON mode: 1. Set the response_format parameter to {"type": "json_object"} in your API request [3][4]. 2. Include the word "json" explicitly in your system or user prompt [4][2]. 3. Provide a clear example of the desired JSON schema/structure within the prompt to guide the model [3][4]. 4. Ensure the max_tokens parameter is set high enough to accommodate the full JSON object to prevent truncation [1][4]. If you are receiving empty content (e.g., content="", completion_tokens=0), it may be caused by several factors: - Timeouts: DeepSeek models, particularly reasoning models, can take significant time to process complex requests. If the request exceeds client-side or gateway timeout limits (often around 60 seconds), the connection may close, resulting in empty or incomplete responses [5][6]. - Backend Inference State: Some reports suggest that certain conditions, such as returning tool/function call results to the model, can intermittently trigger a "silent exit" where the model returns without generating content [7]. - Keep-Alive Lines: The API may send keep-alive comments (e.g., lines starting with ":" or blank lines) during long inference processes. Ensure your client-side parser is designed to ignore these SSE comments and whitespace, and only parse the final valid JSON body [8][5]. For robust implementations, treat empty content as an expected edge case and implement retry logic or validation on the client side [1][2]. If issues persist, consider disabling streaming (if enabled) or testing if the behavior changes with a different model version (e.g., switching between V4-Pro and V4-Flash) [7].
Citations:
- 1: https://deepseekai.guide/api/deepseek-api-documentation/
- 2: https://deepseekv4pro.com/guides/deepseek-json-output-guide
- 3: https://deepseeksr1.com/api-docs/
- 4: https://deepseekai.guide/api/deepseek-api-json-mode/
- 5: https://krater.ai/troubleshoot/deepseek/r1-reasoning-timeout
- 6: [BUG] Frequent JSONDecodeError with DeepSeek API deepseek-ai/DeepSeek-V3#599
- 7: [BUG] deepseek-v4-pro returns empty response (out=0, no content or reasoning) after tool call results are fed back — streaming + function calling deepseek-ai/DeepSeek-V3#1453
- 8: https://chat-deep.ai/docs/deepseek-api-keep-alive-timeouts/
🌐 Web query:
DeepSeek AI Chat Completions response_format json_mode empty content
💡 Result:
When using the DeepSeek API with response_format set to {"type": "json_object"}, encountering empty content (or responses that fail to parse as JSON) is a known issue generally attributed to system instability, timeouts, or configuration gaps [1][2][3]. To enable JSON mode correctly, you must set response_format to {"type": "json_object"} in your API request [4][5][6]. To mitigate empty responses and ensure valid output, developers recommend the following best practices: 1. Prompt Instructions: You must explicitly instruct the model to produce JSON in your system or user message [4][7][6]. Include the word "json" and provide a clear example of the desired JSON schema [4][5][6]. Failure to do so may lead the model to generate non-JSON text or excessive whitespace, which can be interpreted as an empty or invalid response [7][6]. 2. Set Appropriate Parameters: - Max Tokens: Ensure max_tokens is set high enough to accommodate the entire expected JSON string; otherwise, the output may be truncated, leading to invalid JSON [4][5][6]. - Temperature: For deterministic extraction, a temperature of 0.0 is recommended [4]. 3. Handle Backend Instability: Reports indicate that requests occasionally return an HTTP 200 status with empty content [2][3]. This is often tied to backend server timeouts or service instability, especially with more complex models [1][2]. Application-side error handling should include: - Detecting and catching JSONDecodeError when parsing the response [1][2]. - Implementing retry logic for empty responses [6]. - Validating that the returned content is non-empty and adheres to your expected schema before processing it [4][6]. If you are using reasoning models, ensure your parsing logic targets the message content, as reasoning content is returned separately [4][5].
Citations:
- 1: [BUG] Frequent JSONDecodeError with DeepSeek API deepseek-ai/DeepSeek-V3#599
- 2: Problem: Unstable Response When Calling deepseek-reasoner Model (DeepSeek-R1) via API deepseek-ai/DeepSeek-R1#314
- 3: API DeepSeek json_object empty response 0.15.2 langgenius/dify#13068
- 4: https://deepseekai.guide/api/deepseek-api-json-mode/
- 5: https://deepseekai.guide/api/deepseek-openai-compatibility/
- 6: https://chat-deep.ai/docs/json-output/
- 7: https://docs.spring.io/spring-ai/docs/current/api/org/springframework/ai/deepseek/api/ResponseFormat.html
Retry an empty model response.
When the model returns choices[0].message.content as "", the parser treats it as invalid JSON and exits. Retry the request a bounded number of times for empty content before failing the triage job.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/scripts/triage.mjs around lines 106 - 120, Update the model request
flow around raw and parseModelJson so empty choices[0].message.content responses
are retried a bounded number of times before failing. Preserve the existing
fenced-JSON parsing and parse error handling, and only retry empty content
rather than malformed non-empty JSON.
| // Hard-filter everything the model returned against reality. | ||
| const openNumbers = new Set(openIssues.map((i) => i.number)); | ||
| const category = ALLOWED_LABELS.includes(triage.category) ? triage.category : null; | ||
| const dupCandidates = Array.isArray(triage.dup_candidates) | ||
| ? triage.dup_candidates | ||
| .filter((d) => openNumbers.has(d.number)) | ||
| .map((d) => ({ | ||
| number: d.number, | ||
| confidence: ["high", "medium", "low"].includes(d.confidence) ? d.confidence : "low", | ||
| reason: String(d.reason || "").slice(0, 200), | ||
| })) | ||
| : []; | ||
|
|
||
| console.log( | ||
| JSON.stringify({ | ||
| issue: issue.number, | ||
| category, | ||
| labels: category ? [category] : [], | ||
| summary: String(triage.summary || "").slice(0, 300), | ||
| dup_candidates: dupCandidates, |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Tracked candidates:"
git ls-files | rg '(^|/)(triage\.mjs|.*deepseek.*|.*model.*json.*|.*llm.*|.*chat.*completion.*)$' || true
echo
echo "triage.mjs length and relevant sections:"
wc -l .github/scripts/triage.mjs
sed -n '1,220p' .github/scripts/triage.mjsRepository: LargeModGames/spotatui
Length of output: 5842
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
import json
samples = [
"null",
"42",
'[]',
'{}',
'{"dup_candidates": null}',
'{"dup_candidates": [null]}',
'{"dup_candidates": [{"number": "abc"}]}',
]
for raw in samples:
triage = json.loads(raw)
print("payload:", repr(raw), "type:", type(triage).__name__, end=" -- ")
try:
openNumbers = set()
category = allowed if False else "ALLOWED_LABELS check" or triage.get("category")
dup_candidates = triage.get("dup_candidates")
dup_candidates = [d for d in dup_candidates if "dummy" in {"number": d.get("number")} or True if True]
except Exception as e:
print("property access", type(e).__name__ + ":", str(e))
else:
print("no property access error")
PY
node - <<'JS'
const samples = [
"null",
"42",
"[]",
"{}",
'{"dup_candidates": null}',
'{"dup_candidates": [null]}',
];
for (const raw of samples) {
const triage = JSON.parse(raw);
const label = `payload:${raw} type:${typeof triage}`;
try {
const category = ["bug"].includes(triage.category) ? triage.category : null;
const dupCandidates = Array.isArray(triage.dup_candidates)
? triage.dup_candidates.filter((d) => d.number != null && false)
: [];
console.log(label, "property access ok");
} catch (e) {
console.log(label, e.name, e.message);
}
}
JSRepository: LargeModGames/spotatui
Length of output: 1258
Validate the parsed JSON shape before reading fields.
parseModelJson accepts any valid JSON value and then the hard-filter reads candidate entries. Returning null as the root JSON object would avoid this concern, but dup_candidates: [null] causes a crash. Normalize the model response as a non-array object and filter candidate entries before property access.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/scripts/triage.mjs around lines 123 - 142, Validate and normalize
the result from parseModelJson before accessing triage.category,
triage.dup_candidates, or triage.summary, ensuring the root is a non-array
object. In the dup_candidates pipeline, filter out null and non-object entries
before reading d.number, d.confidence, or d.reason, while preserving the
existing open-issue and allowed-label filtering.
| permissions: | ||
| issues: read | ||
| steps: | ||
| - uses: actions/checkout@v4 |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win
Both checkout steps persist the job token. actions/checkout@v4 writes the credential into .git/config by default. Neither job authenticates git operations; every gh call takes GH_TOKEN from its own step env. The shared root cause is the missing persist-credentials: false input.
.github/workflows/triage.yml#L34-L34: addwith: persist-credentials: false. This job processes untrusted issue text, so it is the more exposed instance..github/workflows/triage.yml#L77-L77: addwith: persist-credentials: false. This job also holds the minted App token, so keep the checkout credential out of the workspace.
🧰 Tools
🪛 zizmor (1.28.0)
[warning] 34-34: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false
(artipacked)
📍 Affects 1 file
.github/workflows/triage.yml#L34-L34(this comment).github/workflows/triage.yml#L77-L77
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/triage.yml at line 34, Update both actions/checkout@v4
steps in .github/workflows/triage.yml at lines 34-34 and 77-77 to set the
checkout input persist-credentials to false; no other workflow behavior should
change.
Source: Linters/SAST tools
| - name: Draft triage with DeepSeek | ||
| env: | ||
| DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }} | ||
| run: node .github/scripts/triage.mjs > triage.json |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Add a job timeout around the DeepSeek call.
Neither job sets timeout-minutes, so the default 6 hour limit applies. If the DeepSeek endpoint stalls, the runner stays occupied and the concurrency group stays blocked. The same gap exists in the apply job at lines 95-98.
⏱️ Proposed guard
suggest:
...
runs-on: ubuntu-latest
+ timeout-minutes: 10
permissions:
issues: read🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/triage.yml around lines 51 - 54, Add a finite
timeout-minutes setting to both the triage job containing “Draft triage with
DeepSeek” and the apply job, using a value appropriate for the DeepSeek call so
stalled requests release the runner and concurrency group promptly.
| - name: Mint spotatui[bot] token | ||
| id: app-token | ||
| uses: actions/create-github-app-token@v1 | ||
| with: | ||
| app-id: ${{ secrets.SPOTATUI_APP_ID }} | ||
| private-key: ${{ secrets.SPOTATUI_APP_KEY }} |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
actions/create-github-app-token permission-issues input scoped permissions
💡 Result:
The actions/create-github-app-token action allows you to request granular permissions for the generated installation access token using inputs prefixed with permission- (e.g., permission-issues: write) [1][2]. While these inputs help define the scope, users often encounter permission-related issues due to a misunderstanding of how GitHub App permissions are managed and enforced [1][3][4]. Key points regarding permission-* inputs and common issues: 1. How to configure permissions: You can explicitly list required permissions by prefixing the permission key with permission- (e.g., permission-pull-requests: read) [1][2]. The action then requests a token with these specific scopes from the GitHub API [5]. 2. Important constraints and common causes of "Resource not accessible" errors: - Installation vs. App Permissions: The permissions granted to the generated token are limited by the intersection of the permissions requested in the action and the permissions actually granted to the GitHub App installation [1][5]. Even if you explicitly define permissions in the action, the token will not have access to any scope that was not granted to the app during installation [3][6]. - Administrative Approval: When you modify the permissions of a GitHub App, an organization administrator must approve these new permissions before they take effect on existing installations [1][3][7]. If your action fails with an "insufficient permissions" error despite correct configuration, verify that the app installation has been granted and updated with the necessary permissions [4][7]. - Underlying API Requirements: Some actions require specific permissions that are not immediately obvious [4]. If you receive a "Resource not accessible by integration" error, consult the specific GitHub API documentation for the endpoint you are calling to confirm the exact permission scopes required [4]. - Inheritance Behavior: By default, if no permission-* inputs are provided, the token inherits all permissions granted to the app installation [2][6]. If a workflow fails after you begin explicitly setting permission-* inputs, it is likely that you have inadvertently excluded a required permission; you can troubleshoot this by removing the permission-* settings temporarily to confirm if the broad, inherited set works [8]. If you are debugging, ensure that the GitHub App configuration (specifically the "Permissions & events" section in your App settings) aligns with the needs of the API calls being performed by the generated token [4][7].
Citations:
- 1: https://github.com/actions/create-github-app-token
- 2: https://github.com/actions/create-github-app-token/blob/main/README.md
- 3: https://github.com/actions/create-github-app-token/blob/5c1e00d1af39c515564da2ae21cc9dedf0793346/README.md
- 4: Resource not accessible by integration actions/create-github-app-token#258
- 5: Using
permission-*option withactions/create-github-app-tokendoes not restrict token permissions actions/create-github-app-token#237 - 6: https://github.com/marketplace/actions/create-github-app-token
- 7: GraphQL: Resource not accessible by integration (createRepository) actions/create-github-app-token#200
- 8: Changes in permission-* handling since version 2.0.4 actions/create-github-app-token#248
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== workflow excerpt =="
sed -n '1,140p' .github/workflows/triage.yml
echo
echo "== workflow permissions metadata =="
sed -n '1,60p' .github/workflows/triage.yml
echo
echo "== create-github-app-token references in repo =="
rg -n "create-github-app-token|SPOTATUI_APP_(ID|KEY)|permission-issues|permissions:" .github/workflows/triage.yml || trueRepository: LargeModGames/spotatui
Length of output: 6750
🌐 Web query:
site:github.com/actions/create-github-app-token v1 README permission-issues
💡 Result:
The term "permission-issues" refers to a specific configuration input in newer versions of the actions/create-github-app-token GitHub Action [1][2]. In modern versions of this action (e.g., v3+), you can use the permission-* prefix to explicitly define the permissions granted to the generated installation access token [1][2]. For example, using permission-issues: write in your workflow configuration allows you to specify the required level of access for issues [1][2]. This approach is recommended because it follows the principle of least privilege, mirroring GitHub's own practice of controlling GITHUB_TOKEN permissions in workflows [1][2]. By explicitly listing required permissions, you ensure the generated token has only the access necessary for the specific use case, rather than inheriting all of the GitHub App's granted permissions by default [1][2]. For older versions, such as the v1 branch, this explicit permission input functionality was not available [3][4]. Users encountering "permission issues" in v1 or other versions typically had to ensure the GitHub App itself was configured with the correct permissions via the GitHub App settings UI [5][3], and were sometimes required to grant at least meta:read permissions for the action to function correctly [6]. If you are experiencing access errors (often 403s) with v1, verify that the App has the necessary repository-level permissions granted in the installation settings [3][7].
Citations:
- 1: https://github.com/actions/create-github-app-token/blob/main/README.md
- 2: https://github.com/actions/create-github-app-token/blob/main/README.md?plain=1
- 3: https://github.com/actions/create-github-app-token/tree/v1.9.2
- 4: feat: permissions actions/create-github-app-token#168
- 5: https://github.com/actions/create-github-app-token/blob/34c66235f6ccc38eb01b7aed860890bdf40db33c/README.md
- 6: What is the minimum required permissions for GitHub App actions/create-github-app-token#74
- 7: Unable to checkout repo with this action workflow actions/create-github-app-token#115
🌐 Web query:
actions/create-github-app-token v1 README
💡 Result:
The actions/create-github-app-token action is a GitHub Action designed to generate a GitHub App installation access token [1][2]. Key information regarding its usage and functionality includes: Prerequisites To use the action, you must: 1. Register a GitHub App [1][3]. 2. Store the App's Client ID (or App ID) in your repository variables [3][4][5]. 3. Store the App's private key in your repository secrets [1][3][5]. Core Inputs - client-id (or app-id): The GitHub App Client ID (or App ID, though this is deprecated in favor of client-id) [4][5]. - private-key: The GitHub App private key. Escaped newlines (\n) are automatically handled [4][5]. - owner: Optional. The owner of the GitHub App installation. Defaults to the current repository owner [2][5]. - repositories: Optional. A comma or newline-separated list of repositories to grant access to [2][5]. - skip-token-revoke: Optional. If set to true, the generated token will not be revoked after the job completes [2][5]. How it works The action uses the POST /app/installations/{installation_id}/access_tokens API endpoint to generate the token [1][3]. By default, the action provides the following protections and behaviors: - Scope: The token is scoped to the current repository unless owner or repositories inputs are specified [3][4]. - Permissions: The token inherits all permissions defined for the GitHub App installation [1][2][4]. - Output: The token is provided as an output named token, which can be accessed by subsequent steps [1][3][4]. - Masking: The token is automatically masked to prevent accidental logging [1][6]. - Revocation: Unless skip-token-revoke is enabled, the token is automatically revoked in the action's post step after the job finishes [1][4]. Note: GitHub App installation access tokens have a default expiration of 1 hour [3]. If you have long-running processes that require an active token beyond this limit, you may need to implement alternative handling [3].
Citations:
- 1: https://github.com/actions/create-github-app-token/blob/34c66235f6ccc38eb01b7aed860890bdf40db33c/README.md
- 2: https://github.com/actions/create-github-app-token/tree/v1.9.2
- 3: https://github.com/actions/create-github-app-token
- 4: https://github.com/marketplace/actions/create-github-app-token
- 5: https://github.com/actions/create-github-app-token/blob/1b10c78c/action.yml
- 6: https://github.com/actions/create-github-app-token/tree/beea7b860ac0b14ca14258aca701da842aa65e30
Pin and scope the GitHub App token.
actions/create-github-app-token@v1 mints a token with all permissions granted to the App installation. Scope the generated token so only issue-level access is available when you post comments and update labels, and avoid the floating major tag so the token input can be scoped to the intended action version.
🧰 Tools
🪛 zizmor (1.28.0)
[error] 83-83: dangerous use of GitHub App tokens (github-app): app token inherits blanket installation permissions
(github-app)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/triage.yml around lines 81 - 86, Update the “Mint
spotatui[bot] token” step using a pinned create-github-app-token action version,
and configure its permission inputs to grant only issue-level access needed for
comments and label updates. Keep the existing app credentials and app-token step
wiring unchanged.
Source: Linters/SAST tools
| - name: Re-run triage with DeepSeek | ||
| env: | ||
| DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }} | ||
| run: node .github/scripts/triage.mjs > triage.json |
There was a problem hiding this comment.
🔒 Security & Privacy | 🔴 Critical | 🏗️ Heavy lift
The applied triage is not the triage the maintainer approved.
The suggest job renders draft A into the run summary. A maintainer reads draft A, then adds triage-ok. This step then calls DeepSeek again and produces draft B. Model output is not deterministic, and the open issue list at line 94 may also have changed since the draft. Lines 105-109 post draft B and apply draft B's labels.
The approval gate described in the PR objectives therefore does not bind. A maintainer can approve a safe summary and the bot can post different text, or apply labels the maintainer never saw.
Persist the draft in suggest and consume it in apply instead of regenerating.
🔧 Proposed approach
In the suggest job, upload the generated draft:
- name: Upload draft
uses: actions/upload-artifact@v4
with:
name: triage-${{ steps.n.outputs.number }}
path: triage.json
retention-days: 7In the apply job, replace the gather and re-run steps with a download:
- - name: Gather issue + open issues
- env:
- GH_TOKEN: ${{ github.token }}
- REPO: ${{ github.repository }}
- N: ${{ github.event.issue.number }}
- run: |
- gh issue view "$N" --repo "$REPO" --json number,title,body > issue.json
- gh issue list --repo "$REPO" --state open --limit 100 --json number,title > open-issues.json
- - name: Re-run triage with DeepSeek
- env:
- DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }}
- run: node .github/scripts/triage.mjs > triage.json
+ - name: Download approved draft
+ uses: actions/download-artifact@v4
+ with:
+ name: triage-${{ github.event.issue.number }}
+ github-token: ${{ github.token }}
+ run-id: ${{ needs.suggest.outputs.run_id }}Cross-run artifact download needs the source run id, so you will need actions: read and a way to record which run produced the approved draft. An alternative that avoids artifacts entirely: have suggest post the draft as a hidden or collapsed issue comment, then have apply read that comment back. Do you want me to draft the full workflow for either option?
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/triage.yml around lines 95 - 98, Persist the triage draft
generated by the suggest job and make the apply job consume that exact draft
after approval instead of rerunning triage.mjs. Update the workflow around the
“Re-run triage with DeepSeek” step and the apply job’s gather/post steps to
transfer and retrieve the approved run’s triage.json, preserving the existing
posting and label application against the persisted content.
Summary
An Issue Triage GitHub Action that uses DeepSeek V4-Flash to draft, for each opened issue, a suggested label (from the existing label set), a one-line summary, and possible-duplicate candidates. The draft is written to the workflow run summary only. Nothing is posted publicly until a maintainer approves by adding the
triage-oklabel, which triggers the apply job to post the comment and apply labels asspotatui[bot].Design notes:
suggestjob runs on arbitrary user-opened issues, so it has read-only permission and no GitHub App key; only the approval-gatedapplyjob holds the key and writes.duplicateis never applied as a label (dedup goes in the comment body).DEEPSEEK_API_KEY,SPOTATUI_APP_ID,SPOTATUI_APP_KEY, and thetriage-oklabel (already created).Testing
node --check .github/scripts/triage.mjs(syntax)workflow_dispatchafter merge (Actions only run from the default branch)Additional notes
deepseek-v4-flashmodel string andresponse_format: json_objectsupport; both are one-line fixes at the top of the script.Summary by CodeRabbit