source dump of claude code
at main 325 lines 10 kB view raw
1import { 2 type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 3 logEvent, 4} from 'src/services/analytics/index.js' 5import { saveGlobalConfig } from 'src/utils/config.js' 6import { 7 CODE_REVIEW_PLUGIN_WORKFLOW_CONTENT, 8 PR_BODY, 9 PR_TITLE, 10 WORKFLOW_CONTENT, 11} from '../../constants/github-app.js' 12import { openBrowser } from '../../utils/browser.js' 13import { execFileNoThrow } from '../../utils/execFileNoThrow.js' 14import { logError } from '../../utils/log.js' 15import type { Workflow } from './types.js' 16 17async function createWorkflowFile( 18 repoName: string, 19 branchName: string, 20 workflowPath: string, 21 workflowContent: string, 22 secretName: string, 23 message: string, 24 context?: { 25 useCurrentRepo?: boolean 26 workflowExists?: boolean 27 secretExists?: boolean 28 }, 29): Promise<void> { 30 // Check if workflow file already exists 31 const checkFileResult = await execFileNoThrow('gh', [ 32 'api', 33 `repos/${repoName}/contents/${workflowPath}`, 34 '--jq', 35 '.sha', 36 ]) 37 38 let fileSha: string | null = null 39 if (checkFileResult.code === 0) { 40 fileSha = checkFileResult.stdout.trim() 41 } 42 43 let content = workflowContent 44 if (secretName === 'CLAUDE_CODE_OAUTH_TOKEN') { 45 // For OAuth tokens, use the claude_code_oauth_token parameter 46 content = workflowContent.replace( 47 /anthropic_api_key: \$\{\{ secrets\.ANTHROPIC_API_KEY \}\}/g, 48 `claude_code_oauth_token: \${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}`, 49 ) 50 } else if (secretName !== 'ANTHROPIC_API_KEY') { 51 // For other custom secret names, keep using anthropic_api_key parameter 52 content = workflowContent.replace( 53 /anthropic_api_key: \$\{\{ secrets\.ANTHROPIC_API_KEY \}\}/g, 54 `anthropic_api_key: \${{ secrets.${secretName} }}`, 55 ) 56 } 57 const base64Content = Buffer.from(content).toString('base64') 58 59 const apiParams = [ 60 'api', 61 '--method', 62 'PUT', 63 `repos/${repoName}/contents/${workflowPath}`, 64 '-f', 65 `message=${fileSha ? `"Update ${message}"` : `"${message}"`}`, 66 '-f', 67 `content=${base64Content}`, 68 '-f', 69 `branch=${branchName}`, 70 ] 71 72 if (fileSha) { 73 apiParams.push('-f', `sha=${fileSha}`) 74 } 75 76 const createFileResult = await execFileNoThrow('gh', apiParams) 77 if (createFileResult.code !== 0) { 78 if ( 79 createFileResult.stderr.includes('422') && 80 createFileResult.stderr.includes('sha') 81 ) { 82 logEvent('tengu_setup_github_actions_failed', { 83 reason: 84 'failed_to_create_workflow_file' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 85 exit_code: createFileResult.code, 86 ...context, 87 }) 88 throw new Error( 89 `Failed to create workflow file ${workflowPath}: A Claude workflow file already exists in this repository. Please remove it first or update it manually.`, 90 ) 91 } 92 93 logEvent('tengu_setup_github_actions_failed', { 94 reason: 95 'failed_to_create_workflow_file' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 96 exit_code: createFileResult.code, 97 ...context, 98 }) 99 100 const helpText = 101 '\n\nNeed help? Common issues:\n' + 102 '· Permission denied → Run: gh auth refresh -h github.com -s repo,workflow\n' + 103 '· Not authorized → Ensure you have admin access to the repository\n' + 104 '· For manual setup → Visit: https://github.com/anthropics/claude-code-action' 105 106 throw new Error( 107 `Failed to create workflow file ${workflowPath}: ${createFileResult.stderr}${helpText}`, 108 ) 109 } 110} 111 112export async function setupGitHubActions( 113 repoName: string, 114 apiKeyOrOAuthToken: string | null, 115 secretName: string, 116 updateProgress: () => void, 117 skipWorkflow = false, 118 selectedWorkflows: Workflow[], 119 authType: 'api_key' | 'oauth_token', 120 context?: { 121 useCurrentRepo?: boolean 122 workflowExists?: boolean 123 secretExists?: boolean 124 }, 125) { 126 try { 127 logEvent('tengu_setup_github_actions_started', { 128 skip_workflow: skipWorkflow, 129 has_api_key: !!apiKeyOrOAuthToken, 130 using_default_secret_name: secretName === 'ANTHROPIC_API_KEY', 131 selected_claude_workflow: selectedWorkflows.includes('claude'), 132 selected_claude_review_workflow: 133 selectedWorkflows.includes('claude-review'), 134 ...context, 135 }) 136 137 // Check if repository exists 138 const repoCheckResult = await execFileNoThrow('gh', [ 139 'api', 140 `repos/${repoName}`, 141 '--jq', 142 '.id', 143 ]) 144 if (repoCheckResult.code !== 0) { 145 logEvent('tengu_setup_github_actions_failed', { 146 reason: 147 'repo_not_found' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 148 exit_code: repoCheckResult.code, 149 ...context, 150 }) 151 throw new Error( 152 `Failed to access repository ${repoName}: ${repoCheckResult.stderr}`, 153 ) 154 } 155 156 // Get default branch 157 const defaultBranchResult = await execFileNoThrow('gh', [ 158 'api', 159 `repos/${repoName}`, 160 '--jq', 161 '.default_branch', 162 ]) 163 if (defaultBranchResult.code !== 0) { 164 logEvent('tengu_setup_github_actions_failed', { 165 reason: 166 'failed_to_get_default_branch' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 167 exit_code: defaultBranchResult.code, 168 ...context, 169 }) 170 throw new Error( 171 `Failed to get default branch: ${defaultBranchResult.stderr}`, 172 ) 173 } 174 const defaultBranch = defaultBranchResult.stdout.trim() 175 176 // Get SHA of default branch 177 const shaResult = await execFileNoThrow('gh', [ 178 'api', 179 `repos/${repoName}/git/ref/heads/${defaultBranch}`, 180 '--jq', 181 '.object.sha', 182 ]) 183 if (shaResult.code !== 0) { 184 logEvent('tengu_setup_github_actions_failed', { 185 reason: 186 'failed_to_get_branch_sha' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 187 exit_code: shaResult.code, 188 ...context, 189 }) 190 throw new Error(`Failed to get branch SHA: ${shaResult.stderr}`) 191 } 192 const sha = shaResult.stdout.trim() 193 194 let branchName: string | null = null 195 196 if (!skipWorkflow) { 197 updateProgress() 198 // Create new branch 199 branchName = `add-claude-github-actions-${Date.now()}` 200 const createBranchResult = await execFileNoThrow('gh', [ 201 'api', 202 '--method', 203 'POST', 204 `repos/${repoName}/git/refs`, 205 '-f', 206 `ref=refs/heads/${branchName}`, 207 '-f', 208 `sha=${sha}`, 209 ]) 210 if (createBranchResult.code !== 0) { 211 logEvent('tengu_setup_github_actions_failed', { 212 reason: 213 'failed_to_create_branch' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 214 exit_code: createBranchResult.code, 215 ...context, 216 }) 217 throw new Error(`Failed to create branch: ${createBranchResult.stderr}`) 218 } 219 220 updateProgress() 221 // Create selected workflow files 222 const workflows = [] 223 224 if (selectedWorkflows.includes('claude')) { 225 workflows.push({ 226 path: '.github/workflows/claude.yml', 227 content: WORKFLOW_CONTENT, 228 message: 'Claude PR Assistant workflow', 229 }) 230 } 231 232 if (selectedWorkflows.includes('claude-review')) { 233 workflows.push({ 234 path: '.github/workflows/claude-code-review.yml', 235 content: CODE_REVIEW_PLUGIN_WORKFLOW_CONTENT, 236 message: 'Claude Code Review workflow', 237 }) 238 } 239 240 for (const workflow of workflows) { 241 await createWorkflowFile( 242 repoName, 243 branchName, 244 workflow.path, 245 workflow.content, 246 secretName, 247 workflow.message, 248 context, 249 ) 250 } 251 } 252 253 updateProgress() 254 // Set the API key as a secret if provided 255 if (apiKeyOrOAuthToken) { 256 const setSecretResult = await execFileNoThrow('gh', [ 257 'secret', 258 'set', 259 secretName, 260 '--body', 261 apiKeyOrOAuthToken, 262 '--repo', 263 repoName, 264 ]) 265 if (setSecretResult.code !== 0) { 266 logEvent('tengu_setup_github_actions_failed', { 267 reason: 268 'failed_to_set_api_key_secret' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 269 exit_code: setSecretResult.code, 270 ...context, 271 }) 272 273 const helpText = 274 '\n\nNeed help? Common issues:\n' + 275 '· Permission denied → Run: gh auth refresh -h github.com -s repo\n' + 276 '· Not authorized → Ensure you have admin access to the repository\n' + 277 '· For manual setup → Visit: https://github.com/anthropics/claude-code-action' 278 279 throw new Error( 280 `Failed to set API key secret: ${setSecretResult.stderr || 'Unknown error'}${helpText}`, 281 ) 282 } 283 } 284 285 if (!skipWorkflow && branchName) { 286 updateProgress() 287 // Create PR template URL instead of creating PR directly 288 const compareUrl = `https://github.com/${repoName}/compare/${defaultBranch}...${branchName}?quick_pull=1&title=${encodeURIComponent(PR_TITLE)}&body=${encodeURIComponent(PR_BODY)}` 289 290 await openBrowser(compareUrl) 291 } 292 293 logEvent('tengu_setup_github_actions_completed', { 294 skip_workflow: skipWorkflow, 295 has_api_key: !!apiKeyOrOAuthToken, 296 auth_type: 297 authType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 298 using_default_secret_name: secretName === 'ANTHROPIC_API_KEY', 299 selected_claude_workflow: selectedWorkflows.includes('claude'), 300 selected_claude_review_workflow: 301 selectedWorkflows.includes('claude-review'), 302 ...context, 303 }) 304 saveGlobalConfig(current => ({ 305 ...current, 306 githubActionSetupCount: (current.githubActionSetupCount ?? 0) + 1, 307 })) 308 } catch (error) { 309 if ( 310 !error || 311 !(error instanceof Error) || 312 !error.message.includes('Failed to') 313 ) { 314 logEvent('tengu_setup_github_actions_failed', { 315 reason: 316 'unexpected_error' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 317 ...context, 318 }) 319 } 320 if (error instanceof Error) { 321 logError(error) 322 } 323 throw error 324 } 325}