Skip to content

Commit d9828b0

Browse files
implement apply_requirements flag (#2133)
* implement apply_requirements flag * Update libs/digger_config/digger_config.go Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * add tests * add apply requirement checks * return nil * support skippable merge check for cli too * documentation * reformat code * Update docs/ce/howto/apply-requirements.mdx Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * Update docs/ce/howto/apply-requirements.mdx Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * Update docs/ce/howto/apply-requirements.mdx Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * Update docs/ce/howto/apply-requirements.mdx Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * Update libs/apply_requirements/apply_requirements.go Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * Update libs/apply_requirements/apply_requirements.go Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * Update libs/orchestrator/mock.go Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * Update libs/orchestrator/mock.go Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * Update libs/apply_requirements/apply_requirements.go Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
1 parent fee108c commit d9828b0

File tree

20 files changed

+306
-62
lines changed

20 files changed

+306
-62
lines changed

backend/controllers/github_comment.go

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/diggerhq/digger/backend/models"
1010
"github.com/diggerhq/digger/backend/segment"
1111
"github.com/diggerhq/digger/backend/utils"
12+
"github.com/diggerhq/digger/libs/apply_requirements"
1213
"github.com/diggerhq/digger/libs/ci/generic"
1314
github2 "github.com/diggerhq/digger/libs/ci/github"
1415
"github.com/diggerhq/digger/libs/comment_utils/reporting"
@@ -124,7 +125,7 @@ func handleIssueCommentEvent(gh utils.GithubClientProvider, payload *github.Issu
124125
"orgId", orgId,
125126
)
126127

127-
diggerYmlStr, ghService, config, projectsGraph, branch, commitSha, changedFiles, err := getDiggerConfigForPR(gh, orgId, prLabelsStr, installationId, repoFullName, repoOwner, repoName, cloneURL, issueNumber)
128+
diggerYmlStr, ghService, config, projectsGraph, prSourceBranch, commitSha, changedFiles, err := getDiggerConfigForPR(gh, orgId, prLabelsStr, installationId, repoFullName, repoOwner, repoName, cloneURL, issueNumber)
128129
if err != nil {
129130
slog.Error("Error getting Digger config for PR",
130131
"issueNumber", issueNumber,
@@ -150,7 +151,7 @@ func handleIssueCommentEvent(gh utils.GithubClientProvider, payload *github.Issu
150151
"repoFullName", repoFullName,
151152
)
152153

153-
err = GenerateTerraformFromCode(payload, commentReporterManager, config, defaultBranch, ghService, repoOwner, repoName, commitSha, issueNumber, branch)
154+
err = GenerateTerraformFromCode(payload, commentReporterManager, config, defaultBranch, ghService, repoOwner, repoName, commitSha, issueNumber, prSourceBranch)
154155
if err != nil {
155156
slog.Error("Terraform generation failed",
156157
"issueNumber", issueNumber,
@@ -373,6 +374,15 @@ func handleIssueCommentEvent(gh utils.GithubClientProvider, payload *github.Issu
373374
return nil
374375
}
375376

377+
// Check for apply requirements
378+
if *diggerCommand == scheduler.DiggerCommandApply {
379+
err = apply_requirements.CheckApplyRequirements(ghService, impactedProjectsForComment, issueNumber, *prSourceBranch, targetBranch)
380+
if err != nil {
381+
commentReporterManager.UpdateComment(fmt.Sprintf(":x: Could not proceed with apply since apply requirements checks have failed: %v", err))
382+
return nil
383+
}
384+
}
385+
376386
err = utils.ReportInitialJobsStatus(commentReporter, jobs)
377387
if err != nil {
378388
slog.Error("Failed to comment initial status for jobs",
@@ -460,7 +470,7 @@ func handleIssueCommentEvent(gh utils.GithubClientProvider, payload *github.Issu
460470
impactedProjectsMap,
461471
projectsGraph,
462472
installationId,
463-
*branch,
473+
*prSourceBranch,
464474
issueNumber,
465475
repoOwner,
466476
repoName,

cli/pkg/digger/digger_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,11 @@ func (m *MockPRManager) IsClosed(prNumber int) (bool, error) {
145145
return false, nil
146146
}
147147

148+
// TODO implement me
149+
func (m *MockPRManager) IsDivergedFromBranch(sourceBranch string, targetBranch string) (bool, error) {
150+
return false, nil
151+
}
152+
148153
func (m *MockPRManager) GetComments(prNumber int) ([]ci.Comment, error) {
149154
m.Commands = append(m.Commands, RunInfo{"GetComments", strconv.Itoa(prNumber), time.Now()})
150155
return []ci.Comment{}, nil

docs/ce/howto/apply-requirements.mdx

Lines changed: 78 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -2,83 +2,103 @@
22
title: "Apply Requirements"
33
---
44

5-
Digger currently does not support `apply_requirements` (like in Atlantis). Coming soon - ([#1252](https://github.com/diggerhq/digger/issues/1252))
5+
Digger supports apply_requirements on the project level to enforce certain conditions are met before an apply action occurs.
66

7-
## Workaround
7+
The following apply conditions are supported:
88

9-
You can use mergeability requirements together with Status Checks to achieve the same.
10-
Digger will not apply if the pull request is not in a “mergeable” state as specified by GitHub api. This means that if you have a separate status check and you have this check as “required” by branch protection rules then an attempt of digger apply will not go ahead.
9+
```
10+
projects:
11+
- name: dev
12+
dir: dev
13+
apply_requirements: []
14+
- name: prod
15+
dir: prod
16+
apply_requirements: [mergeable, undiverged, approved]
17+
```
18+
19+
Digger supports *mergeable*, *undiverged* and *approved* conditions. The default value for apply_requirements is [mergeable] if not set.
20+
Here is an explanation of each:
21+
22+
## Mergeable
23+
24+
The mergeable requirement will prevent applies unless a pull request is able to be merged.
25+
In GitHub, if you're not using Protected Branches then all pull requests are mergeable unless there is a conflict.
1126

12-
Note: there is a [known issue](https://github.com/diggerhq/digger/issues/1180) that would
13-
cause the "mergability" check to conflict if you set the digger/apply check as required on github. We are working on a fix and in the meantime you have an option to turn off the mergability check if you want to have this digger/apply check as required. You can turn it off in the workflow configuration
14-
by setting the `skip_merge_check` flag as follows (we have to set the other configurations since they are currently required):
27+
<Note>
28+
There is a [known issue](https://github.com/diggerhq/digger/issues/1180) that would
29+
cause the "mergability" check to conflict if you set the digger/apply check as required on github. We are working on a fix and in the meantime you have an option to turn off the mergability check if you want to have this digger/apply check as required. You can turn it off in the workflow configuration
30+
by setting the `skip_merge_check` flag as follows (we have to set the other configurations since they are currently required):
31+
32+
</Note>
33+
34+
### Usage
35+
36+
You can enable the mergeable requirement on the project level:
1537

1638
```
1739
projects:
18-
- name: dev
19-
dir: dev
20-
workflow: mydev
40+
- name: dev
41+
dir: dev
42+
apply_requirements: [mergeable]
43+
```
2144

22-
workflows:
23-
mydev:
24-
workflow_configuration:
25-
on_pull_request_pushed: ["digger plan"]
26-
on_pull_request_closed: ["digger unlock"]
27-
on_commit_to_default: ["digger unlock"]
28-
skip_merge_check: true
45+
Note that it is set by default so it would be enforced even if you don't specify it.
46+
47+
## Approved
48+
The approved requirement will prevent applies unless the pull request is approved by at least one person other than the author.
49+
50+
### Usage
51+
52+
You can enable the approved requirement on the project level:
53+
54+
```
55+
projects:
56+
- name: dev
57+
dir: dev
58+
apply_requirements: [approved]
2959
```
3060

31-
## Requiring undiverged branches in PRs
61+
## Undiverged
3262

3363
While PR locks prevent you from PRs stepping on eachother in parallel, they still do not protect you from a stale branch
34-
that is behind the current main head. In order to safeguard against this you have a few options:
64+
that is behind the current main head. The undiverged requirement helps enforce this by preventing applies if there are any changes on the base branch
65+
since the PR was opened. It can be resolved by merging main into the PR branch or rebasing the PR branch on top of main. In the case of github this is done by querying the comparecommits api on github side.
3566

36-
Force your repo to always have rebased branches from main. In github this is done by adding the branch protection rule:
67+
### Usage
3768

38-
Under settings > branch protection rules > Require branches to be up to date before merging → check this
69+
You can enable the undiverged requirement on the project level:
3970

40-
Since digger will always query github api for mergability status, this protects you from any stale apply from PRs being performed.
71+
```
72+
projects:
73+
- name: dev
74+
dir: dev
75+
apply_requirements: [undiverged]
76+
```
4177

42-
![](/images/howto/force-refactor-into-main.png)
78+
## Skipping mergeability check
4379

44-
Understandably this may not be feasible to mark especially for monorepos that mix code and terraform. In such cases you can achieve a similar effect by using a custom workflow like below (digger.yml):
80+
The default behaviour of digger is to perform a mergeability check before applying a PR. In order to skip the
81+
mergeability check you can specify an empty list of apply requirements on the project level
4582

4683
```
4784
projects:
48-
- name: gcp-infra
49-
dir: cloud/terraform/gcp
50-
workflow: terraform-strict
85+
- name: dev
86+
dir: dev
87+
apply_requirements: []
88+
89+
Alternatively, you can also skip the mergeability check on the workflow level:
5190
52-
workflows:
53-
terraform-strict:
54-
plan:
55-
steps:
56-
- run: |
57-
echo "Checking if branch is up-to-date with main..."
58-
git fetch --unshallow origin main || git fetch origin main
59-
git fetch --unshallow origin HEAD || git fetch origin HEAD
60-
if ! git merge-base --is-ancestor origin/main HEAD; then
61-
echo "❌ Branch is not up-to-date with main. Please rebase or merge main into your branch."
62-
echo "Run: git fetch origin && git rebase origin/main"
63-
exit 1
64-
fi
65-
echo "✅ Branch is up-to-date with main"
66-
- init
67-
- plan
68-
apply:
69-
steps:
70-
- run: |
71-
echo "Checking if branch is up-to-date with main..."
72-
git fetch --unshallow origin main || git fetch origin main
73-
git fetch --unshallow origin HEAD || git fetch origin HEAD
74-
if ! git merge-base --is-ancestor origin/main HEAD; then
75-
echo "❌ Branch is not up-to-date with main. Please rebase or merge main into your branch."
76-
echo "Run: git fetch origin && git rebase origin/main"
77-
exit 1
78-
fi
79-
echo "✅ Branch is up-to-date with main"
80-
- init
81-
- apply
8291
```
92+
projects:
93+
- name: dev
94+
dir: dev
95+
workflow: mydev
8396

84-
We plan to eventually support this natively as a flag in digger
97+
workflows:
98+
mydev:
99+
workflow_configuration:
100+
on_pull_request_pushed: ["digger plan"]
101+
on_pull_request_closed: ["digger unlock"]
102+
on_commit_to_default: ["digger unlock"]
103+
skip_merge_check: true
104+
```

docs/ce/reference/digger.yml.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ pr_locks: true
2626
projects:
2727
- name: prod
2828
dir: prod
29+
apply_requirements: [approved, mergeable, undiverged]
2930
branch: main
3031
workspace: default
3132
terragrunt: false
@@ -119,6 +120,7 @@ workflows:
119120
| name | string | | yes | name of the project | must be unique |
120121
| branch | string | | yes | the target branch to match this project on | This field is optional and defaults to the repository's default branch when not set |
121122
| dir | string | | yes | directory containing the project | |
123+
| apply_requirements | []string | [mergeable] | no | list of requirements to be met before merged | see [apply requirements](/ce/howto/apply-requirements) for details |
122124
| workspace | string | default | no | terraform workspace to use | |
123125
| opentofu | boolean | false | no | whether to use opentofu | |
124126
| terragrunt | boolean | false | no | whether to use terragrunt | |
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package apply_requirements
2+
3+
import (
4+
"fmt"
5+
"github.com/diggerhq/digger/libs/ci"
6+
"github.com/diggerhq/digger/libs/digger_config"
7+
"log/slog"
8+
)
9+
10+
func CheckApplyRequirements(ghService ci.PullRequestService, impactedProjects []digger_config.Project, prNumber int, sourceBranch string, targetBranch string) error {
11+
isMergeable, err := ghService.IsMergeable(prNumber)
12+
if err != nil {
13+
slog.Error("Error checking if PR is mergeable", "prNumber", prNumber)
14+
return fmt.Errorf("error checking if PR is mergeable")
15+
}
16+
approvals, err := ghService.GetApprovals(prNumber)
17+
if err != nil {
18+
slog.Error("Error getting approvals", "prNumber", prNumber)
19+
return fmt.Errorf("error getting approvals")
20+
}
21+
isApproved := len(approvals) > 0
22+
isDiverged, err := ghService.IsDivergedFromBranch(sourceBranch, targetBranch)
23+
if err != nil {
24+
slog.Error("Error checking if PR is diverged", "prNumber", prNumber)
25+
return fmt.Errorf("error checking if PR is diverged")
26+
}
27+
for _, proj := range impactedProjects {
28+
for _, req := range proj.ApplyRequirements {
29+
if req == digger_config.ApplyRequirementsApproved && isApproved == false {
30+
return fmt.Errorf("PR fails apply requirements for project %v, Expected PR to be approved, a minimum of one approval is required before proceeding", proj.Name)
31+
} else if req == digger_config.ApplyRequirementsUndiverged && isDiverged == true {
32+
return fmt.Errorf("PR fails apply requirements for project %v, Expected PR to be undiverged from target branch. Merge main into the PR branch or rebase the PR branch on top of main", proj.Name)
33+
} else if req == digger_config.ApplyRequirementsMergeable && isMergeable == false {
34+
return fmt.Errorf("PR fails apply requirements for project %v, Expected PR to be mergable. Ensure all status checks are successful in order to proceed", proj.Name)
35+
} else {
36+
slog.Warn("unknown apply requirements found", "project", proj.Name, "requirement", req)
37+
}
38+
}
39+
}
40+
return nil
41+
}

libs/ci/azure/azure.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,11 @@ func (a *AzureReposService) IsClosed(prNumber int) (bool, error) {
315315
return *pullRequest.Status == git.PullRequestStatusValues.Abandoned, nil
316316
}
317317

318+
// TODO implement me
319+
func (a *AzureReposService) IsDivergedFromBranch(sourceBranch string, targetBranch string) (bool, error) {
320+
return false, nil
321+
}
322+
318323
func (a *AzureReposService) IsMerged(prNumber int) (bool, error) {
319324
pullRequest, err := a.Client.GetPullRequestById(context.Background(), git.GetPullRequestByIdArgs{
320325
Project: &a.ProjectName,

libs/ci/bitbucket/bitbucket.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,11 @@ func (b BitbucketAPI) IsClosed(prNumber int) (bool, error) {
477477
return pullRequest.State != "OPEN", nil
478478
}
479479

480+
// TODO implement me
481+
func (b BitbucketAPI) IsDivergedFromBranch(sourceBranch string, targetBranch string) (bool, error) {
482+
return false, nil
483+
}
484+
480485
func (b BitbucketAPI) GetBranchName(prNumber int) (string, string, string, string, error) {
481486
url := fmt.Sprintf("%s/repositories/%s/%s/pullrequests/%d", bitbucketBaseURL, b.RepoWorkspace, b.RepoName, prNumber)
482487

libs/ci/ci.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ type PullRequestService interface {
2323
IsMerged(prNumber int) (bool, error)
2424
// IsClosed closed without merging
2525
IsClosed(prNumber int) (bool, error)
26+
IsDivergedFromBranch(sourceBranch string, targetBranch string) (bool, error)
2627
GetBranchName(prNumber int) (string, string, string, string, error)
2728
SetOutput(prNumber int, key string, value string) error
2829
}

libs/ci/generic/events.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"github.com/diggerhq/digger/libs/digger_config"
77
"github.com/diggerhq/digger/libs/scheduler"
88
"github.com/dominikbraun/graph"
9+
"github.com/samber/lo"
910
"strings"
1011
)
1112

@@ -146,6 +147,9 @@ func CreateJobsForProjects(projects []digger_config.Project, command string, eve
146147
var skipMerge bool
147148
if workflow.Configuration != nil {
148149
skipMerge = workflow.Configuration.SkipMergeCheck
150+
} else if !lo.Contains(project.ApplyRequirements, digger_config.ApplyRequirementsMergeable) {
151+
// if mergeable isn't present in apply requirements we also skip merge check for it
152+
skipMerge = true
149153
} else {
150154
skipMerge = false
151155
}

libs/ci/github/github.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,25 @@ func (svc GithubService) IsMerged(prNumber int) (bool, error) {
415415
return *pr.Merged, nil
416416
}
417417

418+
// IsDivergedFromBranch checks whether sourceBranch has diverged from targetBranch.
419+
// Diverged = both ahead and behind.
420+
func (svc GithubService) IsDivergedFromBranch(sourceBranch string, targetBranch string) (bool, error) {
421+
ctx := context.Background()
422+
423+
// Compare the commits between the two branches
424+
comp, _, err := svc.Client.Repositories.CompareCommits(ctx, svc.Owner, svc.RepoName, targetBranch, sourceBranch, nil)
425+
if err != nil {
426+
return false, fmt.Errorf("failed to compare %s..%s: %w", targetBranch, sourceBranch, err)
427+
}
428+
429+
// Diverged means both sides have unique commits
430+
if comp.GetAheadBy() > 0 && comp.GetBehindBy() > 0 {
431+
return true, nil
432+
}
433+
434+
return false, nil
435+
}
436+
418437
func (svc GithubService) IsClosed(prNumber int) (bool, error) {
419438
pr, _, err := svc.Client.PullRequests.Get(context.Background(), svc.Owner, svc.RepoName, prNumber)
420439
if err != nil {

0 commit comments

Comments
 (0)