326 lines
9.9 KiB
TypeScript
326 lines
9.9 KiB
TypeScript
import {
|
|
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
logEvent,
|
|
} from 'src/services/analytics/index.js'
|
|
import { saveGlobalConfig } from 'src/utils/config.js'
|
|
import {
|
|
CODE_REVIEW_PLUGIN_WORKFLOW_CONTENT,
|
|
PR_BODY,
|
|
PR_TITLE,
|
|
WORKFLOW_CONTENT,
|
|
} from '../../constants/github-app.js'
|
|
import { openBrowser } from '../../utils/browser.js'
|
|
import { execFileNoThrow } from '../../utils/execFileNoThrow.js'
|
|
import { logError } from '../../utils/log.js'
|
|
import type { Workflow } from './types.js'
|
|
|
|
async function createWorkflowFile(
|
|
repoName: string,
|
|
branchName: string,
|
|
workflowPath: string,
|
|
workflowContent: string,
|
|
secretName: string,
|
|
message: string,
|
|
context?: {
|
|
useCurrentRepo?: boolean
|
|
workflowExists?: boolean
|
|
secretExists?: boolean
|
|
},
|
|
): Promise<void> {
|
|
// Check if workflow file already exists
|
|
const checkFileResult = await execFileNoThrow('gh', [
|
|
'api',
|
|
`repos/${repoName}/contents/${workflowPath}`,
|
|
'--jq',
|
|
'.sha',
|
|
])
|
|
|
|
let fileSha: string | null = null
|
|
if (checkFileResult.code === 0) {
|
|
fileSha = checkFileResult.stdout.trim()
|
|
}
|
|
|
|
let content = workflowContent
|
|
if (secretName === 'CLAUDE_CODE_OAUTH_TOKEN') {
|
|
// For OAuth tokens, use the claude_code_oauth_token parameter
|
|
content = workflowContent.replace(
|
|
/anthropic_api_key: \$\{\{ secrets\.ANTHROPIC_API_KEY \}\}/g,
|
|
`claude_code_oauth_token: \${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}`,
|
|
)
|
|
} else if (secretName !== 'ANTHROPIC_API_KEY') {
|
|
// For other custom secret names, keep using anthropic_api_key parameter
|
|
content = workflowContent.replace(
|
|
/anthropic_api_key: \$\{\{ secrets\.ANTHROPIC_API_KEY \}\}/g,
|
|
`anthropic_api_key: \${{ secrets.${secretName} }}`,
|
|
)
|
|
}
|
|
const base64Content = Buffer.from(content).toString('base64')
|
|
|
|
const apiParams = [
|
|
'api',
|
|
'--method',
|
|
'PUT',
|
|
`repos/${repoName}/contents/${workflowPath}`,
|
|
'-f',
|
|
`message=${fileSha ? `"Update ${message}"` : `"${message}"`}`,
|
|
'-f',
|
|
`content=${base64Content}`,
|
|
'-f',
|
|
`branch=${branchName}`,
|
|
]
|
|
|
|
if (fileSha) {
|
|
apiParams.push('-f', `sha=${fileSha}`)
|
|
}
|
|
|
|
const createFileResult = await execFileNoThrow('gh', apiParams)
|
|
if (createFileResult.code !== 0) {
|
|
if (
|
|
createFileResult.stderr.includes('422') &&
|
|
createFileResult.stderr.includes('sha')
|
|
) {
|
|
logEvent('tengu_setup_github_actions_failed', {
|
|
reason:
|
|
'failed_to_create_workflow_file' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
exit_code: createFileResult.code,
|
|
...context,
|
|
})
|
|
throw new Error(
|
|
`Failed to create workflow file ${workflowPath}: A Claude workflow file already exists in this repository. Please remove it first or update it manually.`,
|
|
)
|
|
}
|
|
|
|
logEvent('tengu_setup_github_actions_failed', {
|
|
reason:
|
|
'failed_to_create_workflow_file' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
exit_code: createFileResult.code,
|
|
...context,
|
|
})
|
|
|
|
const helpText =
|
|
'\n\nNeed help? Common issues:\n' +
|
|
'· Permission denied → Run: gh auth refresh -h github.com -s repo,workflow\n' +
|
|
'· Not authorized → Ensure you have admin access to the repository\n' +
|
|
'· For manual setup → Visit: https://github.com/anthropics/claude-code-action'
|
|
|
|
throw new Error(
|
|
`Failed to create workflow file ${workflowPath}: ${createFileResult.stderr}${helpText}`,
|
|
)
|
|
}
|
|
}
|
|
|
|
export async function setupGitHubActions(
|
|
repoName: string,
|
|
apiKeyOrOAuthToken: string | null,
|
|
secretName: string,
|
|
updateProgress: () => void,
|
|
skipWorkflow = false,
|
|
selectedWorkflows: Workflow[],
|
|
authType: 'api_key' | 'oauth_token',
|
|
context?: {
|
|
useCurrentRepo?: boolean
|
|
workflowExists?: boolean
|
|
secretExists?: boolean
|
|
},
|
|
) {
|
|
try {
|
|
logEvent('tengu_setup_github_actions_started', {
|
|
skip_workflow: skipWorkflow,
|
|
has_api_key: !!apiKeyOrOAuthToken,
|
|
using_default_secret_name: secretName === 'ANTHROPIC_API_KEY',
|
|
selected_claude_workflow: selectedWorkflows.includes('claude'),
|
|
selected_claude_review_workflow:
|
|
selectedWorkflows.includes('claude-review'),
|
|
...context,
|
|
})
|
|
|
|
// Check if repository exists
|
|
const repoCheckResult = await execFileNoThrow('gh', [
|
|
'api',
|
|
`repos/${repoName}`,
|
|
'--jq',
|
|
'.id',
|
|
])
|
|
if (repoCheckResult.code !== 0) {
|
|
logEvent('tengu_setup_github_actions_failed', {
|
|
reason:
|
|
'repo_not_found' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
exit_code: repoCheckResult.code,
|
|
...context,
|
|
})
|
|
throw new Error(
|
|
`Failed to access repository ${repoName}: ${repoCheckResult.stderr}`,
|
|
)
|
|
}
|
|
|
|
// Get default branch
|
|
const defaultBranchResult = await execFileNoThrow('gh', [
|
|
'api',
|
|
`repos/${repoName}`,
|
|
'--jq',
|
|
'.default_branch',
|
|
])
|
|
if (defaultBranchResult.code !== 0) {
|
|
logEvent('tengu_setup_github_actions_failed', {
|
|
reason:
|
|
'failed_to_get_default_branch' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
exit_code: defaultBranchResult.code,
|
|
...context,
|
|
})
|
|
throw new Error(
|
|
`Failed to get default branch: ${defaultBranchResult.stderr}`,
|
|
)
|
|
}
|
|
const defaultBranch = defaultBranchResult.stdout.trim()
|
|
|
|
// Get SHA of default branch
|
|
const shaResult = await execFileNoThrow('gh', [
|
|
'api',
|
|
`repos/${repoName}/git/ref/heads/${defaultBranch}`,
|
|
'--jq',
|
|
'.object.sha',
|
|
])
|
|
if (shaResult.code !== 0) {
|
|
logEvent('tengu_setup_github_actions_failed', {
|
|
reason:
|
|
'failed_to_get_branch_sha' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
exit_code: shaResult.code,
|
|
...context,
|
|
})
|
|
throw new Error(`Failed to get branch SHA: ${shaResult.stderr}`)
|
|
}
|
|
const sha = shaResult.stdout.trim()
|
|
|
|
let branchName: string | null = null
|
|
|
|
if (!skipWorkflow) {
|
|
updateProgress()
|
|
// Create new branch
|
|
branchName = `add-claude-github-actions-${Date.now()}`
|
|
const createBranchResult = await execFileNoThrow('gh', [
|
|
'api',
|
|
'--method',
|
|
'POST',
|
|
`repos/${repoName}/git/refs`,
|
|
'-f',
|
|
`ref=refs/heads/${branchName}`,
|
|
'-f',
|
|
`sha=${sha}`,
|
|
])
|
|
if (createBranchResult.code !== 0) {
|
|
logEvent('tengu_setup_github_actions_failed', {
|
|
reason:
|
|
'failed_to_create_branch' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
exit_code: createBranchResult.code,
|
|
...context,
|
|
})
|
|
throw new Error(`Failed to create branch: ${createBranchResult.stderr}`)
|
|
}
|
|
|
|
updateProgress()
|
|
// Create selected workflow files
|
|
const workflows = []
|
|
|
|
if (selectedWorkflows.includes('claude')) {
|
|
workflows.push({
|
|
path: '.github/workflows/claude.yml',
|
|
content: WORKFLOW_CONTENT,
|
|
message: 'Claude PR Assistant workflow',
|
|
})
|
|
}
|
|
|
|
if (selectedWorkflows.includes('claude-review')) {
|
|
workflows.push({
|
|
path: '.github/workflows/claude-code-review.yml',
|
|
content: CODE_REVIEW_PLUGIN_WORKFLOW_CONTENT,
|
|
message: 'Claude Code Review workflow',
|
|
})
|
|
}
|
|
|
|
for (const workflow of workflows) {
|
|
await createWorkflowFile(
|
|
repoName,
|
|
branchName,
|
|
workflow.path,
|
|
workflow.content,
|
|
secretName,
|
|
workflow.message,
|
|
context,
|
|
)
|
|
}
|
|
}
|
|
|
|
updateProgress()
|
|
// Set the API key as a secret if provided
|
|
if (apiKeyOrOAuthToken) {
|
|
const setSecretResult = await execFileNoThrow('gh', [
|
|
'secret',
|
|
'set',
|
|
secretName,
|
|
'--body',
|
|
apiKeyOrOAuthToken,
|
|
'--repo',
|
|
repoName,
|
|
])
|
|
if (setSecretResult.code !== 0) {
|
|
logEvent('tengu_setup_github_actions_failed', {
|
|
reason:
|
|
'failed_to_set_api_key_secret' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
exit_code: setSecretResult.code,
|
|
...context,
|
|
})
|
|
|
|
const helpText =
|
|
'\n\nNeed help? Common issues:\n' +
|
|
'· Permission denied → Run: gh auth refresh -h github.com -s repo\n' +
|
|
'· Not authorized → Ensure you have admin access to the repository\n' +
|
|
'· For manual setup → Visit: https://github.com/anthropics/claude-code-action'
|
|
|
|
throw new Error(
|
|
`Failed to set API key secret: ${setSecretResult.stderr || 'Unknown error'}${helpText}`,
|
|
)
|
|
}
|
|
}
|
|
|
|
if (!skipWorkflow && branchName) {
|
|
updateProgress()
|
|
// Create PR template URL instead of creating PR directly
|
|
const compareUrl = `https://github.com/${repoName}/compare/${defaultBranch}...${branchName}?quick_pull=1&title=${encodeURIComponent(PR_TITLE)}&body=${encodeURIComponent(PR_BODY)}`
|
|
|
|
await openBrowser(compareUrl)
|
|
}
|
|
|
|
logEvent('tengu_setup_github_actions_completed', {
|
|
skip_workflow: skipWorkflow,
|
|
has_api_key: !!apiKeyOrOAuthToken,
|
|
auth_type:
|
|
authType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
using_default_secret_name: secretName === 'ANTHROPIC_API_KEY',
|
|
selected_claude_workflow: selectedWorkflows.includes('claude'),
|
|
selected_claude_review_workflow:
|
|
selectedWorkflows.includes('claude-review'),
|
|
...context,
|
|
})
|
|
saveGlobalConfig(current => ({
|
|
...current,
|
|
githubActionSetupCount: (current.githubActionSetupCount ?? 0) + 1,
|
|
}))
|
|
} catch (error) {
|
|
if (
|
|
!error ||
|
|
!(error instanceof Error) ||
|
|
!error.message.includes('Failed to')
|
|
) {
|
|
logEvent('tengu_setup_github_actions_failed', {
|
|
reason:
|
|
'unexpected_error' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
...context,
|
|
})
|
|
}
|
|
if (error instanceof Error) {
|
|
logError(error)
|
|
}
|
|
throw error
|
|
}
|
|
}
|