Skip to content
Latchkey

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.

Grade your own workflow free or run it on Latchkey →
Source: BuilderIO/qwik.github/workflows/ai-issue-triage.ymlLicense MITView source

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

This workflow runs 1 job per trigger. On Latchkey the same minutes cost up to 58% less than GitHub-hosted, with zero queue time.

Actions used in this workflow