@datarailsshared/dr_renderer 1.5.79 → 1.5.89
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
|
+
|
package/package.json
CHANGED
|
@@ -9877,7 +9877,7 @@ let getHighchartsRenderer = function ($, document, Highcharts, default_colors, h
|
|
|
9877
9877
|
lodash.forEach(values, (value, key) => {
|
|
9878
9878
|
const formatInfo = pivotData[type][key] || {};
|
|
9879
9879
|
const valueToFloat = parseFloat(value);
|
|
9880
|
-
const isDate = formatInfo.type === 'Date';
|
|
9880
|
+
const isDate = formatInfo.type === 'Date' && (moment_lib(valueToFloat).isValid() || moment_lib(value).isValid());
|
|
9881
9881
|
const isDateFormatting = isDate && highchartsRenderer.isFormattingDatesAsOtherAxisTypes();
|
|
9882
9882
|
const isNumberFormatting = !isDate && formatInfo.format && highchartsRenderer.isFormattingNumberAxis(pivotData);
|
|
9883
9883
|
|
|
@@ -5089,6 +5089,10 @@ describe('highcharts_renderer', () => {
|
|
|
5089
5089
|
});
|
|
5090
5090
|
|
|
5091
5091
|
describe('formatting Dates', () => {
|
|
5092
|
+
afterEach(() => {
|
|
5093
|
+
lodash.set(document, 'ReportHippo.user.features', []);
|
|
5094
|
+
});
|
|
5095
|
+
|
|
5092
5096
|
it('should return formatted multivalue colKey as array (format_dates_as_other_axis_types is ON)', () => {
|
|
5093
5097
|
lodash.set(document, 'ReportHippo.user.features', ['format_dates_as_other_axis_types']);
|
|
5094
5098
|
const initialKey = [1687277052, 1687277052];
|
|
@@ -5136,6 +5140,29 @@ describe('highcharts_renderer', () => {
|
|
|
5136
5140
|
const formattedKey = highchartsRenderer.getFormattedKey(initialKeyAlreadyFormatted, pivotData, type);
|
|
5137
5141
|
expect(formattedKey).toEqual(initialKeyAlreadyFormatted);
|
|
5138
5142
|
});
|
|
5143
|
+
|
|
5144
|
+
it('should return NOT formatted value when type is Date but value is not a valid moment date', () => {
|
|
5145
|
+
lodash.set(document, 'ReportHippo.user.features', ['format_dates_as_other_axis_types']);
|
|
5146
|
+
const initialKey = ['Some Text Value', 'Another Text'];
|
|
5147
|
+
const pivotData = {
|
|
5148
|
+
colFormats: [{ type: 'Date' }, { type: 'Date' }],
|
|
5149
|
+
isFormattingAxisLabels: true,
|
|
5150
|
+
};
|
|
5151
|
+
const type = 'colFormats';
|
|
5152
|
+
const formattedKey = highchartsRenderer.getFormattedKey(initialKey, pivotData, type);
|
|
5153
|
+
expect(formattedKey).toEqual(initialKey);
|
|
5154
|
+
});
|
|
5155
|
+
|
|
5156
|
+
it('should return formatted value when type is Date and value is a valid moment date (format_dates_as_other_axis_types is OFF)', () => {
|
|
5157
|
+
const initialKey = ['Feb-24', 'Jan-24'];
|
|
5158
|
+
const pivotData = {
|
|
5159
|
+
colFormats: [{ type: 'Date' }, { type: 'Date' }],
|
|
5160
|
+
isFormattingAxisLabels: true,
|
|
5161
|
+
};
|
|
5162
|
+
const type = 'colFormats';
|
|
5163
|
+
const formattedKey = highchartsRenderer.getFormattedKey(initialKey, pivotData, type);
|
|
5164
|
+
expect(formattedKey).toEqual(initialKey);
|
|
5165
|
+
});
|
|
5139
5166
|
});
|
|
5140
5167
|
});
|
|
5141
5168
|
|
|
@@ -6152,6 +6179,14 @@ describe('highcharts_renderer', () => {
|
|
|
6152
6179
|
describe('Function tableCSVExportRenderer', () => {
|
|
6153
6180
|
window._ = lodash;
|
|
6154
6181
|
|
|
6182
|
+
beforeEach(() => {
|
|
6183
|
+
lodash.set(document, 'ReportHippo.user.features', ['format_dates_as_other_axis_types']);
|
|
6184
|
+
});
|
|
6185
|
+
|
|
6186
|
+
afterEach(() => {
|
|
6187
|
+
lodash.set(document, 'ReportHippo.user.features', []);
|
|
6188
|
+
});
|
|
6189
|
+
|
|
6155
6190
|
it('should create csv json object representation based on pivot data (only cols)', () => {
|
|
6156
6191
|
const chartOptions = {
|
|
6157
6192
|
table_options: {
|
|
@@ -6226,7 +6261,7 @@ describe('highcharts_renderer', () => {
|
|
|
6226
6261
|
const csvJson = highchartsRenderer.tableCSVExportRenderer(pivotData, chartOptions);
|
|
6227
6262
|
expect(csvJson).toEqual(
|
|
6228
6263
|
[
|
|
6229
|
-
[
|
|
6264
|
+
[ '05/01/2022', '05/13/2022', '05/22/2022', '05/31/2022', 'Totals' ],
|
|
6230
6265
|
[ undefined, undefined, undefined, undefined, 'Totals' ],
|
|
6231
6266
|
[],
|
|
6232
6267
|
[ 1, 1, 1, 1, 2 ],
|
|
@@ -6329,7 +6364,7 @@ describe('highcharts_renderer', () => {
|
|
|
6329
6364
|
const csvJson = highchartsRenderer.tableCSVExportRenderer(pivotData, chartOptions);
|
|
6330
6365
|
expect(csvJson).toEqual(
|
|
6331
6366
|
[
|
|
6332
|
-
["Date",
|
|
6367
|
+
["Date", "05/22/2022", "05/31/2022", "Totals"],
|
|
6333
6368
|
["Type"],
|
|
6334
6369
|
["Food", 1, 1, 1],
|
|
6335
6370
|
["Technology", 1, 1, 1],
|
|
@@ -6348,7 +6383,7 @@ describe('highcharts_renderer', () => {
|
|
|
6348
6383
|
const pivotData = $.pivotUtilities.getPivotDataModel(inputs, subOptions);
|
|
6349
6384
|
const csvJson = highchartsRenderer.tableCSVExportRenderer(pivotData, chartOptions);
|
|
6350
6385
|
expect(csvJson).toEqual([
|
|
6351
|
-
["Date",
|
|
6386
|
+
["Date", "05/22/2022", "05/31/2022", "Totals"],
|
|
6352
6387
|
["Type"],
|
|
6353
6388
|
["Food", 1, 1, 1],
|
|
6354
6389
|
["Technology", 1, 1, 1]
|
|
@@ -6366,7 +6401,7 @@ describe('highcharts_renderer', () => {
|
|
|
6366
6401
|
const pivotData = $.pivotUtilities.getPivotDataModel(inputs, subOptions);
|
|
6367
6402
|
const csvJson = highchartsRenderer.tableCSVExportRenderer(pivotData, chartOptions);
|
|
6368
6403
|
expect(csvJson).toEqual([
|
|
6369
|
-
["Date",
|
|
6404
|
+
["Date", "05/22/2022", "05/31/2022"],
|
|
6370
6405
|
["Type"],
|
|
6371
6406
|
["Food", 1, 1],
|
|
6372
6407
|
["Technology", 1, 1],
|
|
@@ -6385,7 +6420,7 @@ describe('highcharts_renderer', () => {
|
|
|
6385
6420
|
const pivotData = $.pivotUtilities.getPivotDataModel(inputs, subOptions);
|
|
6386
6421
|
const csvJson = highchartsRenderer.tableCSVExportRenderer(pivotData, chartOptions);
|
|
6387
6422
|
expect(csvJson).toEqual([
|
|
6388
|
-
["Date",
|
|
6423
|
+
["Date", "05/22/2022", "05/31/2022"],
|
|
6389
6424
|
["Type"],
|
|
6390
6425
|
["Food", 1, 1],
|
|
6391
6426
|
["Technology", 1, 1],
|