Skip to content

Commit 4e5fb8c

Browse files
authored
Merge pull request #203 from htilly/develop
Add feature-request-enhance workflow
2 parents 6c4de8c + fe1cf2e commit 4e5fb8c

1 file changed

Lines changed: 394 additions & 0 deletions

File tree

Lines changed: 394 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,394 @@
1+
name: Feature Request Enhancement
2+
3+
on:
4+
issues:
5+
types: [opened, labeled]
6+
7+
jobs:
8+
check-label:
9+
runs-on: ubuntu-latest
10+
outputs:
11+
has_enhancement: ${{ steps.check.outputs.has_enhancement }}
12+
already_enhanced: ${{ steps.check-enhanced.outputs.already_enhanced }}
13+
steps:
14+
- name: Check if issue has enhancement label
15+
id: check
16+
run: |
17+
# For 'labeled' events, check if the label being added is 'enhancement'
18+
# For 'opened' events, check if issue has 'enhancement' label
19+
if [ "${{ github.event.action }}" = "labeled" ]; then
20+
if [ "${{ github.event.label.name }}" = "enhancement" ]; then
21+
echo "has_enhancement=true" >> $GITHUB_OUTPUT
22+
else
23+
echo "has_enhancement=false" >> $GITHUB_OUTPUT
24+
fi
25+
else
26+
# For 'opened' events, check all labels
27+
LABELS="${{ join(github.event.issue.labels.*.name, ',') }}"
28+
if echo "$LABELS" | grep -q "enhancement"; then
29+
echo "has_enhancement=true" >> $GITHUB_OUTPUT
30+
else
31+
echo "has_enhancement=false" >> $GITHUB_OUTPUT
32+
fi
33+
fi
34+
35+
- name: Check if issue already enhanced
36+
id: check-enhanced
37+
run: |
38+
# Check if issue already has enhanced content
39+
ISSUE_BODY="${{ github.event.issue.body }}"
40+
if echo "$ISSUE_BODY" | grep -q "Enhanced Feature Request"; then
41+
echo "already_enhanced=true" >> $GITHUB_OUTPUT
42+
echo "Issue already has enhanced content, skipping"
43+
else
44+
echo "already_enhanced=false" >> $GITHUB_OUTPUT
45+
fi
46+
47+
preprocess:
48+
runs-on: ubuntu-latest
49+
needs: check-label
50+
if: needs.check-label.outputs.has_enhancement == 'true' && needs.check-label.outputs.already_enhanced == 'false'
51+
permissions:
52+
contents: read
53+
outputs:
54+
enhanced_task: ${{ steps.preprocess.outputs.enhanced_task }}
55+
task_json: ${{ steps.preprocess.outputs.task_json }}
56+
confluence_url: ${{ steps.preprocess.outputs.confluence_url }}
57+
steps:
58+
- name: Checkout develop
59+
uses: actions/checkout@v4
60+
with:
61+
ref: develop
62+
63+
- name: Setup Node.js
64+
uses: actions/setup-node@v4
65+
with:
66+
node-version: 20
67+
cache: 'npm'
68+
69+
- name: Install agent dependencies
70+
working-directory: .github/agent
71+
run: npm install
72+
73+
- name: Preprocess feature request to user story (OpenAI only)
74+
id: preprocess
75+
env:
76+
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
77+
PREPROCESSING_MODEL: ${{ secrets.PREPROCESSING_MODEL || 'gpt-4o-mini' }}
78+
TASK: ${{ github.event.issue.title }}
79+
REQUESTER: ${{ github.event.issue.user.login }}
80+
# Skip Confluence creation in preprocessing - will be done in parallel job
81+
CONFLUENCE_URL: ""
82+
CONFLUENCE_EMAIL: ""
83+
CONFLUENCE_API_TOKEN: ""
84+
CONFLUENCE_SPACE_KEY: ""
85+
CONFLUENCE_PARENT_PAGE_ID: ""
86+
GITHUB_RUN_ID: ${{ github.run_id }}
87+
run: |
88+
OUTPUT=$(node .github/agent/preprocess-task.mjs 2>&1)
89+
echo "$OUTPUT"
90+
91+
# Extract enhanced task from output
92+
if echo "$OUTPUT" | grep -q "ENHANCED_TASK_START"; then
93+
ENHANCED_TASK=$(echo "$OUTPUT" | sed -n '/ENHANCED_TASK_START/,/ENHANCED_TASK_END/p' | sed '1d;$d')
94+
echo "enhanced_task<<EOF" >> $GITHUB_OUTPUT
95+
echo "$ENHANCED_TASK" >> $GITHUB_OUTPUT
96+
echo "EOF" >> $GITHUB_OUTPUT
97+
else
98+
# Fallback to original task
99+
echo "enhanced_task=${{ github.event.issue.title }}" >> $GITHUB_OUTPUT
100+
fi
101+
102+
# Extract JSON if available
103+
JSON_LINE=$(echo "$OUTPUT" | grep "ENHANCED_TASK_JSON:" || true)
104+
if [ -n "$JSON_LINE" ]; then
105+
JSON_DATA=$(echo "$JSON_LINE" | sed 's/ENHANCED_TASK_JSON://')
106+
echo "task_json=$JSON_DATA" >> $GITHUB_OUTPUT
107+
else
108+
echo "task_json=null" >> $GITHUB_OUTPUT
109+
fi
110+
111+
create-confluence:
112+
runs-on: ubuntu-latest
113+
needs: preprocess
114+
if: needs.preprocess.outcome == 'success'
115+
permissions:
116+
contents: read
117+
steps:
118+
- name: Checkout develop
119+
uses: actions/checkout@v4
120+
with:
121+
ref: develop
122+
123+
- name: Setup Node.js
124+
uses: actions/setup-node@v4
125+
with:
126+
node-version: 20
127+
cache: 'npm'
128+
129+
- name: Install agent dependencies
130+
working-directory: .github/agent
131+
run: npm install
132+
133+
- name: Create Confluence page
134+
id: confluence
135+
env:
136+
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
137+
PREPROCESSING_MODEL: ${{ secrets.PREPROCESSING_MODEL || 'gpt-4o-mini' }}
138+
TASK: ${{ github.event.issue.title }}
139+
REQUESTER: ${{ github.event.issue.user.login }}
140+
CONFLUENCE_URL: ${{ secrets.CONFLUENCE_URL }}
141+
CONFLUENCE_EMAIL: ${{ secrets.CONFLUENCE_EMAIL }}
142+
CONFLUENCE_API_TOKEN: ${{ secrets.CONFLUENCE_API_TOKEN }}
143+
CONFLUENCE_SPACE_KEY: ${{ secrets.CONFLUENCE_SPACE_KEY || 'AICODE' }}
144+
CONFLUENCE_PARENT_PAGE_ID: ${{ secrets.CONFLUENCE_PARENT_PAGE_ID || '5177529' }}
145+
GITHUB_RUN_ID: ${{ github.run_id }}
146+
# Use enhanced task from preprocessing
147+
ENHANCED_TASK: ${{ needs.preprocess.outputs.enhanced_task }}
148+
run: |
149+
# Create a temporary script that only creates Confluence page
150+
cat > /tmp/create-confluence.mjs << 'SCRIPT_EOF'
151+
// Use native fetch (Node 20+)
152+
153+
const enhancedTask = process.env.ENHANCED_TASK || "";
154+
const task = process.env.TASK || "";
155+
const requester = process.env.REQUESTER || "unknown";
156+
const confluenceUrl = process.env.CONFLUENCE_URL || "";
157+
const confluenceEmail = process.env.CONFLUENCE_EMAIL || "";
158+
const confluenceApiToken = process.env.CONFLUENCE_API_TOKEN || "";
159+
const confluenceSpaceKey = process.env.CONFLUENCE_SPACE_KEY || "AICODE";
160+
const confluenceParentPageId = process.env.CONFLUENCE_PARENT_PAGE_ID || "";
161+
const githubRunId = process.env.GITHUB_RUN_ID || "unknown";
162+
163+
if (!confluenceUrl || !confluenceEmail || !confluenceApiToken) {
164+
console.log("[CONFLUENCE] Confluence credentials not configured, skipping page creation");
165+
process.exit(0);
166+
}
167+
168+
try {
169+
const timestamp = new Date().toISOString().split('T')[0];
170+
const pageTitle = `AICODE: ${task.substring(0, 80)} (${timestamp})`;
171+
172+
const storageContent = `
173+
<h2>Original Task</h2>
174+
<p>${task.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\n/g, '<br/>')}</p>
175+
176+
<h2>User Story</h2>
177+
<p>${enhancedTask.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\n/g, '<br/>')}</p>
178+
179+
<h2>Implementation Status</h2>
180+
<p><ac:structured-macro ac:name="status"><ac:parameter ac:name="colour">Yellow</ac:parameter><ac:parameter ac:name="title">In Progress</ac:parameter></ac:structured-macro></p>
181+
182+
<h2>Related Links</h2>
183+
<p>GitHub Run: <a href="https://github.com/htilly/SlackONOS/actions/runs/${githubRunId}">View Workflow</a></p>
184+
`.trim();
185+
186+
const auth = Buffer.from(`${confluenceEmail}:${confluenceApiToken}`).toString('base64');
187+
188+
const pageData = {
189+
type: 'page',
190+
title: pageTitle,
191+
space: { key: confluenceSpaceKey },
192+
body: {
193+
storage: {
194+
value: storageContent,
195+
representation: 'storage'
196+
}
197+
}
198+
};
199+
200+
if (confluenceParentPageId) {
201+
pageData.ancestors = [{ id: confluenceParentPageId }];
202+
}
203+
204+
const response = await fetch(`${confluenceUrl}/rest/api/content`, {
205+
method: 'POST',
206+
headers: {
207+
'Authorization': `Basic ${auth}`,
208+
'Content-Type': 'application/json',
209+
'Accept': 'application/json'
210+
},
211+
body: JSON.stringify(pageData)
212+
});
213+
214+
if (!response.ok) {
215+
const errorText = await response.text();
216+
console.error(`[CONFLUENCE] Failed to create page: ${response.status} ${response.statusText}`);
217+
console.error(`[CONFLUENCE] Error details: ${errorText}`);
218+
process.exit(1);
219+
}
220+
221+
const data = await response.json();
222+
const pageUrl = `${confluenceUrl}/wiki${data._links.webui}`;
223+
console.log(`[CONFLUENCE] ✅ Confluence page created: ${pageUrl}`);
224+
console.log(`CONFLUENCE_URL:${pageUrl}`);
225+
} catch (error) {
226+
console.error(`[CONFLUENCE] Error creating page: ${error.message}`);
227+
process.exit(1);
228+
}
229+
SCRIPT_EOF
230+
231+
OUTPUT=$(node /tmp/create-confluence.mjs 2>&1)
232+
echo "$OUTPUT"
233+
234+
# Extract Confluence URL
235+
CONFLUENCE_URL=$(echo "$OUTPUT" | grep "CONFLUENCE_URL:" | sed 's/CONFLUENCE_URL://' || echo "N/A")
236+
echo "confluence_url=$CONFLUENCE_URL" >> $GITHUB_OUTPUT
237+
238+
update-github-issue:
239+
runs-on: ubuntu-latest
240+
needs: preprocess
241+
if: needs.preprocess.outcome == 'success'
242+
permissions:
243+
contents: read
244+
issues: write
245+
steps:
246+
- name: Update GitHub issue with enhanced description
247+
env:
248+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
249+
ISSUE_NUMBER: ${{ github.event.issue.number }}
250+
run: |
251+
# Read enhanced task from JSON output to avoid shell quoting issues
252+
TASK_JSON="${{ needs.preprocess.outputs.task_json }}"
253+
254+
if [ -n "$TASK_JSON" ] && [ "$TASK_JSON" != "null" ]; then
255+
# Extract values from JSON using jq
256+
ENHANCED_TASK=$(echo "$TASK_JSON" | jq -r '.enhanced // empty')
257+
ORIGINAL_TASK=$(echo "$TASK_JSON" | jq -r '.original // empty')
258+
else
259+
# Fallback to outputs if JSON not available
260+
ENHANCED_TASK="${{ needs.preprocess.outputs.enhanced_task }}"
261+
ORIGINAL_TASK="${{ github.event.issue.title }}"
262+
fi
263+
264+
# Get original body (may be null/empty)
265+
ORIGINAL_BODY="${{ github.event.issue.body }}"
266+
if [ -z "$ORIGINAL_BODY" ] || [ "$ORIGINAL_BODY" = "null" ]; then
267+
ORIGINAL_BODY="_(No description provided)_"
268+
fi
269+
270+
# Build enhanced body with proper escaping using jq
271+
# Start with base content (without Confluence link for now)
272+
BASE_BODY=$(jq -n \
273+
--arg enhanced "$ENHANCED_TASK" \
274+
--arg original "$ORIGINAL_BODY" \
275+
--arg requester "${{ github.event.issue.user.login }}" \
276+
--arg created "${{ github.event.issue.created_at }}" \
277+
'## 📝 Enhanced Feature Request
278+
279+
\($enhanced)
280+
281+
---
282+
283+
## 📋 Original Request
284+
285+
\($original)
286+
287+
---
288+
289+
**Requested by:** \($requester)
290+
**Created:** \($created)')
291+
292+
ENHANCED_BODY="$BASE_BODY"
293+
294+
# Use jq to properly construct JSON payload for PATCH
295+
PAYLOAD=$(jq -n \
296+
--arg body "$ENHANCED_BODY" \
297+
'{body: $body}')
298+
299+
# Update the issue
300+
curl -X PATCH "https://api.github.com/repos/${{ github.repository }}/issues/${{ github.event.issue.number }}" \
301+
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
302+
-H "Accept: application/vnd.github+json" \
303+
-H "Content-Type: application/json" \
304+
-d "$PAYLOAD"
305+
306+
echo "✅ Issue #${{ github.event.issue.number }} updated with enhanced description"
307+
308+
add-confluence-link:
309+
runs-on: ubuntu-latest
310+
needs: [preprocess, create-confluence, update-github-issue]
311+
if: needs.create-confluence.outcome == 'success' && needs.update-github-issue.outcome == 'success'
312+
permissions:
313+
issues: write
314+
steps:
315+
- name: Add Confluence link to issue
316+
env:
317+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
318+
run: |
319+
CONFLUENCE_URL="${{ needs.create-confluence.outputs.confluence_url }}"
320+
321+
if [ "$CONFLUENCE_URL" = "N/A" ] || [ -z "$CONFLUENCE_URL" ]; then
322+
echo "No Confluence URL to add"
323+
exit 0
324+
fi
325+
326+
# Get current issue body
327+
CURRENT_BODY=$(curl -s "https://api.github.com/repos/${{ github.repository }}/issues/${{ github.event.issue.number }}" \
328+
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
329+
-H "Accept: application/vnd.github+json" | jq -r '.body')
330+
331+
# Add Confluence link to the end
332+
UPDATED_BODY=$(jq -n \
333+
--arg current "$CURRENT_BODY" \
334+
--arg confluence "$CONFLUENCE_URL" \
335+
'\($current)
336+
337+
📄 **Requirements documented:** \($confluence)')
338+
339+
# Update issue with Confluence link
340+
PAYLOAD=$(jq -n \
341+
--arg body "$UPDATED_BODY" \
342+
'{body: $body}')
343+
344+
curl -X PATCH "https://api.github.com/repos/${{ github.repository }}/issues/${{ github.event.issue.number }}" \
345+
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
346+
-H "Accept: application/vnd.github+json" \
347+
-H "Content-Type: application/json" \
348+
-d "$PAYLOAD"
349+
350+
echo "✅ Added Confluence link to issue"
351+
352+
add-comment:
353+
runs-on: ubuntu-latest
354+
needs: [preprocess, create-confluence, update-github-issue]
355+
if: always() && (needs.preprocess.outcome == 'success' || needs.update-github-issue.outcome == 'success')
356+
permissions:
357+
issues: write
358+
steps:
359+
- name: Add comment to issue
360+
env:
361+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
362+
run: |
363+
CONFLUENCE_URL="${{ needs.create-confluence.outputs.confluence_url }}"
364+
365+
# Build comment with proper escaping using jq
366+
if [ "$CONFLUENCE_URL" != "N/A" ] && [ -n "$CONFLUENCE_URL" ]; then
367+
COMMENT=$(jq -n \
368+
--arg confluence "$CONFLUENCE_URL" \
369+
'🤖 **Feature request enhanced!**
370+
371+
This feature request has been automatically enhanced with a structured user story format.
372+
373+
📄 Requirements documented: \($confluence)
374+
375+
The issue description has been updated with acceptance criteria and technical notes.')
376+
else
377+
COMMENT=$(jq -n \
378+
'🤖 **Feature request enhanced!**
379+
380+
This feature request has been automatically enhanced with a structured user story format.
381+
382+
The issue description has been updated with acceptance criteria and technical notes.')
383+
fi
384+
385+
# Use jq to properly construct JSON payload
386+
PAYLOAD=$(jq -n \
387+
--arg body "$COMMENT" \
388+
'{body: $body}')
389+
390+
curl -X POST "https://api.github.com/repos/${{ github.repository }}/issues/${{ github.event.issue.number }}/comments" \
391+
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
392+
-H "Accept: application/vnd.github+json" \
393+
-H "Content-Type: application/json" \
394+
-d "$PAYLOAD"

0 commit comments

Comments
 (0)