@cyanautomation/kaseki-agent 1.14.0 → 1.15.1
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,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test Utilities: Log Suppression Helpers
|
|
3
|
+
*
|
|
4
|
+
* Provides per-test control over log suppression when you need to debug
|
|
5
|
+
* specific tests or capture logs for assertions.
|
|
6
|
+
*
|
|
7
|
+
* Example usage:
|
|
8
|
+
* import { suppressLogs, restoreLogs, getCapturedLogs } from '@src/test-utils/log-suppression';
|
|
9
|
+
*
|
|
10
|
+
* it('should test something with logging', () => {
|
|
11
|
+
* suppressLogs();
|
|
12
|
+
* // ... test code that would normally print logs
|
|
13
|
+
* restoreLogs();
|
|
14
|
+
* const logs = getCapturedLogs();
|
|
15
|
+
* // ... assertions on captured logs
|
|
16
|
+
* });
|
|
17
|
+
*/
|
|
18
|
+
declare global {
|
|
19
|
+
namespace NodeJS {
|
|
20
|
+
interface Global {
|
|
21
|
+
__kasekiCapturedLogs?: string[];
|
|
22
|
+
__kasekiOriginalLog?: typeof console.log;
|
|
23
|
+
__kasekiLogsEnabled?: boolean;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Suppress JSON event logs for the current test.
|
|
29
|
+
* Logs are still captured in __kasekiCapturedLogs and can be retrieved.
|
|
30
|
+
*/
|
|
31
|
+
export declare function suppressLogs(): void;
|
|
32
|
+
/**
|
|
33
|
+
* Restore console.log to its original state, allowing all logs to print.
|
|
34
|
+
*/
|
|
35
|
+
export declare function restoreLogs(): void;
|
|
36
|
+
/**
|
|
37
|
+
* Get all JSON event logs that were suppressed/captured since the last clearCapturedLogs() call.
|
|
38
|
+
* Useful for asserting on log output without printing it during test execution.
|
|
39
|
+
*
|
|
40
|
+
* @returns Array of JSON event log strings
|
|
41
|
+
*/
|
|
42
|
+
export declare function getCapturedLogs(): string[];
|
|
43
|
+
/**
|
|
44
|
+
* Clear the captured logs buffer. Call this between tests if you need a fresh slate.
|
|
45
|
+
*/
|
|
46
|
+
export declare function clearCapturedLogs(): void;
|
|
47
|
+
/**
|
|
48
|
+
* Get a specific captured log by event type.
|
|
49
|
+
*
|
|
50
|
+
* @param eventType - The event_type field to search for (e.g., 'job_started')
|
|
51
|
+
* @returns The first matching log object or undefined
|
|
52
|
+
*/
|
|
53
|
+
export declare function getCapturedLogByEventType(eventType: string): Record<string, any> | undefined;
|
|
54
|
+
/**
|
|
55
|
+
* Get all captured logs of a specific event type.
|
|
56
|
+
*
|
|
57
|
+
* @param eventType - The event_type field to search for
|
|
58
|
+
* @returns Array of matching log objects
|
|
59
|
+
*/
|
|
60
|
+
export declare function getCapturedLogsByEventType(eventType: string): Record<string, any>[];
|
|
61
|
+
/**
|
|
62
|
+
* Run a test function with logs suppressed, automatically cleaning up.
|
|
63
|
+
* Convenient wrapper for tests that just need suppression without explicit restore.
|
|
64
|
+
*
|
|
65
|
+
* @param testFn - The test function to run with suppressed logs
|
|
66
|
+
*/
|
|
67
|
+
export declare function withSuppressedLogs(testFn: () => void | Promise<void>): void | Promise<void>;
|
|
68
|
+
//# sourceMappingURL=log-suppression.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"log-suppression.d.ts","sourceRoot":"","sources":["../../src/test-utils/log-suppression.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAGH,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM,CAAC;QACf,UAAU,MAAM;YACd,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAC;YAChC,mBAAmB,CAAC,EAAE,OAAO,OAAO,CAAC,GAAG,CAAC;YACzC,mBAAmB,CAAC,EAAE,OAAO,CAAC;SAC/B;KACF;CACF;AAGD;;;GAGG;AACH,wBAAgB,YAAY,IAAI,IAAI,CA0BnC;AAED;;GAEG;AACH,wBAAgB,WAAW,IAAI,IAAI,CAOlC;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,IAAI,MAAM,EAAE,CAE1C;AAED;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,IAAI,CAExC;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,CAa5F;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,CAgBnF;AAqBD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAa3F"}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test Utilities: Log Suppression Helpers
|
|
3
|
+
*
|
|
4
|
+
* Provides per-test control over log suppression when you need to debug
|
|
5
|
+
* specific tests or capture logs for assertions.
|
|
6
|
+
*
|
|
7
|
+
* Example usage:
|
|
8
|
+
* import { suppressLogs, restoreLogs, getCapturedLogs } from '@src/test-utils/log-suppression';
|
|
9
|
+
*
|
|
10
|
+
* it('should test something with logging', () => {
|
|
11
|
+
* suppressLogs();
|
|
12
|
+
* // ... test code that would normally print logs
|
|
13
|
+
* restoreLogs();
|
|
14
|
+
* const logs = getCapturedLogs();
|
|
15
|
+
* // ... assertions on captured logs
|
|
16
|
+
* });
|
|
17
|
+
*/
|
|
18
|
+
/* eslint-enable @typescript-eslint/no-namespace */
|
|
19
|
+
/**
|
|
20
|
+
* Suppress JSON event logs for the current test.
|
|
21
|
+
* Logs are still captured in __kasekiCapturedLogs and can be retrieved.
|
|
22
|
+
*/
|
|
23
|
+
export function suppressLogs() {
|
|
24
|
+
if (global.__kasekiLogsEnabled === false) {
|
|
25
|
+
return; // Already suppressed
|
|
26
|
+
}
|
|
27
|
+
// Store the current console.log
|
|
28
|
+
global.__kasekiOriginalLog = console.log;
|
|
29
|
+
// Replace with suppressing version
|
|
30
|
+
console.log = (...args) => {
|
|
31
|
+
const filteredArgs = args
|
|
32
|
+
.map((arg) => {
|
|
33
|
+
if (typeof arg === 'string' && isJsonEventLog(arg)) {
|
|
34
|
+
global.__kasekiCapturedLogs?.push(arg);
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
return arg;
|
|
38
|
+
})
|
|
39
|
+
.filter((arg) => arg !== null);
|
|
40
|
+
if (filteredArgs.length > 0) {
|
|
41
|
+
global.__kasekiOriginalLog?.(...filteredArgs);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
global.__kasekiLogsEnabled = false;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Restore console.log to its original state, allowing all logs to print.
|
|
48
|
+
*/
|
|
49
|
+
export function restoreLogs() {
|
|
50
|
+
if (global.__kasekiLogsEnabled === true || global.__kasekiOriginalLog === undefined) {
|
|
51
|
+
return; // Already restored or nothing to restore
|
|
52
|
+
}
|
|
53
|
+
console.log = global.__kasekiOriginalLog;
|
|
54
|
+
global.__kasekiLogsEnabled = true;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Get all JSON event logs that were suppressed/captured since the last clearCapturedLogs() call.
|
|
58
|
+
* Useful for asserting on log output without printing it during test execution.
|
|
59
|
+
*
|
|
60
|
+
* @returns Array of JSON event log strings
|
|
61
|
+
*/
|
|
62
|
+
export function getCapturedLogs() {
|
|
63
|
+
return global.__kasekiCapturedLogs ?? [];
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Clear the captured logs buffer. Call this between tests if you need a fresh slate.
|
|
67
|
+
*/
|
|
68
|
+
export function clearCapturedLogs() {
|
|
69
|
+
global.__kasekiCapturedLogs = [];
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Get a specific captured log by event type.
|
|
73
|
+
*
|
|
74
|
+
* @param eventType - The event_type field to search for (e.g., 'job_started')
|
|
75
|
+
* @returns The first matching log object or undefined
|
|
76
|
+
*/
|
|
77
|
+
export function getCapturedLogByEventType(eventType) {
|
|
78
|
+
const logs = getCapturedLogs();
|
|
79
|
+
for (const logStr of logs) {
|
|
80
|
+
try {
|
|
81
|
+
const log = JSON.parse(logStr);
|
|
82
|
+
if (log.event_type === eventType) {
|
|
83
|
+
return log;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// Skip invalid JSON
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Get all captured logs of a specific event type.
|
|
94
|
+
*
|
|
95
|
+
* @param eventType - The event_type field to search for
|
|
96
|
+
* @returns Array of matching log objects
|
|
97
|
+
*/
|
|
98
|
+
export function getCapturedLogsByEventType(eventType) {
|
|
99
|
+
const logs = getCapturedLogs();
|
|
100
|
+
const matches = [];
|
|
101
|
+
for (const logStr of logs) {
|
|
102
|
+
try {
|
|
103
|
+
const log = JSON.parse(logStr);
|
|
104
|
+
if (log.event_type === eventType) {
|
|
105
|
+
matches.push(log);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// Skip invalid JSON
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return matches;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Check if a JSON event log is valid (has required fields).
|
|
116
|
+
* This is the same check used in jest.setup.ts.
|
|
117
|
+
*/
|
|
118
|
+
function isJsonEventLog(str) {
|
|
119
|
+
try {
|
|
120
|
+
const obj = JSON.parse(str);
|
|
121
|
+
const isEventLog = typeof obj === 'object' &&
|
|
122
|
+
obj !== null &&
|
|
123
|
+
'timestamp' in obj &&
|
|
124
|
+
'component' in obj &&
|
|
125
|
+
('event_type' in obj || 'level' in obj || 'message' in obj);
|
|
126
|
+
return isEventLog;
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Run a test function with logs suppressed, automatically cleaning up.
|
|
134
|
+
* Convenient wrapper for tests that just need suppression without explicit restore.
|
|
135
|
+
*
|
|
136
|
+
* @param testFn - The test function to run with suppressed logs
|
|
137
|
+
*/
|
|
138
|
+
export function withSuppressedLogs(testFn) {
|
|
139
|
+
suppressLogs();
|
|
140
|
+
try {
|
|
141
|
+
const result = testFn();
|
|
142
|
+
if (result instanceof Promise) {
|
|
143
|
+
return result.finally(() => restoreLogs());
|
|
144
|
+
}
|
|
145
|
+
restoreLogs();
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
restoreLogs();
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
//# sourceMappingURL=log-suppression.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"log-suppression.js","sourceRoot":"","sources":["../../src/test-utils/log-suppression.ts"],"names":[],"mappings":"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"}
|
package/kaseki-agent.sh
CHANGED
|
@@ -30,6 +30,7 @@ KASEKI_VALIDATION_FAIL_FAST="${KASEKI_VALIDATION_FAIL_FAST:-1}"
|
|
|
30
30
|
KASEKI_STRICT_SCRIPT_CHECK="${KASEKI_STRICT_SCRIPT_CHECK:-0}"
|
|
31
31
|
GITHUB_APP_ENABLED="${GITHUB_APP_ENABLED:-0}"
|
|
32
32
|
KASEKI_PUBLISH_MODE="${KASEKI_PUBLISH_MODE:-auto}"
|
|
33
|
+
KASEKI_GITHUB_PR_RETRIES="${KASEKI_GITHUB_PR_RETRIES:-3}"
|
|
33
34
|
START_EPOCH="$(date +%s)"
|
|
34
35
|
START_ISO="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
35
36
|
CURRENT_STAGE="initializing"
|
|
@@ -50,6 +51,9 @@ QUALITY_FAILURE_REASON=""
|
|
|
50
51
|
SECRET_SCAN_EXIT=0
|
|
51
52
|
GITHUB_PUSH_EXIT=0
|
|
52
53
|
GITHUB_PR_EXIT=0
|
|
54
|
+
GITHUB_API_ERROR_TYPE=""
|
|
55
|
+
GITHUB_API_ERROR_MESSAGE=""
|
|
56
|
+
GITHUB_API_HTTP_STATUS=""
|
|
53
57
|
ACTUAL_MODEL="unknown"
|
|
54
58
|
GITHUB_PR_URL=""
|
|
55
59
|
GITHUB_SKIP_REASONS=()
|
|
@@ -308,6 +312,9 @@ write_metadata() {
|
|
|
308
312
|
"repo_memory_file": $(printf '%s' "$REPO_MEMORY_FILE" | json_encode),
|
|
309
313
|
"repo_memory_ttl_days": $KASEKI_REPO_MEMORY_TTL_DAYS,
|
|
310
314
|
"repo_memory_max_bytes": $KASEKI_REPO_MEMORY_MAX_BYTES,
|
|
315
|
+
"github_api_error_type": $(printf '%s' "$GITHUB_API_ERROR_TYPE" | json_encode),
|
|
316
|
+
"github_api_error_message": $(printf '%s' "$GITHUB_API_ERROR_MESSAGE" | json_encode),
|
|
317
|
+
"github_api_http_status": $(printf '%s' "$GITHUB_API_HTTP_STATUS" | json_encode),
|
|
311
318
|
"node_version": $(node --version 2>/dev/null | json_encode || printf 'null'),
|
|
312
319
|
"npm_version": $(npm --version 2>/dev/null | json_encode || printf 'null'),
|
|
313
320
|
"pi_version": $(printf '%s' "$PI_VERSION" | json_encode)
|
|
@@ -1259,6 +1266,102 @@ $memory_section
|
|
|
1259
1266
|
EOF
|
|
1260
1267
|
}
|
|
1261
1268
|
|
|
1269
|
+
validate_github_api_response() {
|
|
1270
|
+
local http_status response log_file error_type error_message
|
|
1271
|
+
http_status="$1"
|
|
1272
|
+
response="$2"
|
|
1273
|
+
log_file="${3:-/results/git-push.log}"
|
|
1274
|
+
|
|
1275
|
+
# Try to parse error info from response
|
|
1276
|
+
error_type="unknown"
|
|
1277
|
+
error_message=""
|
|
1278
|
+
|
|
1279
|
+
if [ "$http_status" = "201" ]; then
|
|
1280
|
+
# Success - but still need to verify html_url exists
|
|
1281
|
+
return 0
|
|
1282
|
+
fi
|
|
1283
|
+
|
|
1284
|
+
# Attempt to extract error info using Node.js
|
|
1285
|
+
{
|
|
1286
|
+
error_message=$(printf '%s' "$response" | node -e "
|
|
1287
|
+
try {
|
|
1288
|
+
const d = JSON.parse(require('fs').readFileSync(0, 'utf8'));
|
|
1289
|
+
if (d.message) process.stdout.write(d.message);
|
|
1290
|
+
} catch (e) {}
|
|
1291
|
+
" 2>/dev/null || true)
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
# Map HTTP status to error type
|
|
1295
|
+
case "$http_status" in
|
|
1296
|
+
400)
|
|
1297
|
+
error_type="validation_error"
|
|
1298
|
+
[ -z "$error_message" ] && error_message="Bad request"
|
|
1299
|
+
;;
|
|
1300
|
+
401)
|
|
1301
|
+
error_type="authentication_error"
|
|
1302
|
+
[ -z "$error_message" ] && error_message="Unauthorized (check GitHub App token)"
|
|
1303
|
+
;;
|
|
1304
|
+
403)
|
|
1305
|
+
error_type="permission_error"
|
|
1306
|
+
[ -z "$error_message" ] && error_message="Permission denied (insufficient scope or rate limited)"
|
|
1307
|
+
;;
|
|
1308
|
+
404)
|
|
1309
|
+
error_type="not_found_error"
|
|
1310
|
+
[ -z "$error_message" ] && error_message="Repository or branch not found"
|
|
1311
|
+
;;
|
|
1312
|
+
422)
|
|
1313
|
+
error_type="validation_error"
|
|
1314
|
+
[ -z "$error_message" ] && error_message="Unprocessable entity (e.g., branch protection, duplicate PR)"
|
|
1315
|
+
;;
|
|
1316
|
+
429)
|
|
1317
|
+
error_type="rate_limit_error"
|
|
1318
|
+
[ -z "$error_message" ] && error_message="Rate limited by GitHub API"
|
|
1319
|
+
;;
|
|
1320
|
+
500|502|503|504)
|
|
1321
|
+
error_type="server_error"
|
|
1322
|
+
[ -z "$error_message" ] && error_message="GitHub API server error (HTTP $http_status)"
|
|
1323
|
+
;;
|
|
1324
|
+
*)
|
|
1325
|
+
error_type="http_error"
|
|
1326
|
+
[ -z "$error_message" ] && error_message="HTTP $http_status"
|
|
1327
|
+
;;
|
|
1328
|
+
esac
|
|
1329
|
+
|
|
1330
|
+
printf 'GitHub API error (HTTP %s): %s - %s\n' "$http_status" "$error_type" "$error_message" | tee -a "$log_file" >&2
|
|
1331
|
+
|
|
1332
|
+
# Store error info for logging
|
|
1333
|
+
GITHUB_API_ERROR_TYPE="$error_type"
|
|
1334
|
+
GITHUB_API_ERROR_MESSAGE="$error_message"
|
|
1335
|
+
GITHUB_API_HTTP_STATUS="$http_status"
|
|
1336
|
+
|
|
1337
|
+
return 1
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
is_github_pr_error_retryable() {
|
|
1341
|
+
local http_status error_type
|
|
1342
|
+
http_status="$1"
|
|
1343
|
+
error_type="$2"
|
|
1344
|
+
|
|
1345
|
+
# Retryable: transient errors
|
|
1346
|
+
case "$http_status" in
|
|
1347
|
+
429)
|
|
1348
|
+
# Rate limit (retryable)
|
|
1349
|
+
return 0
|
|
1350
|
+
;;
|
|
1351
|
+
500|502|503|504)
|
|
1352
|
+
# Server errors (retryable)
|
|
1353
|
+
return 0
|
|
1354
|
+
;;
|
|
1355
|
+
0)
|
|
1356
|
+
# curl failed (usually transient)
|
|
1357
|
+
return 0
|
|
1358
|
+
;;
|
|
1359
|
+
esac
|
|
1360
|
+
|
|
1361
|
+
# Non-retryable: permanent errors
|
|
1362
|
+
return 1
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1262
1365
|
run_github_operations() {
|
|
1263
1366
|
local app_id private_key_file owner repo feature_branch token token_data
|
|
1264
1367
|
|
|
@@ -1366,7 +1469,8 @@ EOF_ASKPASS
|
|
|
1366
1469
|
|
|
1367
1470
|
# Create pull request
|
|
1368
1471
|
printf 'Creating pull request...\n' | tee -a /results/git-push.log
|
|
1369
|
-
|
|
1472
|
+
emit_progress "github operations" "pr_creation_starting"
|
|
1473
|
+
local pr_title pr_body pr_response pr_url pr_http_status
|
|
1370
1474
|
pr_title="Kaseki: $INSTANCE_NAME"
|
|
1371
1475
|
pr_body=$(cat <<EOF
|
|
1372
1476
|
Generated by Kaseki agent (instance: $INSTANCE_NAME)
|
|
@@ -1383,21 +1487,118 @@ This PR is in draft status. Please review before merging.
|
|
|
1383
1487
|
EOF
|
|
1384
1488
|
)
|
|
1385
1489
|
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
"https://api.github.com/repos/$owner/$repo/pulls" \
|
|
1390
|
-
-d "{\"title\": $(printf '%s' "$pr_title" | node -e "console.log(JSON.stringify(require('fs').readFileSync(0, 'utf8')))"), \"body\": $(printf '%s' "$pr_body" | node -e "console.log(JSON.stringify(require('fs').readFileSync(0, 'utf8')))"), \"head\": \"$feature_branch\", \"base\": \"$GIT_REF\", \"draft\": true}" 2>&1)
|
|
1490
|
+
# Retry loop for transient errors
|
|
1491
|
+
local retry_count=0 max_retries="$KASEKI_GITHUB_PR_RETRIES" pr_created=0
|
|
1492
|
+
local backoff_delay=2
|
|
1391
1493
|
|
|
1392
|
-
|
|
1494
|
+
while [ $retry_count -le "$max_retries" ]; do
|
|
1495
|
+
if [ $retry_count -gt 0 ]; then
|
|
1496
|
+
printf 'Retrying PR creation (attempt %d of %d) after %ds delay...\n' $((retry_count + 1)) "$max_retries" "$backoff_delay" | tee -a /results/git-push.log
|
|
1497
|
+
emit_progress "github operations" "pr_creation_attempt $((retry_count + 1))/$max_retries"
|
|
1498
|
+
sleep "$backoff_delay"
|
|
1499
|
+
# Exponential backoff: 2s, 4s, 8s
|
|
1500
|
+
backoff_delay=$((backoff_delay * 2))
|
|
1501
|
+
if [ $backoff_delay -gt 8 ]; then backoff_delay=8; fi
|
|
1502
|
+
fi
|
|
1503
|
+
|
|
1504
|
+
# Capture both response and HTTP status code
|
|
1505
|
+
local pr_response_file temp_status_file
|
|
1506
|
+
pr_response_file="$(mktemp /tmp/kaseki-pr-response.XXXXXX)" || { printf 'Failed to create temp file for PR response\n' | tee -a /results/git-push.log >&2; GITHUB_PR_EXIT=8; return 8; }
|
|
1507
|
+
temp_status_file="$(mktemp /tmp/kaseki-pr-status.XXXXXX)" || { printf 'Failed to create temp file for PR status\n' | tee -a /results/git-push.log >&2; GITHUB_PR_EXIT=8; return 8; }
|
|
1508
|
+
|
|
1509
|
+
if [ $retry_count -eq 0 ] && [ "${KASEKI_DEBUG:-0}" = "1" ]; then
|
|
1510
|
+
printf 'Debug: Creating PR with head=%s, base=%s, draft=true\n' "$feature_branch" "$GIT_REF" | tee -a /results/git-push.log
|
|
1511
|
+
fi
|
|
1512
|
+
|
|
1513
|
+
# Use curl with -w to capture HTTP status separately
|
|
1514
|
+
# curl exit code: 0=success, non-0=failure
|
|
1515
|
+
local curl_exit
|
|
1516
|
+
curl -s -w '%{http_code}' -X POST \
|
|
1517
|
+
-H "Authorization: token $token" \
|
|
1518
|
+
-H "Accept: application/vnd.github.v3+json" \
|
|
1519
|
+
"https://api.github.com/repos/$owner/$repo/pulls" \
|
|
1520
|
+
-d "{\"title\": $(printf '%s' "$pr_title" | node -e "console.log(JSON.stringify(require('fs').readFileSync(0, 'utf8')))"), \"body\": $(printf '%s' "$pr_body" | node -e "console.log(JSON.stringify(require('fs').readFileSync(0, 'utf8')))"), \"head\": \"$feature_branch\", \"base\": \"$GIT_REF\", \"draft\": true}" > "$temp_status_file" 2>&1
|
|
1521
|
+
curl_exit=$?
|
|
1522
|
+
|
|
1523
|
+
# Split response and status code
|
|
1524
|
+
local response_with_status
|
|
1525
|
+
response_with_status="$(cat "$temp_status_file")"
|
|
1526
|
+
pr_http_status="${response_with_status: -3}"
|
|
1527
|
+
pr_response="${response_with_status%???}"
|
|
1528
|
+
|
|
1529
|
+
rm -f "$temp_status_file"
|
|
1530
|
+
|
|
1531
|
+
if [ $curl_exit -ne 0 ]; then
|
|
1532
|
+
# curl command itself failed (network error, timeout, etc.)
|
|
1533
|
+
printf 'GitHub PR API curl command failed with exit code %d (attempt %d)\n' "$curl_exit" $((retry_count + 1)) | tee -a /results/git-push.log >&2
|
|
1534
|
+
GITHUB_API_HTTP_STATUS="0"
|
|
1535
|
+
if is_github_pr_error_retryable "0" "curl_error"; then
|
|
1536
|
+
retry_count=$((retry_count + 1))
|
|
1537
|
+
rm -f "$pr_response_file"
|
|
1538
|
+
continue
|
|
1539
|
+
else
|
|
1540
|
+
emit_error_event "github_pr_curl_failed" "curl command failed (exit $curl_exit) when creating PR" "exit"
|
|
1541
|
+
GITHUB_API_ERROR_TYPE="curl_error"
|
|
1542
|
+
GITHUB_API_ERROR_MESSAGE="curl exited with code $curl_exit"
|
|
1543
|
+
GITHUB_PR_EXIT=8
|
|
1544
|
+
rm -f "$pr_response_file"
|
|
1545
|
+
return 8
|
|
1546
|
+
fi
|
|
1547
|
+
fi
|
|
1548
|
+
|
|
1549
|
+
if [ "${KASEKI_DEBUG:-0}" = "1" ]; then
|
|
1550
|
+
printf 'Debug: PR API response HTTP status: %s (attempt %d)\n' "$pr_http_status" $((retry_count + 1)) | tee -a /results/git-push.log
|
|
1551
|
+
fi
|
|
1552
|
+
|
|
1553
|
+
# Validate the API response
|
|
1554
|
+
if validate_github_api_response "$pr_http_status" "$pr_response" /results/git-push.log; then
|
|
1555
|
+
# API returned success (201); now extract the URL
|
|
1556
|
+
pr_url="$(printf '%s' "$pr_response" | node -e "const d = JSON.parse(require('fs').readFileSync(0, 'utf8')); process.stdout.write(d.html_url || '')" 2>/dev/null || true)"
|
|
1557
|
+
|
|
1558
|
+
if [ -n "$pr_url" ]; then
|
|
1559
|
+
GITHUB_PR_URL="$pr_url"
|
|
1560
|
+
GITHUB_PR_EXIT=0
|
|
1561
|
+
printf 'Pull request created: %s\n' "$pr_url" | tee -a /results/git-push.log
|
|
1562
|
+
pr_created=1
|
|
1563
|
+
rm -f "$pr_response_file"
|
|
1564
|
+
break
|
|
1565
|
+
else
|
|
1566
|
+
# HTTP 201 but no html_url in response - malformed response
|
|
1567
|
+
printf 'Pull request API returned success (201) but response missing html_url field\n' | tee -a /results/git-push.log >&2
|
|
1568
|
+
emit_error_event "github_pr_response_malformed" "GitHub PR API returned 201 but response missing html_url field" "exit"
|
|
1569
|
+
if [ "${KASEKI_DEBUG:-0}" = "1" ]; then
|
|
1570
|
+
printf 'Debug: Full API response:\n%s\n' "$pr_response" | tee -a /results/git-push.log
|
|
1571
|
+
fi
|
|
1572
|
+
GITHUB_PR_EXIT=9
|
|
1573
|
+
pr_created=0
|
|
1574
|
+
rm -f "$pr_response_file"
|
|
1575
|
+
break
|
|
1576
|
+
fi
|
|
1577
|
+
else
|
|
1578
|
+
# API returned an error
|
|
1579
|
+
if is_github_pr_error_retryable "$pr_http_status" "$GITHUB_API_ERROR_TYPE"; then
|
|
1580
|
+
printf 'GitHub API returned retryable error (attempt %d): %s (HTTP %s)\n' $((retry_count + 1)) "$GITHUB_API_ERROR_TYPE" "$pr_http_status" | tee -a /results/git-push.log
|
|
1581
|
+
retry_count=$((retry_count + 1))
|
|
1582
|
+
rm -f "$pr_response_file"
|
|
1583
|
+
continue
|
|
1584
|
+
else
|
|
1585
|
+
# Permanent error, give up
|
|
1586
|
+
printf 'Failed to create PR. API error: %s\n' "$GITHUB_API_ERROR_MESSAGE" | tee -a /results/git-push.log >&2
|
|
1587
|
+
emit_error_event "github_pr_api_failed" "GitHub API error ($GITHUB_API_ERROR_TYPE): $GITHUB_API_ERROR_MESSAGE (HTTP $GITHUB_API_HTTP_STATUS)" "exit"
|
|
1588
|
+
if [ "${KASEKI_DEBUG:-0}" = "1" ]; then
|
|
1589
|
+
printf 'Debug: API error type: %s, HTTP status: %s\n' "$GITHUB_API_ERROR_TYPE" "$GITHUB_API_HTTP_STATUS" | tee -a /results/git-push.log
|
|
1590
|
+
printf 'Debug: Full response:\n%s\n' "$pr_response" | tee -a /results/git-push.log
|
|
1591
|
+
fi
|
|
1592
|
+
GITHUB_PR_EXIT=9
|
|
1593
|
+
pr_created=0
|
|
1594
|
+
rm -f "$pr_response_file"
|
|
1595
|
+
break
|
|
1596
|
+
fi
|
|
1597
|
+
fi
|
|
1598
|
+
done
|
|
1393
1599
|
|
|
1394
|
-
if [ -
|
|
1395
|
-
|
|
1396
|
-
GITHUB_PR_EXIT=0
|
|
1397
|
-
printf 'Pull request created: %s\n' "$pr_url" | tee -a /results/git-push.log
|
|
1398
|
-
else
|
|
1399
|
-
printf 'Failed to create PR. Response: %s\n' "$pr_response" | tee -a /results/git-push.log >&2
|
|
1400
|
-
GITHUB_PR_EXIT=9
|
|
1600
|
+
if [ $pr_created -eq 0 ] && [ $GITHUB_PR_EXIT -ne 0 ]; then
|
|
1601
|
+
return "$GITHUB_PR_EXIT"
|
|
1401
1602
|
fi
|
|
1402
1603
|
|
|
1403
1604
|
# Clean up token
|
package/package.json
CHANGED