44package limatmpl
55
66import (
7+ "cmp"
78 "context"
9+ "encoding/json"
810 "errors"
911 "fmt"
1012 "io"
@@ -296,6 +298,109 @@ func InstNameFromYAMLPath(yamlPath string) (string, error) {
296298 return s , nil
297299}
298300
301+ // transformGitHubURL transforms a github: URL to a raw.githubusercontent.com URL.
302+ // Input format: ORG/REPO[/PATH][@BRANCH]
303+ //
304+ // Examples:
305+ // - github:lima-vm/lima -> https://raw.githubusercontent.com/lima-vm/lima/master/lima.yaml
306+ // - github:lima-vm/lima/examples -> https://raw.githubusercontent.com/lima-vm/lima/master/examples/lima.yaml
307+ // - github:lima-vm/[email protected] -> https://raw.githubusercontent.com/lima-vm/lima/v1.0.0/lima.yaml 308+ // - github:lima-vm/lima/examples/docker.yaml -> https://raw.githubusercontent.com/lima-vm/lima/master/examples/docker.yaml
309+ func transformGitHubURL (ctx context.Context , input string ) (string , error ) {
310+ // Check for explicit branch specification with @ at the end
311+ var branch string
312+ if idx := strings .LastIndex (input , "@" ); idx != - 1 {
313+ branch = input [idx + 1 :]
314+ input = input [:idx ]
315+ }
316+
317+ parts := strings .Split (input , "/" )
318+ if len (parts ) < 2 {
319+ return "" , fmt .Errorf ("github: URL must be at least ORG/REPO, got %q" , input )
320+ }
321+
322+ org := parts [0 ]
323+ repo := parts [1 ]
324+
325+ // Extract path (everything after ORG/REPO)
326+ var pathPart string
327+ if len (parts ) > 2 {
328+ pathPart = strings .Join (parts [2 :], "/" )
329+ } else {
330+ pathPart = "lima"
331+ }
332+
333+ // If path ends with /, it's a directory, so append lima
334+ if strings .HasSuffix (pathPart , "/" ) {
335+ pathPart += "lima"
336+ }
337+
338+ // If the filename (last component) has no extension, add .yaml
339+ filename := path .Base (pathPart )
340+ if ! strings .Contains (filename , "." ) {
341+ pathPart += ".yaml"
342+ }
343+
344+ // Query default branch if no branch was specified
345+ if branch == "" {
346+ var err error
347+ branch , err = getGitHubDefaultBranch (ctx , org , repo )
348+ if err != nil {
349+ return "" , fmt .Errorf ("failed to get default branch for %s/%s: %w" , org , repo , err )
350+ }
351+ }
352+
353+ return fmt .Sprintf ("https://raw.githubusercontent.com/%s/%s/%s/%s" , org , repo , branch , pathPart ), nil
354+ }
355+
356+ // getGitHubDefaultBranch queries the GitHub API to get the default branch for a repository.
357+ func getGitHubDefaultBranch (ctx context.Context , org , repo string ) (string , error ) {
358+ apiURL := fmt .Sprintf ("https://api.github.com/repos/%s/%s" , org , repo )
359+
360+ req , err := http .NewRequestWithContext (ctx , http .MethodGet , apiURL , http .NoBody )
361+ if err != nil {
362+ return "" , fmt .Errorf ("failed to create request: %w" , err )
363+ }
364+
365+ req .Header .Set ("User-Agent" , "lima" )
366+ req .Header .Set ("Accept" , "application/vnd.github.v3+json" )
367+
368+ // Check for GitHub token in environment for authenticated requests (higher rate limit)
369+ token := cmp .Or (os .Getenv ("GH_TOKEN" ), os .Getenv ("GITHUB_TOKEN" ))
370+ if token != "" {
371+ req .Header .Set ("Authorization" , "token " + token )
372+ }
373+
374+ resp , err := http .DefaultClient .Do (req )
375+ if err != nil {
376+ return "" , fmt .Errorf ("failed to query GitHub API: %w" , err )
377+ }
378+ defer resp .Body .Close ()
379+
380+ body , err := io .ReadAll (resp .Body )
381+ if err != nil {
382+ return "" , fmt .Errorf ("failed to read GitHub API response: %w" , err )
383+ }
384+
385+ if resp .StatusCode != http .StatusOK {
386+ return "" , fmt .Errorf ("GitHub API returned status %d: %s" , resp .StatusCode , string (body ))
387+ }
388+
389+ var repoData struct {
390+ DefaultBranch string `json:"default_branch"`
391+ }
392+
393+ if err := json .Unmarshal (body , & repoData ); err != nil {
394+ return "" , fmt .Errorf ("failed to parse GitHub API response: %w" , err )
395+ }
396+
397+ if repoData .DefaultBranch == "" {
398+ return "" , fmt .Errorf ("repository %s/%s has no default branch" , org , repo )
399+ }
400+
401+ return repoData .DefaultBranch , nil
402+ }
403+
299404func TransformCustomURL (ctx context.Context , locator string ) (string , error ) {
300405 u , err := url .Parse (locator )
301406 if err != nil || len (u .Scheme ) <= 1 {
@@ -312,6 +417,15 @@ func TransformCustomURL(ctx context.Context, locator string) (string, error) {
312417 return newLocator , nil
313418 }
314419
420+ if u .Scheme == "github" {
421+ newLocator , err := transformGitHubURL (ctx , u .Opaque )
422+ if err != nil {
423+ return "" , err
424+ }
425+ logrus .Debugf ("GitHub locator %q replaced with %q" , locator , newLocator )
426+ return newLocator , nil
427+ }
428+
315429 plugin , err := plugins .Find ("url-" + u .Scheme )
316430 if err != nil {
317431 return "" , err
0 commit comments