ha-inlite

Home Assistant integration for in-lite
git clone https://git.stephank.nl/ha-inlite
Log | Files | Refs | README | LICENSE | ZIP

squad-heartbeat.yml (6719B)


      1 name: Squad Heartbeat (Ralph)
      2 # ⚠️ SYNC: This workflow is maintained in 4 locations. Changes must be applied to all:
      3 #   - templates/workflows/squad-heartbeat.yml (source template)
      4 #   - packages/squad-cli/templates/workflows/squad-heartbeat.yml (CLI package)
      5 #   - .squad/templates/workflows/squad-heartbeat.yml (installed template)
      6 #   - .github/workflows/squad-heartbeat.yml (active workflow)
      7 # Run 'squad upgrade' to sync installed copies from source templates.
      8 
      9 on:
     10   schedule:
     11     # Every 30 minutes — adjust via cron expression as needed
     12     - cron: '*/30 * * * *'
     13 
     14   # React to completed work or new squad work
     15   issues:
     16     types: [closed, labeled]
     17   pull_request:
     18     types: [closed]
     19 
     20   # Manual trigger
     21   workflow_dispatch:
     22 
     23 permissions:
     24   issues: write
     25   contents: read
     26   pull-requests: read
     27 
     28 jobs:
     29   heartbeat:
     30     runs-on: ubuntu-latest
     31     steps:
     32       - uses: actions/checkout@v4
     33 
     34       - name: Check triage script
     35         id: check-script
     36         run: |
     37           if [ -f ".squad/templates/ralph-triage.js" ]; then
     38             echo "has_script=true" >> $GITHUB_OUTPUT
     39           else
     40             echo "has_script=false" >> $GITHUB_OUTPUT
     41             echo "⚠️ ralph-triage.js not found — run 'squad upgrade' to install"
     42           fi
     43 
     44       - name: Ralph — Smart triage
     45         if: steps.check-script.outputs.has_script == 'true'
     46         env:
     47           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
     48         run: |
     49           node .squad/templates/ralph-triage.js \
     50             --squad-dir .squad \
     51             --output triage-results.json
     52 
     53       - name: Ralph — Apply triage decisions
     54         if: steps.check-script.outputs.has_script == 'true' && hashFiles('triage-results.json') != ''
     55         uses: actions/github-script@v7
     56         with:
     57           script: |
     58             const fs = require('fs');
     59             const path = 'triage-results.json';
     60             if (!fs.existsSync(path)) {
     61               core.info('No triage results — board is clear');
     62               return;
     63             }
     64             
     65             const results = JSON.parse(fs.readFileSync(path, 'utf8'));
     66             if (results.length === 0) {
     67               core.info('📋 Board is clear — Ralph found no untriaged issues');
     68               return;
     69             }
     70             
     71             for (const decision of results) {
     72               try {
     73                 await github.rest.issues.addLabels({
     74                   owner: context.repo.owner,
     75                   repo: context.repo.repo,
     76                   issue_number: decision.issueNumber,
     77                   labels: [decision.label]
     78                 });
     79                 
     80                 await github.rest.issues.createComment({
     81                   owner: context.repo.owner,
     82                   repo: context.repo.repo,
     83                   issue_number: decision.issueNumber,
     84                   body: [
     85                     '### 🔄 Ralph — Auto-Triage',
     86                     '',
     87                     `**Assigned to:** ${decision.assignTo}`,
     88                     `**Reason:** ${decision.reason}`,
     89                     `**Source:** ${decision.source}`,
     90                     '',
     91                     '> Ralph auto-triaged this issue using routing rules.',
     92                     '> To reassign, swap the `squad:*` label.'
     93                   ].join('\n')
     94                 });
     95                 
     96                 core.info(`Triaged #${decision.issueNumber} → ${decision.assignTo} (${decision.source})`);
     97               } catch (e) {
     98                 core.warning(`Failed to triage #${decision.issueNumber}: ${e.message}`);
     99               }
    100             }
    101             
    102             core.info(`🔄 Ralph triaged ${results.length} issue(s)`);
    103 
    104       # Copilot auto-assign step (uses PAT if available)
    105       - name: Ralph — Assign @copilot issues
    106         if: success()
    107         uses: actions/github-script@v7
    108         with:
    109           github-token: ${{ secrets.COPILOT_ASSIGN_TOKEN || secrets.GITHUB_TOKEN }}
    110           script: |
    111             const fs = require('fs');
    112 
    113             let teamFile = '.squad/team.md';
    114             if (!fs.existsSync(teamFile)) {
    115               teamFile = '.ai-team/team.md';
    116             }
    117             if (!fs.existsSync(teamFile)) return;
    118 
    119             const content = fs.readFileSync(teamFile, 'utf8');
    120 
    121             // Check if @copilot is on the team with auto-assign
    122             const hasCopilot = content.includes('🤖 Coding Agent') || content.includes('@copilot');
    123             const autoAssign = content.includes('<!-- copilot-auto-assign: true -->');
    124             if (!hasCopilot || !autoAssign) return;
    125 
    126             // Find issues labeled squad:copilot with no assignee
    127             try {
    128               const { data: copilotIssues } = await github.rest.issues.listForRepo({
    129                 owner: context.repo.owner,
    130                 repo: context.repo.repo,
    131                 labels: 'squad:copilot',
    132                 state: 'open',
    133                 per_page: 5
    134               });
    135 
    136               const unassigned = copilotIssues.filter(i =>
    137                 !i.assignees || i.assignees.length === 0
    138               );
    139 
    140               if (unassigned.length === 0) {
    141                 core.info('No unassigned squad:copilot issues');
    142                 return;
    143               }
    144 
    145               // Get repo default branch
    146               const { data: repoData } = await github.rest.repos.get({
    147                 owner: context.repo.owner,
    148                 repo: context.repo.repo
    149               });
    150 
    151               for (const issue of unassigned) {
    152                 try {
    153                   await github.request('POST /repos/{owner}/{repo}/issues/{issue_number}/assignees', {
    154                     owner: context.repo.owner,
    155                     repo: context.repo.repo,
    156                     issue_number: issue.number,
    157                     assignees: ['copilot-swe-agent[bot]'],
    158                     agent_assignment: {
    159                       target_repo: `${context.repo.owner}/${context.repo.repo}`,
    160                       base_branch: repoData.default_branch,
    161                       custom_instructions: `Read .squad/team.md (or .ai-team/team.md) for team context and .squad/routing.md (or .ai-team/routing.md) for routing rules.`
    162                     }
    163                   });
    164                   core.info(`Assigned copilot-swe-agent[bot] to #${issue.number}`);
    165                 } catch (e) {
    166                   core.warning(`Failed to assign @copilot to #${issue.number}: ${e.message}`);
    167                 }
    168               }
    169             } catch (e) {
    170               core.info(`No squad:copilot label found or error: ${e.message}`);
    171             }