init
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,15 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React from 'react';
|
||||
import { Text } from '../../ink.js';
|
||||
export function CheckGitHubStep() {
|
||||
const $ = _c(1);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = <Text>Checking GitHub CLI installation…</Text>;
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsIlRleHQiLCJDaGVja0dpdEh1YlN0ZXAiLCIkIiwiX2MiLCJ0MCIsIlN5bWJvbCIsImZvciJdLCJzb3VyY2VzIjpbIkNoZWNrR2l0SHViU3RlcC50c3giXSwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IFJlYWN0IGZyb20gJ3JlYWN0J1xuaW1wb3J0IHsgVGV4dCB9IGZyb20gJy4uLy4uL2luay5qcydcblxuZXhwb3J0IGZ1bmN0aW9uIENoZWNrR2l0SHViU3RlcCgpIHtcbiAgcmV0dXJuIDxUZXh0PkNoZWNraW5nIEdpdEh1YiBDTEkgaW5zdGFsbGF0aW9u4oCmPC9UZXh0PlxufVxuIl0sIm1hcHBpbmdzIjoiO0FBQUEsT0FBT0EsS0FBSyxNQUFNLE9BQU87QUFDekIsU0FBU0MsSUFBSSxRQUFRLGNBQWM7QUFFbkMsT0FBTyxTQUFBQyxnQkFBQTtFQUFBLE1BQUFDLENBQUEsR0FBQUMsRUFBQTtFQUFBLElBQUFDLEVBQUE7RUFBQSxJQUFBRixDQUFBLFFBQUFHLE1BQUEsQ0FBQUMsR0FBQTtJQUNFRixFQUFBLElBQUMsSUFBSSxDQUFDLGlDQUFpQyxFQUF0QyxJQUFJLENBQXlDO0lBQUFGLENBQUEsTUFBQUUsRUFBQTtFQUFBO0lBQUFBLEVBQUEsR0FBQUYsQ0FBQTtFQUFBO0VBQUEsT0FBOUNFLEVBQThDO0FBQUEiLCJpZ25vcmVMaXN0IjpbXX0=
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,13 @@
|
||||
import type { Command } from '../../commands.js'
|
||||
import { isEnvTruthy } from '../../utils/envUtils.js'
|
||||
|
||||
const installGitHubApp = {
|
||||
type: 'local-jsx',
|
||||
name: 'install-github-app',
|
||||
description: 'Set up Claude GitHub Actions for a repository',
|
||||
availability: ['claude-ai', 'console'],
|
||||
isEnabled: () => !isEnvTruthy(process.env.DISABLE_INSTALL_GITHUB_APP_COMMAND),
|
||||
load: () => import('./install-github-app.js'),
|
||||
} satisfies Command
|
||||
|
||||
export default installGitHubApp
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,325 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user