From 58ff8999068339c54150ed63cb5a69d894f31ddc Mon Sep 17 00:00:00 2001
From: Tiffany Chiang <14845269+tlin4194@users.noreply.github.com>
Date: Tue, 3 Dec 2024 10:27:12 -0500
Subject: [PATCH] [DEVCON-2524] output deploy queue to checkrun summary (#772)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
### Issue:
Previously, when a revision is queued, we mark the github status as
queued but have no other indication in the check run itself that this
check run is waiting on something.
Users have to figure out which check run is holding up the queue which
is difficult because deploys for a root can happen off any branch. We
can figure this out using the temporal UI however it would be a better
CX if we could just surface the queue itself in the check run.
### Part 1
Add queue info to checkrun summary on creating new check run or unlock
signal. Queue info is shown as [commit SHA](link to specific check run)
### Part 2
Check run summaries do not get updated in cases where a a Terraform
workflow is pending Confirm/Reject checkrun action between Plan and
Apply steps, since the deploy queue lock only tracks the "Unlock"
checkrun action.
* Surfaces deploy queue from queue package to deploy/terraform package.
Some complication using Queue structs here due to cyclic dependencies
between the two packages.
* To avoid restructuring the two packages, instead we will pass the
GithubCheckRunCache (github package) over from queue worker to
deploy/terraform workflow.
* StateReceiver will use GithubCheckRunCache to update checkrun
summaries for each revision on the queue when a Confirm/Reject action is
pending.
### Test Plan
1. Recreating Unlock action: open 2 PRs, run `atlantis apply -f` on PR
1, and try to merge PR 2. PR 2 will be locked by confirm reject action
on PR 1. Any subsequent queued deploys will show up as queued commits
(with corresponding links) in checkrun summary.
2. Recreating diverged confirm/reject action: run `atlantis apply -f` on
unmerged PR 1 and confirm. Create PR 3 and run `atlantis apply -f` on
unmerged PR 3.
3. Merged PR 1 deploy will update to to show pending action on PR 3.
Links will go directly to the specific run instead of just the commit in
case there were multiple forced applies on PR 1.
### Improve Test Cases
Fix queue_test and updater_test where assertions in CreateOrUpdate were
not getting caught since the function is called within a temporal
worker.
---
.golangci.yml | 2 +-
.../github/revision_url_markdown.go | 5 +
.../workflows/internal/deploy/lock/lock.go | 15 +++
.../deploy/revision/notifier/slack.go | 3 +-
.../deploy/revision/notifier/slack_test.go | 11 +-
.../internal/deploy/revision/queue/queue.go | 43 +++----
.../deploy/revision/queue/queue_test.go | 17 +--
.../internal/deploy/revision/queue/updater.go | 11 +-
.../deploy/revision/queue/updater_test.go | 18 ++-
.../internal/deploy/revision/queue/worker.go | 16 ++-
.../revision/queue/worker_state_test.go | 11 +-
.../deploy/revision/queue/worker_test.go | 43 ++++---
.../internal/deploy/revision/revision.go | 21 ++--
.../internal/deploy/revision/revision_test.go | 107 ++++++++++++------
.../internal/deploy/terraform/runner.go | 11 +-
.../internal/deploy/terraform/state.go | 66 ++++++++++-
.../internal/deploy/terraform/state_test.go | 76 ++++++++++++-
17 files changed, 359 insertions(+), 117 deletions(-)
create mode 100644 server/neptune/workflows/internal/deploy/lock/lock.go
diff --git a/.golangci.yml b/.golangci.yml
index 1fb9e8879..5986dec2e 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -35,4 +35,4 @@ issues:
- dogsled
linters-settings:
interfacebloat:
- max: 6
+ max: 7
diff --git a/server/neptune/workflows/activities/github/revision_url_markdown.go b/server/neptune/workflows/activities/github/revision_url_markdown.go
index b64bdffb8..4d7a0e487 100644
--- a/server/neptune/workflows/activities/github/revision_url_markdown.go
+++ b/server/neptune/workflows/activities/github/revision_url_markdown.go
@@ -6,3 +6,8 @@ func BuildRevisionURLMarkdown(repoFullName string, revision string) string {
// uses Markdown formatting to generate the link on GH
return fmt.Sprintf("[%s](https://github.com/%s/commit/%s)", revision, repoFullName, revision)
}
+
+func BuildRunURLMarkdown(repoFullName string, revision string, runId int64) string {
+ // uses Markdown formatting to generate the link on GH
+ return fmt.Sprintf("[%s](https://github.com/%s/runs/%d)", revision, repoFullName, runId)
+}
diff --git a/server/neptune/workflows/internal/deploy/lock/lock.go b/server/neptune/workflows/internal/deploy/lock/lock.go
new file mode 100644
index 000000000..ec40cb0ff
--- /dev/null
+++ b/server/neptune/workflows/internal/deploy/lock/lock.go
@@ -0,0 +1,15 @@
+package lock
+
+type LockStatus int
+
+type LockState struct {
+ Revision string
+ Status LockStatus
+}
+
+const (
+ UnlockedStatus LockStatus = iota
+ LockedStatus
+
+ QueueDepthStat = "queue.depth"
+)
diff --git a/server/neptune/workflows/internal/deploy/revision/notifier/slack.go b/server/neptune/workflows/internal/deploy/revision/notifier/slack.go
index b4cfc57a6..6c199295b 100644
--- a/server/neptune/workflows/internal/deploy/revision/notifier/slack.go
+++ b/server/neptune/workflows/internal/deploy/revision/notifier/slack.go
@@ -7,6 +7,7 @@ import (
"github.com/pkg/errors"
"github.com/runatlantis/atlantis/server/neptune/workflows/activities"
"github.com/runatlantis/atlantis/server/neptune/workflows/activities/github"
+ "github.com/runatlantis/atlantis/server/neptune/workflows/internal/deploy/lock"
"github.com/runatlantis/atlantis/server/neptune/workflows/internal/deploy/revision/queue"
"github.com/slack-go/slack"
"go.temporal.io/sdk/workflow"
@@ -24,7 +25,7 @@ type Slack struct {
func (s *Slack) Notify(ctx workflow.Context) error {
state := s.DeployQueue.GetLockState()
- if state.Status == queue.UnlockedStatus {
+ if state.Status == lock.UnlockedStatus {
return nil
}
diff --git a/server/neptune/workflows/internal/deploy/revision/notifier/slack_test.go b/server/neptune/workflows/internal/deploy/revision/notifier/slack_test.go
index 14da6d1a6..b95266e92 100644
--- a/server/neptune/workflows/internal/deploy/revision/notifier/slack_test.go
+++ b/server/neptune/workflows/internal/deploy/revision/notifier/slack_test.go
@@ -8,6 +8,7 @@ import (
"github.com/runatlantis/atlantis/server/neptune/workflows/activities"
"github.com/runatlantis/atlantis/server/neptune/workflows/activities/github"
terraformActivities "github.com/runatlantis/atlantis/server/neptune/workflows/activities/terraform"
+ "github.com/runatlantis/atlantis/server/neptune/workflows/internal/deploy/lock"
"github.com/runatlantis/atlantis/server/neptune/workflows/internal/deploy/revision/notifier"
"github.com/runatlantis/atlantis/server/neptune/workflows/internal/deploy/revision/queue"
"github.com/runatlantis/atlantis/server/neptune/workflows/internal/deploy/terraform"
@@ -20,7 +21,7 @@ import (
)
type request struct {
- LockState queue.LockState
+ LockState lock.LockState
InitialItems []terraform.DeploymentInfo
}
@@ -51,7 +52,7 @@ func testWorkflow(ctx workflow.Context, request request) error {
func TestNotifier(t *testing.T) {
t.Run("empty queue", func(t *testing.T) {
- state := queue.LockState{Status: queue.UnlockedStatus}
+ state := lock.LockState{Status: lock.UnlockedStatus}
ts := testsuite.WorkflowTestSuite{}
env := ts.NewTestWorkflowEnvironment()
@@ -68,7 +69,7 @@ func TestNotifier(t *testing.T) {
})
t.Run("locked state", func(t *testing.T) {
- state := queue.LockState{Status: queue.LockedStatus}
+ state := lock.LockState{Status: lock.LockedStatus}
ts := testsuite.WorkflowTestSuite{}
env := ts.NewTestWorkflowEnvironment()
@@ -85,7 +86,7 @@ func TestNotifier(t *testing.T) {
})
t.Run("no slack config", func(t *testing.T) {
- state := queue.LockState{Status: queue.LockedStatus}
+ state := lock.LockState{Status: lock.LockedStatus}
ts := testsuite.WorkflowTestSuite{}
env := ts.NewTestWorkflowEnvironment()
@@ -121,7 +122,7 @@ func TestNotifier(t *testing.T) {
})
t.Run("activity called", func(t *testing.T) {
- state := queue.LockState{Status: queue.LockedStatus}
+ state := lock.LockState{Status: lock.LockedStatus}
ts := testsuite.WorkflowTestSuite{}
env := ts.NewTestWorkflowEnvironment()
diff --git a/server/neptune/workflows/internal/deploy/revision/queue/queue.go b/server/neptune/workflows/internal/deploy/revision/queue/queue.go
index 306ed6b46..051cecce2 100644
--- a/server/neptune/workflows/internal/deploy/revision/queue/queue.go
+++ b/server/neptune/workflows/internal/deploy/revision/queue/queue.go
@@ -3,34 +3,23 @@ package queue
import (
"container/list"
"fmt"
+ "strings"
+ "github.com/runatlantis/atlantis/server/neptune/workflows/activities/github"
activity "github.com/runatlantis/atlantis/server/neptune/workflows/activities/terraform"
+ "github.com/runatlantis/atlantis/server/neptune/workflows/internal/deploy/lock"
"github.com/runatlantis/atlantis/server/neptune/workflows/internal/deploy/terraform"
"github.com/runatlantis/atlantis/server/neptune/workflows/internal/metrics"
"go.temporal.io/sdk/workflow"
)
-type LockStatus int
-
-type LockState struct {
- Revision string
- Status LockStatus
-}
-
-const (
- UnlockedStatus LockStatus = iota
- LockedStatus
-
- QueueDepthStat = "queue.depth"
-)
-
type Deploy struct {
queue *priority
lockStatusCallback func(workflow.Context, *Deploy)
scope metrics.Scope
// mutable: default is unlocked
- lock LockState
+ lock lock.LockState
}
func NewQueue(callback func(workflow.Context, *Deploy), scope metrics.Scope) *Deploy {
@@ -41,12 +30,12 @@ func NewQueue(callback func(workflow.Context, *Deploy), scope metrics.Scope) *De
}
}
-func (q *Deploy) GetLockState() LockState {
+func (q *Deploy) GetLockState() lock.LockState {
return q.lock
}
-func (q *Deploy) SetLockForMergedItems(ctx workflow.Context, state LockState) {
- if state.Status == LockedStatus {
+func (q *Deploy) SetLockForMergedItems(ctx workflow.Context, state lock.LockState) {
+ if state.Status == lock.LockedStatus {
q.scope.Counter("locked").Inc(1)
} else {
q.scope.Counter("unlocked").Inc(1)
@@ -56,11 +45,11 @@ func (q *Deploy) SetLockForMergedItems(ctx workflow.Context, state LockState) {
}
func (q *Deploy) CanPop() bool {
- return q.queue.HasItemsOfPriority(High) || (q.lock.Status == UnlockedStatus && !q.queue.IsEmpty())
+ return q.queue.HasItemsOfPriority(High) || (q.lock.Status == lock.UnlockedStatus && !q.queue.IsEmpty())
}
func (q *Deploy) Pop() (terraform.DeploymentInfo, error) {
- defer q.scope.Gauge(QueueDepthStat).Update(float64(q.queue.Size()))
+ defer q.scope.Gauge(lock.QueueDepthStat).Update(float64(q.queue.Size()))
return q.queue.Pop()
}
@@ -77,7 +66,7 @@ func (q *Deploy) IsEmpty() bool {
}
func (q *Deploy) Push(msg terraform.DeploymentInfo) {
- defer q.scope.Gauge(QueueDepthStat).Update(float64(q.queue.Size()))
+ defer q.scope.Gauge(lock.QueueDepthStat).Update(float64(q.queue.Size()))
if msg.Root.TriggerInfo.Type == activity.ManualTrigger {
q.queue.Push(msg, High)
return
@@ -85,6 +74,18 @@ func (q *Deploy) Push(msg terraform.DeploymentInfo) {
q.queue.Push(msg, Low)
}
+func (q *Deploy) GetQueuedRevisionsSummary() string {
+ var revisions []string
+ if q.IsEmpty() {
+ return "No other runs ahead in queue."
+ }
+ for _, deploy := range q.Scan() {
+ runLink := github.BuildRunURLMarkdown(deploy.Repo.GetFullName(), deploy.Commit.Revision, deploy.CheckRunID)
+ revisions = append(revisions, runLink)
+ }
+ return fmt.Sprintf("Runs in queue: %s", strings.Join(revisions, ", "))
+}
+
// priority is a simple 2 priority queue implementation
// priority is determined before an item enters a queue and does not change
type priority struct {
diff --git a/server/neptune/workflows/internal/deploy/revision/queue/queue_test.go b/server/neptune/workflows/internal/deploy/revision/queue/queue_test.go
index d54178921..9cbac7bf9 100644
--- a/server/neptune/workflows/internal/deploy/revision/queue/queue_test.go
+++ b/server/neptune/workflows/internal/deploy/revision/queue/queue_test.go
@@ -5,6 +5,7 @@ import (
"github.com/runatlantis/atlantis/server/neptune/workflows/activities/github"
activity "github.com/runatlantis/atlantis/server/neptune/workflows/activities/terraform"
+ "github.com/runatlantis/atlantis/server/neptune/workflows/internal/deploy/lock"
"github.com/runatlantis/atlantis/server/neptune/workflows/internal/deploy/revision/queue"
"github.com/runatlantis/atlantis/server/neptune/workflows/internal/deploy/terraform"
"github.com/runatlantis/atlantis/server/neptune/workflows/internal/metrics"
@@ -38,8 +39,8 @@ func TestQueue(t *testing.T) {
q := queue.NewQueue(func(ctx workflow.Context, d *queue.Deploy) {
called = true
}, metrics.NewNullableScope())
- q.SetLockForMergedItems(test.Background(), queue.LockState{
- Status: queue.LockedStatus,
+ q.SetLockForMergedItems(test.Background(), lock.LockState{
+ Status: lock.LockedStatus,
})
assert.True(t, called)
@@ -52,8 +53,8 @@ func TestQueue(t *testing.T) {
t.Run("can pop empty queue locked", func(t *testing.T) {
q := queue.NewQueue(noopCallback, metrics.NewNullableScope())
- q.SetLockForMergedItems(test.Background(), queue.LockState{
- Status: queue.LockedStatus,
+ q.SetLockForMergedItems(test.Background(), lock.LockState{
+ Status: lock.LockedStatus,
})
assert.Equal(t, false, q.CanPop())
})
@@ -61,8 +62,8 @@ func TestQueue(t *testing.T) {
q := queue.NewQueue(noopCallback, metrics.NewNullableScope())
msg1 := wrap("1", activity.ManualTrigger)
q.Push(msg1)
- q.SetLockForMergedItems(test.Background(), queue.LockState{
- Status: queue.LockedStatus,
+ q.SetLockForMergedItems(test.Background(), lock.LockState{
+ Status: lock.LockedStatus,
})
assert.Equal(t, true, q.CanPop())
})
@@ -76,8 +77,8 @@ func TestQueue(t *testing.T) {
q := queue.NewQueue(noopCallback, metrics.NewNullableScope())
msg1 := wrap("1", activity.MergeTrigger)
q.Push(msg1)
- q.SetLockForMergedItems(test.Background(), queue.LockState{
- Status: queue.LockedStatus,
+ q.SetLockForMergedItems(test.Background(), lock.LockState{
+ Status: lock.LockedStatus,
})
assert.Equal(t, false, q.CanPop())
})
diff --git a/server/neptune/workflows/internal/deploy/revision/queue/updater.go b/server/neptune/workflows/internal/deploy/revision/queue/updater.go
index 098c66d5c..dfd42bd30 100644
--- a/server/neptune/workflows/internal/deploy/revision/queue/updater.go
+++ b/server/neptune/workflows/internal/deploy/revision/queue/updater.go
@@ -2,10 +2,12 @@ package queue
import (
"fmt"
+
key "github.com/runatlantis/atlantis/server/neptune/context"
"github.com/runatlantis/atlantis/server/neptune/workflows/internal/notifier"
"github.com/runatlantis/atlantis/server/neptune/workflows/activities/github"
+ "github.com/runatlantis/atlantis/server/neptune/workflows/internal/deploy/lock"
"go.temporal.io/sdk/workflow"
)
@@ -19,17 +21,18 @@ type LockStateUpdater struct {
}
func (u *LockStateUpdater) UpdateQueuedRevisions(ctx workflow.Context, queue *Deploy, repoFullName string) {
- lock := queue.GetLockState()
+ queueLock := queue.GetLockState()
infos := queue.GetOrderedMergedItems()
var actions []github.CheckRunAction
var summary string
+ var revisionsSummary string = queue.GetQueuedRevisionsSummary()
state := github.CheckRunQueued
- if lock.Status == LockedStatus {
+ if queueLock.Status == lock.LockedStatus {
actions = append(actions, github.CreateUnlockAction())
state = github.CheckRunActionRequired
- revisionLink := github.BuildRevisionURLMarkdown(repoFullName, lock.Revision)
- summary = fmt.Sprintf("This deploy is locked from a manual deployment for revision %s. Unlock to proceed.", revisionLink)
+ revisionLink := github.BuildRevisionURLMarkdown(repoFullName, queueLock.Revision)
+ summary = fmt.Sprintf("This deploy is locked from a manual deployment for revision %s. Unlock to proceed.\n%s", revisionLink, revisionsSummary)
}
for _, i := range infos {
diff --git a/server/neptune/workflows/internal/deploy/revision/queue/updater_test.go b/server/neptune/workflows/internal/deploy/revision/queue/updater_test.go
index dd8ade796..9fd6f85df 100644
--- a/server/neptune/workflows/internal/deploy/revision/queue/updater_test.go
+++ b/server/neptune/workflows/internal/deploy/revision/queue/updater_test.go
@@ -1,14 +1,16 @@
package queue_test
import (
- "github.com/runatlantis/atlantis/server/neptune/workflows/internal/notifier"
"testing"
"time"
+ "github.com/runatlantis/atlantis/server/neptune/workflows/internal/notifier"
+
"github.com/google/uuid"
"github.com/runatlantis/atlantis/server/neptune/workflows/activities"
"github.com/runatlantis/atlantis/server/neptune/workflows/activities/github"
tfActivity "github.com/runatlantis/atlantis/server/neptune/workflows/activities/terraform"
+ "github.com/runatlantis/atlantis/server/neptune/workflows/internal/deploy/lock"
"github.com/runatlantis/atlantis/server/neptune/workflows/internal/deploy/revision/queue"
"github.com/runatlantis/atlantis/server/neptune/workflows/internal/deploy/terraform"
"github.com/runatlantis/atlantis/server/neptune/workflows/internal/metrics"
@@ -25,9 +27,13 @@ type testCheckRunClient struct {
}
func (t *testCheckRunClient) CreateOrUpdate(ctx workflow.Context, deploymentID string, request notifier.GithubCheckRunRequest) (int64, error) {
- assert.Equal(t.expectedT, t.expectedRequest, request)
- assert.Equal(t.expectedT, t.expectedDeploymentID, deploymentID)
+ switch {
+ case assert.Equal(t.expectedT, t.expectedRequest, request):
+ case assert.Equal(t.expectedT, t.expectedDeploymentID, deploymentID):
+ default:
+ t.expectedT.FailNow()
+ }
return 1, nil
}
@@ -121,8 +127,8 @@ func TestLockStateUpdater_locked_new_version(t *testing.T) {
env.ExecuteWorkflow(testUpdaterWorkflow, updaterReq{
Queue: []terraform.DeploymentInfo{info},
- Lock: queue.LockState{
- Status: queue.LockedStatus,
+ Lock: lock.LockState{
+ Status: lock.LockedStatus,
Revision: "1234",
},
ExpectedRequest: notifier.GithubCheckRunRequest{
@@ -147,7 +153,7 @@ func TestLockStateUpdater_locked_new_version(t *testing.T) {
type updaterReq struct {
Queue []terraform.DeploymentInfo
- Lock queue.LockState
+ Lock lock.LockState
ExpectedRequest notifier.GithubCheckRunRequest
ExpectedDeploymentID string
ExpectedT *testing.T
diff --git a/server/neptune/workflows/internal/deploy/revision/queue/worker.go b/server/neptune/workflows/internal/deploy/revision/queue/worker.go
index 9fc6c333e..f2acd0287 100644
--- a/server/neptune/workflows/internal/deploy/revision/queue/worker.go
+++ b/server/neptune/workflows/internal/deploy/revision/queue/worker.go
@@ -13,6 +13,7 @@ import (
internalContext "github.com/runatlantis/atlantis/server/neptune/context"
"github.com/runatlantis/atlantis/server/neptune/workflows/activities/deployment"
tfModel "github.com/runatlantis/atlantis/server/neptune/workflows/activities/terraform"
+ "github.com/runatlantis/atlantis/server/neptune/workflows/internal/deploy/lock"
"github.com/runatlantis/atlantis/server/neptune/workflows/internal/deploy/terraform"
"github.com/runatlantis/atlantis/server/neptune/workflows/internal/metrics"
"github.com/runatlantis/atlantis/server/neptune/workflows/plugins"
@@ -24,7 +25,10 @@ type queue interface {
IsEmpty() bool
CanPop() bool
Pop() (terraform.DeploymentInfo, error)
- SetLockForMergedItems(ctx workflow.Context, state LockState)
+ SetLockForMergedItems(ctx workflow.Context, state lock.LockState)
+ GetOrderedMergedItems() []terraform.DeploymentInfo
+ GetQueuedRevisionsSummary() string
+ GetLockState() lock.LockState
}
type deployer interface {
@@ -96,7 +100,7 @@ func NewWorker(
},
}
- tfWorkflowRunner := terraform.NewWorkflowRunner(tfWorkflow, notifiers, additionalNotifiers...)
+ tfWorkflowRunner := terraform.NewWorkflowRunner(q, tfWorkflow, githubCheckRunCache, notifiers, additionalNotifiers...)
deployer := &Deployer{
Activities: a,
TerraformWorkflowRunner: tfWorkflowRunner,
@@ -112,8 +116,8 @@ func NewWorker(
// we don't persist lock state anywhere so in the case of workflow completion we need to rebuild
// the lock state
if latestDeployment != nil && latestDeployment.Root.Trigger == string(tfModel.ManualTrigger) {
- q.SetLockForMergedItems(ctx, LockState{
- Status: LockedStatus,
+ q.SetLockForMergedItems(ctx, lock.LockState{
+ Status: lock.LockedStatus,
Revision: latestDeployment.Revision,
})
}
@@ -179,8 +183,8 @@ func (w *Worker) Work(ctx workflow.Context) {
workflow.GetMetricsHandler(ctx).WithTags(map[string]string{metricNames.SignalNameTag: UnlockSignalName}).
Counter(metricNames.SignalReceive).
Inc(1)
- w.Queue.SetLockForMergedItems(ctx, LockState{
- Status: UnlockedStatus,
+ w.Queue.SetLockForMergedItems(ctx, lock.LockState{
+ Status: lock.UnlockedStatus,
})
continue
default:
diff --git a/server/neptune/workflows/internal/deploy/revision/queue/worker_state_test.go b/server/neptune/workflows/internal/deploy/revision/queue/worker_state_test.go
index 26de4050a..55ec9f36c 100644
--- a/server/neptune/workflows/internal/deploy/revision/queue/worker_state_test.go
+++ b/server/neptune/workflows/internal/deploy/revision/queue/worker_state_test.go
@@ -13,6 +13,7 @@ import (
"go.temporal.io/sdk/testsuite"
"go.temporal.io/sdk/workflow"
+ "github.com/runatlantis/atlantis/server/neptune/workflows/internal/deploy/lock"
"github.com/runatlantis/atlantis/server/neptune/workflows/internal/deploy/revision/queue"
internalTerraform "github.com/runatlantis/atlantis/server/neptune/workflows/internal/deploy/terraform"
"github.com/runatlantis/atlantis/server/neptune/workflows/internal/metrics"
@@ -47,7 +48,7 @@ func testStateWorkflow(ctx workflow.Context, r workerRequest) (workerResponse, e
Queue: list.New(),
}
- q.SetLockForMergedItems(ctx, queue.LockState{Status: r.InitialLockStatus})
+ q.SetLockForMergedItems(ctx, lock.LockState{Status: r.InitialLockStatus})
workflow.Go(ctx, func(ctx workflow.Context) {
ch := workflow.GetSignalChannel(ctx, "add-to-queue")
@@ -109,8 +110,8 @@ func TestWorker_StartsWithEmptyQueue(t *testing.T) {
assert.NoError(t, err)
assert.True(t, q.QueueIsEmpty)
- assert.Equal(t, queue.LockState{
- Status: queue.UnlockedStatus,
+ assert.Equal(t, lock.LockState{
+ Status: lock.UnlockedStatus,
}, q.Lock)
assert.Equal(t, queue.WaitingWorkerState, q.State)
}, 2*time.Second)
@@ -145,8 +146,8 @@ func TestWorker_StartsWithEmptyQueue(t *testing.T) {
assert.NoError(t, err)
assert.True(t, q.QueueIsEmpty)
- assert.Equal(t, queue.LockState{
- Status: queue.UnlockedStatus,
+ assert.Equal(t, lock.LockState{
+ Status: lock.UnlockedStatus,
}, q.Lock)
assert.Equal(t, queue.WorkingWorkerState, q.State)
}, 6*time.Second)
diff --git a/server/neptune/workflows/internal/deploy/revision/queue/worker_test.go b/server/neptune/workflows/internal/deploy/revision/queue/worker_test.go
index 25a927610..8f0362e85 100644
--- a/server/neptune/workflows/internal/deploy/revision/queue/worker_test.go
+++ b/server/neptune/workflows/internal/deploy/revision/queue/worker_test.go
@@ -12,6 +12,7 @@ import (
"github.com/runatlantis/atlantis/server/neptune/workflows/activities/deployment"
"github.com/runatlantis/atlantis/server/neptune/workflows/activities/github"
"github.com/runatlantis/atlantis/server/neptune/workflows/activities/terraform"
+ "github.com/runatlantis/atlantis/server/neptune/workflows/internal/deploy/lock"
"github.com/runatlantis/atlantis/server/neptune/workflows/internal/deploy/revision/queue"
internalTerraform "github.com/runatlantis/atlantis/server/neptune/workflows/internal/deploy/terraform"
"github.com/runatlantis/atlantis/server/neptune/workflows/internal/metrics"
@@ -25,7 +26,7 @@ import (
type testQueue struct {
Queue *list.List
- Lock queue.LockState
+ Lock lock.LockState
}
func (q *testQueue) IsEmpty() bool {
@@ -47,16 +48,32 @@ func (q *testQueue) Push(msg internalTerraform.DeploymentInfo) {
q.Queue.PushBack(msg)
}
-func (q *testQueue) SetLockForMergedItems(ctx workflow.Context, state queue.LockState) {
+func (q *testQueue) GetOrderedMergedItems() []internalTerraform.DeploymentInfo {
+ var result []internalTerraform.DeploymentInfo
+ for e := q.Queue.Front(); e != nil; e = e.Next() {
+ result = append(result, e.Value.(internalTerraform.DeploymentInfo))
+ }
+ return result
+}
+
+func (q *testQueue) SetLockForMergedItems(ctx workflow.Context, state lock.LockState) {
q.Lock = state
}
+func (q *testQueue) GetLockState() lock.LockState {
+ return q.Lock
+}
+
+func (q *testQueue) GetQueuedRevisionsSummary() string {
+ return "Revisions in queue"
+}
+
type workerRequest struct {
Queue []internalTerraform.DeploymentInfo
ExpectedValidationErrors []*queue.ValidationError
ExpectedPlanRejectionErrros []*internalTerraform.PlanRejectionError
ExpectedTerraformClientErrors []*activities.TerraformClientError
- InitialLockStatus queue.LockStatus
+ InitialLockStatus lock.LockStatus
}
type workerResponse struct {
@@ -68,7 +85,7 @@ type workerResponse struct {
type queueAndState struct {
QueueIsEmpty bool
State queue.WorkerState
- Lock queue.LockState
+ Lock lock.LockState
CurrentDeployment queue.CurrentDeployment
LatestDeployment *deployment.Info
}
@@ -113,7 +130,7 @@ func testWorkerWorkflow(ctx workflow.Context, r workerRequest) (workerResponse,
Queue: list.New(),
}
- q.SetLockForMergedItems(ctx, queue.LockState{Status: r.InitialLockStatus})
+ q.SetLockForMergedItems(ctx, lock.LockState{Status: r.InitialLockStatus})
var infos []*deployment.Info
for _, s := range r.Queue {
@@ -179,8 +196,8 @@ func TestWorker_ReceivesUnlockSignal(t *testing.T) {
assert.NoError(t, err)
assert.True(t, q.QueueIsEmpty)
- assert.Equal(t, queue.LockState{
- Status: queue.UnlockedStatus,
+ assert.Equal(t, lock.LockState{
+ Status: lock.UnlockedStatus,
}, q.Lock)
assert.Equal(t, queue.WaitingWorkerState, q.State)
@@ -189,7 +206,7 @@ func TestWorker_ReceivesUnlockSignal(t *testing.T) {
env.ExecuteWorkflow(testWorkerWorkflow, workerRequest{
// start locked and ensure we can unlock it.
- InitialLockStatus: queue.LockedStatus,
+ InitialLockStatus: lock.LockedStatus,
})
env.AssertExpectations(t)
@@ -388,7 +405,7 @@ func TestNewWorker(t *testing.T) {
}
type res struct {
- Lock queue.LockState
+ Lock lock.LockState
}
testWorkflow := func(ctx workflow.Context) (res, error) {
@@ -432,9 +449,9 @@ func TestNewWorker(t *testing.T) {
err := env.GetWorkflowResult(&r)
assert.NoError(t, err)
- assert.Equal(t, queue.LockState{
+ assert.Equal(t, lock.LockState{
Revision: "1234",
- Status: queue.LockedStatus,
+ Status: lock.LockedStatus,
}, r.Lock)
})
@@ -463,7 +480,7 @@ func TestNewWorker(t *testing.T) {
err := env.GetWorkflowResult(&r)
assert.NoError(t, err)
- assert.Equal(t, queue.LockState{}, r.Lock)
+ assert.Equal(t, lock.LockState{}, r.Lock)
})
t.Run("first deploy", func(t *testing.T) {
@@ -485,7 +502,7 @@ func TestNewWorker(t *testing.T) {
err := env.GetWorkflowResult(&r)
assert.NoError(t, err)
- assert.Equal(t, queue.LockState{}, r.Lock)
+ assert.Equal(t, lock.LockState{}, r.Lock)
})
}
diff --git a/server/neptune/workflows/internal/deploy/revision/revision.go b/server/neptune/workflows/internal/deploy/revision/revision.go
index 02f0d1424..9d2b6a924 100644
--- a/server/neptune/workflows/internal/deploy/revision/revision.go
+++ b/server/neptune/workflows/internal/deploy/revision/revision.go
@@ -13,6 +13,7 @@ import (
"github.com/runatlantis/atlantis/server/neptune/workflows/activities"
"github.com/runatlantis/atlantis/server/neptune/workflows/activities/github"
activity "github.com/runatlantis/atlantis/server/neptune/workflows/activities/terraform"
+ "github.com/runatlantis/atlantis/server/neptune/workflows/internal/deploy/lock"
"github.com/runatlantis/atlantis/server/neptune/workflows/internal/deploy/request"
"github.com/runatlantis/atlantis/server/neptune/workflows/internal/deploy/request/converter"
"github.com/runatlantis/atlantis/server/neptune/workflows/internal/deploy/revision/queue"
@@ -43,9 +44,10 @@ type NewRevisionRequest struct {
type Queue interface {
Push(terraform.DeploymentInfo)
- GetLockState() queue.LockState
- SetLockForMergedItems(ctx workflow.Context, state queue.LockState)
+ GetLockState() lock.LockState
+ SetLockForMergedItems(ctx workflow.Context, state lock.LockState)
Scan() []terraform.DeploymentInfo
+ GetQueuedRevisionsSummary() string
}
type DeploymentStore interface {
@@ -114,8 +116,8 @@ func (n *Receiver) Receive(c workflow.ReceiveChannel, more bool) {
// lock the queue on a manual deployment
if root.TriggerInfo.Type == activity.ManualTrigger {
// Lock the queue on a manual deployment
- n.queue.SetLockForMergedItems(ctx, queue.LockState{
- Status: queue.LockedStatus,
+ n.queue.SetLockForMergedItems(ctx, lock.LockState{
+ Status: lock.LockedStatus,
Revision: request.Revision,
})
}
@@ -134,16 +136,17 @@ func (n *Receiver) Receive(c workflow.ReceiveChannel, more bool) {
}
func (n *Receiver) createCheckRun(ctx workflow.Context, id, revision string, root activity.Root, repo github.Repo) int64 {
- lock := n.queue.GetLockState()
+ queueLock := n.queue.GetLockState()
var actions []github.CheckRunAction
- summary := "This deploy is queued and will be processed as soon as possible."
+ var revisionsSummary string = n.queue.GetQueuedRevisionsSummary()
+ summary := "This deploy is queued and will be processed as soon as possible.\n" + revisionsSummary
state := github.CheckRunQueued
- if lock.Status == queue.LockedStatus && (root.TriggerInfo.Type == activity.MergeTrigger) {
+ if queueLock.Status == lock.LockedStatus && (root.TriggerInfo.Type == activity.MergeTrigger) {
actions = append(actions, github.CreateUnlockAction())
state = github.CheckRunActionRequired
- revisionLink := github.BuildRevisionURLMarkdown(repo.GetFullName(), lock.Revision)
- summary = fmt.Sprintf("This deploy is locked from a manual deployment for revision %s. Unlock to proceed.", revisionLink)
+ revisionLink := github.BuildRevisionURLMarkdown(repo.GetFullName(), queueLock.Revision)
+ summary = fmt.Sprintf("This deploy is locked from a manual deployment for revision %s. Unlock to proceed.\n%s", revisionLink, revisionsSummary)
}
cid, err := n.checkRunClient.CreateOrUpdate(ctx, id, notifier.GithubCheckRunRequest{
diff --git a/server/neptune/workflows/internal/deploy/revision/revision_test.go b/server/neptune/workflows/internal/deploy/revision/revision_test.go
index d80d7073a..e43215176 100644
--- a/server/neptune/workflows/internal/deploy/revision/revision_test.go
+++ b/server/neptune/workflows/internal/deploy/revision/revision_test.go
@@ -1,13 +1,17 @@
package revision_test
import (
- "github.com/runatlantis/atlantis/server/neptune/workflows/internal/notifier"
+ "fmt"
+ "strings"
"testing"
"time"
+ "github.com/runatlantis/atlantis/server/neptune/workflows/internal/notifier"
+
"github.com/google/uuid"
"github.com/runatlantis/atlantis/server/neptune/workflows/activities/github"
"github.com/runatlantis/atlantis/server/neptune/workflows/activities/terraform"
+ "github.com/runatlantis/atlantis/server/neptune/workflows/internal/deploy/lock"
"github.com/runatlantis/atlantis/server/neptune/workflows/internal/deploy/request"
"github.com/runatlantis/atlantis/server/neptune/workflows/internal/deploy/revision"
"github.com/runatlantis/atlantis/server/neptune/workflows/internal/deploy/revision/queue"
@@ -23,14 +27,16 @@ type testCheckRunClient struct {
}
func (t *testCheckRunClient) CreateOrUpdate(ctx workflow.Context, deploymentID string, request notifier.GithubCheckRunRequest) (int64, error) {
- assert.Equal(t.expectedT, t.expectedRequest, request)
-
+ ok := assert.Equal(t.expectedT, t.expectedRequest, request)
+ if !ok {
+ t.expectedT.FailNow()
+ }
return 1, nil
}
type testQueue struct {
Queue []terraformWorkflow.DeploymentInfo
- Lock queue.LockState
+ Lock lock.LockState
}
func (q *testQueue) Scan() []terraformWorkflow.DeploymentInfo {
@@ -41,14 +47,29 @@ func (q *testQueue) Push(msg terraformWorkflow.DeploymentInfo) {
q.Queue = append(q.Queue, msg)
}
-func (q *testQueue) GetLockState() queue.LockState {
+func (q *testQueue) GetLockState() lock.LockState {
return q.Lock
}
-func (q *testQueue) SetLockForMergedItems(ctx workflow.Context, state queue.LockState) {
+func (q *testQueue) SetLockForMergedItems(ctx workflow.Context, state lock.LockState) {
q.Lock = state
}
+func (q *testQueue) IsEmpty() bool {
+ return len(q.Queue) == 0
+}
+
+func (q *testQueue) GetQueuedRevisionsSummary() string {
+ var revisions []string
+ if q.IsEmpty() {
+ return "No other revisions ahead In queue."
+ }
+ for _, deploy := range q.Scan() {
+ revisions = append(revisions, deploy.Commit.Revision)
+ }
+ return fmt.Sprintf("Revisions in queue: %s", strings.Join(revisions, ", "))
+}
+
type testWorker struct {
Current queue.CurrentDeployment
}
@@ -59,7 +80,7 @@ func (t testWorker) GetCurrentDeploymentState() queue.CurrentDeployment {
type req struct {
ID uuid.UUID
- Lock queue.LockState
+ Lock lock.LockState
Current queue.CurrentDeployment
InitialElements []terraformWorkflow.DeploymentInfo
ExpectedRequest notifier.GithubCheckRunRequest
@@ -68,7 +89,7 @@ type req struct {
type response struct {
Queue []terraformWorkflow.DeploymentInfo
- Lock queue.LockState
+ Lock lock.LockState
Timeout bool
}
@@ -137,10 +158,11 @@ func TestEnqueue(t *testing.T) {
env.ExecuteWorkflow(testWorkflow, req{
ID: id,
ExpectedRequest: notifier.GithubCheckRunRequest{
- Title: "atlantis/deploy: root",
- Sha: rev,
- Repo: github.Repo{Name: "nish"},
- State: github.CheckRunQueued,
+ Title: "atlantis/deploy: root",
+ Sha: rev,
+ Repo: github.Repo{Name: "nish"},
+ State: github.CheckRunQueued,
+ Summary: "This deploy is queued and will be processed as soon as possible.\nNo other revisions ahead In queue.",
},
ExpectedT: t,
})
@@ -165,8 +187,8 @@ func TestEnqueue(t *testing.T) {
Repo: github.Repo{Name: "nish"},
},
}, resp.Queue)
- assert.Equal(t, queue.LockState{
- Status: queue.UnlockedStatus,
+ assert.Equal(t, lock.LockState{
+ Status: lock.UnlockedStatus,
}, resp.Lock)
assert.False(t, resp.Timeout)
}
@@ -197,10 +219,11 @@ func TestEnqueue_ManualTrigger(t *testing.T) {
env.ExecuteWorkflow(testWorkflow, req{
ID: id,
ExpectedRequest: notifier.GithubCheckRunRequest{
- Title: "atlantis/deploy: root",
- Sha: rev,
- Repo: github.Repo{Name: "nish"},
- State: github.CheckRunQueued,
+ Title: "atlantis/deploy: root",
+ Sha: rev,
+ Repo: github.Repo{Name: "nish"},
+ State: github.CheckRunQueued,
+ Summary: "This deploy is queued and will be processed as soon as possible.\nNo other revisions ahead In queue.",
},
ExpectedT: t,
})
@@ -225,8 +248,8 @@ func TestEnqueue_ManualTrigger(t *testing.T) {
Repo: github.Repo{Name: "nish"},
},
}, resp.Queue)
- assert.Equal(t, queue.LockState{
- Status: queue.LockedStatus,
+ assert.Equal(t, lock.LockState{
+ Status: lock.LockedStatus,
Revision: "1234",
}, resp.Lock)
assert.False(t, resp.Timeout)
@@ -257,16 +280,17 @@ func TestEnqueue_ManualTrigger_QueueAlreadyLocked(t *testing.T) {
env.ExecuteWorkflow(testWorkflow, req{
ID: id,
- Lock: queue.LockState{
+ Lock: lock.LockState{
// ensure that the lock gets updated
- Status: queue.LockedStatus,
+ Status: lock.LockedStatus,
Revision: "123334444555",
},
ExpectedRequest: notifier.GithubCheckRunRequest{
- Title: "atlantis/deploy: root",
- Sha: rev,
- Repo: github.Repo{Name: "nish"},
- State: github.CheckRunQueued,
+ Title: "atlantis/deploy: root",
+ Sha: rev,
+ Repo: github.Repo{Name: "nish"},
+ State: github.CheckRunQueued,
+ Summary: "This deploy is queued and will be processed as soon as possible.\nNo other revisions ahead In queue.",
},
ExpectedT: t,
})
@@ -291,8 +315,8 @@ func TestEnqueue_ManualTrigger_QueueAlreadyLocked(t *testing.T) {
Repo: github.Repo{Name: "nish"},
},
}, resp.Queue)
- assert.Equal(t, queue.LockState{
- Status: queue.LockedStatus,
+ assert.Equal(t, lock.LockState{
+ Status: lock.LockedStatus,
Revision: "1234",
}, resp.Lock)
assert.False(t, resp.Timeout)
@@ -321,18 +345,32 @@ func TestEnqueue_MergeTrigger_QueueAlreadyLocked(t *testing.T) {
id := uuid.Must(uuid.NewUUID())
+ deploymentInfo := terraformWorkflow.DeploymentInfo{
+ Commit: github.Commit{
+ Revision: "123334444555",
+ Branch: "locking-branch",
+ },
+ CheckRunID: 0,
+ Root: terraform.Root{Name: "root", TriggerInfo: terraform.TriggerInfo{
+ Type: terraform.MergeTrigger,
+ }, Trigger: terraform.MergeTrigger},
+ ID: id,
+ Repo: github.Repo{Name: "nish"},
+ }
+
env.ExecuteWorkflow(testWorkflow, req{
- ID: id,
- Lock: queue.LockState{
+ ID: id,
+ InitialElements: []terraformWorkflow.DeploymentInfo{deploymentInfo},
+ Lock: lock.LockState{
// ensure that the lock gets updated
- Status: queue.LockedStatus,
+ Status: lock.LockedStatus,
Revision: "123334444555",
},
ExpectedRequest: notifier.GithubCheckRunRequest{
Title: "atlantis/deploy: root",
Sha: rev,
Repo: github.Repo{Name: "nish"},
- Summary: "This deploy is locked from a manual deployment for revision [123334444555](https://github.com//nish/commit/123334444555). Unlock to proceed.",
+ Summary: "This deploy is locked from a manual deployment for revision [123334444555](https://github.com//nish/commit/123334444555). Unlock to proceed.\nRevisions in queue: 123334444555",
Actions: []github.CheckRunAction{github.CreateUnlockAction()},
State: github.CheckRunActionRequired,
},
@@ -346,6 +384,7 @@ func TestEnqueue_MergeTrigger_QueueAlreadyLocked(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, []terraformWorkflow.DeploymentInfo{
+ deploymentInfo,
{
Commit: github.Commit{
Revision: rev,
@@ -359,8 +398,8 @@ func TestEnqueue_MergeTrigger_QueueAlreadyLocked(t *testing.T) {
Repo: github.Repo{Name: "nish"},
},
}, resp.Queue)
- assert.Equal(t, queue.LockState{
- Status: queue.LockedStatus,
+ assert.Equal(t, lock.LockState{
+ Status: lock.LockedStatus,
Revision: "123334444555",
}, resp.Lock)
assert.False(t, resp.Timeout)
diff --git a/server/neptune/workflows/internal/deploy/terraform/runner.go b/server/neptune/workflows/internal/deploy/terraform/runner.go
index fcc556b4e..e07619f93 100644
--- a/server/neptune/workflows/internal/deploy/terraform/runner.go
+++ b/server/neptune/workflows/internal/deploy/terraform/runner.go
@@ -3,6 +3,7 @@ package terraform
import (
"github.com/pkg/errors"
terraformActivities "github.com/runatlantis/atlantis/server/neptune/workflows/activities/terraform"
+ "github.com/runatlantis/atlantis/server/neptune/workflows/internal/deploy/lock"
"github.com/runatlantis/atlantis/server/neptune/workflows/internal/metrics"
"github.com/runatlantis/atlantis/server/neptune/workflows/internal/terraform"
"github.com/runatlantis/atlantis/server/neptune/workflows/internal/terraform/state"
@@ -33,10 +34,18 @@ type stateReceiver interface {
Receive(ctx workflow.Context, c workflow.ReceiveChannel, deploymentInfo DeploymentInfo)
}
-func NewWorkflowRunner(w Workflow, internalNotifiers []WorkflowNotifier, additionalNotifiers ...plugins.TerraformWorkflowNotifier) *WorkflowRunner {
+type deployQueue interface {
+ GetOrderedMergedItems() []DeploymentInfo
+ GetQueuedRevisionsSummary() string
+ GetLockState() lock.LockState
+}
+
+func NewWorkflowRunner(queue deployQueue, w Workflow, githubCheckRunCache CheckRunClient, internalNotifiers []WorkflowNotifier, additionalNotifiers ...plugins.TerraformWorkflowNotifier) *WorkflowRunner {
return &WorkflowRunner{
Workflow: w,
StateReceiver: &StateReceiver{
+ Queue: queue,
+ CheckRunCache: githubCheckRunCache,
InternalNotifiers: internalNotifiers,
AdditionalNotifiers: additionalNotifiers,
},
diff --git a/server/neptune/workflows/internal/deploy/terraform/state.go b/server/neptune/workflows/internal/deploy/terraform/state.go
index fe94b07b0..8d806199a 100644
--- a/server/neptune/workflows/internal/deploy/terraform/state.go
+++ b/server/neptune/workflows/internal/deploy/terraform/state.go
@@ -1,10 +1,13 @@
package terraform
import (
+ "fmt"
+
"github.com/pkg/errors"
"github.com/runatlantis/atlantis/server/metrics"
+ "github.com/runatlantis/atlantis/server/neptune/workflows/activities/github"
+ "github.com/runatlantis/atlantis/server/neptune/workflows/internal/deploy/lock"
"github.com/runatlantis/atlantis/server/neptune/workflows/internal/notifier"
-
"github.com/runatlantis/atlantis/server/neptune/workflows/internal/terraform/state"
"github.com/runatlantis/atlantis/server/neptune/workflows/plugins"
"go.temporal.io/sdk/workflow"
@@ -14,15 +17,23 @@ type WorkflowNotifier interface {
Notify(workflow.Context, notifier.Info, *state.Workflow) error
}
+type CheckRunClient interface {
+ CreateOrUpdate(ctx workflow.Context, deploymentID string, request notifier.GithubCheckRunRequest) (int64, error)
+}
+
type StateReceiver struct {
// We have separate classes of notifiers since we can be more flexible with our internal ones in terms of the data model
// What we support externally should be well thought out so for now this is kept to a minimum.
+ Queue deployQueue
+ CheckRunCache CheckRunClient
InternalNotifiers []WorkflowNotifier
AdditionalNotifiers []plugins.TerraformWorkflowNotifier
}
func (n *StateReceiver) Receive(ctx workflow.Context, c workflow.ReceiveChannel, deploymentInfo DeploymentInfo) {
+ // deploymentInfo is the current deployment being processed in TerraformWorkflow. Receive is triggered whenever the TerraformWorkflow has a state change.
+
var workflowState *state.Workflow
c.Receive(ctx, &workflowState)
@@ -38,11 +49,62 @@ func (n *StateReceiver) Receive(ctx workflow.Context, c workflow.ReceiveChannel,
workflow.GetLogger(ctx).Error(errors.Wrap(err, "notifying workflow state change").Error())
}
}
-
+ // Updates github check run with Terraform statuses for the current running deployment
+ // TODO: do not notify github if workflowState.Result.Status == InProgressWorkflowStatus && workflowState.Result.Reason == UnknownCompletionReason
for _, notifier := range n.InternalNotifiers {
if err := notifier.Notify(ctx, deploymentInfo.ToInternalInfo(), workflowState); err != nil {
workflow.GetMetricsHandler(ctx).Counter("notifier_failure").Inc(1)
workflow.GetLogger(ctx).Error(errors.Wrap(err, "notifying workflow state change").Error())
}
}
+
+ // Updates all other deployments waiting in queue when the current deployment is pending a confirm/reject user action. Current deployment is not on the queue at this point since its child TerraformWorkflow was started.
+ // CheckRunCache.CreateOrUpdate executes an activity and is a nondeterministic operation (i.e. it is not guaranteed to be executed in the same order across different workflow runs). This is why we need to check the workflow version to determine if we should update the check run.
+ // See https://docs.temporal.io/develop/go/versioning#patching for how to upgrade workflow version.
+ v := workflow.GetVersion(ctx, "SurfaceQueueInCheckRuns", workflow.DefaultVersion, 1)
+ if v == workflow.DefaultVersion {
+ return
+ }
+ if workflowState.Apply != nil &&
+ len(workflowState.Apply.OnWaitingActions.Actions) > 0 {
+ queuedDeployments := n.Queue.GetOrderedMergedItems()
+ revisionsSummary := n.Queue.GetQueuedRevisionsSummary()
+ runState := github.CheckRunQueued
+ var actions []github.CheckRunAction
+ var summary string
+
+ if workflowState.Apply.Status == state.WaitingJobStatus {
+ runLink := github.BuildRunURLMarkdown(deploymentInfo.Repo.GetFullName(), deploymentInfo.Commit.Revision, deploymentInfo.CheckRunID)
+ summary = fmt.Sprintf("This deploy is queued pending action on run for revision %s.\n%s", runLink, revisionsSummary)
+ } else if workflowState.Apply.Status == state.RejectedJobStatus || workflowState.Apply.Status == state.InProgressJobStatus {
+ // If the current deployment is Rejected or In Progress status, we need to restore the queued check runs to reflect that the queued deployments are not blocked.
+ // If the queue is currently locked we need to provide the unlock action.
+ queueLock := n.Queue.GetLockState()
+ if queueLock.Status == lock.LockedStatus {
+ actions = append(actions, github.CreateUnlockAction())
+ runState = github.CheckRunActionRequired
+ revisionLink := github.BuildRevisionURLMarkdown(deploymentInfo.Repo.GetFullName(), queueLock.Revision)
+ summary = fmt.Sprintf("This deploy is locked from a manual deployment for revision %s. Unlock to proceed.\n%s", revisionLink, revisionsSummary)
+ } else {
+ summary = "This deploy is queued and will be processed as soon as possible.\n" + revisionsSummary
+ }
+ }
+ for _, i := range queuedDeployments {
+ request := notifier.GithubCheckRunRequest{
+ Title: notifier.BuildDeployCheckRunTitle(i.Root.Name),
+ Sha: i.Commit.Revision,
+ State: runState,
+ Repo: i.Repo,
+ Summary: summary,
+ Actions: actions,
+ }
+
+ workflow.GetLogger(ctx).Debug(fmt.Sprintf("Updating action pending summary for deployment id: %s", i.ID.String()))
+ _, err := n.CheckRunCache.CreateOrUpdate(ctx, i.ID.String(), request)
+
+ if err != nil {
+ workflow.GetLogger(ctx).Debug(fmt.Sprintf("updating check run for revision %s", i.Commit.Revision), err)
+ }
+ }
+ }
}
diff --git a/server/neptune/workflows/internal/deploy/terraform/state_test.go b/server/neptune/workflows/internal/deploy/terraform/state_test.go
index edd3cfd8f..4f4041966 100644
--- a/server/neptune/workflows/internal/deploy/terraform/state_test.go
+++ b/server/neptune/workflows/internal/deploy/terraform/state_test.go
@@ -1,14 +1,18 @@
package terraform_test
import (
- "github.com/runatlantis/atlantis/server/neptune/workflows/internal/notifier"
+ "fmt"
"net/url"
+ "strings"
"testing"
"time"
+ "github.com/runatlantis/atlantis/server/neptune/workflows/internal/notifier"
+
"github.com/google/uuid"
"github.com/runatlantis/atlantis/server/neptune/workflows/activities/github"
"github.com/runatlantis/atlantis/server/neptune/workflows/activities/terraform"
+ "github.com/runatlantis/atlantis/server/neptune/workflows/internal/deploy/lock"
internalTerraform "github.com/runatlantis/atlantis/server/neptune/workflows/internal/deploy/terraform"
"github.com/runatlantis/atlantis/server/neptune/workflows/internal/terraform/state"
"github.com/runatlantis/atlantis/server/neptune/workflows/plugins"
@@ -49,13 +53,46 @@ func (n *testExternalNotifier) Notify(ctx workflow.Context, info plugins.Terrafo
return nil
}
+type testCheckRunClient struct {
+ called bool
+}
+
+func (t *testCheckRunClient) CreateOrUpdate(ctx workflow.Context, deploymentID string, request notifier.GithubCheckRunRequest) (int64, error) {
+ t.called = true
+ return 1, nil
+}
+
+type testQueue struct {
+ Queue []internalTerraform.DeploymentInfo
+ Lock lock.LockState
+}
+
+func (q *testQueue) GetLockState() lock.LockState {
+ return q.Lock
+}
+
+func (q *testQueue) GetQueuedRevisionsSummary() string {
+ var revisions []string
+ for _, deploy := range q.Queue {
+ revisions = append(revisions, deploy.Commit.Revision)
+ }
+ return fmt.Sprintf("Revisions in queue: %s", strings.Join(revisions, ", "))
+}
+
+func (q *testQueue) GetOrderedMergedItems() []internalTerraform.DeploymentInfo {
+ return q.Queue
+}
+
type stateReceiveRequest struct {
+ Queue *testQueue
+ CheckRunCache testCheckRunClient
State *state.Workflow
DeploymentInfo internalTerraform.DeploymentInfo
T *testing.T
}
type stateReceiveResponse struct {
+ CheckRunCacheCalled bool
NotifierCalled bool
ExternalNotifierCalled bool
}
@@ -79,6 +116,8 @@ func testStateReceiveWorkflow(ctx workflow.Context, r stateReceiveRequest) (stat
}
receiver := &internalTerraform.StateReceiver{
+ Queue: r.Queue,
+ CheckRunCache: &r.CheckRunCache,
InternalNotifiers: []internalTerraform.WorkflowNotifier{
notifier,
},
@@ -94,6 +133,7 @@ func testStateReceiveWorkflow(ctx workflow.Context, r stateReceiveRequest) (stat
receiver.Receive(ctx, ch, r.DeploymentInfo)
return stateReceiveResponse{
+ CheckRunCacheCalled: r.CheckRunCache.called,
NotifierCalled: notifier.called,
ExternalNotifierCalled: externalNotifier.called,
}, nil
@@ -117,16 +157,49 @@ func TestStateReceive(t *testing.T) {
},
}
+ queue := &testQueue{
+ Queue: []internalTerraform.DeploymentInfo{
+ {
+ CheckRunID: 0,
+ ID: uuid.New(),
+ Root: terraform.Root{Name: "root"},
+ Repo: github.Repo{Name: "hello"},
+ Commit: github.Commit{
+ Revision: "56789",
+ },
+ },
+ },
+ }
+
t.Run("calls notifiers with state", func(t *testing.T) {
ts := testsuite.WorkflowTestSuite{}
env := ts.NewTestWorkflowEnvironment()
env.ExecuteWorkflow(testStateReceiveWorkflow, stateReceiveRequest{
+ Queue: queue,
+ CheckRunCache: testCheckRunClient{},
State: &state.Workflow{
Plan: &state.Job{
Output: jobOutput,
Status: state.WaitingJobStatus,
},
+ Apply: &state.Job{
+ Output: jobOutput,
+ Status: state.WaitingJobStatus,
+ OnWaitingActions: state.JobActions{
+ Actions: []state.JobAction{
+ {
+ ID: state.ConfirmAction,
+ Info: "Confirm this plan to proceed to apply",
+ },
+ {
+ ID: state.RejectAction,
+ Info: "Reject this plan to prevent the apply",
+ },
+ },
+ Summary: "some reason",
+ },
+ },
},
DeploymentInfo: internalDeploymentInfo,
T: t,
@@ -136,6 +209,7 @@ func TestStateReceive(t *testing.T) {
var result stateReceiveResponse
err = env.GetWorkflowResult(&result)
+ assert.True(t, result.CheckRunCacheCalled)
assert.True(t, result.NotifierCalled)
assert.True(t, result.ExternalNotifierCalled)
assert.NoError(t, err)