瀏覽代碼

gitlab repo buildpack detection works

Mohammed Nafees 4 年之前
父節點
當前提交
3c40ae3015

+ 1 - 1
api/server/handlers/gitinstallation/get_buildpack.go

@@ -103,7 +103,7 @@ func (c *GithubGetBuildpackHandler) ServeHTTP(w http.ResponseWriter, r *http.Req
 					return
 				}
 			}()
-			buildpacks.Runtimes[idx].Detect(
+			buildpacks.Runtimes[idx].DetectGithub(
 				client, directoryContents, owner, name, request.Dir, repoContentOptions,
 				builderInfoMap[buildpacks.PaketoBuilder], builderInfoMap[buildpacks.HerokuBuilder],
 			)

+ 39 - 1
api/server/handlers/project_integration/get_gitlab_repo_buildpack.go

@@ -4,6 +4,7 @@ import (
 	"errors"
 	"fmt"
 	"net/http"
+	"sync"
 
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
@@ -132,7 +133,44 @@ func (p *GetGitlabRepoBuildpackHandler) ServeHTTP(w http.ResponseWriter, r *http
 		return
 	}
 
-	p.WriteResult(w, r, tree)
+	builderInfoMap := initBuilderInfo()
+	var wg sync.WaitGroup
+	wg.Add(len(buildpacks.Runtimes))
+	for i := range buildpacks.Runtimes {
+		go func(idx int) {
+			defer func() {
+				if rec := recover(); rec != nil {
+					p.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("panic detected in runtime detection")))
+					return
+				}
+			}()
+			buildpacks.Runtimes[idx].DetectGitlab(
+				client, tree, owner, name, request.Dir, branch,
+				builderInfoMap[buildpacks.PaketoBuilder], builderInfoMap[buildpacks.HerokuBuilder],
+			)
+			wg.Done()
+		}(i)
+	}
+	wg.Wait()
+
+	// FIXME: add Java buildpacks
+	builderInfoMap[buildpacks.PaketoBuilder].Others = append(builderInfoMap[buildpacks.PaketoBuilder].Others,
+		buildpacks.BuildpackInfo{
+			Name:      "Java",
+			Buildpack: "gcr.io/paketo-buildpacks/java",
+		})
+	builderInfoMap[buildpacks.HerokuBuilder].Others = append(builderInfoMap[buildpacks.HerokuBuilder].Others,
+		buildpacks.BuildpackInfo{
+			Name:      "Java",
+			Buildpack: "heroku/java",
+		})
+
+	var builders []*buildpacks.BuilderInfo
+	for _, v := range builderInfoMap {
+		builders = append(builders, v)
+	}
+
+	p.WriteResult(w, r, builders)
 }
 
 func initBuilderInfo() map[string]*buildpacks.BuilderInfo {

+ 91 - 5
internal/integrations/buildpacks/go.go

@@ -4,6 +4,7 @@ import (
 	"sync"
 
 	"github.com/google/go-github/v41/github"
+	"github.com/xanzy/go-gitlab"
 )
 
 type goRuntime struct {
@@ -14,7 +15,7 @@ func NewGoRuntime() Runtime {
 	return &goRuntime{}
 }
 
-func (runtime *goRuntime) detectMod(results chan struct {
+func (runtime *goRuntime) detectModGithub(results chan struct {
 	string
 	bool
 }, directoryContent []*github.RepositoryContent) {
@@ -35,7 +36,28 @@ func (runtime *goRuntime) detectMod(results chan struct {
 	runtime.wg.Done()
 }
 
-func (runtime *goRuntime) detectDep(results chan struct {
+func (runtime *goRuntime) detectModGitlab(results chan struct {
+	string
+	bool
+}, tree []*gitlab.TreeNode) {
+	goModFound := false
+	for i := 0; i < len(tree); i++ {
+		name := tree[i].Name
+		if name == "go.mod" {
+			goModFound = true
+			break
+		}
+	}
+	if goModFound {
+		results <- struct {
+			string
+			bool
+		}{mod, true}
+	}
+	runtime.wg.Done()
+}
+
+func (runtime *goRuntime) detectDepGithub(results chan struct {
 	string
 	bool
 }, directoryContent []*github.RepositoryContent) {
@@ -61,7 +83,33 @@ func (runtime *goRuntime) detectDep(results chan struct {
 	runtime.wg.Done()
 }
 
-func (runtime *goRuntime) Detect(
+func (runtime *goRuntime) detectDepGitlab(results chan struct {
+	string
+	bool
+}, tree []*gitlab.TreeNode) {
+	gopkgFound := false
+	vendorFound := false
+	for i := 0; i < len(tree); i++ {
+		name := tree[i].Name
+		if name == "Gopkg.toml" {
+			gopkgFound = true
+		} else if name == "vendor" && tree[i].Type == "tree" {
+			vendorFound = true
+		}
+		if gopkgFound && vendorFound {
+			break
+		}
+	}
+	if gopkgFound && vendorFound {
+		results <- struct {
+			string
+			bool
+		}{dep, true}
+	}
+	runtime.wg.Done()
+}
+
+func (runtime *goRuntime) DetectGithub(
 	client *github.Client,
 	directoryContent []*github.RepositoryContent,
 	owner, name, path string,
@@ -74,8 +122,46 @@ func (runtime *goRuntime) Detect(
 	}, 2)
 
 	runtime.wg.Add(2)
-	go runtime.detectMod(results, directoryContent)
-	go runtime.detectDep(results, directoryContent)
+	go runtime.detectModGithub(results, directoryContent)
+	go runtime.detectDepGithub(results, directoryContent)
+	runtime.wg.Wait()
+	close(results)
+
+	paketoBuildpackInfo := BuildpackInfo{
+		Name:      "Go",
+		Buildpack: "gcr.io/paketo-buildpacks/go",
+	}
+	herokuBuildpackInfo := BuildpackInfo{
+		Name:      "Go",
+		Buildpack: "heroku/go",
+	}
+
+	if len(results) == 0 {
+		paketo.Others = append(paketo.Others, paketoBuildpackInfo)
+		heroku.Others = append(heroku.Others, herokuBuildpackInfo)
+		return nil
+	}
+
+	paketo.Detected = append(paketo.Detected, paketoBuildpackInfo)
+	heroku.Detected = append(heroku.Detected, herokuBuildpackInfo)
+
+	return nil
+}
+
+func (runtime *goRuntime) DetectGitlab(
+	client *gitlab.Client,
+	tree []*gitlab.TreeNode,
+	owner, name, path, ref string,
+	paketo, heroku *BuilderInfo,
+) error {
+	results := make(chan struct {
+		string
+		bool
+	}, 2)
+
+	runtime.wg.Add(2)
+	go runtime.detectModGitlab(results, tree)
+	go runtime.detectDepGitlab(results, tree)
 	runtime.wg.Wait()
 	close(results)
 

+ 242 - 7
internal/integrations/buildpacks/nodejs.go

@@ -9,6 +9,7 @@ import (
 
 	"github.com/Masterminds/semver/v3"
 	"github.com/google/go-github/v41/github"
+	"github.com/xanzy/go-gitlab"
 )
 
 var (
@@ -28,7 +29,7 @@ func NewNodeRuntime() Runtime {
 	return &nodejsRuntime{}
 }
 
-func (runtime *nodejsRuntime) detectYarn(results chan struct {
+func (runtime *nodejsRuntime) detectYarnGithub(results chan struct {
 	string
 	bool
 }, directoryContent []*github.RepositoryContent) {
@@ -54,7 +55,33 @@ func (runtime *nodejsRuntime) detectYarn(results chan struct {
 	runtime.wg.Done()
 }
 
-func (runtime *nodejsRuntime) detectNPM(results chan struct {
+func (runtime *nodejsRuntime) detectYarnGitlab(results chan struct {
+	string
+	bool
+}, tree []*gitlab.TreeNode) {
+	yarnLockFound := false
+	packageJSONFound := false
+	for i := 0; i < len(tree); i++ {
+		name := tree[i].Name
+		if name == "yarn.lock" {
+			yarnLockFound = true
+		} else if name == "package.json" {
+			packageJSONFound = true
+		}
+		if yarnLockFound && packageJSONFound {
+			break
+		}
+	}
+	if yarnLockFound && packageJSONFound {
+		results <- struct {
+			string
+			bool
+		}{yarn, true}
+	}
+	runtime.wg.Done()
+}
+
+func (runtime *nodejsRuntime) detectNPMGithub(results chan struct {
 	string
 	bool
 }, directoryContent []*github.RepositoryContent) {
@@ -75,7 +102,28 @@ func (runtime *nodejsRuntime) detectNPM(results chan struct {
 	runtime.wg.Done()
 }
 
-func (runtime *nodejsRuntime) detectStandalone(results chan struct {
+func (runtime *nodejsRuntime) detectNPMGitlab(results chan struct {
+	string
+	bool
+}, tree []*gitlab.TreeNode) {
+	packageJSONFound := false
+	for i := 0; i < len(tree); i++ {
+		name := tree[i].Name
+		if name == "package.json" {
+			packageJSONFound = true
+			break
+		}
+	}
+	if packageJSONFound {
+		results <- struct {
+			string
+			bool
+		}{npm, true}
+	}
+	runtime.wg.Done()
+}
+
+func (runtime *nodejsRuntime) detectStandaloneGithub(results chan struct {
 	string
 	bool
 }, directoryContent []*github.RepositoryContent) {
@@ -96,6 +144,27 @@ func (runtime *nodejsRuntime) detectStandalone(results chan struct {
 	runtime.wg.Done()
 }
 
+func (runtime *nodejsRuntime) detectStandaloneGitlab(results chan struct {
+	string
+	bool
+}, tree []*gitlab.TreeNode) {
+	jsFileFound := false
+	for i := 0; i < len(tree); i++ {
+		name := tree[i].Name
+		if name == "server.js" || name == "app.js" || name == "main.js" || name == "index.js" {
+			jsFileFound = true
+			break
+		}
+	}
+	if jsFileFound {
+		results <- struct {
+			string
+			bool
+		}{standalone, true}
+	}
+	runtime.wg.Done()
+}
+
 // copied directly from https://github.com/paketo-buildpacks/node-engine/blob/main/nvmrc_parser.go
 func validateNvmrc(content string) (string, error) {
 	content = strings.TrimSpace(strings.ToLower(content))
@@ -157,7 +226,7 @@ func validateNodeVersion(content string) (string, error) {
 	return content, nil
 }
 
-func (runtime *nodejsRuntime) Detect(
+func (runtime *nodejsRuntime) DetectGithub(
 	client *github.Client,
 	directoryContent []*github.RepositoryContent,
 	owner, name, path string,
@@ -170,9 +239,9 @@ func (runtime *nodejsRuntime) Detect(
 	}, 3)
 
 	runtime.wg.Add(3)
-	go runtime.detectYarn(results, directoryContent)
-	go runtime.detectNPM(results, directoryContent)
-	go runtime.detectStandalone(results, directoryContent)
+	go runtime.detectYarnGithub(results, directoryContent)
+	go runtime.detectNPMGithub(results, directoryContent)
+	go runtime.detectStandaloneGithub(results, directoryContent)
 	runtime.wg.Wait()
 	close(results)
 
@@ -337,3 +406,169 @@ func (runtime *nodejsRuntime) Detect(
 
 	return nil
 }
+
+func (runtime *nodejsRuntime) DetectGitlab(
+	client *gitlab.Client,
+	tree []*gitlab.TreeNode,
+	owner, name, path, ref string,
+	paketo, heroku *BuilderInfo,
+) error {
+	results := make(chan struct {
+		string
+		bool
+	}, 3)
+
+	runtime.wg.Add(3)
+	go runtime.detectYarnGitlab(results, tree)
+	go runtime.detectNPMGitlab(results, tree)
+	go runtime.detectStandaloneGitlab(results, tree)
+	runtime.wg.Wait()
+	close(results)
+
+	paketoBuildpackInfo := BuildpackInfo{
+		Name:      "NodeJS",
+		Buildpack: "gcr.io/paketo-buildpacks/nodejs",
+	}
+	herokuBuildpackInfo := BuildpackInfo{
+		Name:      "NodeJS",
+		Buildpack: "heroku/nodejs",
+	}
+
+	if len(results) == 0 {
+		paketo.Others = append(paketo.Others, paketoBuildpackInfo)
+		heroku.Others = append(heroku.Others, herokuBuildpackInfo)
+		return nil
+	}
+
+	foundYarn := false
+	foundNPM := false
+	foundStandalone := false
+	for result := range results {
+		if result.string == yarn {
+			foundYarn = true
+		} else if result.string == npm {
+			foundNPM = true
+		} else if result.string == standalone {
+			foundStandalone = true
+		}
+	}
+
+	if foundYarn || foundNPM {
+		// it is safe to assume that the project contains a package.json
+		fileContent, _, err := client.RepositoryFiles.GetRawFile(
+			fmt.Sprintf("%s/%s", owner, name), fmt.Sprintf("%s/package.json", path),
+			&gitlab.GetRawFileOptions{
+				Ref: gitlab.String(ref),
+			},
+		)
+		if err != nil {
+			paketo.Others = append(paketo.Others, paketoBuildpackInfo)
+			heroku.Others = append(heroku.Others, herokuBuildpackInfo)
+			return fmt.Errorf("error fetching contents of package.json: %v", err)
+		}
+		var packageJSON struct {
+			Scripts map[string]string `json:"scripts"`
+			Engines struct {
+				Node string `json:"node"`
+			} `json:"engines"`
+		}
+
+		data := string(fileContent)
+
+		err = json.NewDecoder(strings.NewReader(data)).Decode(&packageJSON)
+		if err != nil {
+			paketo.Others = append(paketo.Others, paketoBuildpackInfo)
+			heroku.Others = append(heroku.Others, herokuBuildpackInfo)
+			return fmt.Errorf("error decoding package.json contents to struct: %v", err)
+		}
+
+		if packageJSON.Engines.Node == "" {
+			// we should now check for the node engine version in .nvmrc and then .node-version
+			nvmrcFound := false
+			nodeVersionFound := false
+			for i := 0; i < len(tree); i++ {
+				name := tree[i].Name
+				if name == ".nvmrc" {
+					nvmrcFound = true
+				} else if name == ".node-version" {
+					nodeVersionFound = true
+				}
+			}
+
+			if nvmrcFound {
+				// copy exact behavior of https://github.com/paketo-buildpacks/node-engine/blob/main/nvmrc_parser.go
+				fileContent, _, err = client.RepositoryFiles.GetRawFile(
+					fmt.Sprintf("%s/%s", owner, name), fmt.Sprintf("%s/.nvmrc", path),
+					&gitlab.GetRawFileOptions{
+						Ref: gitlab.String(ref),
+					},
+				)
+				if err != nil {
+					paketo.Others = append(paketo.Others, paketoBuildpackInfo)
+					heroku.Others = append(heroku.Others, herokuBuildpackInfo)
+					return fmt.Errorf("error fetching contents of .nvmrc: %v", err)
+				}
+				data = string(fileContent)
+
+				nvmrcVersion, err := validateNvmrc(data)
+				if err != nil {
+					paketo.Others = append(paketo.Others, paketoBuildpackInfo)
+					heroku.Others = append(heroku.Others, herokuBuildpackInfo)
+					return fmt.Errorf("error validating .nvmrc: %v", err)
+				}
+				nvmrcVersion = formatNvmrcContent(nvmrcVersion)
+
+				if nvmrcVersion != "*" {
+					packageJSON.Engines.Node = data
+				}
+			}
+
+			if packageJSON.Engines.Node == "" && nodeVersionFound {
+				// copy exact behavior of https://github.com/paketo-buildpacks/node-engine/blob/main/node_version_parser.go
+				fileContent, _, err = client.RepositoryFiles.GetRawFile(
+					fmt.Sprintf("%s/%s", owner, name), fmt.Sprintf("%s/.node-version", path),
+					&gitlab.GetRawFileOptions{
+						Ref: gitlab.String(ref),
+					},
+				)
+				if err != nil {
+					paketo.Others = append(paketo.Others, paketoBuildpackInfo)
+					heroku.Others = append(heroku.Others, herokuBuildpackInfo)
+					return fmt.Errorf("error fetching contents of .node-version: %v", err)
+				}
+
+				data = string(fileContent)
+
+				nodeVersion, err := validateNodeVersion(data)
+				if err != nil {
+					paketo.Others = append(paketo.Others, paketoBuildpackInfo)
+					heroku.Others = append(heroku.Others, herokuBuildpackInfo)
+					return fmt.Errorf("error validating .node-version: %v", err)
+				}
+				if nodeVersion != "" {
+					packageJSON.Engines.Node = nodeVersion
+				}
+			}
+		}
+
+		if packageJSON.Engines.Node == "" {
+			// use the default node engine version from https://github.com/paketo-buildpacks/node-engine/blob/main/buildpack.toml
+			packageJSON.Engines.Node = "16.*.*"
+		}
+
+		paketoBuildpackInfo.Config = make(map[string]interface{})
+		paketoBuildpackInfo.Config["scripts"] = packageJSON.Scripts
+		paketoBuildpackInfo.Config["node_engine"] = packageJSON.Engines.Node
+		paketo.Detected = append(paketo.Detected, paketoBuildpackInfo)
+
+		herokuBuildpackInfo.Config = make(map[string]interface{})
+		herokuBuildpackInfo.Config["scripts"] = packageJSON.Scripts
+		herokuBuildpackInfo.Config["node_engine"] = packageJSON.Engines.Node
+		heroku.Detected = append(heroku.Detected, herokuBuildpackInfo)
+	} else if foundStandalone {
+		paketo.Detected = append(paketo.Detected, paketoBuildpackInfo)
+		heroku.Detected = append(heroku.Detected, herokuBuildpackInfo)
+	}
+
+	return nil
+}

+ 142 - 9
internal/integrations/buildpacks/python.go

@@ -5,6 +5,7 @@ import (
 	"sync"
 
 	"github.com/google/go-github/v41/github"
+	"github.com/xanzy/go-gitlab"
 )
 
 type pythonRuntime struct {
@@ -15,7 +16,7 @@ func NewPythonRuntime() Runtime {
 	return &pythonRuntime{}
 }
 
-func (runtime *pythonRuntime) detectPipenv(results chan struct {
+func (runtime *pythonRuntime) detectPipenvGithub(results chan struct {
 	string
 	bool
 }, directoryContent []*github.RepositoryContent) {
@@ -41,7 +42,33 @@ func (runtime *pythonRuntime) detectPipenv(results chan struct {
 	runtime.wg.Done()
 }
 
-func (runtime *pythonRuntime) detectPip(results chan struct {
+func (runtime *pythonRuntime) detectPipenvGitlab(results chan struct {
+	string
+	bool
+}, tree []*gitlab.TreeNode) {
+	pipfileFound := false
+	pipfileLockFound := false
+	for i := 0; i < len(tree); i++ {
+		name := tree[i].Name
+		if name == "Pipfile" {
+			pipfileFound = true
+		} else if name == "Pipfile.lock" {
+			pipfileLockFound = true
+		}
+		if pipfileFound && pipfileLockFound {
+			break
+		}
+	}
+	if pipfileFound && pipfileLockFound {
+		results <- struct {
+			string
+			bool
+		}{pipenv, true}
+	}
+	runtime.wg.Done()
+}
+
+func (runtime *pythonRuntime) detectPipGithub(results chan struct {
 	string
 	bool
 }, directoryContent []*github.RepositoryContent) {
@@ -61,7 +88,27 @@ func (runtime *pythonRuntime) detectPip(results chan struct {
 	runtime.wg.Done()
 }
 
-func (runtime *pythonRuntime) detectConda(results chan struct {
+func (runtime *pythonRuntime) detectPipGitlab(results chan struct {
+	string
+	bool
+}, tree []*gitlab.TreeNode) {
+	requirementsTxtFound := false
+	for i := 0; i < len(tree); i++ {
+		name := tree[i].Name
+		if name == "requirements.txt" {
+			requirementsTxtFound = true
+		}
+	}
+	if requirementsTxtFound {
+		results <- struct {
+			string
+			bool
+		}{pip, true}
+	}
+	runtime.wg.Done()
+}
+
+func (runtime *pythonRuntime) detectCondaGithub(results chan struct {
 	string
 	bool
 }, directoryContent []*github.RepositoryContent) {
@@ -86,7 +133,32 @@ func (runtime *pythonRuntime) detectConda(results chan struct {
 	runtime.wg.Done()
 }
 
-func (runtime *pythonRuntime) detectStandalone(results chan struct {
+func (runtime *pythonRuntime) detectCondaGitlab(results chan struct {
+	string
+	bool
+}, tree []*gitlab.TreeNode) {
+	environmentFound := false
+	packageListFound := false
+	for i := 0; i < len(tree); i++ {
+		name := tree[i].Name
+		if name == "environment.yml" {
+			environmentFound = true
+			break
+		} else if name == "package-list.txt" {
+			packageListFound = true
+			break
+		}
+	}
+	if environmentFound || packageListFound {
+		results <- struct {
+			string
+			bool
+		}{conda, true}
+	}
+	runtime.wg.Done()
+}
+
+func (runtime *pythonRuntime) detectStandaloneGithub(results chan struct {
 	string
 	bool
 }, directoryContent []*github.RepositoryContent) {
@@ -107,7 +179,28 @@ func (runtime *pythonRuntime) detectStandalone(results chan struct {
 	runtime.wg.Done()
 }
 
-func (runtime *pythonRuntime) Detect(
+func (runtime *pythonRuntime) detectStandaloneGitlab(results chan struct {
+	string
+	bool
+}, tree []*gitlab.TreeNode) {
+	pyFound := false
+	for i := 0; i < len(tree); i++ {
+		name := tree[i].Name
+		if strings.HasSuffix(name, ".py") {
+			pyFound = true
+			break
+		}
+	}
+	if pyFound {
+		results <- struct {
+			string
+			bool
+		}{standalone, true}
+	}
+	runtime.wg.Done()
+}
+
+func (runtime *pythonRuntime) DetectGithub(
 	client *github.Client,
 	directoryContent []*github.RepositoryContent,
 	owner, name, path string,
@@ -120,10 +213,50 @@ func (runtime *pythonRuntime) Detect(
 	}, 4)
 
 	runtime.wg.Add(4)
-	go runtime.detectPipenv(results, directoryContent)
-	go runtime.detectPip(results, directoryContent)
-	go runtime.detectConda(results, directoryContent)
-	go runtime.detectStandalone(results, directoryContent)
+	go runtime.detectPipenvGithub(results, directoryContent)
+	go runtime.detectPipGithub(results, directoryContent)
+	go runtime.detectCondaGithub(results, directoryContent)
+	go runtime.detectStandaloneGithub(results, directoryContent)
+	runtime.wg.Wait()
+	close(results)
+
+	paketoBuildpackInfo := BuildpackInfo{
+		Name:      "Python",
+		Buildpack: "gcr.io/paketo-buildpacks/python",
+	}
+	herokuBuildpackInfo := BuildpackInfo{
+		Name:      "Python",
+		Buildpack: "heroku/python",
+	}
+
+	if len(results) == 0 {
+		paketo.Others = append(paketo.Others, paketoBuildpackInfo)
+		heroku.Others = append(heroku.Others, herokuBuildpackInfo)
+		return nil
+	}
+
+	paketo.Detected = append(paketo.Detected, paketoBuildpackInfo)
+	heroku.Detected = append(heroku.Detected, herokuBuildpackInfo)
+
+	return nil
+}
+
+func (runtime *pythonRuntime) DetectGitlab(
+	client *gitlab.Client,
+	tree []*gitlab.TreeNode,
+	owner, name, path, ref string,
+	paketo, heroku *BuilderInfo,
+) error {
+	results := make(chan struct {
+		string
+		bool
+	}, 4)
+
+	runtime.wg.Add(4)
+	go runtime.detectPipenvGitlab(results, tree)
+	go runtime.detectPipGitlab(results, tree)
+	go runtime.detectCondaGitlab(results, tree)
+	go runtime.detectStandaloneGitlab(results, tree)
 	runtime.wg.Wait()
 	close(results)
 

+ 138 - 3
internal/integrations/buildpacks/ruby.go

@@ -9,6 +9,7 @@ import (
 	"sync"
 
 	"github.com/google/go-github/v41/github"
+	"github.com/xanzy/go-gitlab"
 )
 
 type rubyRuntime struct {
@@ -115,7 +116,7 @@ func (runtime *rubyRuntime) detectPassenger(gemfileContent string, results chan
 	runtime.wg.Done()
 }
 
-func (runtime *rubyRuntime) detectRackup(
+func (runtime *rubyRuntime) detectRackupGithub(
 	client *github.Client, owner, name string,
 	repoContentOptions github.RepositoryContentGetOptions, results chan struct {
 		string
@@ -155,6 +156,43 @@ func (runtime *rubyRuntime) detectRackup(
 	runtime.wg.Done()
 }
 
+func (runtime *rubyRuntime) detectRackupGitlab(
+	client *gitlab.Client, owner, name, ref string, results chan struct {
+		string
+		bool
+	},
+) {
+	fileContent, _, err := client.RepositoryFiles.GetRawFile(
+		fmt.Sprintf("%s/%s", owner, name), "Gemfile.lock", &gitlab.GetRawFileOptions{
+			Ref: gitlab.String(ref),
+		})
+	if err != nil {
+		runtime.wg.Done()
+		return
+	}
+	gemfileLockContent := string(fileContent)
+
+	rackFound := false
+	scanner := bufio.NewScanner(strings.NewReader(gemfileLockContent))
+	for scanner.Scan() {
+		if strings.TrimSpace(scanner.Text()) == "GEM" {
+			for scanner.Scan() {
+				if strings.Contains(scanner.Text(), "rack") {
+					rackFound = true
+					break
+				}
+			}
+		}
+	}
+	if rackFound {
+		results <- struct {
+			string
+			bool
+		}{rackup, true}
+	}
+	runtime.wg.Done()
+}
+
 func (runtime *rubyRuntime) detectRake(gemfileContent string, results chan struct {
 	string
 	bool
@@ -179,7 +217,7 @@ func (runtime *rubyRuntime) detectRake(gemfileContent string, results chan struc
 	runtime.wg.Done()
 }
 
-func (runtime *rubyRuntime) Detect(
+func (runtime *rubyRuntime) DetectGithub(
 	client *github.Client,
 	directoryContent []*github.RepositoryContent,
 	owner, name, path string,
@@ -265,7 +303,104 @@ func (runtime *rubyRuntime) Detect(
 	}
 	go runtime.detectPassenger(gemfileContent, results)
 	if !configRuFound && gemfileLockFound {
-		go runtime.detectRackup(client, owner, name, repoContentOptions, results)
+		go runtime.detectRackupGithub(client, owner, name, repoContentOptions, results)
+	}
+	if rakefileFound {
+		go runtime.detectRake(gemfileContent, results)
+	}
+	runtime.wg.Wait()
+	close(results)
+
+	paketo.Detected = append(paketo.Detected, paketoBuildpackInfo)
+	heroku.Detected = append(heroku.Detected, herokuBuildpackInfo)
+
+	return nil
+}
+
+func (runtime *rubyRuntime) DetectGitlab(
+	client *gitlab.Client,
+	tree []*gitlab.TreeNode,
+	owner, name, path, ref string,
+	paketo, heroku *BuilderInfo,
+) error {
+	gemfileFound := false
+	gemfileLockFound := false
+	configRuFound := false
+	rakefileFound := false
+	for i := range tree {
+		name := tree[i].Name
+		if name == "Gemfile" {
+			gemfileFound = true
+		} else if name == "Gemfile.lock" {
+			gemfileLockFound = true
+		} else if name == "config.ru" {
+			configRuFound = true
+		} else if name == "Rakefile" || name == "Rakefile.rb" || name == "rakefile" || name == "rakefile.rb" {
+			rakefileFound = true
+		}
+	}
+
+	paketoBuildpackInfo := BuildpackInfo{
+		Name:      "Ruby",
+		Buildpack: "gcr.io/paketo-buildpacks/ruby",
+	}
+	herokuBuildpackInfo := BuildpackInfo{
+		Name:      "Ruby",
+		Buildpack: "heroku/ruby",
+	}
+
+	if !gemfileFound {
+		paketo.Others = append(paketo.Others, paketoBuildpackInfo)
+		heroku.Others = append(heroku.Others, herokuBuildpackInfo)
+		return nil
+	}
+
+	fileContent, _, err := client.RepositoryFiles.GetRawFile(
+		fmt.Sprintf("%s/%s", owner, name), "Gemfile", &gitlab.GetRawFileOptions{
+			Ref: gitlab.String(ref),
+		})
+	if err != nil {
+		paketo.Others = append(paketo.Others, paketoBuildpackInfo)
+		heroku.Others = append(heroku.Others, herokuBuildpackInfo)
+		return fmt.Errorf("error fetching contents of Gemfile for %s/%s: %v", owner, name, err)
+	}
+	gemfileContent := string(fileContent)
+
+	count := 6
+	if !configRuFound {
+		// unicorn needs config.ru
+		count -= 1
+		if !gemfileLockFound {
+			// rackup needs one of Gemfile.lock or config.ru
+			count -= 1
+		}
+	}
+	if !rakefileFound {
+		count -= 1
+	}
+	results := make(chan struct {
+		string
+		bool
+	}, count)
+
+	runtime.wg.Add(count)
+	go runtime.detectPuma(gemfileContent, results)
+	go runtime.detectThin(gemfileContent, results)
+	if configRuFound {
+		{
+			// FIXME: find a better, more readable way of doing this
+			results <- struct {
+				string
+				bool
+			}{rackup, true}
+			runtime.wg.Done()
+		}
+
+		go runtime.detectUnicorn(gemfileContent, results)
+	}
+	go runtime.detectPassenger(gemfileContent, results)
+	if !configRuFound && gemfileLockFound {
+		go runtime.detectRackupGitlab(client, owner, name, ref, results)
 	}
 	if rakefileFound {
 		go runtime.detectRake(gemfileContent, results)

+ 12 - 1
internal/integrations/buildpacks/shared.go

@@ -2,6 +2,7 @@ package buildpacks
 
 import (
 	"github.com/google/go-github/v41/github"
+	"github.com/xanzy/go-gitlab"
 )
 
 const (
@@ -48,7 +49,7 @@ type BuilderInfo struct {
 }
 
 type Runtime interface {
-	Detect(
+	DetectGithub(
 		*github.Client, // github client to pull contents of files
 		[]*github.RepositoryContent, // the root folder structure of the git repo
 		string, // owner
@@ -58,6 +59,16 @@ type Runtime interface {
 		*BuilderInfo, // paketo
 		*BuilderInfo, // heroku
 	) error
+	DetectGitlab(
+		*gitlab.Client, // github client to pull contents of files
+		[]*gitlab.TreeNode, // the root folder structure of the git repo
+		string, // owner
+		string, // name
+		string, // path
+		string, // SHA, branch or tag
+		*BuilderInfo, // paketo
+		*BuilderInfo, // heroku
+	) error
 }
 
 // Runtimes is a list of all API runtimes