Skip to content

Commit 26aeb49

Browse files
authored
CLOUD-1721 Add TFBUDDY_DELETE_OLD_COMMENTS feature (#53)
* CLOUD-1721 Add TFBUDDY_DELETE_OLD_COMMENTS feature Signed-off-by: Ihor Horak <ihor.horak@zapier.com>
1 parent b083d24 commit 26aeb49

19 files changed

Lines changed: 1281 additions & 111 deletions

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ apply-before-merge workflow.
1313
This tool provides a server function that processes webhooks from Gitlab/Github, triggers a Run in TFC for Merge/Pull Requests
1414
and then passes status updates of those Runs back to the Merge/Pull Request in the form of comments.
1515

16+
For MRs with multiple workspaces, TFBuddy tracks each workspace independently and can automatically clean up old plan/apply comments, keeping only the most recent one per workspace and action type. Set `TFBUDDY_DELETE_OLD_COMMENTS` to enable this.
17+
1618

1719
### Architecture
1820

docs/architecture.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ If you're happy with the plan you can issue a apply command `tfc apply` or if yo
1616

1717
Once the apply completes TF Buddy will update the PR indicating what was changed and if there was any errors.
1818

19+
#### Comment Cleanup
20+
21+
When `TFBUDDY_DELETE_OLD_COMMENTS` is set, TFBuddy automatically cleans up old discussion threads to keep MRs/PRs readable. Each comment is tagged with an invisible HTML marker identifying its workspace and action (plan or apply). When a new run completes, TFBuddy deletes older discussions that match the same workspace and action, keeping only the latest one. This is workspace-scoped: if your MR triggers runs in multiple workspaces, each workspace retains its own most recent plan and apply comment independently. Previous run URLs are collected into a collapsible "Previous TFC Urls" table on the latest comment.
22+
1923
Example of how an error is reported
2024

2125
![error](img/error.png)

docs/usage.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ env:
5555
# Optional: fail CI if Sentinel policy checks soft-fail on plan (default: disabled)
5656
# Set to "true" to mark the plan commit status as failed when policies soft-fail
5757
TFBUDDY_FAIL_CI_ON_SENTINEL_SOFT_FAIL: "false"
58+
# Optional: enable automatic cleanup of old discussion comments per workspace and action.
59+
# When enabled ("true"), TFBuddy deletes previous plan/apply discussions for the same
60+
# workspace, keeping only the most recent one. Discussions for other workspaces or
61+
# actions are preserved. Set to "false" or remove to disable.
62+
TFBUDDY_DELETE_OLD_COMMENTS: "true"
5863
```
5964
6065
For sensitive environment variables use `secrets.envs` which can contain a list of key/value pairs

localdev/manifests/deployment.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ spec:
2828
- name: TFBUDDY_OTEL_COLLECTOR_PORT
2929
value: "4317"
3030
- name: TFBUDDY_ALLOW_AUTO_MERGE
31+
value: "false"
32+
- name: TFBUDDY_DELETE_OLD_COMMENTS
3133
value: "true"
3234
- name: TFBUDDY_FAIL_CI_ON_SENTINEL_SOFT_FAIL
3335
value: "true"

pkg/comment_actions/parsing.go

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,16 @@ func ParseCommentCommand(noteBody string) (*CommentOpts, error) {
3838
return nil, ErrNoNotePassed
3939
}
4040

41+
switch words[0] {
42+
case "tfc":
43+
// valid agent, continue parsing
44+
case "terraform", "atlantis":
45+
log.Debug().Str("comment", words[0]).Msg("Use tfc to interact with tfbuddy")
46+
return nil, ErrOtherTFTool
47+
default:
48+
return nil, ErrNotTFCCommand
49+
}
50+
4151
opts := &CommentOpts{
4252
TriggerOpts: &tfc_trigger.TFCTriggerOptions{},
4353
}
@@ -47,14 +57,6 @@ func ParseCommentCommand(noteBody string) (*CommentOpts, error) {
4757
return nil, ErrPermanent
4858
}
4959

50-
if opts.Args.Agent == "terraform" || opts.Args.Agent == "atlantis" {
51-
log.Debug().Str("comment", opts.Args.Agent).Msg("Use tfc to interact with tfbuddy")
52-
return nil, ErrOtherTFTool
53-
}
54-
if opts.Args.Agent != "tfc" {
55-
return nil, ErrNotTFCCommand
56-
}
57-
5860
opts.TriggerOpts.Action = tfc_trigger.CheckTriggerAction(opts.Args.Command)
5961
if opts.TriggerOpts.Action == tfc_trigger.InvalidAction {
6062
return nil, ErrInvalidAction

pkg/comment_formatter/tfc_status_update.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/zapier/tfbuddy/pkg/runstream"
1212
"github.com/zapier/tfbuddy/pkg/terraform_plan"
1313
"github.com/zapier/tfbuddy/pkg/tfc_api"
14+
"github.com/zapier/tfbuddy/pkg/utils"
1415
)
1516

1617
func getProperApplyText(rmd runstream.RunMetadata, wsName string) string {
@@ -132,7 +133,7 @@ func FormatRunStatusCommentBody(tfc tfc_api.ApiClient, run *tfe.Run, rmd runstre
132133
rmd.GetAction(),
133134
run.Status,
134135
run.ID, runUrl,
135-
)
136+
) + "\n" + utils.FormatTFBuddyMarker(wsName, rmd.GetAction())
136137

137138
return extraInfo, topLevelNoteBody, resolveDiscussion
138139

pkg/mocks/helpers.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ func (ts *TestSuite) InitTestSuite() {
168168
ts.MockGitClient.EXPECT().GetMergeRequestModifiedFiles(gomock.Any(), ts.MetaData.MRIID, ts.MetaData.ProjectNameNS).Return([]string{"main.tf"}, nil).AnyTimes()
169169
ts.MockGitClient.EXPECT().GetRepoFile(gomock.Any(), ts.MetaData.ProjectNameNS, ".tfbuddy.yaml", ts.MetaData.SourceBranch).Return(ts.MetaData.TFBuddyConfig, nil).AnyTimes()
170170
ts.MockGitClient.EXPECT().CloneMergeRequest(gomock.Any(), ts.MetaData.ProjectNameNS, gomock.Any(), gomock.Any()).Return(ts.MockGitRepo, nil).AnyTimes()
171-
ts.MockGitClient.EXPECT().CreateMergeRequestDiscussion(gomock.Any(), ts.MetaData.MRIID, ts.MetaData.ProjectNameNS, &RegexMatcher{regex: regexp.MustCompile("Starting TFC apply for Workspace: `([A-z\\-]){1,}/([A-z\\-]){1,}`.")}).Return(ts.MockGitDisc, nil).AnyTimes()
171+
ts.MockGitClient.EXPECT().CreateMergeRequestDiscussion(gomock.Any(), ts.MetaData.MRIID, ts.MetaData.ProjectNameNS, &RegexMatcher{regex: regexp.MustCompile(`Starting TFC apply for Workspace: ` + "`" + `([A-Za-z0-9\-]){1,}/([A-Za-z0-9\-]){1,}` + "`" + `\.\n<!-- tfbuddy:ws=.+:action=.+ -->`)}).Return(ts.MockGitDisc, nil).AnyTimes()
172172

173173
ts.MockApiClient.EXPECT().GetWorkspaceByName(gomock.Any(), gomock.Any(), gomock.Any()).Return(&tfe.Workspace{ID: "service-tfbuddy"}, nil).AnyTimes()
174174
ts.MockApiClient.EXPECT().GetTagsByQuery(gomock.Any(), gomock.Any(), "tfbuddylock").AnyTimes()

pkg/mocks/mock_vcs.go

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/tfc_trigger/tfc_trigger.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -585,7 +585,8 @@ func (t *TFCTrigger) triggerRunForWorkspace(ctx context.Context, cfgWS *TFCWorks
585585
// create a new Merge Request discussion thread where status updates will be nested
586586
disc, err := t.gl.CreateMergeRequestDiscussion(ctx, mr.GetInternalID(),
587587
t.GetProjectNameWithNamespace(),
588-
fmt.Sprintf("Starting TFC %v for Workspace: `%s/%s`.", t.GetAction(), org, wsName),
588+
fmt.Sprintf("Starting TFC %v for Workspace: `%s/%s`.\n", t.GetAction(), org, wsName)+
589+
utils.FormatTFBuddyMarker(wsName, t.GetAction().String()),
589590
)
590591
if err != nil {
591592
return fmt.Errorf("could not create MR discussion thread for TFC run status updates. %w", err)

pkg/tfc_trigger/tfc_trigger_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ func TestTFCEvents_SingleWorkspacePlan(t *testing.T) {
108108
mockCtrl := gomock.NewController(t)
109109
defer mockCtrl.Finish()
110110
testSuite := mocks.CreateTestSuite(mockCtrl, mocks.TestOverrides{ProjectConfig: ws}, t)
111-
testSuite.MockGitClient.EXPECT().CreateMergeRequestDiscussion(gomock.Any(), testSuite.MetaData.MRIID, testSuite.MetaData.ProjectNameNS, "Starting TFC plan for Workspace: `zapier-test/service-tfbuddy`.").Return(testSuite.MockGitDisc, nil)
111+
testSuite.MockGitClient.EXPECT().CreateMergeRequestDiscussion(gomock.Any(), testSuite.MetaData.MRIID, testSuite.MetaData.ProjectNameNS, "Starting TFC plan for Workspace: `zapier-test/service-tfbuddy`.\n<!-- tfbuddy:ws=service-tfbuddy:action=plan -->").Return(testSuite.MockGitDisc, nil)
112112
testSuite.MockApiClient.EXPECT().CreateRunFromSource(gomock.Any(), gomock.Any()).Return(&tfe.Run{
113113
ID: "101",
114114
Workspace: &tfe.Workspace{Name: "service-tfbuddy",
@@ -160,7 +160,7 @@ func TestTFCEvents_SingleWorkspacePlanError(t *testing.T) {
160160
defer mockCtrl.Finish()
161161
testSuite := mocks.CreateTestSuite(mockCtrl, mocks.TestOverrides{ProjectConfig: ws}, t)
162162

163-
testSuite.MockGitClient.EXPECT().CreateMergeRequestDiscussion(gomock.Any(), testSuite.MetaData.MRIID, testSuite.MetaData.ProjectNameNS, "Starting TFC plan for Workspace: `zapier-test/service-tfbuddy`.").Return(testSuite.MockGitDisc, nil)
163+
testSuite.MockGitClient.EXPECT().CreateMergeRequestDiscussion(gomock.Any(), testSuite.MetaData.MRIID, testSuite.MetaData.ProjectNameNS, "Starting TFC plan for Workspace: `zapier-test/service-tfbuddy`.\n<!-- tfbuddy:ws=service-tfbuddy:action=plan -->").Return(testSuite.MockGitDisc, nil)
164164

165165
testSuite.MockApiClient.EXPECT().CreateRunFromSource(gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("could not create run from source"))
166166

@@ -279,8 +279,8 @@ func TestTFCEvents_MultiWorkspaceApply(t *testing.T) {
279279
testSuite := mocks.CreateTestSuite(mockCtrl, mocks.TestOverrides{ProjectConfig: ws}, t)
280280
testSuite.MockGitClient.EXPECT().GetMergeRequestModifiedFiles(gomock.Any(), testSuite.MetaData.MRIID, testSuite.MetaData.ProjectNameNS).Return([]string{"main.tf", "staging/terraform.tf"}, nil)
281281

282-
testSuite.MockGitClient.EXPECT().CreateMergeRequestDiscussion(gomock.Any(), testSuite.MetaData.MRIID, testSuite.MetaData.ProjectNameNS, "Starting TFC apply for Workspace: `zapier-test/service-tfbuddy`.").Return(testSuite.MockGitDisc, nil)
283-
testSuite.MockGitClient.EXPECT().CreateMergeRequestDiscussion(gomock.Any(), testSuite.MetaData.MRIID, testSuite.MetaData.ProjectNameNS, "Starting TFC apply for Workspace: `zapier-test/service-tfbuddy-staging`.").Return(testSuite.MockGitDisc, nil)
282+
testSuite.MockGitClient.EXPECT().CreateMergeRequestDiscussion(gomock.Any(), testSuite.MetaData.MRIID, testSuite.MetaData.ProjectNameNS, "Starting TFC apply for Workspace: `zapier-test/service-tfbuddy`.\n<!-- tfbuddy:ws=service-tfbuddy:action=apply -->").Return(testSuite.MockGitDisc, nil)
283+
testSuite.MockGitClient.EXPECT().CreateMergeRequestDiscussion(gomock.Any(), testSuite.MetaData.MRIID, testSuite.MetaData.ProjectNameNS, "Starting TFC apply for Workspace: `zapier-test/service-tfbuddy-staging`.\n<!-- tfbuddy:ws=service-tfbuddy-staging:action=apply -->").Return(testSuite.MockGitDisc, nil)
284284

285285
testSuite.MockApiClient.EXPECT().GetWorkspaceByName(gomock.Any(), "zapier-test", gomock.Any()).DoAndReturn(func(a interface{}, c, d string) (*tfe.Workspace, error) {
286286
return &tfe.Workspace{ID: c}, nil
@@ -409,8 +409,8 @@ func TestTFCEvents_MultiWorkspaceApplyError(t *testing.T) {
409409
testSuite := mocks.CreateTestSuite(mockCtrl, mocks.TestOverrides{ProjectConfig: ws}, t)
410410
testSuite.MockGitClient.EXPECT().GetMergeRequestModifiedFiles(gomock.Any(), testSuite.MetaData.MRIID, testSuite.MetaData.ProjectNameNS).Return([]string{"main.tf", "staging/terraform.tf"}, nil)
411411

412-
testSuite.MockGitClient.EXPECT().CreateMergeRequestDiscussion(gomock.Any(), testSuite.MetaData.MRIID, testSuite.MetaData.ProjectNameNS, "Starting TFC apply for Workspace: `zapier-test/service-tfbuddy`.").Return(testSuite.MockGitDisc, nil)
413-
testSuite.MockGitClient.EXPECT().CreateMergeRequestDiscussion(gomock.Any(), testSuite.MetaData.MRIID, testSuite.MetaData.ProjectNameNS, "Starting TFC apply for Workspace: `zapier-test/service-tfbuddy-staging`.").Return(testSuite.MockGitDisc, nil)
412+
testSuite.MockGitClient.EXPECT().CreateMergeRequestDiscussion(gomock.Any(), testSuite.MetaData.MRIID, testSuite.MetaData.ProjectNameNS, "Starting TFC apply for Workspace: `zapier-test/service-tfbuddy`.\n<!-- tfbuddy:ws=service-tfbuddy:action=apply -->").Return(testSuite.MockGitDisc, nil)
413+
testSuite.MockGitClient.EXPECT().CreateMergeRequestDiscussion(gomock.Any(), testSuite.MetaData.MRIID, testSuite.MetaData.ProjectNameNS, "Starting TFC apply for Workspace: `zapier-test/service-tfbuddy-staging`.\n<!-- tfbuddy:ws=service-tfbuddy-staging:action=apply -->").Return(testSuite.MockGitDisc, nil)
414414

415415
testSuite.MockApiClient.EXPECT().GetWorkspaceByName(gomock.Any(), "zapier-test", gomock.Any()).DoAndReturn(func(a interface{}, b, c string) (*tfe.Workspace, error) {
416416
return &tfe.Workspace{ID: c}, nil

0 commit comments

Comments
 (0)