setupGitHubActions.ts
commands/install-github-app/setupGitHubActions.ts
326
Lines
10168
Bytes
1
Exports
7
Imports
10
Keywords
What this is
This page documents one file from the repository and includes its full source so you can read it without leaving the docs site.
Beginner explanation
This file lives in the command layer. It likely turns a user action into concrete program behavior.
How it is used
Start from the exports list and related files. Those are the easiest clues for where this file fits into the system.
Expert explanation
Architecturally, this file intersects with commands, integrations. It contains 326 lines, 7 detected imports, and 1 detected exports.
Important relationships
- commands/install-github-app/ApiKeyStep.tsx
- commands/install-github-app/CheckExistingSecretStep.tsx
- commands/install-github-app/CheckGitHubStep.tsx
- commands/install-github-app/ChooseRepoStep.tsx
- commands/install-github-app/CreatingStep.tsx
- commands/install-github-app/ErrorStep.tsx
- commands/install-github-app/ExistingWorkflowStep.tsx
- commands/install-github-app/InstallAppStep.tsx
Detected exports
setupGitHubActions
Keywords
codeworkflowcontextreponamelogeventanalyticsmetadata_i_verified_this_is_not_code_or_filepathsexecfilenothrowsecretnameanthropic_api_keyincludes
Detected imports
src/services/analytics/index.jssrc/utils/config.js../../constants/github-app.js../../utils/browser.js../../utils/execFileNoThrow.js../../utils/log.js./types.js
Source notes
This page embeds the full file contents. Small or leaf files are still indexed honestly instead of being over-explained.
Full source
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
}
}