Repository: runatlantis/atlantis (language: go)

Issue:
Autoplan modules functionality not working with per-repo config
<!--- Please keep this note for the community --->

### Community Note

* Please vote on this issue by adding a 👍 [reaction](https://blog.github.com/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/) to the original issue to help the community and maintainers prioritize this request. Searching for pre-existing feature requests helps us consolidate datapoints for identical requirements into a single place, thank you!
* Please do not leave "+1" or other comments that do not add relevant new information or questions, they generate extra noise for issue followers and do not help prioritize the request.
* If you are interested in working on this issue or have submitted a pull request, please leave a comment.

<!--- Thank you for keeping this note for the community --->

---

### Overview of the Issue

We're running Atlantis v0.22.3 with:
*  `ATLANTIS_AUTOPLAN_MODULES=true`
* Per-repo atlantis.yml config generated as part of a custom pre_workflow_hooks command
* Local modules in our git repo

Unfortunately, when we make changes to any of the local modules, the projects in repo atlantis.yml that are dependent on those modules don't register as updated/autoplanned.

We traced the code that [invokes `FindModuleProjects`](https://github.com/runatlantis/atlantis/blob/v0.22.3/server/events/project_command_builder.go#L367) in `server/events/project_command_builder.go` and it looks like this is part of a conditional that is only executed when per-repo config is _not_ present. We couldn't find any other calls to the dependent module resolution.

Is this expected, or is there a way to enable the dependent module auto-planning for per-repo config projects/stacks?

### Reproduction Steps

1. Use a project structure like the following (simplified):

```
terraform/app
├── README.md
├── main.tf
├── modules.tf
terraform/modules/ecs
├── ecs.tf
```

2. Add a repo-level atlantis.yml file that defines `terraform/app` as a project, where `terraform/app` depends on `../modules/ecs` as a module.
3. Commit changes to `terraform/modules/ecs/ecs.tf`
4. Observe no autoplan


### Environment details

Repo `atlantis.yaml` file:

```yaml
version: 3
projects:
- name: app
  dir: terraform/app
  workspace: prod
```

### Additional Context

We're using the per-repo atlantis.yml config in order to skip planning for the `default` workspace **when other workspaces exist**. In cases where only the default workspace exists, we use it.

We'd love to avoid using the per-repo config if it'd be possible to replicate that behavior without it. Particularly if it means missing-out on functionality like module autoplanning.



Discussion / hints:
Thanks for creating this issue - this mirrors / confirms what we're seeing as well -- we've been tearing our hair out to figure out why it didn't work.

note: we also explicitly set `autoplan` to `false` per directory / project in our config, though my read is that shouldn't be an issue, and we've tested removing that bit anyway for debugging
cc: @iamnoah 

related PR https://github.com/runatlantis/atlantis/pull/2507
FWIW, the only reason this isn't supported is because I didn't have a per-repo atlantis.yml setup to dev and test on. I don't know of any reason it shouldn't be possible.
Ah that makes sense. For now, I suppose we'll have to document the limitation.

If anyone feels like contributing this change, it could be as simple as duplicating this block or encapsulating this block into a function and executing the call in both the "if" and "else" conditions (or before the if/else block). It will probably require changes to `DetermineProjectsViaConfig` and `DetermineProjects` functions.

This block

https://github.com/runatlantis/atlantis/blob/892a43b5948d863c72d7c68e91535d019421ad2e/server/events/project_command_builder.go#L388-L394

Is in the `else` block of this `if` statement

https://github.com/runatlantis/atlantis/blob/892a43b5948d863c72d7c68e91535d019421ad2e/server/events/project_command_builder.go#L354-L380

so if there are more than zero projects defined, the `--autoplan-modules` flag logic will not execute.


Fix patch:
diff --git a/server/events/modules.go b/server/events/modules.go
index a545d380cf..396cd59d8a 100644
--- a/server/events/modules.go
+++ b/server/events/modules.go
@@ -130,7 +130,7 @@ func findModuleDependants(files fs.FS, autoplanModuleDependants string) (ModuleP
 	filter, _ := patternmatcher.New(strings.Split(autoplanModuleDependants, ","))
 	var projects []string
 	err := fs.WalkDir(files, ".", func(rel string, info fs.DirEntry, err error) error {
-		if match, _ := filter.Matches(rel); match {
+		if match, _ := filter.MatchesOrParentMatches(rel); match {
 			if projectDir := getProjectDirFromFs(files, rel); projectDir != "" {
 				projects = append(projects, projectDir)
 			}
diff --git a/server/events/project_command_builder.go b/server/events/project_command_builder.go
index 1069a80480..37fe51301a 100644
--- a/server/events/project_command_builder.go
+++ b/server/events/project_command_builder.go
@@ -297,7 +297,7 @@ func (p *DefaultProjectCommandBuilder) buildAllCommandsByCfg(ctx *command.Contex
 			}
 			ctx.Log.Info("successfully parsed remote %s file", repoCfgFile)
 			if len(repoCfg.Projects) > 0 {
-				matchingProjects, err := p.ProjectFinder.DetermineProjectsViaConfig(ctx.Log, modifiedFiles, repoCfg, "")
+				matchingProjects, err := p.ProjectFinder.DetermineProjectsViaConfig(ctx.Log, modifiedFiles, repoCfg, "", nil)
 				if err != nil {
 					return nil, err
 				}
@@ -351,8 +351,14 @@ func (p *DefaultProjectCommandBuilder) buildAllCommandsByCfg(ctx *command.Contex
 		ctx.Log.Info("successfully parsed %s file", repoCfgFile)
 	}
 
+	moduleInfo, err := FindModuleProjects(repoDir, p.AutoDetectModuleFiles)
+	if err != nil {
+		ctx.Log.Warn("error(s) loading project module dependencies: %s", err)
+	}
+	ctx.Log.Debug("moduleInfo for %s (matching %q) = %v", repoDir, p.AutoDetectModuleFiles, moduleInfo)
+
 	if len(repoCfg.Projects) > 0 {
-		matchingProjects, err := p.ProjectFinder.DetermineProjectsViaConfig(ctx.Log, modifiedFiles, repoCfg, repoDir)
+		matchingProjects, err := p.ProjectFinder.DetermineProjectsViaConfig(ctx.Log, modifiedFiles, repoCfg, repoDir, moduleInfo)
 		if err != nil {
 			return nil, err
 		}
@@ -386,11 +392,6 @@ func (p *DefaultProjectCommandBuilder) buildAllCommandsByCfg(ctx *command.Contex
 			ctx.Log.Info("found no %s file", repoCfgFile)
 		}
 		// build a module index for projects that are explicitly included
-		moduleInfo, err := FindModuleProjects(repoDir, p.AutoDetectModuleFiles)
-		if err != nil {
-			ctx.Log.Warn("error(s) loading project module dependencies: %s", err)
-		}
-		ctx.Log.Debug("moduleInfo for %s (matching %q) = %v", repoDir, p.AutoDetectModuleFiles, moduleInfo)
 		modifiedProjects := p.ProjectFinder.DetermineProjects(ctx.Log, modifiedFiles, ctx.Pull.BaseRepo.FullName, repoDir, p.AutoplanFileList, moduleInfo)
 		ctx.Log.Info("automatically determined that there were %d projects modified in this pull request: %s", len(modifiedProjects), modifiedProjects)
 		for _, mp := range modifiedProjects {
diff --git a/server/events/project_finder.go b/server/events/project_finder.go
index 80049cc4f0..8d5ecafe52 100644
--- a/server/events/project_finder.go
+++ b/server/events/project_finder.go
@@ -22,6 +22,7 @@ import (
 	"strings"
 
 	"github.com/runatlantis/atlantis/server/core/config/valid"
+	"github.com/runatlantis/atlantis/server/utils"
 
 	"github.com/moby/patternmatcher"
 	"github.com/pkg/errors"
@@ -44,7 +45,7 @@ type ProjectFinder interface {
 	// DetermineProjectsViaConfig returns the list of projects that were modified
 	// based on modifiedFiles and the repo's config.
 	// absRepoDir is the path to the cloned repo on disk.
-	DetermineProjectsViaConfig(log logging.SimpleLogging, modifiedFiles []string, config valid.RepoCfg, absRepoDir string) ([]valid.Project, error)
+	DetermineProjectsViaConfig(log logging.SimpleLogging, modifiedFiles []string, config valid.RepoCfg, absRepoDir string, moduleInfo ModuleProjects) ([]valid.Project, error)
 
 	DetermineWorkspaceFromHCL(log logging.SimpleLogging, absRepoDir string) (string, error)
 }
@@ -174,10 +175,27 @@ func (p *DefaultProjectFinder) DetermineProjects(log logging.SimpleLogging, modi
 }
 
 // See ProjectFinder.DetermineProjectsViaConfig.
-func (p *DefaultProjectFinder) DetermineProjectsViaConfig(log logging.SimpleLogging, modifiedFiles []string, config valid.RepoCfg, absRepoDir string) ([]valid.Project, error) {
+func (p *DefaultProjectFinder) DetermineProjectsViaConfig(log logging.SimpleLogging, modifiedFiles []string, config valid.RepoCfg, absRepoDir string, moduleInfo ModuleProjects) ([]valid.Project, error) {
+
+	// Check moduleInfo for downstream project dependencies
+	var dependentProjects []string
+	for _, file := range modifiedFiles {
+		if moduleInfo != nil {
+			downstreamProjects := moduleInfo.DependentProjects(path.Dir(file))
+			log.Debug("found downstream projects for %q: %v", file, downstreamProjects)
+			dependentProjects = append(dependentProjects, downstreamProjects...)
+		}
+	}
+
 	var projects []valid.Project
 	for _, project := range config.Projects {
 		log.Debug("checking if project at dir %q workspace %q was modified", project.Dir, project.Workspace)
+
+		if utils.SlicesContains(dependentProjects, project.Dir) {
+			projects = append(projects, project)
+			continue
+		}
+
 		var whenModifiedRelToRepoRoot []string
 		for _, wm := range project.Autoplan.WhenModified {
 			wm = strings.TrimSpace(wm)


Test patch:
diff --git a/server/events/project_finder_test.go b/server/events/project_finder_test.go
index c88b0f123a..f3405494dd 100644
--- a/server/events/project_finder_test.go
+++ b/server/events/project_finder_test.go
@@ -538,7 +538,7 @@ func TestDefaultProjectFinder_DetermineProjectsViaConfig(t *testing.T) {
 	for _, c := range cases {
 		t.Run(c.description, func(t *testing.T) {
 			pf := events.DefaultProjectFinder{}
-			projects, err := pf.DetermineProjectsViaConfig(logging.NewNoopLogger(t), c.modified, c.config, tmpDir)
+			projects, err := pf.DetermineProjectsViaConfig(logging.NewNoopLogger(t), c.modified, c.config, tmpDir, nil)
 			Ok(t, err)
 			Equals(t, len(c.expProjPaths), len(projects))
 			for i, proj := range projects {