diff --git a/server/events/atlantis_workspace.go b/server/events/atlantis_workspace.go
index 190b87790..67112b1ce 100644
--- a/server/events/atlantis_workspace.go
+++ b/server/events/atlantis_workspace.go
@@ -18,6 +18,7 @@ import (
"os/exec"
"path/filepath"
"strconv"
+ "strings"
"github.com/pkg/errors"
"github.com/runatlantis/atlantis/server/events/models"
@@ -48,7 +49,9 @@ type FileWorkspace struct {
}
// Clone git clones headRepo, checks out the branch and then returns the absolute
-// path to the root of the cloned repo.
+// path to the root of the cloned repo. If the repo already exists and is at
+// the right commit it does nothing. This is to support running commands in
+// multiple dirs of the same repo without deleting existing plans.
func (w *FileWorkspace) Clone(
log *logging.SimpleLogger,
baseRepo models.Repo,
@@ -57,11 +60,27 @@ func (w *FileWorkspace) Clone(
workspace string) (string, error) {
cloneDir := w.cloneDir(baseRepo, p, workspace)
- // This is safe to do because we lock runs on repo/pull/workspace so no one else
- // is using this workspace.
- log.Info("cleaning clone directory %q", cloneDir)
- if err := os.RemoveAll(cloneDir); err != nil {
- return "", errors.Wrap(err, "deleting old workspace")
+ // If the directory already exists, check if it's at the right commit.
+ // If so, then we do nothing.
+ if _, err := os.Stat(cloneDir); err == nil {
+ revParseCmd := exec.Command("git", "rev-parse", "HEAD") // #nosec
+ revParseCmd.Dir = cloneDir
+ output, err := revParseCmd.CombinedOutput()
+ if err != nil {
+ return "", errors.Wrapf(err, "running git rev-parse HEAD: %s", string(output))
+ }
+ currCommit := strings.Trim(string(output), "\n")
+ if string(currCommit) == p.HeadCommit {
+ log.Debug("repo is at correct commit %q so will not re-clone", p.HeadCommit)
+ return cloneDir, nil
+ }
+ log.Debug("repo was already cloned but is not at correct commit, wanted %q got %q", p.HeadCommit, string(currCommit))
+
+ // It's okay to delete all plans now since they're out of date.
+ log.Info("cleaning clone directory %q", cloneDir)
+ if err := os.RemoveAll(cloneDir); err != nil {
+ return "", errors.Wrap(err, "deleting old workspace")
+ }
}
// Create the directory and parents if necessary.
diff --git a/server/events/command_handler.go b/server/events/command_handler.go
index 601d3be09..bfd25dd93 100644
--- a/server/events/command_handler.go
+++ b/server/events/command_handler.go
@@ -195,7 +195,7 @@ func (c *CommandHandler) updatePull(ctx *CommandContext, res CommandResponse) {
if err := c.CommitStatusUpdater.UpdateProjectResult(ctx, res); err != nil {
ctx.Log.Warn("unable to update commit status: %s", err)
}
- comment := c.MarkdownRenderer.Render(res, ctx.Command.Name, ctx.Log.History.String(), ctx.Command.Verbose)
+ comment := c.MarkdownRenderer.Render(res, ctx.Command.Name, ctx.Log.History.String(), ctx.Command.Verbose, ctx.Command.Autoplan)
c.VCSClient.CreateComment(ctx.BaseRepo, ctx.Pull.Num, comment) // nolint: errcheck
}
diff --git a/server/events/markdown_renderer.go b/server/events/markdown_renderer.go
index 4094c3861..0f623f04a 100644
--- a/server/events/markdown_renderer.go
+++ b/server/events/markdown_renderer.go
@@ -58,7 +58,7 @@ type ProjectResultTmplData struct {
// Render formats the data into a markdown string.
// nolint: interfacer
-func (m *MarkdownRenderer) Render(res CommandResponse, cmdName CommandName, log string, verbose bool) string {
+func (m *MarkdownRenderer) Render(res CommandResponse, cmdName CommandName, log string, verbose bool, autoplan bool) string {
commandStr := strings.Title(cmdName.String())
common := CommonData{commandStr, verbose, log}
if res.Error != nil {
@@ -67,6 +67,9 @@ func (m *MarkdownRenderer) Render(res CommandResponse, cmdName CommandName, log
if res.Failure != "" {
return m.renderTemplate(failureWithLogTmpl, FailureData{res.Failure, common})
}
+ if len(res.ProjectResults) == 0 && autoplan {
+ return m.renderTemplate(autoplanNoProjectsWithLogTmpl, common)
+ }
return m.renderProjectResults(res.ProjectResults, common)
}
@@ -145,9 +148,12 @@ var errTmplText = "**{{.Command}} Error**\n" +
"```\n" +
"{{.Error}}\n" +
"```\n"
+var autoplanNoProjectsTmplText = "Ran `plan` in 0 projects because Atlantis detected no Terraform changes or could not determine where to run `plan`.\n"
var errTmpl = template.Must(template.New("").Parse(errTmplText))
var errWithLogTmpl = template.Must(template.New("").Parse(errTmplText + logTmpl))
var failureTmplText = "**{{.Command}} Failed**: {{.Failure}}\n"
var failureTmpl = template.Must(template.New("").Parse(failureTmplText))
var failureWithLogTmpl = template.Must(template.New("").Parse(failureTmplText + logTmpl))
+var autoplanNoProjectsTmpl = template.Must(template.New("").Parse(autoplanNoProjectsTmplText))
+var autoplanNoProjectsWithLogTmpl = template.Must(template.New("").Parse(autoplanNoProjectsTmplText + logTmpl))
var logTmpl = "{{if .Verbose}}\nLog
\n \n\n```\n{{.Log}}```\n
{{end}}\n"
diff --git a/server/events_controller_e2e_test.go b/server/events_controller_e2e_test.go
index e2bb30ff6..2cd7183c0 100644
--- a/server/events_controller_e2e_test.go
+++ b/server/events_controller_e2e_test.go
@@ -6,6 +6,7 @@ import (
"io/ioutil"
"net/http"
"net/http/httptest"
+ "os"
"os/exec"
"path/filepath"
"regexp"
@@ -58,40 +59,40 @@ func TestGitHubWorkflow(t *testing.T) {
ExpMergeCommentFile string
CommentAndReplies []string
}{
- //{
- // Description: "simple",
- // RepoDir: "simple",
- // ModifiedFiles: []string{"main.tf"},
- // ExpAutoplanCommentFile: "exp-output-autoplan.txt",
- // CommentAndReplies: []string{
- // "atlantis apply", "exp-output-apply.txt",
- // },
- // ExpMergeCommentFile: "exp-output-merge.txt",
- //},
- //{
- // Description: "simple with comment -var",
- // RepoDir: "simple",
- // ModifiedFiles: []string{"main.tf"},
- // ExpAutoplanCommentFile: "exp-output-autoplan.txt",
- // CommentAndReplies: []string{
- // "atlantis plan -- -var var=overridden", "exp-output-atlantis-plan.txt",
- // "atlantis apply", "exp-output-apply-var.txt",
- // },
- // ExpMergeCommentFile: "exp-output-merge.txt",
- //},
- //{
- // Description: "simple with workspaces",
- // RepoDir: "simple",
- // ModifiedFiles: []string{"main.tf"},
- // ExpAutoplanCommentFile: "exp-output-autoplan.txt",
- // CommentAndReplies: []string{
- // "atlantis plan -- -var var=default_workspace", "exp-output-atlantis-plan.txt",
- // "atlantis plan -w new_workspace -- -var var=new_workspace", "exp-output-atlantis-plan-new-workspace.txt",
- // "atlantis apply", "exp-output-apply-var-default-workspace.txt",
- // "atlantis apply -w new_workspace", "exp-output-apply-var-new-workspace.txt",
- // },
- // ExpMergeCommentFile: "exp-output-merge-workspaces.txt",
- //},
+ {
+ Description: "simple",
+ RepoDir: "simple",
+ ModifiedFiles: []string{"main.tf"},
+ ExpAutoplanCommentFile: "exp-output-autoplan.txt",
+ CommentAndReplies: []string{
+ "atlantis apply", "exp-output-apply.txt",
+ },
+ ExpMergeCommentFile: "exp-output-merge.txt",
+ },
+ {
+ Description: "simple with comment -var",
+ RepoDir: "simple",
+ ModifiedFiles: []string{"main.tf"},
+ ExpAutoplanCommentFile: "exp-output-autoplan.txt",
+ CommentAndReplies: []string{
+ "atlantis plan -- -var var=overridden", "exp-output-atlantis-plan.txt",
+ "atlantis apply", "exp-output-apply-var.txt",
+ },
+ ExpMergeCommentFile: "exp-output-merge.txt",
+ },
+ {
+ Description: "simple with workspaces",
+ RepoDir: "simple",
+ ModifiedFiles: []string{"main.tf"},
+ ExpAutoplanCommentFile: "exp-output-autoplan.txt",
+ CommentAndReplies: []string{
+ "atlantis plan -- -var var=default_workspace", "exp-output-atlantis-plan.txt",
+ "atlantis plan -w new_workspace -- -var var=new_workspace", "exp-output-atlantis-plan-new-workspace.txt",
+ "atlantis apply", "exp-output-apply-var-default-workspace.txt",
+ "atlantis apply -w new_workspace", "exp-output-apply-var-new-workspace.txt",
+ },
+ ExpMergeCommentFile: "exp-output-merge-workspaces.txt",
+ },
{
Description: "simple with atlantis.yaml",
RepoDir: "simple-yaml",
@@ -103,18 +104,52 @@ func TestGitHubWorkflow(t *testing.T) {
},
ExpMergeCommentFile: "exp-output-merge.txt",
},
+ {
+ Description: "modules staging only",
+ RepoDir: "modules",
+ ModifiedFiles: []string{"staging/main.tf"},
+ ExpAutoplanCommentFile: "exp-output-autoplan-only-staging.txt",
+ CommentAndReplies: []string{
+ "atlantis apply -d staging", "exp-output-apply-staging.txt",
+ },
+ ExpMergeCommentFile: "exp-output-merge-only-staging.txt",
+ },
+ {
+ Description: "modules modules only",
+ RepoDir: "modules",
+ ModifiedFiles: []string{"modules/null/main.tf"},
+ ExpAutoplanCommentFile: "exp-output-autoplan-only-modules.txt",
+ CommentAndReplies: []string{
+ "atlantis plan -d staging", "exp-output-plan-staging.txt",
+ "atlantis plan -d production", "exp-output-plan-production.txt",
+ "atlantis apply -d staging", "exp-output-apply-staging.txt",
+ "atlantis apply -d production", "exp-output-apply-production.txt",
+ },
+ ExpMergeCommentFile: "exp-output-merge-all-dirs.txt",
+ },
+ {
+ Description: "modules-yaml",
+ RepoDir: "modules-yaml",
+ ModifiedFiles: []string{"modules/null/main.tf"},
+ ExpAutoplanCommentFile: "exp-output-autoplan.txt",
+ CommentAndReplies: []string{
+ "atlantis apply -d staging", "exp-output-apply-staging.txt",
+ "atlantis apply -d production", "exp-output-apply-production.txt",
+ },
+ ExpMergeCommentFile: "exp-output-merge-all-dirs.txt",
+ },
}
for _, c := range cases {
t.Run(c.Description, func(t *testing.T) {
ctrl, vcsClient, githubGetter, atlantisWorkspace := setupE2E(t)
// Set the repo to be cloned through the testing backdoor.
- repoDir, cleanup := initializeRepo(t, c.RepoDir)
+ repoDir, headSHA, cleanup := initializeRepo(t, c.RepoDir)
defer cleanup()
atlantisWorkspace.TestingOverrideCloneURL = fmt.Sprintf("file://%s", repoDir)
// Setup test dependencies.
w := httptest.NewRecorder()
- When(githubGetter.GetPullRequest(AnyRepo(), AnyInt())).ThenReturn(GitHubPullRequestParsed(), nil)
+ When(githubGetter.GetPullRequest(AnyRepo(), AnyInt())).ThenReturn(GitHubPullRequestParsed(headSHA), nil)
When(vcsClient.GetModifiedFiles(AnyRepo(), matchers.AnyModelsPullRequest())).ThenReturn(c.ModifiedFiles, nil)
// First, send the open pull request event and trigger an autoplan.
@@ -122,9 +157,7 @@ func TestGitHubWorkflow(t *testing.T) {
ctrl.Post(w, pullOpenedReq)
responseContains(t, w, 200, "Processing...")
_, _, autoplanComment := vcsClient.VerifyWasCalledOnce().CreateComment(AnyRepo(), AnyInt(), AnyString()).GetCapturedArguments()
- exp, err := ioutil.ReadFile(filepath.Join(repoDir, c.ExpAutoplanCommentFile))
- Ok(t, err)
- Equals(t, string(exp), autoplanComment)
+ assertCommentEquals(t, c.ExpAutoplanCommentFile, autoplanComment, c.RepoDir)
// Now send any other comments.
for i := 0; i < len(c.CommentAndReplies); i += 2 {
@@ -136,16 +169,7 @@ func TestGitHubWorkflow(t *testing.T) {
ctrl.Post(w, commentReq)
responseContains(t, w, 200, "Processing...")
_, _, atlantisComment := vcsClient.VerifyWasCalled(Times((i/2)+2)).CreateComment(AnyRepo(), AnyInt(), AnyString()).GetCapturedArguments()
-
- exp, err = ioutil.ReadFile(filepath.Join(repoDir, expOutputFile))
- Ok(t, err)
- // Replace all 'ID: 1111818181' strings with * so we can do a comparison.
- idRegex := regexp.MustCompile(`\(ID: [0-9]+\)`)
- atlantisComment = idRegex.ReplaceAllString(atlantisComment, "(ID: ******************)")
- if string(exp) != atlantisComment {
- t.Logf("comment: %s", comment)
- }
- Equals(t, string(exp), atlantisComment)
+ assertCommentEquals(t, expOutputFile, atlantisComment, c.RepoDir)
}
// Finally, send the pull request merged event.
@@ -155,9 +179,7 @@ func TestGitHubWorkflow(t *testing.T) {
responseContains(t, w, 200, "Pull request cleaned successfully")
numPrevComments := (len(c.CommentAndReplies) / 2) + 1
_, _, pullClosedComment := vcsClient.VerifyWasCalled(Times(numPrevComments+1)).CreateComment(AnyRepo(), AnyInt(), AnyString()).GetCapturedArguments()
- exp, err = ioutil.ReadFile(filepath.Join(repoDir, c.ExpMergeCommentFile))
- Ok(t, err)
- Equals(t, string(exp), pullClosedComment)
+ assertCommentEquals(t, c.ExpMergeCommentFile, pullClosedComment, c.RepoDir)
})
}
}
@@ -166,8 +188,6 @@ func setupE2E(t *testing.T) (server.EventsController, *vcsmocks.MockClientProxy,
allowForkPRs := false
dataDir, cleanup := TempDir(t)
defer cleanup()
- testRepoDir, err := filepath.Abs("testfixtures/test-repos/simple")
- Ok(t, err)
// Mocks.
e2eVCSClient := vcsmocks.NewMockClientProxy()
@@ -199,7 +219,7 @@ func setupE2E(t *testing.T) (server.EventsController, *vcsmocks.MockClientProxy,
}
atlantisWorkspace := &events.FileWorkspace{
DataDir: dataDir,
- TestingOverrideCloneURL: testRepoDir,
+ TestingOverrideCloneURL: "override-me",
}
defaultTFVersion := terraformClient.Version()
@@ -318,7 +338,11 @@ func GitHubPullRequestClosedEvent(t *testing.T) *http.Request {
return req
}
-func GitHubPullRequestParsed() *github.PullRequest {
+func GitHubPullRequestParsed(headSHA string) *github.PullRequest {
+ // headSHA can't be empty so default if not set.
+ if headSHA == "" {
+ headSHA = "13940d121be73f656e2132c6d7b4c8e87878ac8d"
+ }
return &github.PullRequest{
Number: github.Int(1),
State: github.String("open"),
@@ -328,7 +352,7 @@ func GitHubPullRequestParsed() *github.PullRequest {
FullName: github.String("runatlantis/atlantis-tests"),
CloneURL: github.String("/runatlantis/atlantis-tests.git"),
},
- SHA: github.String("sha"),
+ SHA: github.String(headSHA),
Ref: github.String("branch"),
},
Base: &github.PullRequestBranch{
@@ -343,15 +367,21 @@ func GitHubPullRequestParsed() *github.PullRequest {
}
}
+// absRepoPath returns the absolute path to the test repo under dir repoDir.
+func absRepoPath(t *testing.T, repoDir string) string {
+ path, err := filepath.Abs(filepath.Join("testfixtures", "test-repos", repoDir))
+ Ok(t, err)
+ return path
+}
+
// initializeRepo copies the repo data from testfixtures and initializes a new
// git repo in a temp directory. It returns that directory and a function
// to run in a defer that will delete the dir.
// The purpose of this function is to create a real git repository with a branch
// called 'branch' from the files under repoDir. This is so we can check in
// those files normally without needing a .git directory.
-func initializeRepo(t *testing.T, repoDir string) (string, func()) {
- originRepo, err := filepath.Abs(filepath.Join("testfixtures", "test-repos", repoDir))
- Ok(t, err)
+func initializeRepo(t *testing.T, repoDir string) (string, string, func()) {
+ originRepo := absRepoPath(t, repoDir)
// Copy the files to the temp dir.
destDir, cleanup := TempDir(t)
@@ -365,13 +395,37 @@ func initializeRepo(t *testing.T, repoDir string) (string, func()) {
runCmd(t, destDir, "git", "checkout", "-b", "branch")
runCmd(t, destDir, "git", "add", ".")
runCmd(t, destDir, "git", "commit", "-am", "branch commit")
+ headSHA := runCmd(t, destDir, "git", "rev-parse", "HEAD")
+ headSHA = strings.Trim(headSHA, "\n")
- return destDir, cleanup
+ return destDir, headSHA, cleanup
}
-func runCmd(t *testing.T, dir string, name string, args ...string) {
+func runCmd(t *testing.T, dir string, name string, args ...string) string {
cpCmd := exec.Command(name, args...)
cpCmd.Dir = dir
cpOut, err := cpCmd.CombinedOutput()
Assert(t, err == nil, "err running %q: %s", strings.Join(append([]string{name}, args...), " "), cpOut)
+ return string(cpOut)
+}
+
+func assertCommentEquals(t *testing.T, expFile string, act string, repoDir string) {
+ t.Helper()
+ exp, err := ioutil.ReadFile(filepath.Join(absRepoPath(t, repoDir), expFile))
+ Ok(t, err)
+
+ // Replace all 'ID: 1111818181' strings with * so we can do a comparison.
+ idRegex := regexp.MustCompile(`\(ID: [0-9]+\)`)
+ act = idRegex.ReplaceAllString(act, "(ID: ******************)")
+
+ if string(exp) != act {
+ actFile := filepath.Join(absRepoPath(t, repoDir), expFile+".act")
+ err := ioutil.WriteFile(actFile, []byte(act), 0600)
+ Ok(t, err)
+ cwd, err := os.Getwd()
+ Ok(t, err)
+ rel, err := filepath.Rel(cwd, actFile)
+ Ok(t, err)
+ t.Errorf("%q was different, wrote actual comment to %q", expFile, rel)
+ }
}
diff --git a/server/testfixtures/test-repos/modules-yaml/atlantis.yaml b/server/testfixtures/test-repos/modules-yaml/atlantis.yaml
new file mode 100644
index 000000000..e5915f391
--- /dev/null
+++ b/server/testfixtures/test-repos/modules-yaml/atlantis.yaml
@@ -0,0 +1,8 @@
+version: 2
+projects:
+- dir: staging
+ autoplan:
+ when_modified: ["**/*.tf", "../modules/null/*"]
+- dir: production
+ autoplan:
+ when_modified: ["**/*.tf", "../modules/null/*"]
diff --git a/server/testfixtures/test-repos/modules-yaml/exp-output-apply-production.txt b/server/testfixtures/test-repos/modules-yaml/exp-output-apply-production.txt
new file mode 100644
index 000000000..81c3ec8bf
--- /dev/null
+++ b/server/testfixtures/test-repos/modules-yaml/exp-output-apply-production.txt
@@ -0,0 +1,13 @@
+Ran Apply in dir: `production` workspace: `default`
+```diff
+module.null.null_resource.this: Creating...
+module.null.null_resource.this: Creation complete after 0s (ID: ******************)
+
+Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
+
+Outputs:
+
+var = production
+
+```
+
diff --git a/server/testfixtures/test-repos/modules-yaml/exp-output-apply-staging.txt b/server/testfixtures/test-repos/modules-yaml/exp-output-apply-staging.txt
new file mode 100644
index 000000000..2ec35e278
--- /dev/null
+++ b/server/testfixtures/test-repos/modules-yaml/exp-output-apply-staging.txt
@@ -0,0 +1,13 @@
+Ran Apply in dir: `staging` workspace: `default`
+```diff
+module.null.null_resource.this: Creating...
+module.null.null_resource.this: Creation complete after 0s (ID: ******************)
+
+Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
+
+Outputs:
+
+var = staging
+
+```
+
diff --git a/server/testfixtures/test-repos/modules-yaml/exp-output-autoplan.txt b/server/testfixtures/test-repos/modules-yaml/exp-output-autoplan.txt
new file mode 100644
index 000000000..f9a2c26eb
--- /dev/null
+++ b/server/testfixtures/test-repos/modules-yaml/exp-output-autoplan.txt
@@ -0,0 +1,51 @@
+Ran Plan for 2 projects:
+1. workspace: `default` path: `staging`
+1. workspace: `default` path: `production`
+
+### 1. workspace: `default` path: `staging`
+```diff
+Refreshing Terraform state in-memory prior to plan...
+The refreshed state will be used to calculate this plan, but will not be
+persisted to local or remote state storage.
+
+
+------------------------------------------------------------------------
+
+An execution plan has been generated and is shown below.
+Resource actions are indicated with the following symbols:
+ + create
+
+Terraform will perform the following actions:
+
++ module.null.null_resource.this
+ id:
+Plan: 1 to add, 0 to change, 0 to destroy.
+
+```
+
+* To **discard** this plan click [here](lock-url).
+---
+### 2. workspace: `default` path: `production`
+```diff
+Refreshing Terraform state in-memory prior to plan...
+The refreshed state will be used to calculate this plan, but will not be
+persisted to local or remote state storage.
+
+
+------------------------------------------------------------------------
+
+An execution plan has been generated and is shown below.
+Resource actions are indicated with the following symbols:
+ + create
+
+Terraform will perform the following actions:
+
++ module.null.null_resource.this
+ id:
+Plan: 1 to add, 0 to change, 0 to destroy.
+
+```
+
+* To **discard** this plan click [here](lock-url).
+---
+
diff --git a/server/testfixtures/test-repos/modules-yaml/exp-output-merge-all-dirs.txt b/server/testfixtures/test-repos/modules-yaml/exp-output-merge-all-dirs.txt
new file mode 100644
index 000000000..9712df1ee
--- /dev/null
+++ b/server/testfixtures/test-repos/modules-yaml/exp-output-merge-all-dirs.txt
@@ -0,0 +1,4 @@
+Locks and plans deleted for the projects and workspaces modified in this pull request:
+
+- path: `runatlantis/atlantis-tests/production` workspace: `default`
+- path: `runatlantis/atlantis-tests/staging` workspace: `default`
\ No newline at end of file
diff --git a/server/testfixtures/test-repos/modules-yaml/exp-output-merge-only-staging.txt b/server/testfixtures/test-repos/modules-yaml/exp-output-merge-only-staging.txt
new file mode 100644
index 000000000..49c8312cd
--- /dev/null
+++ b/server/testfixtures/test-repos/modules-yaml/exp-output-merge-only-staging.txt
@@ -0,0 +1,3 @@
+Locks and plans deleted for the projects and workspaces modified in this pull request:
+
+- path: `runatlantis/atlantis-tests/staging` workspace: `default`
\ No newline at end of file
diff --git a/server/testfixtures/test-repos/modules-yaml/exp-output-merge.txt b/server/testfixtures/test-repos/modules-yaml/exp-output-merge.txt
new file mode 100644
index 000000000..b64103b41
--- /dev/null
+++ b/server/testfixtures/test-repos/modules-yaml/exp-output-merge.txt
@@ -0,0 +1,4 @@
+Locks and plans deleted for the projects and workspaces modified in this pull request:
+
+- path: `runatlantis/atlantis-tests/staging` workspace: `default`
+- path: `runatlantis/atlantis-tests/.` workspace: `default`
diff --git a/server/testfixtures/test-repos/modules-yaml/exp-output-plan-production.txt b/server/testfixtures/test-repos/modules-yaml/exp-output-plan-production.txt
new file mode 100644
index 000000000..caea5e643
--- /dev/null
+++ b/server/testfixtures/test-repos/modules-yaml/exp-output-plan-production.txt
@@ -0,0 +1,23 @@
+Ran Plan in dir: `production` workspace: `default`
+```diff
+Refreshing Terraform state in-memory prior to plan...
+The refreshed state will be used to calculate this plan, but will not be
+persisted to local or remote state storage.
+
+
+------------------------------------------------------------------------
+
+An execution plan has been generated and is shown below.
+Resource actions are indicated with the following symbols:
+ + create
+
+Terraform will perform the following actions:
+
++ module.null.null_resource.this
+ id:
+Plan: 1 to add, 0 to change, 0 to destroy.
+
+```
+
+* To **discard** this plan click [here](lock-url).
+
diff --git a/server/testfixtures/test-repos/modules-yaml/exp-output-plan-staging.txt b/server/testfixtures/test-repos/modules-yaml/exp-output-plan-staging.txt
new file mode 100644
index 000000000..0e77a9442
--- /dev/null
+++ b/server/testfixtures/test-repos/modules-yaml/exp-output-plan-staging.txt
@@ -0,0 +1,23 @@
+Ran Plan in dir: `staging` workspace: `default`
+```diff
+Refreshing Terraform state in-memory prior to plan...
+The refreshed state will be used to calculate this plan, but will not be
+persisted to local or remote state storage.
+
+
+------------------------------------------------------------------------
+
+An execution plan has been generated and is shown below.
+Resource actions are indicated with the following symbols:
+ + create
+
+Terraform will perform the following actions:
+
++ module.null.null_resource.this
+ id:
+Plan: 1 to add, 0 to change, 0 to destroy.
+
+```
+
+* To **discard** this plan click [here](lock-url).
+
diff --git a/server/testfixtures/test-repos/modules-yaml/modules/null/main.tf b/server/testfixtures/test-repos/modules-yaml/modules/null/main.tf
new file mode 100644
index 000000000..14f6a189c
--- /dev/null
+++ b/server/testfixtures/test-repos/modules-yaml/modules/null/main.tf
@@ -0,0 +1,10 @@
+variable "var" {}
+resource "null_resource" "this" {
+}
+output "var" {
+ value = "${var.var}"
+}
+
+output "workspace" {
+ value = "${terraform.workspace}"
+}
diff --git a/server/testfixtures/test-repos/modules-yaml/production/main.tf b/server/testfixtures/test-repos/modules-yaml/production/main.tf
new file mode 100644
index 000000000..94a103ffb
--- /dev/null
+++ b/server/testfixtures/test-repos/modules-yaml/production/main.tf
@@ -0,0 +1,7 @@
+module "null" {
+ source = "../modules/null"
+ var = "production"
+}
+output "var" {
+ value = "${module.null.var}"
+}
\ No newline at end of file
diff --git a/server/testfixtures/test-repos/modules-yaml/staging/main.tf b/server/testfixtures/test-repos/modules-yaml/staging/main.tf
new file mode 100644
index 000000000..15fa81303
--- /dev/null
+++ b/server/testfixtures/test-repos/modules-yaml/staging/main.tf
@@ -0,0 +1,7 @@
+module "null" {
+ source = "../modules/null"
+ var = "staging"
+}
+output "var" {
+ value = "${module.null.var}"
+}
\ No newline at end of file
diff --git a/server/testfixtures/test-repos/modules/exp-output-apply-production.txt b/server/testfixtures/test-repos/modules/exp-output-apply-production.txt
new file mode 100644
index 000000000..81c3ec8bf
--- /dev/null
+++ b/server/testfixtures/test-repos/modules/exp-output-apply-production.txt
@@ -0,0 +1,13 @@
+Ran Apply in dir: `production` workspace: `default`
+```diff
+module.null.null_resource.this: Creating...
+module.null.null_resource.this: Creation complete after 0s (ID: ******************)
+
+Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
+
+Outputs:
+
+var = production
+
+```
+
diff --git a/server/testfixtures/test-repos/modules/exp-output-apply-staging.txt b/server/testfixtures/test-repos/modules/exp-output-apply-staging.txt
new file mode 100644
index 000000000..2ec35e278
--- /dev/null
+++ b/server/testfixtures/test-repos/modules/exp-output-apply-staging.txt
@@ -0,0 +1,13 @@
+Ran Apply in dir: `staging` workspace: `default`
+```diff
+module.null.null_resource.this: Creating...
+module.null.null_resource.this: Creation complete after 0s (ID: ******************)
+
+Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
+
+Outputs:
+
+var = staging
+
+```
+
diff --git a/server/testfixtures/test-repos/modules/exp-output-autoplan-only-modules.txt b/server/testfixtures/test-repos/modules/exp-output-autoplan-only-modules.txt
new file mode 100644
index 000000000..63b09ca64
--- /dev/null
+++ b/server/testfixtures/test-repos/modules/exp-output-autoplan-only-modules.txt
@@ -0,0 +1,2 @@
+Ran `plan` in 0 projects because Atlantis detected no Terraform changes or could not determine where to run `plan`.
+
diff --git a/server/testfixtures/test-repos/modules/exp-output-autoplan-only-staging.txt b/server/testfixtures/test-repos/modules/exp-output-autoplan-only-staging.txt
new file mode 100644
index 000000000..0e77a9442
--- /dev/null
+++ b/server/testfixtures/test-repos/modules/exp-output-autoplan-only-staging.txt
@@ -0,0 +1,23 @@
+Ran Plan in dir: `staging` workspace: `default`
+```diff
+Refreshing Terraform state in-memory prior to plan...
+The refreshed state will be used to calculate this plan, but will not be
+persisted to local or remote state storage.
+
+
+------------------------------------------------------------------------
+
+An execution plan has been generated and is shown below.
+Resource actions are indicated with the following symbols:
+ + create
+
+Terraform will perform the following actions:
+
++ module.null.null_resource.this
+ id:
+Plan: 1 to add, 0 to change, 0 to destroy.
+
+```
+
+* To **discard** this plan click [here](lock-url).
+
diff --git a/server/testfixtures/test-repos/modules/exp-output-merge-all-dirs.txt b/server/testfixtures/test-repos/modules/exp-output-merge-all-dirs.txt
new file mode 100644
index 000000000..9712df1ee
--- /dev/null
+++ b/server/testfixtures/test-repos/modules/exp-output-merge-all-dirs.txt
@@ -0,0 +1,4 @@
+Locks and plans deleted for the projects and workspaces modified in this pull request:
+
+- path: `runatlantis/atlantis-tests/production` workspace: `default`
+- path: `runatlantis/atlantis-tests/staging` workspace: `default`
\ No newline at end of file
diff --git a/server/testfixtures/test-repos/modules/exp-output-merge-only-staging.txt b/server/testfixtures/test-repos/modules/exp-output-merge-only-staging.txt
new file mode 100644
index 000000000..49c8312cd
--- /dev/null
+++ b/server/testfixtures/test-repos/modules/exp-output-merge-only-staging.txt
@@ -0,0 +1,3 @@
+Locks and plans deleted for the projects and workspaces modified in this pull request:
+
+- path: `runatlantis/atlantis-tests/staging` workspace: `default`
\ No newline at end of file
diff --git a/server/testfixtures/test-repos/modules/exp-output-merge.txt b/server/testfixtures/test-repos/modules/exp-output-merge.txt
new file mode 100644
index 000000000..b64103b41
--- /dev/null
+++ b/server/testfixtures/test-repos/modules/exp-output-merge.txt
@@ -0,0 +1,4 @@
+Locks and plans deleted for the projects and workspaces modified in this pull request:
+
+- path: `runatlantis/atlantis-tests/staging` workspace: `default`
+- path: `runatlantis/atlantis-tests/.` workspace: `default`
diff --git a/server/testfixtures/test-repos/modules/exp-output-plan-production.txt b/server/testfixtures/test-repos/modules/exp-output-plan-production.txt
new file mode 100644
index 000000000..caea5e643
--- /dev/null
+++ b/server/testfixtures/test-repos/modules/exp-output-plan-production.txt
@@ -0,0 +1,23 @@
+Ran Plan in dir: `production` workspace: `default`
+```diff
+Refreshing Terraform state in-memory prior to plan...
+The refreshed state will be used to calculate this plan, but will not be
+persisted to local or remote state storage.
+
+
+------------------------------------------------------------------------
+
+An execution plan has been generated and is shown below.
+Resource actions are indicated with the following symbols:
+ + create
+
+Terraform will perform the following actions:
+
++ module.null.null_resource.this
+ id:
+Plan: 1 to add, 0 to change, 0 to destroy.
+
+```
+
+* To **discard** this plan click [here](lock-url).
+
diff --git a/server/testfixtures/test-repos/modules/exp-output-plan-staging.txt b/server/testfixtures/test-repos/modules/exp-output-plan-staging.txt
new file mode 100644
index 000000000..0e77a9442
--- /dev/null
+++ b/server/testfixtures/test-repos/modules/exp-output-plan-staging.txt
@@ -0,0 +1,23 @@
+Ran Plan in dir: `staging` workspace: `default`
+```diff
+Refreshing Terraform state in-memory prior to plan...
+The refreshed state will be used to calculate this plan, but will not be
+persisted to local or remote state storage.
+
+
+------------------------------------------------------------------------
+
+An execution plan has been generated and is shown below.
+Resource actions are indicated with the following symbols:
+ + create
+
+Terraform will perform the following actions:
+
++ module.null.null_resource.this
+ id:
+Plan: 1 to add, 0 to change, 0 to destroy.
+
+```
+
+* To **discard** this plan click [here](lock-url).
+
diff --git a/server/testfixtures/test-repos/modules/modules/null/main.tf b/server/testfixtures/test-repos/modules/modules/null/main.tf
new file mode 100644
index 000000000..14f6a189c
--- /dev/null
+++ b/server/testfixtures/test-repos/modules/modules/null/main.tf
@@ -0,0 +1,10 @@
+variable "var" {}
+resource "null_resource" "this" {
+}
+output "var" {
+ value = "${var.var}"
+}
+
+output "workspace" {
+ value = "${terraform.workspace}"
+}
diff --git a/server/testfixtures/test-repos/modules/production/main.tf b/server/testfixtures/test-repos/modules/production/main.tf
new file mode 100644
index 000000000..94a103ffb
--- /dev/null
+++ b/server/testfixtures/test-repos/modules/production/main.tf
@@ -0,0 +1,7 @@
+module "null" {
+ source = "../modules/null"
+ var = "production"
+}
+output "var" {
+ value = "${module.null.var}"
+}
\ No newline at end of file
diff --git a/server/testfixtures/test-repos/modules/staging/main.tf b/server/testfixtures/test-repos/modules/staging/main.tf
new file mode 100644
index 000000000..15fa81303
--- /dev/null
+++ b/server/testfixtures/test-repos/modules/staging/main.tf
@@ -0,0 +1,7 @@
+module "null" {
+ source = "../modules/null"
+ var = "staging"
+}
+output "var" {
+ value = "${module.null.var}"
+}
\ No newline at end of file
diff --git a/server/testfixtures/test-repos/simple/exp-output-merge-workspaces.txt b/server/testfixtures/test-repos/simple/exp-output-merge-workspaces.txt
index 1b856aa5c..5489b642a 100644
--- a/server/testfixtures/test-repos/simple/exp-output-merge-workspaces.txt
+++ b/server/testfixtures/test-repos/simple/exp-output-merge-workspaces.txt
@@ -1,3 +1,3 @@
Locks and plans deleted for the projects and workspaces modified in this pull request:
-- path: `runatlantis/atlantis-tests/.` workspaces: `default`, `new_workspace`
+- path: `runatlantis/atlantis-tests/.` workspaces: `default`, `new_workspace`
\ No newline at end of file