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