-
Notifications
You must be signed in to change notification settings - Fork 11
feat(discovery): add project discovery package #262
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: refactor/abstract-fs-2-fsys
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| package discovery | ||
|
|
||
| import ( | ||
| "path" | ||
| "slices" | ||
|
|
||
| "github.com/platformsh/platformify/internal/utils" | ||
| ) | ||
|
|
||
| // Returns the application root, either from memory or by discovering it on the spot | ||
| func (d *Discoverer) ApplicationRoot() (string, error) { | ||
| if applicationRoot, ok := d.memory["application_root"]; ok { | ||
| return applicationRoot.(string), nil | ||
| } | ||
|
|
||
| appRoot, err := d.discoverApplicationRoot() | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
|
|
||
| d.memory["application_root"] = appRoot | ||
| return appRoot, nil | ||
| } | ||
|
|
||
| func (d *Discoverer) discoverApplicationRoot() (string, error) { | ||
| depManagers, err := d.DependencyManagers() | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
|
|
||
| for _, dependencyManager := range dependencyManagersMap { | ||
| if !slices.Contains(depManagers, dependencyManager.name) { | ||
| continue | ||
| } | ||
|
|
||
| lockPath := utils.FindFile(d.fileSystem, "", dependencyManager.lockFile) | ||
| if lockPath == "" { | ||
| continue | ||
| } | ||
|
|
||
| return path.Dir(lockPath), nil | ||
| } | ||
|
|
||
| return "", nil | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| package discovery | ||
|
|
||
| import ( | ||
| "io/fs" | ||
| "testing" | ||
| "testing/fstest" | ||
| ) | ||
|
|
||
| func TestDiscoverer_discoverApplicationRoot(t *testing.T) { | ||
| type fields struct { | ||
| fileSystem fs.FS | ||
| memory map[string]any | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| fields fields | ||
| want string | ||
| wantErr bool | ||
| }{ | ||
| { | ||
| name: "Simple", | ||
| fields: fields{ | ||
| fileSystem: fstest.MapFS{ | ||
| "package-lock.json": &fstest.MapFile{}, | ||
| }, | ||
| }, | ||
| want: ".", | ||
| }, | ||
| { | ||
| name: "No root", | ||
| fields: fields{ | ||
| fileSystem: fstest.MapFS{}, | ||
| }, | ||
| want: "", | ||
| }, | ||
| { | ||
| name: "Priority", | ||
| fields: fields{ | ||
| fileSystem: fstest.MapFS{ | ||
| "yarn/yarn.lock": &fstest.MapFile{}, | ||
| "poetry/poetry.lock": &fstest.MapFile{}, | ||
| "composer/composer-lock.json": &fstest.MapFile{}, | ||
| }, | ||
| }, | ||
| want: "poetry", | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| d := &Discoverer{ | ||
| fileSystem: tt.fields.fileSystem, | ||
| memory: tt.fields.memory, | ||
| } | ||
| if d.memory == nil { | ||
| d.memory = make(map[string]any) | ||
| } | ||
| got, err := d.discoverApplicationRoot() | ||
| if (err != nil) != tt.wantErr { | ||
| t.Errorf("Discoverer.discoverApplicationRoot() error = %v, wantErr %v", err, tt.wantErr) | ||
| return | ||
| } | ||
| if got != tt.want { | ||
| t.Errorf("Discoverer.discoverApplicationRoot() = %v, want %v", got, tt.want) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,137 @@ | ||
| package discovery | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "slices" | ||
|
|
||
| "github.com/platformsh/platformify/internal/utils" | ||
| "github.com/platformsh/platformify/platformifier" | ||
| ) | ||
|
|
||
| // Returns the application build steps, either from memory or by discovering it on the spot | ||
| func (d *Discoverer) BuildSteps() ([]string, error) { | ||
| if buildSteps, ok := d.memory["build_steps"]; ok { | ||
| return buildSteps.([]string), nil | ||
| } | ||
|
|
||
| buildSteps, err := d.discoverBuildSteps() | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| d.memory["build_steps"] = buildSteps | ||
| return buildSteps, nil | ||
| } | ||
|
|
||
| func (d *Discoverer) discoverBuildSteps() ([]string, error) { | ||
| dependencyManagers, err := d.DependencyManagers() | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| typ, err := d.Type() | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| stack, err := d.Stack() | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| appRoot, err := d.ApplicationRoot() | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| buildSteps := make([]string, 0) | ||
|
|
||
| // Start with lower priority dependency managers first | ||
| slices.Reverse(dependencyManagers) | ||
| for _, dm := range dependencyManagers { | ||
| switch dm { | ||
| case "poetry": | ||
| buildSteps = append( | ||
| buildSteps, | ||
| "# Set PIP_USER to 0 so that Poetry does not complain", | ||
| "export PIP_USER=0", | ||
| "# Install poetry as a global tool", | ||
| "python -m venv /app/.global", | ||
| "pip install poetry==$POETRY_VERSION", | ||
| "poetry install", | ||
| ) | ||
| case "pipenv": | ||
| buildSteps = append( | ||
| buildSteps, | ||
| "# Set PIP_USER to 0 so that Pipenv does not complain", | ||
| "export PIP_USER=0", | ||
| "# Install Pipenv as a global tool", | ||
| "python -m venv /app/.global", | ||
| "pip install pipenv==$PIPENV_TOOL_VERSION", | ||
| "pipenv sync", | ||
| ) | ||
| case "pip": | ||
| buildSteps = append( | ||
| buildSteps, | ||
| "pip install -r requirements.txt", | ||
| ) | ||
| case "yarn", "npm": | ||
| // Install n, if on different runtime | ||
| if typ != "nodejs" { | ||
| buildSteps = append( | ||
| buildSteps, | ||
| "n auto || n lts", | ||
| "hash -r", | ||
| ) | ||
| } | ||
|
|
||
| if dm == "yarn" { | ||
| buildSteps = append( | ||
| buildSteps, | ||
| "yarn", | ||
| ) | ||
| } else { | ||
| buildSteps = append( | ||
| buildSteps, | ||
| "npm i", | ||
| ) | ||
| } | ||
|
|
||
| if _, ok := utils.GetJSONValue( | ||
| d.fileSystem, | ||
| []string{"scripts", "build"}, | ||
| "package.json", | ||
| true, | ||
| ); ok { | ||
| buildSteps = append(buildSteps, d.nodeScriptPrefix()+"build") | ||
| } | ||
| case "composer": | ||
| buildSteps = append( | ||
| buildSteps, | ||
| "composer --no-ansi --no-interaction install --no-progress --prefer-dist --optimize-autoloader --no-dev", | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| switch stack { | ||
| case platformifier.Django: | ||
| if managePyPath := utils.FindFile( | ||
| d.fileSystem, | ||
| appRoot, | ||
| managePyFile, | ||
| ); managePyPath != "" { | ||
| buildSteps = append( | ||
| buildSteps, | ||
| "# Collect static files", | ||
| fmt.Sprintf("%spython %s collectstatic --noinput", d.pythonPrefix(), managePyPath), | ||
| ) | ||
|
Comment on lines
+118
to
+127
|
||
| } | ||
| case platformifier.NextJS: | ||
| // If there is no custom build script, fallback to next build for Next.js projects | ||
| if !slices.Contains(buildSteps, "yarn build") && !slices.Contains(buildSteps, "npm run build") { | ||
| buildSteps = append(buildSteps, d.nodeExecPrefix()+"next build") | ||
| } | ||
| } | ||
|
|
||
| return buildSteps, nil | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| package discovery | ||
|
|
||
| import ( | ||
| "io/fs" | ||
| "reflect" | ||
| "testing" | ||
| "testing/fstest" | ||
|
|
||
| "github.com/platformsh/platformify/platformifier" | ||
| ) | ||
|
|
||
| func TestDiscoverer_discoverBuildSteps(t *testing.T) { | ||
| type fields struct { | ||
| fileSystem fs.FS | ||
| memory map[string]any | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| fields fields | ||
| want []string | ||
| wantErr bool | ||
| }{ | ||
| { | ||
| name: "Poetry Django", | ||
| fields: fields{ | ||
| fileSystem: fstest.MapFS{ | ||
| "project/manage.py": &fstest.MapFile{}, | ||
| }, | ||
| memory: map[string]any{ | ||
| "stack": platformifier.Django, | ||
| "type": "python", | ||
| "dependency_managers": []string{"poetry"}, | ||
| "application_root": ".", | ||
| }, | ||
| }, | ||
| want: []string{ | ||
| "# Set PIP_USER to 0 so that Poetry does not complain", | ||
| "export PIP_USER=0", | ||
| "# Install poetry as a global tool", | ||
| "python -m venv /app/.global", | ||
| "pip install poetry==$POETRY_VERSION", | ||
| "poetry install", | ||
| "# Collect static files", | ||
| "poetry run python project/manage.py collectstatic --noinput", | ||
| }, | ||
| }, | ||
| { | ||
| name: "Pipenv Django with Yarn build", | ||
| fields: fields{ | ||
| fileSystem: fstest.MapFS{ | ||
| "project/manage.py": &fstest.MapFile{}, | ||
| "package.json": &fstest.MapFile{Data: []byte(`{"scripts": {"build": "nuxt build"}}`)}, | ||
| }, | ||
| memory: map[string]any{ | ||
| "stack": platformifier.Django, | ||
| "type": "python", | ||
| "dependency_managers": []string{"poetry", "yarn"}, | ||
| "application_root": ".", | ||
| }, | ||
|
Comment on lines
+47
to
+59
|
||
| }, | ||
| want: []string{ | ||
| "n auto || n lts", | ||
| "hash -r", | ||
| "yarn", | ||
| "yarn build", | ||
| "# Set PIP_USER to 0 so that Poetry does not complain", | ||
| "export PIP_USER=0", | ||
| "# Install poetry as a global tool", | ||
| "python -m venv /app/.global", | ||
| "pip install poetry==$POETRY_VERSION", | ||
| "poetry install", | ||
| "# Collect static files", | ||
| "poetry run python project/manage.py collectstatic --noinput", | ||
| }, | ||
| }, | ||
| { | ||
| name: "Next.js without build script", | ||
| fields: fields{ | ||
| fileSystem: fstest.MapFS{}, | ||
| memory: map[string]any{ | ||
| "stack": platformifier.NextJS, | ||
| "type": "nodejs", | ||
| "dependency_managers": []string{"npm"}, | ||
| "application_root": ".", | ||
| }, | ||
| }, | ||
| want: []string{ | ||
| "npm i", | ||
| "npm exec next build", | ||
| }, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| d := &Discoverer{ | ||
| fileSystem: tt.fields.fileSystem, | ||
| memory: tt.fields.memory, | ||
| } | ||
| got, err := d.discoverBuildSteps() | ||
| if (err != nil) != tt.wantErr { | ||
| t.Errorf("Discoverer.discoverBuildSteps() error = %v, wantErr %v", err, tt.wantErr) | ||
| return | ||
| } | ||
| if !reflect.DeepEqual(got, tt.want) { | ||
| t.Errorf("Discoverer.discoverBuildSteps() = %v, want %v", got, tt.want) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test fixture uses
composer/composer-lock.json, but the code detects Composer viacomposer.lock. As written, this test doesn’t exercise Composer detection at all. Rename the fixture tocomposer/composer.lock(or adjust the detector if the intended file is different).