diff --git a/internal/exec/vendor_utils.go b/internal/exec/vendor_utils.go index 64c371977..f4b9e81e8 100644 --- a/internal/exec/vendor_utils.go +++ b/internal/exec/vendor_utils.go @@ -2,6 +2,7 @@ package exec import ( "fmt" + "net/url" "os" "path/filepath" "sort" @@ -372,7 +373,7 @@ func ExecuteAtmosVendorInternal( // Handle GitHub source if isGitHubSource { - u.LogInfo(atmosConfig, fmt.Sprintf("Fetching GitHub source: %s", uri)) + u.LogDebug(atmosConfig, fmt.Sprintf("Fetching GitHub source: %s", uri)) fileContents, err := u.DownloadFileFromGitHub(uri) if err != nil { return fmt.Errorf("failed to download GitHub file: %w", err) @@ -534,17 +535,20 @@ func determineSourceType(uri *string, vendorConfigFilePath string) (bool, bool, sourceIsLocalFile := false isGitHubSource := false + // If not OCI, we proceed with checks if !useOciScheme { - if strings.Contains(*uri, "github.com") { - // Check if the URL is a GitHub source - isGitHubSource = true - } else { - // Handle local file system sources + parsedURL, err := url.Parse(*uri) + if err != nil || parsedURL.Scheme == "" || parsedURL.Host == "" { + // Not a valid URL or no host: consider local filesystem if absPath, err := u.JoinAbsolutePathWithPath(vendorConfigFilePath, *uri); err == nil { uri = &absPath useLocalFileSystem = true sourceIsLocalFile = u.FileExists(*uri) } + } else { + if parsedURL.Host == "github.com" && parsedURL.Scheme == "https" { + isGitHubSource = true + } } } diff --git a/pkg/utils/file_utils.go b/pkg/utils/file_utils.go index a4d881dda..d76285393 100644 --- a/pkg/utils/file_utils.go +++ b/pkg/utils/file_utils.go @@ -245,24 +245,21 @@ func GetFileNameFromURL(rawURL string) (string, error) { // ParseGitHubURL parses a GitHub URL and returns the owner, repo, file path and branch func ParseGitHubURL(rawURL string) (owner, repo, filePath, branch string, err error) { - parsedURL, err := url.Parse(rawURL) + u, err := url.Parse(rawURL) if err != nil { return "", "", "", "", fmt.Errorf("invalid URL: %w", err) } - if !strings.Contains(parsedURL.Host, "github.com") { - return "", "", "", "", fmt.Errorf("URL is not a GitHub URL") - } - - pathParts := strings.Split(strings.Trim(parsedURL.Path, "/"), "/") - if len(pathParts) < 4 || pathParts[2] != "blob" { - return "", "", "", "", fmt.Errorf("URL format not supported. Expected: /owner/repo/blob/branch/filepath") + // Expected format: https://github.com/owner/repo/blob/branch/path/to/file + parts := strings.Split(u.Path, "/") + if len(parts) < 5 || (parts[3] != "blob" && parts[3] != "raw") { + return "", "", "", "", fmt.Errorf("invalid GitHub URL format") } - owner = pathParts[0] - repo = pathParts[1] - branch = pathParts[3] - filePath = strings.Join(pathParts[4:], "/") + owner = parts[1] + repo = parts[2] + branch = parts[4] + filePath = strings.Join(parts[5:], "/") return owner, repo, filePath, branch, nil } diff --git a/pkg/utils/github_utils.go b/pkg/utils/github_utils.go index ade25301b..40fe39c19 100644 --- a/pkg/utils/github_utils.go +++ b/pkg/utils/github_utils.go @@ -2,68 +2,83 @@ package utils import ( "context" + "encoding/base64" "fmt" - "io" - "net/http" "os" "time" "github.com/google/go-github/v59/github" + "golang.org/x/oauth2" ) -// GetLatestGitHubRepoRelease returns the latest release tag for a GitHub repository -func GetLatestGitHubRepoRelease(owner string, repo string) (string, error) { - opt := &github.ListOptions{Page: 1, PerPage: 1} - client := github.NewClient(nil) +// newGitHubClient creates a new GitHub client. If a token is provided, it returns an authenticated client; +// otherwise, it returns an unauthenticated client. +func newGitHubClient(ctx context.Context) *github.Client { + githubToken := os.Getenv("GITHUB_TOKEN") + if githubToken == "" { + return github.NewClient(nil) + } - ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) + // Token found, create an authenticated client + ts := oauth2.StaticTokenSource( + &oauth2.Token{AccessToken: githubToken}, + ) + tc := oauth2.NewClient(ctx, ts) + + return github.NewClient(tc) +} + +// GetLatestGitHubRepoRelease returns the latest release tag for a GitHub repository. +func GetLatestGitHubRepoRelease(owner string, repo string) (string, error) { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() + client := newGitHubClient(ctx) + opt := &github.ListOptions{Page: 1, PerPage: 1} + releases, _, err := client.Repositories.ListReleases(ctx, owner, repo, opt) if err != nil { - return "", err + return "", fmt.Errorf("failed to list releases: %w", err) } - if len(releases) > 0 { - latestRelease := releases[0] - latestReleaseTag := *latestRelease.TagName - return latestReleaseTag, nil + if len(releases) > 0 && releases[0].TagName != nil { + return *releases[0].TagName, nil } return "", nil } -// ParseGitHubURL parses a GitHub URL and returns the owner, repo, file path and branch +// DownloadFileFromGitHub downloads a file from a GitHub repository using the GitHub API. func DownloadFileFromGitHub(rawURL string) ([]byte, error) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + owner, repo, filePath, branch, err := ParseGitHubURL(rawURL) if err != nil { return nil, fmt.Errorf("failed to parse GitHub URL: %w", err) } - githubToken := os.Getenv("GITHUB_TOKEN") - if githubToken == "" { - return nil, fmt.Errorf("GITHUB_TOKEN is not set") - } + client := newGitHubClient(ctx) - apiURL := fmt.Sprintf("https://api.github.com/repos/%s/%s/contents/%s?ref=%s", owner, repo, filePath, branch) - req, err := http.NewRequest("GET", apiURL, nil) + // Get the file content + opt := &github.RepositoryContentGetOptions{Ref: branch} + fileContent, _, _, err := client.Repositories.GetContents(ctx, owner, repo, filePath, opt) if err != nil { - return nil, fmt.Errorf("failed to create request: %w", err) + return nil, fmt.Errorf("failed to get file content from GitHub: %w", err) + } + if fileContent == nil { + return nil, fmt.Errorf("no content returned for the requested file") } - req.Header.Set("Authorization", "Bearer "+githubToken) - req.Header.Set("Accept", "application/vnd.github.v3.raw") - - client := &http.Client{} - resp, err := client.Do(req) + // Decode the base64 encoded content + content, err := fileContent.GetContent() if err != nil { - return nil, fmt.Errorf("failed to perform request: %w", err) + return nil, fmt.Errorf("failed to get file content: %w", err) } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("failed to download file: %s", resp.Status) + data, err := base64.StdEncoding.DecodeString(content) + if err != nil { + return nil, fmt.Errorf("failed to decode file content: %w", err) } - return io.ReadAll(resp.Body) + return data, nil }