Skip to content
Latchkey

Issue Triage workflow (ruaan-deysel/ha-unraid)

The Issue Triage workflow from ruaan-deysel/ha-unraid, 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: ruaan-deysel/ha-unraid.github/workflows/issue-triage.ymlLicense Apache-2.0View source

What it does

This is the Issue Triage workflow from the ruaan-deysel/ha-unraid repository, a real project running GitHub Actions. It is shown here with attribution under its Apache-2.0 license.

Below, Latchkey shows a faster, safer version produced by its optimization engine.

The workflow

workflow (.yml)
name: Issue Triage

on:
  issues:
    types: [opened]

permissions:
  issues: write

jobs:
  triage:
    runs-on: ubuntu-latest
    steps:
      - name: Auto-label and triage new issues
        uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            const issue = context.payload.issue;
            const body = issue.body || '';
            const title = issue.title || '';
            const bodyLower = body.toLowerCase();
            const titleLower = title.toLowerCase();
            const labelsToAdd = [];

            // Always remove legacy/default triage label on issue open.
            try {
              await github.rest.issues.removeLabel({
                owner: context.repo.owner,
                repo: context.repo.repo,
                issue_number: issue.number,
                name: 'needs triage',
              });
              core.info('Removed label: needs triage');
            } catch (error) {
              if (error.status !== 404) {
                throw error;
              }
              core.info('Label not present: needs triage');
            }

            // --- Map bug_category / feature_category dropdown to area labels ---
            const categoryAreaMap = {
              'setup/configuration': 'area: setup',
              'config flow': 'area: setup',
              'system monitoring': 'area: system',
              'cpu, ram': 'area: system',
              'storage monitoring': 'area: storage',
              'array': 'area: storage',
              'docker management': 'area: docker',
              'container': 'area: docker',
              'vm management': 'area: vm',
              'virtual machine': 'area: vm',
              'ups monitoring': 'area: ups',
              'battery status': 'area: ups',
              'notification': 'area: notifications',
              'unraid alerts': 'area: notifications',
              'network monitoring': 'area: infra',
              'shares': 'area: storage',
              'parity': 'area: storage',
              'configuration': 'area: setup',
              'services': 'area: infra',
              'service management': 'area: infra',
              'registration': 'area: infra',
              'plugins': 'area: infra',
              'cloud': 'area: infra',
            };

            for (const [keyword, label] of Object.entries(categoryAreaMap)) {
              if (bodyLower.includes(keyword)) {
                labelsToAdd.push(label);
                break;  // Only apply first matching area label
              }
            }

            // --- Detect entity/performance/websocket from title or body ---
            const keywordLabelMap = {
              'websocket': 'area: websocket',
              'real-time': 'area: websocket',
              'container stats': 'area: docker',
              'parity check': 'area: storage',
              'smart': 'area: storage',
              'array': 'area: storage',
            };

            for (const [keyword, label] of Object.entries(keywordLabelMap)) {
              if ((titleLower.includes(keyword) || bodyLower.includes(keyword)) && !labelsToAdd.includes(label)) {
                labelsToAdd.push(label);
              }
            }

            // --- Apply labels ---
            if (labelsToAdd.length > 0) {
              // De-duplicate
              const uniqueLabels = [...new Set(labelsToAdd)];
              await github.rest.issues.addLabels({
                owner: context.repo.owner,
                repo: context.repo.repo,
                issue_number: issue.number,
                labels: uniqueLabels,
              });
              core.info(`Applied labels: ${uniqueLabels.join(', ')}`);
            }

The same workflow, on Latchkey

Removes redundant runs and caps runaway jobs. Added and changed lines are highlighted.

name: Issue Triage
 
on:
  issues:
    types: [opened]
 
permissions:
  issues: write
 
jobs:
  triage:
    timeout-minutes: 30
    runs-on: latchkey-small
    steps:
      - name: Auto-label and triage new issues
        uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            const issue = context.payload.issue;
            const body = issue.body || '';
            const title = issue.title || '';
            const bodyLower = body.toLowerCase();
            const titleLower = title.toLowerCase();
            const labelsToAdd = [];
 
            // Always remove legacy/default triage label on issue open.
            try {
              await github.rest.issues.removeLabel({
                owner: context.repo.owner,
                repo: context.repo.repo,
                issue_number: issue.number,
                name: 'needs triage',
              });
              core.info('Removed label: needs triage');
            } catch (error) {
              if (error.status !== 404) {
                throw error;
              }
              core.info('Label not present: needs triage');
            }
 
            // --- Map bug_category / feature_category dropdown to area labels ---
            const categoryAreaMap = {
              'setup/configuration': 'area: setup',
              'config flow': 'area: setup',
              'system monitoring': 'area: system',
              'cpu, ram': 'area: system',
              'storage monitoring': 'area: storage',
              'array': 'area: storage',
              'docker management': 'area: docker',
              'container': 'area: docker',
              'vm management': 'area: vm',
              'virtual machine': 'area: vm',
              'ups monitoring': 'area: ups',
              'battery status': 'area: ups',
              'notification': 'area: notifications',
              'unraid alerts': 'area: notifications',
              'network monitoring': 'area: infra',
              'shares': 'area: storage',
              'parity': 'area: storage',
              'configuration': 'area: setup',
              'services': 'area: infra',
              'service management': 'area: infra',
              'registration': 'area: infra',
              'plugins': 'area: infra',
              'cloud': 'area: infra',
            };
 
            for (const [keyword, label] of Object.entries(categoryAreaMap)) {
              if (bodyLower.includes(keyword)) {
                labelsToAdd.push(label);
                break;  // Only apply first matching area label
              }
            }
 
            // --- Detect entity/performance/websocket from title or body ---
            const keywordLabelMap = {
              'websocket': 'area: websocket',
              'real-time': 'area: websocket',
              'container stats': 'area: docker',
              'parity check': 'area: storage',
              'smart': 'area: storage',
              'array': 'area: storage',
            };
 
            for (const [keyword, label] of Object.entries(keywordLabelMap)) {
              if ((titleLower.includes(keyword) || bodyLower.includes(keyword)) && !labelsToAdd.includes(label)) {
                labelsToAdd.push(label);
              }
            }
 
            // --- Apply labels ---
            if (labelsToAdd.length > 0) {
              // De-duplicate
              const uniqueLabels = [...new Set(labelsToAdd)];
              await github.rest.issues.addLabels({
                owner: context.repo.owner,
                repo: context.repo.repo,
                issue_number: issue.number,
                labels: uniqueLabels,
              });
              core.info(`Applied labels: ${uniqueLabels.join(', ')}`);
            }
 

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