@datarailsshared/dr_renderer 1.5.79 → 1.5.88
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,915 @@
|
|
|
1
|
+
# AI Coder Jira Integration Workflow
|
|
2
|
+
#
|
|
3
|
+
# This workflow integrates AI-powered code generation with Jira issues.
|
|
4
|
+
# It can be triggered by Jira webhooks, GitHub issues, or manual dispatch.
|
|
5
|
+
#
|
|
6
|
+
# MANDATORY REPOSITORY SECRETS:
|
|
7
|
+
# ==============================
|
|
8
|
+
# - OPENAI_API_KEY: OpenAI API key for AI code generation (required for Codex)
|
|
9
|
+
# - ANTHROPIC_API_KEY: Anthropic API key for Claude Code (required for Claude)
|
|
10
|
+
# - JIRA_EMAIL: Email address for Jira API authentication
|
|
11
|
+
# - JIRA_API_TOKEN: Jira API token for authentication
|
|
12
|
+
# - JIRA_BASE_URL: Base URL for Jira instance (e.g., https://yourcompany.atlassian.net)
|
|
13
|
+
# If not provided, will be derived from payload baseUrl
|
|
14
|
+
# WORKFLOW INPUTS (for manual dispatch):
|
|
15
|
+
# ======================================
|
|
16
|
+
# - jira_issue_key: Jira issue key (default: DR-38896)
|
|
17
|
+
# - jira_issue_summary: Brief description of the task
|
|
18
|
+
# - ai_provider: AI provider to use (codex or claude) - default: codex
|
|
19
|
+
#
|
|
20
|
+
# TRIGGER EVENTS:
|
|
21
|
+
# ===============
|
|
22
|
+
# 1. workflow_dispatch: Manual trigger with optional inputs
|
|
23
|
+
# 2. repository_dispatch: Triggered by Jira webhook (event_type: jira_trigger)
|
|
24
|
+
# 3. issues: Triggered when GitHub issue gets 'ai-coder' label
|
|
25
|
+
#
|
|
26
|
+
|
|
27
|
+
name: ai-coder-jira
|
|
28
|
+
|
|
29
|
+
on:
|
|
30
|
+
workflow_dispatch:
|
|
31
|
+
inputs:
|
|
32
|
+
jira_issue_key:
|
|
33
|
+
description: "Jira issue key (if dispatching manually)"
|
|
34
|
+
required: false
|
|
35
|
+
default: DR-38896
|
|
36
|
+
jira_issue_summary:
|
|
37
|
+
description: "Remove unreferenced import in Badge"
|
|
38
|
+
required: false
|
|
39
|
+
ai_provider:
|
|
40
|
+
description: "AI provider to use"
|
|
41
|
+
required: false
|
|
42
|
+
default: codex
|
|
43
|
+
type: choice
|
|
44
|
+
options:
|
|
45
|
+
- codex
|
|
46
|
+
- claude
|
|
47
|
+
repository_dispatch:
|
|
48
|
+
types: [jira_trigger] # Matches "event_type" in your JSON
|
|
49
|
+
issues:
|
|
50
|
+
types: [labeled] # Run when a GH issue gets 'ai-coder'
|
|
51
|
+
|
|
52
|
+
jobs:
|
|
53
|
+
run-ai-agent:
|
|
54
|
+
if: |
|
|
55
|
+
(github.event_name == 'issues' && contains(github.event.issue.labels.*.name, 'ai-coder')) ||
|
|
56
|
+
(github.event_name == 'repository_dispatch' && github.event.action == 'jira_trigger') ||
|
|
57
|
+
(github.event_name == 'workflow_dispatch')
|
|
58
|
+
runs-on: ubuntu-latest
|
|
59
|
+
|
|
60
|
+
permissions:
|
|
61
|
+
contents: write
|
|
62
|
+
pull-requests: write
|
|
63
|
+
issues: write
|
|
64
|
+
|
|
65
|
+
steps:
|
|
66
|
+
- uses: actions/checkout@v4
|
|
67
|
+
with:
|
|
68
|
+
fetch-depth: 0
|
|
69
|
+
|
|
70
|
+
- name: Set up Node
|
|
71
|
+
uses: actions/setup-node@v4
|
|
72
|
+
with:
|
|
73
|
+
node-version: 22
|
|
74
|
+
|
|
75
|
+
- name: Create GitHub App token
|
|
76
|
+
id: app-token
|
|
77
|
+
uses: actions/create-github-app-token@v2
|
|
78
|
+
with:
|
|
79
|
+
app-id: ${{ secrets.GH_APP_ID }}
|
|
80
|
+
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
|
81
|
+
|
|
82
|
+
- name: Set token as env var
|
|
83
|
+
run: echo "GH_TOKEN=${{ steps.app-token.outputs.token }}" >> $GITHUB_ENV
|
|
84
|
+
|
|
85
|
+
- name: Debug environment
|
|
86
|
+
run: |
|
|
87
|
+
echo "Event name: ${{ github.event_name }}"
|
|
88
|
+
if [ "${{ github.event_name }}" = "repository_dispatch" ]; then
|
|
89
|
+
echo "Triggered from Jira via repository_dispatch"
|
|
90
|
+
echo "Jira issueKey: ${{ github.event.client_payload.jira.issueKey }}"
|
|
91
|
+
echo "Jira summary: ${{ github.event.client_payload.jira.summary }}"
|
|
92
|
+
echo "AI Provider: ${{ github.event.client_payload.ai_provider || 'codex' }}"
|
|
93
|
+
elif [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
|
94
|
+
echo "Manual dispatch"
|
|
95
|
+
echo "Jira key: ${{ github.event.inputs.jira_issue_key }}"
|
|
96
|
+
echo "Jira summary: ${{ github.event.inputs.jira_issue_summary }}"
|
|
97
|
+
echo "AI Provider: ${{ github.event.inputs.ai_provider }}"
|
|
98
|
+
else
|
|
99
|
+
echo "Triggered from GitHub Issue"
|
|
100
|
+
echo "Issue title: ${{ github.event.issue.title }}"
|
|
101
|
+
echo "Issue number: ${{ github.event.issue.number }}"
|
|
102
|
+
echo "Issue URL: ${{ github.event.issue.html_url }}"
|
|
103
|
+
echo "Labels: ${{ toJson(github.event.issue.labels.*.name) }}"
|
|
104
|
+
echo "AI Provider: codex (default for issue triggers)"
|
|
105
|
+
fi
|
|
106
|
+
echo "Repository: ${{ github.repository }}"
|
|
107
|
+
|
|
108
|
+
- name: Determine AI provider
|
|
109
|
+
run: |
|
|
110
|
+
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
|
111
|
+
AI_PROVIDER="${{ github.event.inputs.ai_provider }}"
|
|
112
|
+
elif [ "${{ github.event_name }}" = "repository_dispatch" ]; then
|
|
113
|
+
AI_PROVIDER="${{ github.event.client_payload.ai_provider }}"
|
|
114
|
+
else
|
|
115
|
+
AI_PROVIDER="codex" # Default for issue triggers
|
|
116
|
+
fi
|
|
117
|
+
|
|
118
|
+
# Default to codex if not specified
|
|
119
|
+
AI_PROVIDER="${AI_PROVIDER:-codex}"
|
|
120
|
+
|
|
121
|
+
echo "AI_PROVIDER=$AI_PROVIDER" >> $GITHUB_ENV
|
|
122
|
+
echo "✅ Using AI provider: $AI_PROVIDER"
|
|
123
|
+
|
|
124
|
+
- name: Validate secrets (will fallback to payload baseUrl if needed)
|
|
125
|
+
run: |
|
|
126
|
+
fail=0
|
|
127
|
+
|
|
128
|
+
# Check AI provider specific secrets
|
|
129
|
+
if [ "${{ env.AI_PROVIDER }}" = "claude" ]; then
|
|
130
|
+
if [ -z "${{ secrets.ANTHROPIC_API_KEY }}" ]; then
|
|
131
|
+
echo "❌ Error: ANTHROPIC_API_KEY not set (required for Claude)"
|
|
132
|
+
fail=1
|
|
133
|
+
else
|
|
134
|
+
echo "✅ ANTHROPIC_API_KEY present"
|
|
135
|
+
fi
|
|
136
|
+
else
|
|
137
|
+
if [ -z "${{ secrets.OPENAI_API_KEY }}" ]; then
|
|
138
|
+
echo "❌ Error: OPENAI_API_KEY not set (required for Codex)"
|
|
139
|
+
fail=1
|
|
140
|
+
else
|
|
141
|
+
echo "✅ OPENAI_API_KEY present"
|
|
142
|
+
fi
|
|
143
|
+
fi
|
|
144
|
+
|
|
145
|
+
if [ -z "${{ secrets.OPENAI_API_KEY }}" ]; then echo "❌ Error: OPENAI_API_KEY not set"; fail=1; fi
|
|
146
|
+
# Jira API secrets preferred; but API base can fallback to payload origin later if JIRA_BASE_URL is empty
|
|
147
|
+
if [ -z "${{ secrets.JIRA_EMAIL }}" ]; then echo "❌ Error: JIRA_EMAIL not set"; fail=1; fi
|
|
148
|
+
if [ -z "${{ secrets.JIRA_API_TOKEN }}" ]; then echo "❌ Error: JIRA_API_TOKEN not set"; fail=1; fi
|
|
149
|
+
if [ $fail -eq 1 ]; then exit 1; fi
|
|
150
|
+
echo "✅ Required secrets present (JIRA_BASE_URL is optional if payload provides baseUrl)"
|
|
151
|
+
|
|
152
|
+
- name: Install AI CLI
|
|
153
|
+
run: |
|
|
154
|
+
if [ "${{ env.AI_PROVIDER }}" = "claude" ]; then
|
|
155
|
+
echo "Installing Claude Code CLI..."
|
|
156
|
+
MAX_ATTEMPTS=15
|
|
157
|
+
ATTEMPT=1
|
|
158
|
+
|
|
159
|
+
while [ $ATTEMPT -le $MAX_ATTEMPTS ]; do
|
|
160
|
+
echo "Attempt $ATTEMPT of $MAX_ATTEMPTS"
|
|
161
|
+
|
|
162
|
+
if npm install -g @anthropic-ai/claude-code; then
|
|
163
|
+
echo "✅ Claude Code CLI installed successfully on attempt $ATTEMPT"
|
|
164
|
+
break
|
|
165
|
+
else
|
|
166
|
+
echo "❌ Claude Code CLI install failed on attempt $ATTEMPT"
|
|
167
|
+
if [ $ATTEMPT -eq $MAX_ATTEMPTS ]; then
|
|
168
|
+
echo "All install attempts failed"
|
|
169
|
+
exit 1
|
|
170
|
+
fi
|
|
171
|
+
sleep 15
|
|
172
|
+
ATTEMPT=$((ATTEMPT + 1))
|
|
173
|
+
fi
|
|
174
|
+
done
|
|
175
|
+
echo "CLI=claude" >> $GITHUB_ENV
|
|
176
|
+
echo "CLI_COMMAND=claude code" >> $GITHUB_ENV
|
|
177
|
+
else
|
|
178
|
+
echo "Installing Codex CLI..."
|
|
179
|
+
MAX_ATTEMPTS=15
|
|
180
|
+
ATTEMPT=1
|
|
181
|
+
|
|
182
|
+
while [ $ATTEMPT -le $MAX_ATTEMPTS ]; do
|
|
183
|
+
echo "Attempt $ATTEMPT of $MAX_ATTEMPTS"
|
|
184
|
+
|
|
185
|
+
if npm i -g @openai/codex; then
|
|
186
|
+
echo "✅ Codex CLI installed successfully on attempt $ATTEMPT"
|
|
187
|
+
break
|
|
188
|
+
else
|
|
189
|
+
echo "❌ Codex CLI install failed on attempt $ATTEMPT"
|
|
190
|
+
if [ $ATTEMPT -eq $MAX_ATTEMPTS ]; then
|
|
191
|
+
echo "All install attempts failed"
|
|
192
|
+
exit 1
|
|
193
|
+
fi
|
|
194
|
+
sleep 15
|
|
195
|
+
ATTEMPT=$((ATTEMPT + 1))
|
|
196
|
+
fi
|
|
197
|
+
done
|
|
198
|
+
echo "CLI=codex" >> $GITHUB_ENV
|
|
199
|
+
echo "CLI_COMMAND=codex" >> $GITHUB_ENV
|
|
200
|
+
fi
|
|
201
|
+
|
|
202
|
+
- name: Verify CLI installation
|
|
203
|
+
run: |
|
|
204
|
+
if [ "${{ env.AI_PROVIDER }}" = "claude" ]; then
|
|
205
|
+
if command -v claude >/dev/null 2>&1; then
|
|
206
|
+
echo "✅ Claude CLI available"
|
|
207
|
+
claude --version || echo "ℹ️ Could not get claude version"
|
|
208
|
+
else
|
|
209
|
+
echo "❌ Claude CLI not found after installation"
|
|
210
|
+
exit 1
|
|
211
|
+
fi
|
|
212
|
+
else
|
|
213
|
+
if command -v codex >/dev/null 2>&1; then
|
|
214
|
+
echo "✅ Codex CLI available"
|
|
215
|
+
codex --version || echo "ℹ️ Could not get codex version"
|
|
216
|
+
else
|
|
217
|
+
echo "❌ Codex CLI not found after installation"
|
|
218
|
+
exit 1
|
|
219
|
+
fi
|
|
220
|
+
fi
|
|
221
|
+
|
|
222
|
+
- id: vars
|
|
223
|
+
shell: bash
|
|
224
|
+
run: |
|
|
225
|
+
set -euo pipefail
|
|
226
|
+
EVENT="${{ github.event_name }}"
|
|
227
|
+
|
|
228
|
+
JIRA_ISSUE_URL=""
|
|
229
|
+
if [ "$EVENT" = "repository_dispatch" ]; then
|
|
230
|
+
ISSUE_KEY="${{ github.event.client_payload.jira.issueKey }}"
|
|
231
|
+
TITLE="${{ github.event.client_payload.jira.summary }}"
|
|
232
|
+
elif [ "$EVENT" = "workflow_dispatch" ]; then
|
|
233
|
+
ISSUE_KEY="${{ github.event.inputs.jira_issue_key }}"
|
|
234
|
+
TITLE="${{ github.event.inputs.jira_issue_summary }}"
|
|
235
|
+
fi
|
|
236
|
+
|
|
237
|
+
TITLE="${TITLE:-Automated task}"
|
|
238
|
+
RAW_DESC="${RAW_DESC:-No description provided.}"
|
|
239
|
+
|
|
240
|
+
DESC_CLEANED="$(echo "$RAW_DESC" | tr '\n' ' ' | sed 's/"/'\''/g')"
|
|
241
|
+
SAFE_TITLE="$(echo "$TITLE" | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9-' '-' | sed 's/^-*\|-*$//g')"
|
|
242
|
+
BRANCH="${ISSUE_KEY}-${SAFE_TITLE}"
|
|
243
|
+
|
|
244
|
+
echo "ISSUE_KEY=$ISSUE_KEY" >> $GITHUB_ENV
|
|
245
|
+
echo "TITLE=$TITLE" >> $GITHUB_ENV
|
|
246
|
+
echo "RAW_DESC=$RAW_DESC" >> $GITHUB_ENV
|
|
247
|
+
echo "DESC=$DESC_CLEANED" >> $GITHUB_ENV
|
|
248
|
+
echo "BRANCH=$BRANCH" >> $GITHUB_ENV
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
# Derive Jira API base (origin) from secret OR payload URL
|
|
252
|
+
# Prefer secret JIRA_BASE_URL; else derive "https://your-domain.atlassian.net" from JIRA_ISSUE_URL
|
|
253
|
+
SECRET_BASE="${{ secrets.JIRA_BASE_URL }}"
|
|
254
|
+
if [ -n "$SECRET_BASE" ]; then
|
|
255
|
+
API_BASE="$SECRET_BASE"
|
|
256
|
+
else
|
|
257
|
+
if [ -n "$JIRA_ISSUE_URL" ]; then
|
|
258
|
+
API_BASE="$(echo "$JIRA_ISSUE_URL" | awk -F/ '{print $1"//"$3}')"
|
|
259
|
+
else
|
|
260
|
+
API_BASE=""
|
|
261
|
+
fi
|
|
262
|
+
fi
|
|
263
|
+
|
|
264
|
+
# Construct the full Jira ticket URL
|
|
265
|
+
JIRA_ISSUE_URL=""
|
|
266
|
+
if [ -n "$API_BASE" ] && [ -n "$ISSUE_KEY" ]; then
|
|
267
|
+
# Only construct Jira URL for actual Jira issues (not GitHub issues)
|
|
268
|
+
JIRA_ISSUE_URL="${API_BASE}/browse/${ISSUE_KEY}"
|
|
269
|
+
echo "✅ Constructed Jira URL: $JIRA_ISSUE_URL"
|
|
270
|
+
else
|
|
271
|
+
echo "ℹ️ No Jira URL constructed (API_BASE='$API_BASE', ISSUE_KEY='$ISSUE_KEY')"
|
|
272
|
+
fi
|
|
273
|
+
echo "JIRA_ISSUE_URL=$JIRA_ISSUE_URL" >> $GITHUB_ENV
|
|
274
|
+
echo "API_BASE=$API_BASE" >> $GITHUB_ENV
|
|
275
|
+
|
|
276
|
+
- name: Jira – Transition to In Progress (if looks like a Jira key)
|
|
277
|
+
if: (startsWith(env.ISSUE_KEY, 'DR-') == true || startsWith(env.ISSUE_KEY, 'CA-') == true) && env.API_BASE != ''
|
|
278
|
+
env:
|
|
279
|
+
ISSUE_KEY: ${{ env.ISSUE_KEY }}
|
|
280
|
+
API_BASE: ${{ env.API_BASE }}
|
|
281
|
+
JIRA_EMAIL: ${{ secrets.JIRA_EMAIL }}
|
|
282
|
+
JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }}
|
|
283
|
+
run: |
|
|
284
|
+
set -euo pipefail
|
|
285
|
+
|
|
286
|
+
echo "🔧 Starting Jira transition to 'In Progress'"
|
|
287
|
+
echo " ISSUE_KEY: $ISSUE_KEY"
|
|
288
|
+
echo " API_BASE: $API_BASE"
|
|
289
|
+
echo " JIRA_EMAIL: ${JIRA_EMAIL:0:5}***@${JIRA_EMAIL#*@}"
|
|
290
|
+
echo " Token length: ${#JIRA_API_TOKEN} chars"
|
|
291
|
+
|
|
292
|
+
# Install jq if needed
|
|
293
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
294
|
+
echo "📦 Installing jq..."
|
|
295
|
+
sudo apt-get update && sudo apt-get install -y jq >/dev/null
|
|
296
|
+
fi
|
|
297
|
+
|
|
298
|
+
# Fetch available transitions with error handling
|
|
299
|
+
echo "📡 Fetching available transitions for $ISSUE_KEY..."
|
|
300
|
+
TRANSITIONS_URL="$API_BASE/rest/api/3/issue/$ISSUE_KEY/transitions"
|
|
301
|
+
echo " Request URL: $TRANSITIONS_URL"
|
|
302
|
+
|
|
303
|
+
HTTP_STATUS=$(curl -sS -X GET \
|
|
304
|
+
--url "$TRANSITIONS_URL" \
|
|
305
|
+
--user "$JIRA_EMAIL:$JIRA_API_TOKEN" \
|
|
306
|
+
--header 'Accept: application/json' \
|
|
307
|
+
-w '%{http_code}' \
|
|
308
|
+
-o transitions.json)
|
|
309
|
+
|
|
310
|
+
echo " HTTP Status: $HTTP_STATUS"
|
|
311
|
+
|
|
312
|
+
if [ "$HTTP_STATUS" != "200" ]; then
|
|
313
|
+
echo "❌ Failed to fetch transitions (HTTP $HTTP_STATUS)"
|
|
314
|
+
echo "Response body:"
|
|
315
|
+
cat transitions.json
|
|
316
|
+
exit 1
|
|
317
|
+
fi
|
|
318
|
+
|
|
319
|
+
# Show raw response for debugging
|
|
320
|
+
echo "📋 Raw transitions response (first 500 chars):"
|
|
321
|
+
head -c 500 transitions.json; echo
|
|
322
|
+
|
|
323
|
+
# Validate JSON structure
|
|
324
|
+
if ! jq -e '.transitions' transitions.json >/dev/null 2>&1; then
|
|
325
|
+
echo "❌ Invalid JSON structure - missing .transitions array"
|
|
326
|
+
echo "Full response:"
|
|
327
|
+
cat transitions.json
|
|
328
|
+
exit 1
|
|
329
|
+
fi
|
|
330
|
+
|
|
331
|
+
# Show all available transitions
|
|
332
|
+
echo "📋 Available transitions:"
|
|
333
|
+
jq -r '.transitions[] | " - \(.name) (id: \(.id)) -> \(.to.name)"' transitions.json || echo " Failed to parse transitions"
|
|
334
|
+
|
|
335
|
+
# Try to find "In Progress" transition with various name patterns
|
|
336
|
+
TRANSITIONS_JSON="$(cat transitions.json)"
|
|
337
|
+
|
|
338
|
+
echo "🔍 Looking for 'In Progress' transition..."
|
|
339
|
+
INPROG_ID="$(echo "$TRANSITIONS_JSON" | jq -r '.transitions[] | select(.name=="In Progress") | .id' 2>/dev/null || echo "")"
|
|
340
|
+
|
|
341
|
+
if [ -z "$INPROG_ID" ] || [ "$INPROG_ID" = "null" ]; then
|
|
342
|
+
echo " Exact 'In Progress' not found, trying alternatives..."
|
|
343
|
+
|
|
344
|
+
# Try alternative names
|
|
345
|
+
INPROG_ID="$(echo "$TRANSITIONS_JSON" | jq -r '.transitions[] | select(.name=="Start Progress") | .id' 2>/dev/null || echo "")"
|
|
346
|
+
[ -z "$INPROG_ID" ] && INPROG_ID="$(echo "$TRANSITIONS_JSON" | jq -r '.transitions[] | select(.name=="Start Work") | .id | head -n 1' 2>/dev/null || echo "")"
|
|
347
|
+
[ -z "$INPROG_ID" ] && INPROG_ID="$(echo "$TRANSITIONS_JSON" | jq -r '.transitions[] | select(.to.name=="In Progress") | .id | head -n 1' 2>/dev/null || echo "")"
|
|
348
|
+
|
|
349
|
+
if [ -z "$INPROG_ID" ] || [ "$INPROG_ID" = "null" ]; then
|
|
350
|
+
echo "⚠️ Could not find any 'In Progress' transition with these patterns:"
|
|
351
|
+
echo " - 'In Progress'"
|
|
352
|
+
echo " - 'Start Progress'"
|
|
353
|
+
echo " - 'Start Work'"
|
|
354
|
+
echo " - Any transition leading to 'In Progress' status"
|
|
355
|
+
echo " Available transitions listed above. Skipping transition."
|
|
356
|
+
exit 0
|
|
357
|
+
fi
|
|
358
|
+
fi
|
|
359
|
+
|
|
360
|
+
echo "✅ Found 'In Progress' transition with ID: $INPROG_ID"
|
|
361
|
+
|
|
362
|
+
# Apply the transition
|
|
363
|
+
echo "⚡ Applying transition id=$INPROG_ID to $ISSUE_KEY"
|
|
364
|
+
TRANSITION_URL="$API_BASE/rest/api/3/issue/$ISSUE_KEY/transitions"
|
|
365
|
+
TRANSITION_DATA="{\"transition\":{\"id\":\"$INPROG_ID\"}}"
|
|
366
|
+
|
|
367
|
+
echo " POST URL: $TRANSITION_URL"
|
|
368
|
+
echo " POST Data: $TRANSITION_DATA"
|
|
369
|
+
|
|
370
|
+
TRANSITION_STATUS=$(curl -sS -X POST \
|
|
371
|
+
--url "$TRANSITION_URL" \
|
|
372
|
+
--user "$JIRA_EMAIL:$JIRA_API_TOKEN" \
|
|
373
|
+
--header 'Content-Type: application/json' \
|
|
374
|
+
--data "$TRANSITION_DATA" \
|
|
375
|
+
-w '%{http_code}' \
|
|
376
|
+
-o transition_response.json)
|
|
377
|
+
|
|
378
|
+
echo " Transition HTTP Status: $TRANSITION_STATUS"
|
|
379
|
+
|
|
380
|
+
if [ "$TRANSITION_STATUS" = "204" ]; then
|
|
381
|
+
echo "✅ Successfully transitioned $ISSUE_KEY to 'In Progress'"
|
|
382
|
+
else
|
|
383
|
+
echo "❌ Transition failed (HTTP $TRANSITION_STATUS)"
|
|
384
|
+
echo "Response body:"
|
|
385
|
+
cat transition_response.json 2>/dev/null || echo "No response body"
|
|
386
|
+
exit 1
|
|
387
|
+
fi
|
|
388
|
+
|
|
389
|
+
- name: Fetch Jira issue (debug & export)
|
|
390
|
+
env:
|
|
391
|
+
ISSUE_KEY: ${{ env.ISSUE_KEY }}
|
|
392
|
+
JIRA_EMAIL: ${{ secrets.JIRA_EMAIL }}
|
|
393
|
+
JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }}
|
|
394
|
+
API_BASE: ${{ env.API_BASE }}
|
|
395
|
+
run: |
|
|
396
|
+
set -euo pipefail
|
|
397
|
+
|
|
398
|
+
echo "ISSUE_KEY=${ISSUE_KEY}"
|
|
399
|
+
echo "API_BASE host: $(echo "$API_BASE" | sed -E 's#https?://([^/]+)/?.*#\1#')"
|
|
400
|
+
if [ -z "${ISSUE_KEY:-}" ]; then
|
|
401
|
+
echo "::error::ISSUE_KEY is empty"; exit 1
|
|
402
|
+
fi
|
|
403
|
+
|
|
404
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
405
|
+
echo "jq not found; installing"
|
|
406
|
+
sudo apt-get update -y && sudo apt-get install -y jq
|
|
407
|
+
fi
|
|
408
|
+
|
|
409
|
+
HTTP_STATUS=$(curl -sS -X GET \
|
|
410
|
+
--url "$API_BASE/rest/api/3/issue/$ISSUE_KEY?fields=summary,description&expand=renderedFields" \
|
|
411
|
+
--user "$JIRA_EMAIL:$JIRA_API_TOKEN" \
|
|
412
|
+
--header 'Accept: application/json' \
|
|
413
|
+
-D headers.txt -o issue.json -w '%{http_code}')
|
|
414
|
+
|
|
415
|
+
echo "HTTP_STATUS=$HTTP_STATUS"
|
|
416
|
+
echo "Raw JSON response (first 500 chars):"
|
|
417
|
+
head -c 500 issue.json; echo
|
|
418
|
+
|
|
419
|
+
if [ "$HTTP_STATUS" != "200" ]; then
|
|
420
|
+
echo "::error::Jira GET /issue returned HTTP $HTTP_STATUS"
|
|
421
|
+
echo "Error response:"
|
|
422
|
+
cat issue.json
|
|
423
|
+
exit 1
|
|
424
|
+
fi
|
|
425
|
+
|
|
426
|
+
# Check if the response has the expected structure
|
|
427
|
+
echo "Checking JSON structure..."
|
|
428
|
+
if ! jq -e '.fields' issue.json >/dev/null 2>&1; then
|
|
429
|
+
echo "::error::Invalid JSON structure - missing .fields"
|
|
430
|
+
cat issue.json
|
|
431
|
+
exit 1
|
|
432
|
+
fi
|
|
433
|
+
|
|
434
|
+
# Extract summary with null safety
|
|
435
|
+
SUMMARY="$(jq -r 'if .fields.summary then .fields.summary else "No summary available" end' issue.json)"
|
|
436
|
+
echo "Extracted SUMMARY: $SUMMARY"
|
|
437
|
+
|
|
438
|
+
# Extract description with comprehensive null handling
|
|
439
|
+
RENDERED="$(jq -r 'if .renderedFields and .renderedFields.description then .renderedFields.description else "" end' issue.json)"
|
|
440
|
+
if [ -n "$RENDERED" ] && [ "$RENDERED" != "null" ]; then
|
|
441
|
+
echo "Using renderedFields.description (HTML)."
|
|
442
|
+
DESC="$(printf '%s' "$RENDERED" \
|
|
443
|
+
| sed -e 's/<[^>]*>//g' \
|
|
444
|
+
-e 's/ / /g' -e 's/&/\&/g' -e 's/</</g' -e 's/>/>/g')"
|
|
445
|
+
else
|
|
446
|
+
echo "renderedFields.description missing or null; trying to extract from ADF format."
|
|
447
|
+
# More robust ADF extraction with null safety
|
|
448
|
+
DESC="$(jq -r '
|
|
449
|
+
if .fields.description and .fields.description.content then
|
|
450
|
+
[.fields.description.content[] |
|
|
451
|
+
if .content then
|
|
452
|
+
[.content[] | if .text then .text else empty end]
|
|
453
|
+
else empty end] |
|
|
454
|
+
flatten | join("\n")
|
|
455
|
+
else
|
|
456
|
+
"No description available"
|
|
457
|
+
end
|
|
458
|
+
' issue.json 2>/dev/null || echo "No description available")"
|
|
459
|
+
fi
|
|
460
|
+
|
|
461
|
+
# Ensure DESC is not null or empty
|
|
462
|
+
if [ -z "$DESC" ] || [ "$DESC" = "null" ]; then
|
|
463
|
+
DESC="No description provided"
|
|
464
|
+
fi
|
|
465
|
+
|
|
466
|
+
echo "DESC length: $(printf '%s' "$DESC" | wc -c)"
|
|
467
|
+
echo "DESC preview (first 200 chars):"
|
|
468
|
+
printf '%s' "$DESC" | head -c 200; echo
|
|
469
|
+
|
|
470
|
+
# Export to env (multi-line safe)
|
|
471
|
+
echo "ISSUE_KEY=$ISSUE_KEY" >> "$GITHUB_ENV"
|
|
472
|
+
{ echo "SUMMARY<<EOF"; printf '%s\n' "$SUMMARY"; echo "EOF"; } >> "$GITHUB_ENV"
|
|
473
|
+
{ echo "DESC<<EOF"; printf '%s\n' "$DESC"; echo "EOF"; } >> "$GITHUB_ENV"
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
- name: Resolve base & branch
|
|
477
|
+
run: |
|
|
478
|
+
set -euo pipefail
|
|
479
|
+
BASE="${{ github.event.repository.default_branch }}" # e.g. main
|
|
480
|
+
BR="${{ env.BRANCH }}" # optional preset
|
|
481
|
+
|
|
482
|
+
# If not provided or equals BASE, synthesize a unique branch
|
|
483
|
+
if [ -z "${BR:-}" ] || [ "$BR" = "$BASE" ]; then
|
|
484
|
+
BR="codex/${ISSUE_KEY:-issue}-${GITHUB_RUN_ID}"
|
|
485
|
+
fi
|
|
486
|
+
|
|
487
|
+
echo "BASE=$BASE" >> "$GITHUB_ENV"
|
|
488
|
+
echo "BRANCH=$BR" >> "$GITHUB_ENV"
|
|
489
|
+
echo "Resolved BASE=$BASE, BRANCH=$BR"
|
|
490
|
+
|
|
491
|
+
# (Re)checkout the base branch to ensure correct diff target
|
|
492
|
+
- uses: actions/checkout@v4
|
|
493
|
+
with:
|
|
494
|
+
ref: ${{ env.BASE }}
|
|
495
|
+
fetch-depth: 0
|
|
496
|
+
|
|
497
|
+
- name: Create or update branch
|
|
498
|
+
env:
|
|
499
|
+
BRANCH: ${{ env.BRANCH }}
|
|
500
|
+
run: |
|
|
501
|
+
# Fetch all remote branches
|
|
502
|
+
git fetch origin
|
|
503
|
+
|
|
504
|
+
# Check if remote branch exists
|
|
505
|
+
if git ls-remote --heads origin "${BRANCH}" | grep -q "${BRANCH}"; then
|
|
506
|
+
echo "Branch ${BRANCH} exists on remote, checking it out and pulling"
|
|
507
|
+
git checkout -B "${BRANCH}" origin/"${BRANCH}"
|
|
508
|
+
git pull origin "${BRANCH}"
|
|
509
|
+
else
|
|
510
|
+
echo "Branch ${BRANCH} doesn't exist on remote, creating new branch"
|
|
511
|
+
git checkout -B "${BRANCH}"
|
|
512
|
+
fi
|
|
513
|
+
|
|
514
|
+
- name: Implement with AI
|
|
515
|
+
env:
|
|
516
|
+
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
|
517
|
+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
518
|
+
run: |
|
|
519
|
+
set -euo pipefail
|
|
520
|
+
|
|
521
|
+
# Build PROMPT
|
|
522
|
+
PROMPT=$(cat <<EOF
|
|
523
|
+
${ISSUE_KEY}: ${SUMMARY}
|
|
524
|
+
|
|
525
|
+
Details:
|
|
526
|
+
${DESC}
|
|
527
|
+
EOF
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
echo "PROMPT length: $(printf '%s' "$PROMPT" | wc -c)"
|
|
531
|
+
echo "---- PROMPT preview (first 20 lines) ----"
|
|
532
|
+
printf '%s\n' "$PROMPT" | sed -n '1,20p'
|
|
533
|
+
echo "----------------------------------------"
|
|
534
|
+
|
|
535
|
+
AI_OUTPUT_FILE="ai_output_${ISSUE_KEY}_${GITHUB_RUN_ID}.txt"
|
|
536
|
+
|
|
537
|
+
if [ "${{ env.AI_PROVIDER }}" = "claude" ]; then
|
|
538
|
+
echo "Running Claude Code..."
|
|
539
|
+
if claude --dangerously-skip-permissions -p "$PROMPT" > "$AI_OUTPUT_FILE" 2>&1; then
|
|
540
|
+
echo "✅ Claude Code completed successfully"
|
|
541
|
+
AI_SUCCESS=true
|
|
542
|
+
else
|
|
543
|
+
echo "❌ Claude Code failed"
|
|
544
|
+
AI_SUCCESS=false
|
|
545
|
+
fi
|
|
546
|
+
else
|
|
547
|
+
echo "Running Codex..."
|
|
548
|
+
if codex exec --full-auto "$PROMPT" > "$AI_OUTPUT_FILE" 2>&1; then
|
|
549
|
+
echo "✅ Codex completed successfully"
|
|
550
|
+
AI_SUCCESS=true
|
|
551
|
+
else
|
|
552
|
+
echo "❌ Codex failed"
|
|
553
|
+
AI_SUCCESS=false
|
|
554
|
+
fi
|
|
555
|
+
fi
|
|
556
|
+
|
|
557
|
+
AI_OUTPUT=$(cat "$AI_OUTPUT_FILE")
|
|
558
|
+
echo "---- AI OUTPUT (first 50 lines) ----"
|
|
559
|
+
head -50 "$AI_OUTPUT_FILE"
|
|
560
|
+
echo "---- END OUTPUT PREVIEW ----"
|
|
561
|
+
|
|
562
|
+
echo "AI_SUCCESS=$AI_SUCCESS" >> $GITHUB_ENV
|
|
563
|
+
|
|
564
|
+
AI_OUTPUT_PREVIEW=$(head -50 "$AI_OUTPUT_FILE")
|
|
565
|
+
ESCAPED_PREVIEW=$(echo "$AI_OUTPUT_PREVIEW" | jq -Rs .)
|
|
566
|
+
echo "AI_OUTPUT_PREVIEW=$ESCAPED_PREVIEW" >> $GITHUB_ENV
|
|
567
|
+
echo "AI_OUTPUT_FILE=$AI_OUTPUT_FILE" >> $GITHUB_ENV
|
|
568
|
+
|
|
569
|
+
if [ "$AI_SUCCESS" = "false" ]; then
|
|
570
|
+
exit 1
|
|
571
|
+
fi
|
|
572
|
+
|
|
573
|
+
- name: Upload AI Output as Artifact
|
|
574
|
+
if: always()
|
|
575
|
+
uses: actions/upload-artifact@v4
|
|
576
|
+
with:
|
|
577
|
+
name: ${{ env.AI_PROVIDER }}-output-${{ env.ISSUE_KEY }}-${{ github.run_id }}
|
|
578
|
+
path: ${{ env.AI_OUTPUT_FILE }}
|
|
579
|
+
retention-days: 30
|
|
580
|
+
|
|
581
|
+
- name: Move AI Output to Temp Directory
|
|
582
|
+
if: always()
|
|
583
|
+
run: |
|
|
584
|
+
# Create temp directory if it doesn't exist
|
|
585
|
+
mkdir -p /tmp/ai-artifacts
|
|
586
|
+
|
|
587
|
+
# Move the file to temp directory
|
|
588
|
+
if [ -f "${{ env.AI_OUTPUT_FILE }}" ]; then
|
|
589
|
+
TEMP_FILE="/tmp/ai-artifacts/$(basename "${{ env.AI_OUTPUT_FILE }}")"
|
|
590
|
+
mv "${{ env.AI_OUTPUT_FILE }}" "$TEMP_FILE"
|
|
591
|
+
echo "📁 Moved file to: $TEMP_FILE"
|
|
592
|
+
|
|
593
|
+
# Update environment variable with new path
|
|
594
|
+
echo "AI_OUTPUT_FILE=$TEMP_FILE" >> $GITHUB_ENV
|
|
595
|
+
echo "✅ Updated AI_OUTPUT_FILE environment variable"
|
|
596
|
+
else
|
|
597
|
+
echo "❌ File not found: ${{ env.AI_OUTPUT_FILE }}"
|
|
598
|
+
fi
|
|
599
|
+
|
|
600
|
+
- name: Configure Git & Commit changes (if any)
|
|
601
|
+
env:
|
|
602
|
+
ISSUE_KEY: ${{ env.ISSUE_KEY }}
|
|
603
|
+
TITLE: ${{ env.TITLE }}
|
|
604
|
+
BRANCH: ${{ env.BRANCH }}
|
|
605
|
+
run: |
|
|
606
|
+
git config user.name "GitHub Actions Bot"
|
|
607
|
+
git config user.email "github-actions@github.com"
|
|
608
|
+
|
|
609
|
+
if git diff --quiet; then
|
|
610
|
+
echo "ℹ️ No changes to commit."
|
|
611
|
+
# Still need to push the empty branch for PR creation
|
|
612
|
+
git push --set-upstream origin "${BRANCH}"
|
|
613
|
+
else
|
|
614
|
+
git add -A
|
|
615
|
+
git commit -m "${ISSUE_KEY}: ${TITLE} (via ${{ env.AI_PROVIDER }})"
|
|
616
|
+
git push --set-upstream origin "${BRANCH}"
|
|
617
|
+
echo "✅ Committed and pushed changes"
|
|
618
|
+
fi
|
|
619
|
+
|
|
620
|
+
- name: Create label if it doesn't exist
|
|
621
|
+
run: |
|
|
622
|
+
# Create the ai-coder label if it doesn't exist
|
|
623
|
+
gh label create "ai-coder" --description "AI-generated code" --color "1f883d" || echo "Label already exists"
|
|
624
|
+
|
|
625
|
+
- name: Create or Update PR with gh CLI
|
|
626
|
+
run: |
|
|
627
|
+
# Check if PR already exists for this branch
|
|
628
|
+
EXISTING_PR=$(gh pr list --head "${{ env.BRANCH }}" --base "${{ env.BASE }}" --json number --jq '.[0].number')
|
|
629
|
+
|
|
630
|
+
if [ "$EXISTING_PR" == "null" ] || [ -z "$EXISTING_PR" ]; then
|
|
631
|
+
echo "Creating new PR"
|
|
632
|
+
gh pr create \
|
|
633
|
+
--head "${{ env.BRANCH }}" \
|
|
634
|
+
--title "${{ env.ISSUE_KEY }}: ${{ env.TITLE }}" \
|
|
635
|
+
--body "🤖 AI-generated implementation for **${{ env.ISSUE_KEY }}** using **${{ env.AI_PROVIDER }}**.
|
|
636
|
+
|
|
637
|
+
- Branch: \`${{ env.BRANCH }}\`
|
|
638
|
+
- Trigger: \`${{ github.event_name }}\`
|
|
639
|
+
- AI Provider: \`${{ env.AI_PROVIDER }}\`
|
|
640
|
+
- Jira: ${{ env.JIRA_ISSUE_URL }}
|
|
641
|
+
- AI Output Artifact: [Download from Actions](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})" \
|
|
642
|
+
--label "ai-coder" \
|
|
643
|
+
--base "${{ env.BASE }}"
|
|
644
|
+
else
|
|
645
|
+
echo "Updating existing PR #$EXISTING_PR"
|
|
646
|
+
gh pr edit $EXISTING_PR \
|
|
647
|
+
--title "${{ env.ISSUE_KEY }}: ${{ env.TITLE }}" \
|
|
648
|
+
--body "🤖 AI-generated implementation for **${{ env.ISSUE_KEY }}** using **${{ env.AI_PROVIDER }}**.
|
|
649
|
+
|
|
650
|
+
- Branch: \`${{ env.BRANCH }}\`
|
|
651
|
+
- Trigger: \`${{ github.event_name }}\`
|
|
652
|
+
- AI Provider: \`${{ env.AI_PROVIDER }}\`
|
|
653
|
+
- Jira: ${{ env.JIRA_ISSUE_URL }}
|
|
654
|
+
- AI Output Artifact: [Download from Actions](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})" \
|
|
655
|
+
--add-label "ai-coder"
|
|
656
|
+
echo "Updated PR: ${{ github.server_url }}/${{ github.repository }}/pull/$EXISTING_PR"
|
|
657
|
+
fi
|
|
658
|
+
|
|
659
|
+
- name: Upload AI Output to Jira as Attachment
|
|
660
|
+
if: success() && (startsWith(env.ISSUE_KEY, 'DR-') == true || startsWith(env.ISSUE_KEY, 'CA-') == true) && env.API_BASE != ''
|
|
661
|
+
env:
|
|
662
|
+
JIRA_BASE_URL: ${{ env.API_BASE }}
|
|
663
|
+
JIRA_EMAIL: ${{ secrets.JIRA_EMAIL }}
|
|
664
|
+
JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }}
|
|
665
|
+
ISSUE_KEY: ${{ env.ISSUE_KEY }}
|
|
666
|
+
run: |
|
|
667
|
+
echo "Uploading ${{ env.AI_PROVIDER }} output to Jira as attachment..."
|
|
668
|
+
|
|
669
|
+
ATTACHMENT_FILE="${{ env.AI_OUTPUT_FILE }}"
|
|
670
|
+
|
|
671
|
+
if [ -f "$ATTACHMENT_FILE" ]; then
|
|
672
|
+
echo "Uploading $ATTACHMENT_FILE to Jira issue $ISSUE_KEY"
|
|
673
|
+
|
|
674
|
+
curl -X POST \
|
|
675
|
+
-H "X-Atlassian-Token: no-check" \
|
|
676
|
+
-u "$JIRA_EMAIL:$JIRA_API_TOKEN" \
|
|
677
|
+
-F "file=@$ATTACHMENT_FILE" \
|
|
678
|
+
"$JIRA_BASE_URL/rest/api/3/issue/$ISSUE_KEY/attachments"
|
|
679
|
+
|
|
680
|
+
echo "✅ Attachment uploaded successfully"
|
|
681
|
+
else
|
|
682
|
+
echo "❌ Codex output file not found: $ATTACHMENT_FILE"
|
|
683
|
+
fi
|
|
684
|
+
|
|
685
|
+
- name: Update Jira on Success
|
|
686
|
+
if: success() && (startsWith(env.ISSUE_KEY, 'DR-') == true || startsWith(env.ISSUE_KEY, 'CA-') == true) && env.API_BASE != ''
|
|
687
|
+
env:
|
|
688
|
+
JIRA_BASE_URL: ${{ env.API_BASE }}
|
|
689
|
+
JIRA_EMAIL: ${{ secrets.JIRA_EMAIL }}
|
|
690
|
+
JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }}
|
|
691
|
+
ISSUE_KEY: ${{ env.ISSUE_KEY }}
|
|
692
|
+
AI_OUTPUT_PREVIEW: ${{ env.AI_OUTPUT_PREVIEW }}
|
|
693
|
+
run: |
|
|
694
|
+
echo "Updating Jira ticket to Done status and adding comment..."
|
|
695
|
+
|
|
696
|
+
PR_URL="https://github.com/${{ github.repository }}/pull/$(gh pr list --head ${{ env.BRANCH }} --json number --jq '.[0].number')"
|
|
697
|
+
|
|
698
|
+
# Create the comment using jq for proper JSON escaping
|
|
699
|
+
COMMENT_JSON=$(jq -n \
|
|
700
|
+
--arg pr_url "$PR_URL" \
|
|
701
|
+
--arg ai_provider "${{ env.AI_PROVIDER }}" \
|
|
702
|
+
--arg preview "$AI_OUTPUT_PREVIEW" \
|
|
703
|
+
'{
|
|
704
|
+
"body": {
|
|
705
|
+
"type": "doc",
|
|
706
|
+
"version": 1,
|
|
707
|
+
"content": [
|
|
708
|
+
{
|
|
709
|
+
"type": "paragraph",
|
|
710
|
+
"content": [
|
|
711
|
+
{
|
|
712
|
+
"type": "text",
|
|
713
|
+
"text": ("🤖 AI Coding Agent Results - Success! (using " + $ai_provider + ")"),
|
|
714
|
+
"marks": [{"type": "strong"}]
|
|
715
|
+
}
|
|
716
|
+
]
|
|
717
|
+
},
|
|
718
|
+
{
|
|
719
|
+
"type": "paragraph",
|
|
720
|
+
"content": [
|
|
721
|
+
{
|
|
722
|
+
"type": "text",
|
|
723
|
+
"text": "Pull Request: "
|
|
724
|
+
},
|
|
725
|
+
{
|
|
726
|
+
"type": "text",
|
|
727
|
+
"text": $pr_url,
|
|
728
|
+
"marks": [{"type": "link", "attrs": {"href": $pr_url}}]
|
|
729
|
+
}
|
|
730
|
+
]
|
|
731
|
+
},
|
|
732
|
+
{
|
|
733
|
+
"type": "paragraph",
|
|
734
|
+
"content": [
|
|
735
|
+
{
|
|
736
|
+
"type": "text",
|
|
737
|
+
"text": "Full output attached to this issue. Preview (first 50 lines):"
|
|
738
|
+
}
|
|
739
|
+
]
|
|
740
|
+
},
|
|
741
|
+
{
|
|
742
|
+
"type": "codeBlock",
|
|
743
|
+
"attrs": {"language": "text"},
|
|
744
|
+
"content": [
|
|
745
|
+
{
|
|
746
|
+
"type": "text",
|
|
747
|
+
"text": $preview
|
|
748
|
+
}
|
|
749
|
+
]
|
|
750
|
+
}
|
|
751
|
+
]
|
|
752
|
+
}
|
|
753
|
+
}')
|
|
754
|
+
|
|
755
|
+
curl -X POST \
|
|
756
|
+
-H "Content-Type: application/json" \
|
|
757
|
+
-u "$JIRA_EMAIL:$JIRA_API_TOKEN" \
|
|
758
|
+
"$JIRA_BASE_URL/rest/api/3/issue/$ISSUE_KEY/comment" \
|
|
759
|
+
-d "$COMMENT_JSON"
|
|
760
|
+
|
|
761
|
+
# Transition to Done
|
|
762
|
+
TRANSITIONS=$(curl -s -u "$JIRA_EMAIL:$JIRA_API_TOKEN" \
|
|
763
|
+
"$JIRA_BASE_URL/rest/api/3/issue/$ISSUE_KEY/transitions")
|
|
764
|
+
|
|
765
|
+
DONE_TRANSITION_ID=$(echo "$TRANSITIONS" | jq -r '.transitions[] | select(.name == "Done" or .to.name == "Done") | .id' | head -n 1)
|
|
766
|
+
|
|
767
|
+
if [ -n "$DONE_TRANSITION_ID" ] && [ "$DONE_TRANSITION_ID" != "null" ]; then
|
|
768
|
+
echo "Transitioning to Done (ID: $DONE_TRANSITION_ID)"
|
|
769
|
+
curl -X POST \
|
|
770
|
+
-H "Content-Type: application/json" \
|
|
771
|
+
-u "$JIRA_EMAIL:$JIRA_API_TOKEN" \
|
|
772
|
+
"$JIRA_BASE_URL/rest/api/3/issue/$ISSUE_KEY/transitions" \
|
|
773
|
+
-d "{
|
|
774
|
+
\"transition\": {
|
|
775
|
+
\"id\": \"$DONE_TRANSITION_ID\"
|
|
776
|
+
}
|
|
777
|
+
}"
|
|
778
|
+
else
|
|
779
|
+
echo "❌ Could not find Done transition"
|
|
780
|
+
fi
|
|
781
|
+
|
|
782
|
+
- name: Update Jira on Failure
|
|
783
|
+
if: failure() && (startsWith(env.ISSUE_KEY, 'DR-') == true || startsWith(env.ISSUE_KEY, 'CA-') == true) && env.API_BASE != ''
|
|
784
|
+
env:
|
|
785
|
+
JIRA_BASE_URL: ${{ env.API_BASE }}
|
|
786
|
+
JIRA_EMAIL: ${{ secrets.JIRA_EMAIL }}
|
|
787
|
+
JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }}
|
|
788
|
+
ISSUE_KEY: ${{ env.ISSUE_KEY }}
|
|
789
|
+
AI_OUTPUT_PREVIEW: ${{ env.AI_OUTPUT_PREVIEW }}
|
|
790
|
+
run: |
|
|
791
|
+
echo "Updating Jira ticket to Blocked status and adding error comment..."
|
|
792
|
+
|
|
793
|
+
ATTACHMENT_FILE="${{ env.AI_OUTPUT_FILE }}"
|
|
794
|
+
if [ -f "$ATTACHMENT_FILE" ]; then
|
|
795
|
+
echo "Uploading error output to Jira as attachment..."
|
|
796
|
+
curl -X POST \
|
|
797
|
+
-H "X-Atlassian-Token: no-check" \
|
|
798
|
+
-u "$JIRA_EMAIL:$JIRA_API_TOKEN" \
|
|
799
|
+
-F "file=@$ATTACHMENT_FILE" \
|
|
800
|
+
"$JIRA_BASE_URL/rest/api/3/issue/$ISSUE_KEY/attachments" || echo "Failed to upload attachment"
|
|
801
|
+
fi
|
|
802
|
+
|
|
803
|
+
# Add comment with error details
|
|
804
|
+
WORKFLOW_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
|
805
|
+
|
|
806
|
+
# Create the comment using jq for proper JSON escaping
|
|
807
|
+
COMMENT_JSON=$(jq -n \
|
|
808
|
+
--arg workflow_url "$WORKFLOW_URL" \
|
|
809
|
+
--arg ai_provider "${{ env.AI_PROVIDER }}" \
|
|
810
|
+
--arg preview "$AI_OUTPUT_PREVIEW" \
|
|
811
|
+
'{
|
|
812
|
+
"body": {
|
|
813
|
+
"type": "doc",
|
|
814
|
+
"version": 1,
|
|
815
|
+
"content": [
|
|
816
|
+
{
|
|
817
|
+
"type": "paragraph",
|
|
818
|
+
"content": [
|
|
819
|
+
{
|
|
820
|
+
"type": "text",
|
|
821
|
+
"text": ("❌ AI Coding Agent Failed (using " + $ai_provider + ")"),
|
|
822
|
+
"marks": [{"type": "strong"}]
|
|
823
|
+
}
|
|
824
|
+
]
|
|
825
|
+
},
|
|
826
|
+
{
|
|
827
|
+
"type": "paragraph",
|
|
828
|
+
"content": [
|
|
829
|
+
{
|
|
830
|
+
"type": "text",
|
|
831
|
+
"text": "Workflow run: "
|
|
832
|
+
},
|
|
833
|
+
{
|
|
834
|
+
"type": "text",
|
|
835
|
+
"text": $workflow_url,
|
|
836
|
+
"marks": [{"type": "link", "attrs": {"href": $workflow_url}}]
|
|
837
|
+
}
|
|
838
|
+
]
|
|
839
|
+
},
|
|
840
|
+
{
|
|
841
|
+
"type": "paragraph",
|
|
842
|
+
"content": [
|
|
843
|
+
{
|
|
844
|
+
"type": "text",
|
|
845
|
+
"text": "Error output (first 50 lines, full output attached):"
|
|
846
|
+
}
|
|
847
|
+
]
|
|
848
|
+
},
|
|
849
|
+
{
|
|
850
|
+
"type": "codeBlock",
|
|
851
|
+
"attrs": {"language": "text"},
|
|
852
|
+
"content": [
|
|
853
|
+
{
|
|
854
|
+
"type": "text",
|
|
855
|
+
"text": $preview
|
|
856
|
+
}
|
|
857
|
+
]
|
|
858
|
+
}
|
|
859
|
+
]
|
|
860
|
+
}
|
|
861
|
+
}')
|
|
862
|
+
|
|
863
|
+
curl -X POST \
|
|
864
|
+
-H "Content-Type: application/json" \
|
|
865
|
+
-u "$JIRA_EMAIL:$JIRA_API_TOKEN" \
|
|
866
|
+
"$JIRA_BASE_URL/rest/api/3/issue/$ISSUE_KEY/comment" \
|
|
867
|
+
-d "$COMMENT_JSON"
|
|
868
|
+
|
|
869
|
+
# Get available transitions for debugging
|
|
870
|
+
echo "Fetching available transitions..."
|
|
871
|
+
TRANSITIONS=$(curl -s -u "$JIRA_EMAIL:$JIRA_API_TOKEN" \
|
|
872
|
+
"$JIRA_BASE_URL/rest/api/3/issue/$ISSUE_KEY/transitions")
|
|
873
|
+
|
|
874
|
+
echo "Debug: Available transitions:"
|
|
875
|
+
echo "$TRANSITIONS" | jq '.transitions[] | {id: .id, name: .name, to_name: .to.name}'
|
|
876
|
+
|
|
877
|
+
# Try to find blocked transition with multiple approaches
|
|
878
|
+
BLOCKED_TRANSITION_ID=$(echo "$TRANSITIONS" | jq -r '
|
|
879
|
+
.transitions[] |
|
|
880
|
+
select(
|
|
881
|
+
(.name | ascii_downcase | contains("block")) or
|
|
882
|
+
(.to.name | ascii_downcase | contains("block")) or
|
|
883
|
+
(.name | ascii_downcase | contains("fail")) or
|
|
884
|
+
(.to.name | ascii_downcase | contains("fail")) or
|
|
885
|
+
(.name | ascii_downcase | contains("pause")) or
|
|
886
|
+
(.to.name | ascii_downcase | contains("pause")) or
|
|
887
|
+
(.name | ascii_downcase | contains("stop")) or
|
|
888
|
+
(.to.name | ascii_downcase | contains("stop"))
|
|
889
|
+
) |
|
|
890
|
+
.id' | head -1)
|
|
891
|
+
|
|
892
|
+
if [ -n "$BLOCKED_TRANSITION_ID" ] && [ "$BLOCKED_TRANSITION_ID" != "null" ]; then
|
|
893
|
+
echo "Found transition ID: $BLOCKED_TRANSITION_ID"
|
|
894
|
+
echo "Transitioning to Blocked status..."
|
|
895
|
+
|
|
896
|
+
TRANSITION_RESULT=$(curl -s -X POST \
|
|
897
|
+
-H "Content-Type: application/json" \
|
|
898
|
+
-u "$JIRA_EMAIL:$JIRA_API_TOKEN" \
|
|
899
|
+
"$JIRA_BASE_URL/rest/api/3/issue/$ISSUE_KEY/transitions" \
|
|
900
|
+
-d "{
|
|
901
|
+
\"transition\": {
|
|
902
|
+
\"id\": \"$BLOCKED_TRANSITION_ID\"
|
|
903
|
+
}
|
|
904
|
+
}")
|
|
905
|
+
|
|
906
|
+
if [ $? -eq 0 ]; then
|
|
907
|
+
echo "✅ Successfully transitioned issue to blocked status"
|
|
908
|
+
else
|
|
909
|
+
echo "❌ Failed to transition issue: $TRANSITION_RESULT"
|
|
910
|
+
fi
|
|
911
|
+
else
|
|
912
|
+
echo "❌ Could not find a suitable transition. Available transitions:"
|
|
913
|
+
echo "$TRANSITIONS" | jq -r '.transitions[] | "\(.id): \(.name) -> \(.to.name)"'
|
|
914
|
+
echo "❌ Please check your Jira workflow configuration for transitions containing: block, fail, pause, or stop"
|
|
915
|
+
fi
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
name: AI-CODER-CALLER-FULL
|
|
2
|
+
on:
|
|
3
|
+
workflow_dispatch:
|
|
4
|
+
inputs:
|
|
5
|
+
prompt:
|
|
6
|
+
description: 'Base64 encoded prompt'
|
|
7
|
+
required: true
|
|
8
|
+
type: string
|
|
9
|
+
webhook_uuid:
|
|
10
|
+
description: 'Webhook uuid param'
|
|
11
|
+
required: true
|
|
12
|
+
type: string
|
|
13
|
+
create_pr:
|
|
14
|
+
description: 'create pr'
|
|
15
|
+
required: true
|
|
16
|
+
type: boolean
|
|
17
|
+
title:
|
|
18
|
+
description: 'Base64 encoded title for the PR'
|
|
19
|
+
required: true
|
|
20
|
+
type: string
|
|
21
|
+
default: ""
|
|
22
|
+
reviewer:
|
|
23
|
+
description: 'Optional reviewer username for the PR'
|
|
24
|
+
required: false
|
|
25
|
+
type: string
|
|
26
|
+
default: ""
|
|
27
|
+
ai_provider:
|
|
28
|
+
description: 'AI provider to use (claude or codex)'
|
|
29
|
+
required: false
|
|
30
|
+
type: string
|
|
31
|
+
default: "claude"
|
|
32
|
+
repository_dispatch:
|
|
33
|
+
types: [ai-coder-proxy]
|
|
34
|
+
|
|
35
|
+
jobs:
|
|
36
|
+
prepare-inputs:
|
|
37
|
+
runs-on: ubuntu-latest
|
|
38
|
+
outputs:
|
|
39
|
+
prompt: ${{ steps.parse.outputs.prompt }}
|
|
40
|
+
webhook_uuid: ${{ steps.parse.outputs.webhook_uuid }}
|
|
41
|
+
create_pr: ${{ steps.parse.outputs.create_pr }}
|
|
42
|
+
title: ${{ steps.parse.outputs.title }}
|
|
43
|
+
reviewer: ${{ steps.parse.outputs.reviewer }}
|
|
44
|
+
ai_provider: ${{ steps.parse.outputs.ai_provider }}
|
|
45
|
+
steps:
|
|
46
|
+
- name: Parse inputs
|
|
47
|
+
id: parse
|
|
48
|
+
run: |
|
|
49
|
+
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
|
50
|
+
echo "prompt=${{ github.event.inputs.prompt }}" >> $GITHUB_OUTPUT
|
|
51
|
+
echo "webhook_uuid=${{ github.event.inputs.webhook_uuid }}" >> $GITHUB_OUTPUT
|
|
52
|
+
echo "create_pr=${{ github.event.inputs.create_pr }}" >> $GITHUB_OUTPUT
|
|
53
|
+
echo "title=${{ github.event.inputs.title }}" >> $GITHUB_OUTPUT
|
|
54
|
+
echo "reviewer=${{ github.event.inputs.reviewer }}" >> $GITHUB_OUTPUT
|
|
55
|
+
echo "ai_provider=${{ github.event.inputs.ai_provider }}" >> $GITHUB_OUTPUT
|
|
56
|
+
elif [ "${{ github.event_name }}" = "repository_dispatch" ]; then
|
|
57
|
+
echo "prompt=${{ github.event.client_payload.prompt }}" >> $GITHUB_OUTPUT
|
|
58
|
+
echo "webhook_uuid=${{ github.event.client_payload.webhook_uuid }}" >> $GITHUB_OUTPUT
|
|
59
|
+
echo "create_pr=${{ github.event.client_payload.create_pr }}" >> $GITHUB_OUTPUT
|
|
60
|
+
echo "title=${{ github.event.client_payload.title }}" >> $GITHUB_OUTPUT
|
|
61
|
+
echo "reviewer=${{ github.event.client_payload.reviewer }}" >> $GITHUB_OUTPUT
|
|
62
|
+
echo "ai_provider=${{ github.event.client_payload.ai_provider || 'claude' }}" >> $GITHUB_OUTPUT
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
call-ai-coder:
|
|
66
|
+
needs: prepare-inputs
|
|
67
|
+
uses: Datarails/dr_github_reusable/.github/workflows/ai-coder-n8n-reusable.yml@DR-38894-Automation
|
|
68
|
+
with:
|
|
69
|
+
prompt: ${{ needs.prepare-inputs.outputs.prompt }}
|
|
70
|
+
webhook_uuid: ${{ needs.prepare-inputs.outputs.webhook_uuid }}
|
|
71
|
+
create_pr: ${{ needs.prepare-inputs.outputs.create_pr == 'true' }}
|
|
72
|
+
title: ${{ needs.prepare-inputs.outputs.title }}
|
|
73
|
+
reviewer: ${{ needs.prepare-inputs.outputs.reviewer }}
|
|
74
|
+
ai_provider: ${{ needs.prepare-inputs.outputs.ai_provider }}
|
|
75
|
+
secrets:
|
|
76
|
+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
77
|
+
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
|
78
|
+
GH_APP_ID: ${{ secrets.GH_APP_ID }}
|
|
79
|
+
GH_APP_PRIVATE_KEY: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
|
80
|
+
N8N_WEBHOOK_URL: ${{ secrets.N8N_WEBHOOK_URL }}
|
|
81
|
+
GH_PER_TOKEN_TEST: ${{ secrets.GH_PER_TOKEN_TEST }}
|
|
82
|
+
|