Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Fix GitHub pull request mergeability for multiple required workflow runs #5057

Open
wants to merge 15 commits into
base: main
Choose a base branch
from

Conversation

ajax-ryzhyi-r
Copy link

@ajax-ryzhyi-r ajax-ryzhyi-r commented Nov 4, 2024

what

This PR modifies the mergeability evaluation logic for GitHub pull requests by evaluating the latest workflow check run (by comparing runNumber's) instead of the first one returned from the GitHub API.

why

There is an issue described in detail in #5048 where Atlantis treats a PR as unmergeable when there are multiple required GiHub Actions workflow runs for the last commit of the branch and the first of them is failed atlantis responds with the error Apply Failed: Pull request must be mergeable before running apply. when attempting to use the atlantis apply command.

Upon investigating this issue, I discovered that GitHub returns all checkRuns in chronological order when Atlantis retrieves a pull request's last commit mergeability status. However, Atlantis iterates through these checkRuns and retrieves the first checkRun instead of the last one, which represents the current status.

tests

  • I have tested my changes by running this fix in our Atlantis setup and everything works as expected
  • I have updated automated tests to cover these cases

references

Fixes the issue discussed in comments of #5048

@ajax-ryzhyi-r ajax-ryzhyi-r requested review from a team as code owners November 4, 2024 12:47
@ajax-ryzhyi-r ajax-ryzhyi-r requested review from chenrui333, nitrocode and X-Guardian and removed request for a team November 4, 2024 12:47
@github-actions github-actions bot added go Pull requests that update Go code provider/github labels Nov 4, 2024
@dosubot dosubot bot added the bug Something isn't working label Nov 4, 2024
for _, checkRun := range checkRuns {
if checkRun.Name == expectedContext {
return CheckRunPassed(checkRun)
// Iterate through checkRuns from the end, as GitHub returns them in chronological order
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure the order can be relied upon. Is it documented that it shall be chronological?

Alternatively, there is the checkSuite.workflowRun.runNumber that could be added to the query and used to find the latest check run for the given check.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure the order can be relied upon. Is it documented that it shall be chronological?

Alternatively, there is the checkSuite.workflowRun.runNumber that could be added to the query and used to find the latest check run for the given check.

I couldn't find any mentions of order in the documentation; this observation is based solely on empirical evidence.

Thank you for the suggestion. It looks more reliable, and I'll try to implement it 🤝

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure the order can be relied upon. Is it documented that it shall be chronological?

Alternatively, there is the checkSuite.workflowRun.runNumber that could be added to the query and used to find the latest check run for the given check.

@henriklundstrom I've rewritten the fix to use checkSuite.workflowRun.runNumber for determining the latest check run. Take a look, please.

@X-Guardian
Copy link
Contributor

Hi @ajax-ryzhyi-r, you have some spurious file changes in this PR that aren't relevant to your change. Can you revert them please? (Adding EOF LFs t files you haven't changed.)

Comment on lines 744 to 750
for _, checkRun := range checkRuns {
if checkRun.Name == expectedContext {
if checkRun.CheckSuite.WorkflowRun == nil {
return CheckRunPassed(checkRun)
} else if checkRun.Name == expectedContext {
matchedCheckRuns = append(matchedCheckRuns, checkRun)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Name matching must always be done, otherwise the first check in the response without a workflow run would be evaluated in place of the expected check!

for _, checkRun := range checkRuns {
    if checkRun.Name != expectedContext {
        continue
    }
    if checkRun.CheckSuite.WorkflowRun == nil {
        return CheckRunPassed(checkRun)
    }
    matchedCheckRuns = append(matchedCheckRuns, checkRun)
}

@@ -709,6 +710,24 @@ pagination:
return reviewDecision, requiredChecks, requiredWorkflows, checkRuns, statusContexts, nil
}

// GetLatestCheckRun returns the checkRun with the highest runNumber, i.e., the latest checkRun whose status we need to evaluate.
func GetLatestCheckRun(checkRuns []CheckRun) (CheckRun, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this could just return a pointer to a CheckRun or nil if none found:

Suggested change
func GetLatestCheckRun(checkRuns []CheckRun) (CheckRun, error) {
func GetLatestCheckRun(checkRuns []CheckRun) *CheckRun {

Although I do think it is confusing that the function makes no distinction among the different checks, just returns whichever has the highest run number regardless the context. It would be less confusing if the function accepted the check to look for as an argument and was called GetLatestMatchingCheckRun instead. But since there is different logic to how to match expected checks and expected workflows, I think it would make more sense to just do this logic inside of the ExpectedCheckPassed and ExpectedWorkflowPassed respectively instead.

func GetLatestCheckRun(checkRuns []CheckRun) (CheckRun, error) {
latestCheckRunNumber := 0
for _, checkRun := range checkRuns {
if int(checkRun.CheckSuite.WorkflowRun.RunNumber) > latestCheckRunNumber {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would cause a panic if there is no workflow run.

}

for _, checkRun := range checkRuns {
if int(checkRun.CheckSuite.WorkflowRun.RunNumber) == latestCheckRunNumber {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also can cause a panic.

}
}

for _, checkRun := range checkRuns {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no need to iterate twice, the check run with the highest run number can be returned immediately after the first loop if it is kept as a variable pointer.

Signed-off-by: Roman Ryzhyi <[email protected]>
Signed-off-by: Roman Ryzhyi <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working go Pull requests that update Go code provider/github
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants