@link-assistant/hive-mind 1.21.1 → 1.21.2
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.
- package/CHANGELOG.md +8 -0
- package/package.json +1 -1
- package/src/solve.repository.lib.mjs +125 -41
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 1.21.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 586b84d: Add retry mechanism for GitHub 500 errors during repository clone
|
|
8
|
+
|
|
9
|
+
This change adds intelligent retry logic with exponential backoff to handle transient GitHub server errors during repository cloning operations.
|
|
10
|
+
|
|
3
11
|
## 1.21.1
|
|
4
12
|
|
|
5
13
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -884,56 +884,140 @@ Thank you!`;
|
|
|
884
884
|
return { repoToClone, forkedRepo, upstreamRemote, prForkOwner: forkOwner };
|
|
885
885
|
};
|
|
886
886
|
|
|
887
|
-
//
|
|
887
|
+
// Classify git clone errors to determine if they are retryable
|
|
888
|
+
export const classifyCloneError = errorOutput => {
|
|
889
|
+
const output = errorOutput.toLowerCase();
|
|
890
|
+
|
|
891
|
+
// Transient server errors (5xx) - typically retryable
|
|
892
|
+
if (output.includes('error: 500') || output.includes('internal server error') || output.includes('error: 502') || output.includes('error: 503') || output.includes('error: 504')) {
|
|
893
|
+
return { type: 'TRANSIENT', retryable: true, description: 'GitHub server error' };
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// Network-related errors - typically retryable
|
|
897
|
+
if (output.includes('connection refused') || output.includes('connection timed out') || output.includes('connection reset') || output.includes('unable to connect') || output.includes('network is unreachable') || output.includes('ssl error')) {
|
|
898
|
+
return { type: 'NETWORK', retryable: true, description: 'Network connectivity issue' };
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// Authentication/permission errors - not retryable
|
|
902
|
+
if (output.includes('error: 401') || output.includes('error: 403') || output.includes('authentication failed') || output.includes('permission denied')) {
|
|
903
|
+
return { type: 'PERMISSION', retryable: false, description: 'Authentication or permission error' };
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// Repository not found - not retryable
|
|
907
|
+
if (output.includes('error: 404') || output.includes('not found') || output.includes('repository not found')) {
|
|
908
|
+
return { type: 'NOT_FOUND', retryable: false, description: 'Repository not found' };
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// Rate limiting - retryable with backoff
|
|
912
|
+
if (output.includes('rate limit') || output.includes('too many requests') || output.includes('api rate limit exceeded')) {
|
|
913
|
+
return { type: 'RATE_LIMIT', retryable: true, description: 'Rate limit exceeded' };
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// Default to retryable for unknown errors
|
|
917
|
+
return { type: 'UNKNOWN', retryable: true, description: 'Unknown error' };
|
|
918
|
+
};
|
|
919
|
+
|
|
920
|
+
// Clone repository and set up remotes with retry mechanism
|
|
888
921
|
export const cloneRepository = async (repoToClone, tempDir, argv, owner, repo) => {
|
|
889
|
-
|
|
890
|
-
|
|
922
|
+
const maxRetries = 3;
|
|
923
|
+
const baseDelay = 2000; // Start with 2 seconds
|
|
891
924
|
|
|
892
|
-
|
|
893
|
-
const cloneResult = await $`gh repo clone ${repoToClone} ${tempDir} 2>&1`;
|
|
925
|
+
await log(`\n${formatAligned('📥', 'Cloning repository:', repoToClone)}`);
|
|
894
926
|
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
await log('');
|
|
899
|
-
await log(`${formatAligned('❌', 'CLONE FAILED', '')}`, { level: 'error' });
|
|
900
|
-
await log('');
|
|
901
|
-
await log(' 🔍 What happened:');
|
|
902
|
-
await log(` Failed to clone repository ${repoToClone}`);
|
|
903
|
-
await log('');
|
|
904
|
-
await log(' 📦 Error details:');
|
|
905
|
-
for (const line of errorOutput.split('\n')) {
|
|
906
|
-
if (line.trim()) await log(` ${line}`);
|
|
927
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
928
|
+
if (attempt > 1) {
|
|
929
|
+
await log(`${formatAligned('⏳', 'Clone attempt:', `${attempt}/${maxRetries} (with retry logic)`)}`);
|
|
907
930
|
}
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
await
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
if (
|
|
914
|
-
await log('
|
|
931
|
+
|
|
932
|
+
// Use 2>&1 to capture all output and filter "Cloning into" message
|
|
933
|
+
const cloneResult = await $`gh repo clone ${repoToClone} ${tempDir} 2>&1`;
|
|
934
|
+
|
|
935
|
+
// Verify clone was successful
|
|
936
|
+
if (cloneResult.code === 0) {
|
|
937
|
+
await log(`${formatAligned('✅', 'Cloned to:', tempDir)}`);
|
|
938
|
+
|
|
939
|
+
// Verify and fix remote configuration
|
|
940
|
+
const remoteCheckResult = await $({ cwd: tempDir })`git remote -v 2>&1`;
|
|
941
|
+
if (!remoteCheckResult.stdout || !remoteCheckResult.stdout.toString().includes('origin')) {
|
|
942
|
+
await log(' Setting up git remote...', { verbose: true });
|
|
943
|
+
// Add origin remote manually
|
|
944
|
+
await $({ cwd: tempDir })`git remote add origin https://github.com/${repoToClone}.git 2>&1`;
|
|
945
|
+
}
|
|
946
|
+
return; // Success - exit function
|
|
915
947
|
}
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
948
|
+
|
|
949
|
+
// Clone failed - analyze error and determine if retry is appropriate
|
|
950
|
+
const errorOutput = (cloneResult.stderr || cloneResult.stdout || 'Unknown error').toString().trim();
|
|
951
|
+
|
|
952
|
+
const errorClassification = classifyCloneError(errorOutput);
|
|
953
|
+
|
|
954
|
+
if (!errorClassification.retryable || attempt === maxRetries) {
|
|
955
|
+
// Non-retryable error or max retries reached - fail with detailed error
|
|
956
|
+
await log('');
|
|
957
|
+
await log(`${formatAligned('❌', 'CLONE FAILED', '')}`, { level: 'error' });
|
|
958
|
+
await log('');
|
|
959
|
+
await log(' 🔍 What happened:');
|
|
960
|
+
await log(` Failed to clone repository ${repoToClone}`);
|
|
961
|
+
|
|
962
|
+
if (!errorClassification.retryable) {
|
|
963
|
+
await log(` Error type: ${errorClassification.description} (not retryable)`);
|
|
964
|
+
} else {
|
|
965
|
+
await log(` Error type: ${errorClassification.description} (max retries exceeded)`);
|
|
966
|
+
}
|
|
967
|
+
await log('');
|
|
968
|
+
await log(' 📦 Error details:');
|
|
969
|
+
for (const line of errorOutput.split('\n')) {
|
|
970
|
+
if (line.trim()) await log(` ${line}`);
|
|
971
|
+
}
|
|
972
|
+
await log('');
|
|
973
|
+
await log(' 💡 Common causes:');
|
|
974
|
+
await log(" • Repository doesn't exist or is private");
|
|
975
|
+
await log(' • No GitHub authentication');
|
|
976
|
+
await log(' • Network connectivity issues');
|
|
977
|
+
if (errorClassification.type === 'TRANSIENT') {
|
|
978
|
+
await log(' • GitHub server issues (temporary)');
|
|
979
|
+
}
|
|
980
|
+
if (errorClassification.type === 'RATE_LIMIT') {
|
|
981
|
+
await log(' • API rate limiting exceeded');
|
|
982
|
+
}
|
|
983
|
+
if (argv.fork) {
|
|
984
|
+
await log(' • Fork not ready yet (try again in a moment)');
|
|
985
|
+
}
|
|
986
|
+
await log('');
|
|
987
|
+
await log(' 🔧 How to fix:');
|
|
988
|
+
await log(' 1. Check authentication: gh auth status');
|
|
989
|
+
await log(' 2. Login if needed: gh auth login');
|
|
990
|
+
await log(` 3. Verify access: gh repo view ${owner}/${repo}`);
|
|
991
|
+
if (argv.fork) {
|
|
992
|
+
await log(` 4. Check fork: gh repo view ${repoToClone}`);
|
|
993
|
+
}
|
|
994
|
+
if (errorClassification.type === 'TRANSIENT') {
|
|
995
|
+
await log(' 5. Wait a few minutes and retry (GitHub server issue)');
|
|
996
|
+
await log(' 6. Check GitHub status: https://www.githubstatus.com');
|
|
997
|
+
}
|
|
998
|
+
if (errorClassification.type === 'RATE_LIMIT') {
|
|
999
|
+
await log(' 5. Wait for rate limit to reset (check your quota)');
|
|
1000
|
+
await log(' 6. Use --token flag with different token if available');
|
|
1001
|
+
}
|
|
1002
|
+
await log('');
|
|
1003
|
+
await safeExit(1, 'Repository setup failed');
|
|
923
1004
|
}
|
|
924
|
-
await log('');
|
|
925
|
-
await safeExit(1, 'Repository setup failed');
|
|
926
|
-
}
|
|
927
1005
|
|
|
928
|
-
|
|
1006
|
+
// Retryable error and we have attempts left
|
|
1007
|
+
const delay = baseDelay * Math.pow(2, attempt - 1); // Exponential backoff
|
|
1008
|
+
await log(`${formatAligned('⚠️', 'Clone failed:', errorClassification.description)}`);
|
|
1009
|
+
await log(`${formatAligned('⏳', 'Retrying:', `Waiting ${delay / 1000}s before attempt ${attempt + 1}/${maxRetries}...`)}`);
|
|
1010
|
+
|
|
1011
|
+
if (errorClassification.type === 'RATE_LIMIT') {
|
|
1012
|
+
await log(' 💡 Tip: Rate limiting detected - using longer delay');
|
|
1013
|
+
}
|
|
929
1014
|
|
|
930
|
-
|
|
931
|
-
const remoteCheckResult = await $({ cwd: tempDir })`git remote -v 2>&1`;
|
|
932
|
-
if (!remoteCheckResult.stdout || !remoteCheckResult.stdout.toString().includes('origin')) {
|
|
933
|
-
await log(' Setting up git remote...', { verbose: true });
|
|
934
|
-
// Add origin remote manually
|
|
935
|
-
await $({ cwd: tempDir })`git remote add origin https://github.com/${repoToClone}.git 2>&1`;
|
|
1015
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
936
1016
|
}
|
|
1017
|
+
|
|
1018
|
+
// This should never be reached due to the loop logic above
|
|
1019
|
+
await log(`${formatAligned('❌', 'UNEXPECTED ERROR:', 'Clone logic failed')}`);
|
|
1020
|
+
await safeExit(1, 'Repository setup failed');
|
|
937
1021
|
};
|
|
938
1022
|
|
|
939
1023
|
// Set up upstream remote and sync fork
|