Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions env-cloudrun.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ CONFIG_REPO_BRANCH: "main"

# Secret Manager References
GITHUB_APP_PRIVATE_KEY_SECRET_NAME: "projects/1054147886816/secrets/CODE_COPIER_PEM/versions/latest"
PEM_NAME: "projects/1054147886816/secrets/CODE_COPIER_PEM/versions/latest"
WEBHOOK_SECRET_NAME: "projects/1054147886816/secrets/webhook-secret/versions/latest"
MONGO_URI_SECRET_NAME: "projects/1054147886816/secrets/mongo-uri/versions/latest"

Expand Down
6 changes: 6 additions & 0 deletions services/config_loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"
"os"
"strings"

"github.com/google/go-github/v48/github"
"gopkg.in/yaml.v3"
Expand Down Expand Up @@ -91,6 +92,11 @@ func retrieveConfigFileContent(ctx context.Context, filePath string, config *con
},
)
if err != nil {
// Check if this is an authentication error
errStr := err.Error()
if strings.Contains(errStr, "401") || strings.Contains(errStr, "Bad credentials") {
return "", fmt.Errorf("GITHUB APP AUTHENTICATION FAILED: Unable to fetch config file due to authentication error. The GitHub App private key (PEM) may be invalid or expired. Please check the CODE_COPIER_PEM secret in GCP Secret Manager. Original error: %w", err)
}
return "", fmt.Errorf("failed to get config file: %w", err)
}
if fileContent == nil {
Expand Down
44 changes: 44 additions & 0 deletions services/github_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,9 @@ func getInstallationAccessToken(installationId, jwtToken string, hc *http.Client

if resp.StatusCode != http.StatusCreated {
b, _ := io.ReadAll(resp.Body)
if resp.StatusCode == http.StatusUnauthorized {
return "", fmt.Errorf("GITHUB APP AUTHENTICATION FAILED (401): Failed to get installation access token. The GitHub App private key (PEM) may be invalid or expired. Please check the CODE_COPIER_PEM secret in GCP Secret Manager. Response: %s", string(b))
}
return "", fmt.Errorf("status %d: %s", resp.StatusCode, string(b))
}
var out struct {
Expand Down Expand Up @@ -287,6 +290,44 @@ func GetGraphQLClient() (*graphql.Client, error) {
return client, nil
}

// GetGraphQLClientForOrg returns a GitHub GraphQL API client authenticated for a specific organization
func GetGraphQLClientForOrg(org string) (*graphql.Client, error) {
// Check if we have a cached token for this org
if token, ok := installationTokenCache[org]; ok && token != "" {
client := graphql.NewClient("https://api.github.com/graphql", &http.Client{
Transport: &transport{token: token},
})
return client, nil
}

// Get installation ID for the organization
installationID, err := getInstallationIDForOrg(org)
if err != nil {
return nil, fmt.Errorf("failed to get installation ID for org %s: %w", org, err)
}

// Get JWT token
token, err := getOrRefreshJWT()
if err != nil {
return nil, fmt.Errorf("failed to get JWT: %w", err)
}

// Get installation access token
installationToken, err := getInstallationAccessToken(installationID, token, HTTPClient)
if err != nil {
return nil, fmt.Errorf("failed to get installation token for org %s: %w", org, err)
}

// Cache the token
installationTokenCache[org] = installationToken

// Create and return client
client := graphql.NewClient("https://api.github.com/graphql", &http.Client{
Transport: &transport{token: installationToken},
})
return client, nil
}

// getOrRefreshJWT returns a valid JWT token, generating a new one if expired
func getOrRefreshJWT() (string, error) {
// Check if we have a valid cached JWT
Expand Down Expand Up @@ -345,6 +386,9 @@ func getInstallationIDForOrg(org string) (string, error) {

if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode == http.StatusUnauthorized {
return "", fmt.Errorf("GITHUB APP AUTHENTICATION FAILED (401): The GitHub App private key (PEM) may be invalid or expired. Please check the CODE_COPIER_PEM secret in GCP Secret Manager. Response: %s", string(body))
}
return "", fmt.Errorf("GET %s: %d %s %s", url, resp.StatusCode, resp.Status, body)
}

Expand Down
5 changes: 3 additions & 2 deletions services/github_read.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@ func GetFilesChangedInPr(owner string, repo string, pr_number int) ([]ChangedFil
}
}

client, err := GetGraphQLClient()
// Use org-specific client to ensure we have the right installation token
client, err := GetGraphQLClientForOrg(owner)
if err != nil {
return nil, fmt.Errorf("failed to get GraphQL client: %w", err)
return nil, fmt.Errorf("failed to get GraphQL client for org %s: %w", owner, err)
}
ctx := context.Background()

Expand Down
28 changes: 26 additions & 2 deletions services/github_write_to_target.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,25 @@ func normalizeRepoName(repoName string) string {
return repoOwner() + "/" + repoName
}

// normalizeRefPath ensures a ref path is in the correct format for different GitHub API calls.
// For GetRef: expects "heads/main" (no "refs/" prefix)
// For UpdateRef: expects "refs/heads/main" (full ref path)
func normalizeRefPath(branchPath string, fullPath bool) string {
// Strip "refs/" prefix if present
refPath := strings.TrimPrefix(branchPath, "refs/")

// Ensure "heads/" prefix exists (unless it's a tag)
if !strings.HasPrefix(refPath, "heads/") && !strings.HasPrefix(refPath, "tags/") {
refPath = "heads/" + refPath
}

// Add "refs/" prefix back if full path is needed
if fullPath {
return "refs/" + refPath
}
return refPath
}

// AddFilesToTargetRepoBranch uploads files to the target repository branch
// using the specified commit strategy (direct or via pull request).
func AddFilesToTargetRepoBranch() {
Expand Down Expand Up @@ -344,8 +363,11 @@ func createCommitTree(ctx context.Context, client *github.Client, targetBranch U

retryDelay := time.Duration(initialRetryDelay) * time.Millisecond

// GetRef expects "heads/main" format (no "refs/" prefix)
refPath := normalizeRefPath(targetBranch.BranchPath, false)

for attempt := 1; attempt <= maxRetries; attempt++ {
ref, _, err = client.Git.GetRef(ctx, owner, repoName, targetBranch.BranchPath)
ref, _, err = client.Git.GetRef(ctx, owner, repoName, refPath)
if err == nil && ref != nil {
break // Success
}
Expand Down Expand Up @@ -405,8 +427,10 @@ func createCommit(ctx context.Context, client *github.Client, targetBranch Uploa
}

// Update branch ref directly (no second GET)
// UpdateRef expects full ref path "refs/heads/main"
fullRefPath := normalizeRefPath(targetBranch.BranchPath, true)
ref := &github.Reference{
Ref: github.String(targetBranch.BranchPath), // e.g., "refs/heads/main"
Ref: github.String(fullRefPath),
Object: &github.GitObject{SHA: github.String(newCommit.GetSHA())},
}
if _, _, err := client.Git.UpdateRef(ctx, owner, repoName, ref, false); err != nil {
Expand Down
5 changes: 5 additions & 0 deletions services/main_config_loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ func (mcl *DefaultMainConfigLoader) LoadMainConfig(ctx context.Context, config *
// Fall back to fetching from repository
content, err = retrieveConfigFileContent(ctx, configFile, config)
if err != nil {
// Check if this is an authentication error and make it more prominent
errStr := err.Error()
if strings.Contains(errStr, "GITHUB APP AUTHENTICATION FAILED") || strings.Contains(errStr, "401") || strings.Contains(errStr, "Bad credentials") {
return nil, fmt.Errorf("GITHUB APP AUTHENTICATION FAILED: Unable to retrieve main config file. The GitHub App private key (PEM) may be invalid or expired. Please check the CODE_COPIER_PEM secret in GCP Secret Manager and redeploy the service. Original error: %w", err)
}
return nil, fmt.Errorf("failed to retrieve main config file: %w", err)
}
}
Expand Down
6 changes: 5 additions & 1 deletion services/webhook_handler_new.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,11 @@ func simpleVerifySignature(sigHeader string, body, secret []byte) bool {

// RetrieveFileContentsWithConfigAndBranch fetches file contents from a specific branch
func RetrieveFileContentsWithConfigAndBranch(ctx context.Context, filePath string, branch string, repoOwner string, repoName string) (*github.RepositoryContent, error) {
client := GetRestClient()
// Use org-specific client to ensure we have the right installation token
client, err := GetRestClientForOrg(repoOwner)
if err != nil {
return nil, fmt.Errorf("failed to get GitHub client for org %s: %w", repoOwner, err)
}

fileContent, _, _, err := client.Repositories.GetContents(
ctx,
Expand Down