Skip to content

Commit 1e0a9f9

Browse files
authored
Move drift reconciliation to CE (#2346)
* Agent task to port drift reconciliation to CE * Move drift reconciliation hook from EE to CE
1 parent 35a6d3a commit 1e0a9f9

File tree

5 files changed

+257
-194
lines changed

5 files changed

+257
-194
lines changed
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# CE Backend: Port Drift Reconciliation Hook
2+
3+
## Goal
4+
Enable drift reconciliation in Community Edition: when a user comments "digger apply" or "digger unlock" on a drift Issue (non-PR) created by the drift service, the CE backend should trigger the appropriate jobs and manage locks — parity with EE behavior.
5+
6+
## Findings
7+
- EE registers `hooks.DriftReconcilliationHook` via `ee/backend/main.go` and implements it in `ee/backend/hooks/github.go`.
8+
- CE exposes `GithubWebhookPostIssueCommentHooks` but does not register a hook; `backend/hooks` has no implementation. As a result, CE ignores drift Issue comments.
9+
10+
## Scope
11+
- CE functional addition; no behavior change for PR comments.
12+
- Wire the new hook into CE `backend` and dedupe EE to reuse the CE hook in the same change (not optional).
13+
14+
## Constraints
15+
- Keep implementation as-is: copy the EE hook logic verbatim, only adjusting import/package paths for CE. Do not introduce new logic, behaviors, refactors, or changes to allowed commands.
16+
17+
## Plan
18+
1) Implement CE hook
19+
- Add `backend/hooks/drift_reconciliation.go` exporting:
20+
- `var DriftReconcilliationHook controllers.IssueCommentHook`
21+
- Copy logic from `ee/backend/hooks/github.go` with CE imports only: `backend/*` and `libs/*`.
22+
- Behavior:
23+
- Only handle IssueComment events on Issues (ignore PR comments).
24+
- Issue title must match `^Drift detected in project:\s*(\S+)` to extract `projectName`.
25+
- Accept commands: `digger apply` and `digger unlock`.
26+
- Lock project, run jobs for the target project on apply, then unlock (mirroring EE flow).
27+
- Post reactions and reporter comments as in EE.
28+
29+
2) Wire into CE backend
30+
- Update `backend/main.go` to register:
31+
- `GithubWebhookPostIssueCommentHooks: []controllers.IssueCommentHook{hooks.DriftReconcilliationHook}`
32+
33+
3) Dedupe EE to reuse CE hook
34+
- Switch `ee/backend/main.go` to import `github.com/diggerhq/digger/backend/hooks` and remove EE-local hook implementation.
35+
36+
4) Verification
37+
- Build: `go build ./backend` and `go build ./ee/backend`.
38+
- Manual: Comment `digger apply` on a generated drift Issue; verify locks and jobs are triggered; `digger unlock` removes locks.
39+
40+
## Acceptance Criteria
41+
- CE backend reacts to drift Issue comments (not PRs) with title pattern above.
42+
- `digger apply` triggers jobs for the extracted project and unlocks afterward.
43+
- `digger unlock` removes locks and acknowledges success.
44+
- No regressions to PR comment handling; existing PR workflows remain unchanged.
45+
- `go build ./backend`, `go build ./ee/backend` succeed.
46+
47+
## Tasks Checklist
48+
- [ ] Add `backend/hooks/drift_reconciliation.go` with CE implementation.
49+
- [ ] Register hook in `backend/main.go`.
50+
- [ ] Build CE and EE backends.
51+
- [ ] Point EE to CE hook and delete EE duplicate.
52+
- [ ] Smoke-test via Issue comments.
53+
54+
## Notes
55+
- Hook name keeps EE spelling (`DriftReconcilliationHook`) for parity; consider a later rename only if safe.
56+
- Keep allowed commands restricted to `digger apply` and `digger unlock` for drift Issues.
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
package hooks
2+
3+
import (
4+
"fmt"
5+
"log"
6+
"regexp"
7+
"strconv"
8+
"strings"
9+
10+
"github.com/diggerhq/digger/backend/ci_backends"
11+
controllers "github.com/diggerhq/digger/backend/controllers"
12+
"github.com/diggerhq/digger/backend/locking"
13+
"github.com/diggerhq/digger/backend/models"
14+
"github.com/diggerhq/digger/backend/utils"
15+
"github.com/diggerhq/digger/libs/ci/generic"
16+
dg_github "github.com/diggerhq/digger/libs/ci/github"
17+
comment_updater "github.com/diggerhq/digger/libs/comment_utils/reporting"
18+
"github.com/diggerhq/digger/libs/digger_config"
19+
dg_locking "github.com/diggerhq/digger/libs/locking"
20+
"github.com/diggerhq/digger/libs/scheduler"
21+
"github.com/google/go-github/v61/github"
22+
"github.com/samber/lo"
23+
)
24+
25+
// DriftReconcilliationHook handles drift Issue comments (not PRs) allowing
26+
// "digger apply" or "digger unlock" on issues with titles like
27+
// "Drift detected in project: <projectName>".
28+
//
29+
// Implementation is verbatim from EE, with CE imports.
30+
var DriftReconcilliationHook controllers.IssueCommentHook = func(gh utils.GithubClientProvider, payload *github.IssueCommentEvent, ciBackendProvider ci_backends.CiBackendProvider) error {
31+
log.Printf("handling the drift reconcilliation hook")
32+
installationId := *payload.Installation.ID
33+
repoName := *payload.Repo.Name
34+
repoOwner := *payload.Repo.Owner.Login
35+
repoFullName := *payload.Repo.FullName
36+
cloneURL := *payload.Repo.CloneURL
37+
issueTitle := *payload.Issue.Title
38+
issueNumber := *payload.Issue.Number
39+
userCommentId := *payload.GetComment().ID
40+
actor := *payload.Sender.Login
41+
commentBody := *payload.Comment.Body
42+
defaultBranch := *payload.Repo.DefaultBranch
43+
isPullRequest := payload.Issue.IsPullRequest()
44+
45+
if isPullRequest {
46+
log.Printf("Comment is not an issue, ignoring")
47+
return nil
48+
}
49+
50+
// checking that the title of the issue matches regex
51+
var projectName string
52+
re := regexp.MustCompile(`^Drift detected in project:\s*(\S+)`)
53+
matches := re.FindStringSubmatch(issueTitle)
54+
if len(matches) > 1 {
55+
projectName = matches[1]
56+
} else {
57+
log.Printf("does not look like a drift issue, ignoring")
58+
}
59+
60+
link, err := models.DB.GetGithubAppInstallationLink(installationId)
61+
if err != nil {
62+
log.Printf("Error getting GetGithubAppInstallationLink: %v", err)
63+
return fmt.Errorf("error getting github app link")
64+
}
65+
orgId := link.OrganisationId
66+
67+
if *payload.Action != "created" {
68+
log.Printf("comment is not of type 'created', ignoring")
69+
return nil
70+
}
71+
72+
allowedCommands := []string{"digger apply", "digger unlock"}
73+
if !lo.Contains(allowedCommands, strings.TrimSpace(*payload.Comment.Body)) {
74+
log.Printf("comment is not in allowed commands, ignoring")
75+
log.Printf("allowed commands: %v", allowedCommands)
76+
return nil
77+
}
78+
79+
diggerYmlStr, ghService, config, projectsGraph, err := controllers.GetDiggerConfigForBranch(gh, installationId, repoFullName, repoOwner, repoName, cloneURL, defaultBranch, nil, nil)
80+
if err != nil {
81+
log.Printf("Error loading digger.yml: %v", err)
82+
return fmt.Errorf("error loading digger.yml")
83+
}
84+
85+
commentIdStr := strconv.FormatInt(userCommentId, 10)
86+
err = ghService.CreateCommentReaction(commentIdStr, string(dg_github.GithubCommentEyesReaction))
87+
if err != nil {
88+
log.Printf("CreateCommentReaction error: %v", err)
89+
}
90+
91+
diggerCommand, err := scheduler.GetCommandFromComment(*payload.Comment.Body)
92+
if err != nil {
93+
log.Printf("unknown digger command in comment: %v", *payload.Comment.Body)
94+
utils.InitCommentReporter(ghService, issueNumber, fmt.Sprintf(":x: Could not recognise comment, error: %v", err))
95+
return fmt.Errorf("unknown digger command in comment %v", err)
96+
}
97+
98+
// attempting to lock for performing drift apply command
99+
prLock := dg_locking.PullRequestLock{
100+
InternalLock: locking.BackendDBLock{
101+
OrgId: orgId,
102+
},
103+
CIService: ghService,
104+
Reporter: comment_updater.NoopReporter{},
105+
ProjectName: projectName,
106+
ProjectNamespace: repoFullName,
107+
PrNumber: issueNumber,
108+
}
109+
err = dg_locking.PerformLockingActionFromCommand(prLock, *diggerCommand)
110+
if err != nil {
111+
utils.InitCommentReporter(ghService, issueNumber, fmt.Sprintf(":x: Failed perform lock action on project: %v %v", projectName, err))
112+
return fmt.Errorf("failed perform lock action on project: %v %v", projectName, err)
113+
}
114+
115+
if *diggerCommand == scheduler.DiggerCommandUnlock {
116+
utils.InitCommentReporter(ghService, issueNumber, fmt.Sprintf(":white_check_mark: Command %v completed successfully", *diggerCommand))
117+
return nil
118+
}
119+
120+
// === if we get here its a "digger apply command and we are already locked for this project ====
121+
// perform apply here then unlock the project
122+
commentReporter, err := utils.InitCommentReporter(ghService, issueNumber, ":construction_worker: Digger starting....")
123+
if err != nil {
124+
log.Printf("Error initializing comment reporter: %v", err)
125+
return fmt.Errorf("error initializing comment reporter")
126+
}
127+
128+
impactedProjects := config.GetProjects(projectName)
129+
jobs, coverAllImpactedProjects, err := generic.ConvertIssueCommentEventToJobs(repoFullName, actor, issueNumber, commentBody, impactedProjects, nil, config.Workflows, defaultBranch, defaultBranch, false)
130+
if err != nil {
131+
log.Printf("Error converting event to jobs: %v", err)
132+
utils.InitCommentReporter(ghService, issueNumber, fmt.Sprintf(":x: Error converting event to jobs: %v", err))
133+
return fmt.Errorf("error converting event to jobs")
134+
}
135+
log.Printf("GitHub IssueComment event converted to Jobs successfully\n")
136+
137+
err = utils.ReportInitialJobsStatus(commentReporter, jobs)
138+
if err != nil {
139+
log.Printf("Failed to comment initial status for jobs: %v", err)
140+
utils.InitCommentReporter(ghService, issueNumber, fmt.Sprintf(":x: Failed to comment initial status for jobs: %v", err))
141+
return fmt.Errorf("failed to comment initial status for jobs")
142+
}
143+
144+
impactedProjectsMap := make(map[string]digger_config.Project)
145+
for _, p := range impactedProjects {
146+
impactedProjectsMap[p.Name] = p
147+
}
148+
149+
impactedProjectsJobMap := make(map[string]scheduler.Job)
150+
for _, j := range jobs {
151+
impactedProjectsJobMap[j.ProjectName] = j
152+
}
153+
154+
reporterCommentId, err := strconv.ParseInt(commentReporter.CommentId, 10, 64)
155+
if err != nil {
156+
log.Printf("strconv.ParseInt error: %v", err)
157+
utils.InitCommentReporter(ghService, issueNumber, fmt.Sprintf(":x: could not handle commentId: %v", err))
158+
}
159+
160+
batchId, _, err := utils.ConvertJobsToDiggerJobs(*diggerCommand, "github", orgId, impactedProjectsJobMap, impactedProjectsMap, projectsGraph, installationId, defaultBranch, issueNumber, repoOwner, repoName, repoFullName, "", reporterCommentId, diggerYmlStr, 0, "", false, coverAllImpactedProjects, nil)
161+
if err != nil {
162+
log.Printf("ConvertJobsToDiggerJobs error: %v", err)
163+
utils.InitCommentReporter(ghService, issueNumber, fmt.Sprintf(":x: ConvertJobsToDiggerJobs error: %v", err))
164+
return fmt.Errorf("error convertingjobs")
165+
}
166+
167+
ciBackend, err := ciBackendProvider.GetCiBackend(
168+
ci_backends.CiBackendOptions{
169+
GithubClientProvider: gh,
170+
GithubInstallationId: installationId,
171+
RepoName: repoName,
172+
RepoOwner: repoOwner,
173+
RepoFullName: repoFullName,
174+
},
175+
)
176+
if err != nil {
177+
log.Printf("GetCiBackend error: %v", err)
178+
utils.InitCommentReporter(ghService, issueNumber, fmt.Sprintf(":x: GetCiBackend error: %v", err))
179+
return fmt.Errorf("error fetching ci backed %v", err)
180+
}
181+
182+
err = controllers.TriggerDiggerJobs(ciBackend, repoFullName, repoOwner, repoName, batchId, issueNumber, ghService, gh)
183+
if err != nil {
184+
log.Printf("TriggerDiggerJobs error: %v", err)
185+
utils.InitCommentReporter(ghService, issueNumber, fmt.Sprintf(":x: TriggerDiggerJobs error: %v", err))
186+
return fmt.Errorf("error triggering Digger Jobs")
187+
}
188+
189+
// === now unlocking the project ===
190+
err = dg_locking.PerformLockingActionFromCommand(prLock, scheduler.DiggerCommandUnlock)
191+
if err != nil {
192+
utils.InitCommentReporter(ghService, issueNumber, fmt.Sprintf(":x: Failed perform lock action on project: %v %v", projectName, err))
193+
return fmt.Errorf("failed perform lock action on project: %v %v", projectName, err)
194+
}
195+
196+
return nil
197+
}
198+

backend/main.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"github.com/diggerhq/digger/backend/ci_backends"
88
"github.com/diggerhq/digger/backend/config"
99
"github.com/diggerhq/digger/backend/controllers"
10+
"github.com/diggerhq/digger/backend/hooks"
1011
"github.com/diggerhq/digger/backend/utils"
1112
)
1213

@@ -17,7 +18,7 @@ func main() {
1718
ghController := controllers.DiggerController{
1819
CiBackendProvider: ci_backends.DefaultBackendProvider{},
1920
GithubClientProvider: utils.DiggerGithubRealClientProvider{},
20-
GithubWebhookPostIssueCommentHooks: make([]controllers.IssueCommentHook, 0),
21+
GithubWebhookPostIssueCommentHooks: []controllers.IssueCommentHook{hooks.DriftReconcilliationHook},
2122
}
2223
r := bootstrap.Bootstrap(templates, ghController)
2324
r.GET("/", controllers.Home)

0 commit comments

Comments
 (0)