AI Issue Triage workflow (BuilderIO/qwik)
The AI Issue Triage workflow from BuilderIO/qwik, explained and optimized by Latchkey.
A
CI health: A - excellent
Point runs-on at Latchkey and get job timeouts, self-healing for flaky steps, and up to 58% lower cost, applied automatically.
What it does
This is the AI Issue Triage workflow from the BuilderIO/qwik repository, a real project running GitHub Actions. It is shown here with attribution under its MIT license.
Below, Latchkey shows a faster, safer version produced by its optimization engine.
The workflow
workflow (.yml)
name: AI Issue Triage
on:
issues:
types: [opened]
permissions:
issues: write
jobs:
triage:
if: github.repository == 'QwikDev/qwik'
runs-on: ubuntu-latest
steps:
- name: Check account age (skip accounts < 30 days)
id: check
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const user = await github.rest.users.getByUsername({
username: context.payload.issue.user.login
});
const created = new Date(user.data.created_at);
const days = (Date.now() - created) / (1000 * 60 * 60 * 24);
return days >= 30;
- name: AI triage and apply labels
if: steps.check.outputs.result == 'true'
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
with:
script: |
const issue = context.payload.issue;
const issueText = `Title: ${issue.title}\n\nBody: ${issue.body || '(empty)'}`;
// Call OpenCode Zen API (OpenAI-compatible)
const response = await fetch('https://opencode.ai/zen/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.OPENCODE_API_KEY}`
},
body: JSON.stringify({
model: 'kimi-k2.5',
messages: [
{
role: 'system',
content: `You are a Qwik framework issue triager. Given an issue, respond with ONLY a JSON array of label strings to apply. No explanation, no markdown, just the JSON array.
Available labels:
Type (pick exactly one):
- "bug" - Something isn't working
- "enhancement" - New feature or request
Component (pick if clearly relevant):
- "runtime", "Optimizer", "Router", "SSR", "Preloader", "starters", "styling", "types", "reactivity", "Insights", "docs", "DX"
Status (apply if applicable):
- "needs reproduction" - Bug report lacks a reproduction link
Rules:
1. Always include exactly ONE type label.
2. For bugs without a reproduction link, include "needs reproduction".
3. Do NOT include "good first issue" unless the fix is obviously trivial.
4. Respond with ONLY a JSON array like: ["bug", "runtime", "needs reproduction"]`
},
{
role: 'user',
content: issueText
}
],
temperature: 0
})
});
if (!response.ok) {
const text = await response.text();
core.setFailed(`Zen API error ${response.status}: ${text}`);
return;
}
const data = await response.json();
const content = data.choices[0].message.content.trim();
// Parse labels from response
let labels;
try {
labels = JSON.parse(content);
} catch {
// Try extracting JSON array from response
const match = content.match(/\[[\s\S]*\]/);
if (match) {
labels = JSON.parse(match[0]);
} else {
core.setFailed(`Could not parse labels from: ${content}`);
return;
}
}
if (!Array.isArray(labels) || labels.length === 0) {
core.setFailed(`Invalid labels response: ${content}`);
return;
}
// Fetch actual repo labels to validate against
const repoLabels = await github.paginate(github.rest.issues.listLabelsForRepo, {
owner: context.repo.owner,
repo: context.repo.repo,
per_page: 100
});
const allowed = new Set(repoLabels.map(l => l.name));
const valid = labels.filter(l => allowed.has(l));
if (valid.length === 0) {
core.setFailed(`No valid labels found in: ${JSON.stringify(labels)}`);
return;
}
// Apply labels
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
labels: valid
});
core.info(`Applied labels: ${valid.join(', ')}`);
// Post follow-up comment for needs reproduction
// (labeling-issues.yml won't fire because GITHUB_TOKEN labels don't trigger labeled events)
if (valid.includes('needs reproduction')) {
const author = issue.user.login;
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body: `Hello @${author}. Please provide a [minimal reproduction](https://stackoverflow.com/help/minimal-reproducible-example) using a GitHub repository or [StackBlitz](https://qwik.new).\n[Here](https://antfu.me/posts/why-reproductions-are-required#why-reproduction) is why reproductions help us fix issues faster.\nThanks 🙏`
});
}
The same workflow, on Latchkey
Removes redundant runs and caps runaway jobs. Added and changed lines are highlighted.
name: AI Issue Triage on: issues: types: [opened] permissions: issues: write jobs: triage: timeout-minutes: 30 if: github.repository == 'QwikDev/qwik' runs-on: latchkey-small steps: - name: Check account age (skip accounts < 30 days) id: check uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | const user = await github.rest.users.getByUsername({ username: context.payload.issue.user.login }); const created = new Date(user.data.created_at); const days = (Date.now() - created) / (1000 * 60 * 60 * 24); return days >= 30; - name: AI triage and apply labels if: steps.check.outputs.result == 'true' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} with: script: | const issue = context.payload.issue; const issueText = `Title: ${issue.title}\n\nBody: ${issue.body || '(empty)'}`; // Call OpenCode Zen API (OpenAI-compatible) const response = await fetch('https://opencode.ai/zen/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${process.env.OPENCODE_API_KEY}` }, body: JSON.stringify({ model: 'kimi-k2.5', messages: [ { role: 'system', content: `You are a Qwik framework issue triager. Given an issue, respond with ONLY a JSON array of label strings to apply. No explanation, no markdown, just the JSON array. Available labels: Type (pick exactly one): - "bug" - Something isn't working - "enhancement" - New feature or request Component (pick if clearly relevant): - "runtime", "Optimizer", "Router", "SSR", "Preloader", "starters", "styling", "types", "reactivity", "Insights", "docs", "DX" Status (apply if applicable): - "needs reproduction" - Bug report lacks a reproduction link Rules: 1. Always include exactly ONE type label. 2. For bugs without a reproduction link, include "needs reproduction". 3. Do NOT include "good first issue" unless the fix is obviously trivial. 4. Respond with ONLY a JSON array like: ["bug", "runtime", "needs reproduction"]` }, { role: 'user', content: issueText } ], temperature: 0 }) }); if (!response.ok) { const text = await response.text(); core.setFailed(`Zen API error ${response.status}: ${text}`); return; } const data = await response.json(); const content = data.choices[0].message.content.trim(); // Parse labels from response let labels; try { labels = JSON.parse(content); } catch { // Try extracting JSON array from response const match = content.match(/\[[\s\S]*\]/); if (match) { labels = JSON.parse(match[0]); } else { core.setFailed(`Could not parse labels from: ${content}`); return; } } if (!Array.isArray(labels) || labels.length === 0) { core.setFailed(`Invalid labels response: ${content}`); return; } // Fetch actual repo labels to validate against const repoLabels = await github.paginate(github.rest.issues.listLabelsForRepo, { owner: context.repo.owner, repo: context.repo.repo, per_page: 100 }); const allowed = new Set(repoLabels.map(l => l.name)); const valid = labels.filter(l => allowed.has(l)); if (valid.length === 0) { core.setFailed(`No valid labels found in: ${JSON.stringify(labels)}`); return; } // Apply labels await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: issue.number, labels: valid }); core.info(`Applied labels: ${valid.join(', ')}`); // Post follow-up comment for needs reproduction // (labeling-issues.yml won't fire because GITHUB_TOKEN labels don't trigger labeled events) if (valid.includes('needs reproduction')) { const author = issue.user.login; await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: issue.number, body: `Hello @${author}. Please provide a [minimal reproduction](https://stackoverflow.com/help/minimal-reproducible-example) using a GitHub repository or [StackBlitz](https://qwik.new).\n[Here](https://antfu.me/posts/why-reproductions-are-required#why-reproduction) is why reproductions help us fix issues faster.\nThanks 🙏` }); }
What changed
- Run on Latchkey managed runners with one line (
runs-on), which apply the fixes below automatically and self-heal transient failures. This example useslatchkey-small; pick the runner size that fits the job. - Add a job timeout so a hung step cannot burn hours of runner time.
This workflow runs 1 job per trigger. On Latchkey the same minutes cost up to 58% less than GitHub-hosted, with zero queue time.