Keeping a linear history with GitHub Actions
workflows triggered by the pull_request event uses merged code
GitHub Actions surprised me with an unexpected behavior when it runs jobs triggered from a pull request.
Let's assume the following git history:
I originally assumed that a pull request would checkout the branch feature for the pipeline steps.
However, what actually happens is that the workflow's github context uses a
ref which already has feature branch merged into main, and this is what
will be checked used by the checkout action by default:
Here are a few traces recovered from a job to show this:
-
dumping the
${{ github.context }}confirms thatrefis not the branch from the pull request:{ ... "ref": "refs/pull/1/merge", ... } -
a graph of the commit history (
git log --all --decorate --oneline --graph) confirms the diagram above:* 55af0f6 (HEAD, pull/1/merge) Merge ecd17b69ece19b75fa1fbafe30add8e951ab2e2f into a96f9e46bb1f59db25c108ae20f7064dc4dca685 |\ | * ecd17b6 (origin/feature) baz * | a96f9e4 (origin/main) bar |/ * 18a4b2e foo
keeping pull requests up-to-date
As we understand now, GitHub's workflows run the checks as if the code had been merged. This can get outdated fast if someone else's pull request is merged before ours, and potentially disastrous under certain circumstances.
disaster case study
Let's imagine this scenario where a pull request for feature-1 and feature-2 have been opened, and feature-2 is merged into upstream.
The act of merging feature-2 will invalidate the pipeline run #1, but the check remains green.
At revision foo, there is simple configuration file (config.json):
{
"foobar": "helloworld"
}Now assume branch feature-1 introduces a script (tested in the pipeline) which reads this:
jq '.foobar' config.json
Finally, imagine that the code change introduced by feature-2 renames the
foobar key to foobaz.
It would be foolish to continue trusting the green check mark from pipeline run #1!
requiring branches to be up-to-date before merging
GitHub is able to require that status checks (workflows) pass in order to merge a branch. While using this feature, we can also require that branches be up-to-date before merging.
Enabling this feature will surround the merge button with a red warning if a check fails and hinting that the base branch needs to be updated if it is outdated. If branch protection bypassing is disabled, then the button will simply be greyed out.
Note that this feature toggle requires at least one status check to be required, which sensibly should be the build step of the pipeline.
Linear History
Finally, if we only merge pull requests that have run tests with up-to-date branches, then we might as well require linear history in the commits!
Otherwise we would have commits that are older than others, but could have been tested later. Let's use our previous example to show this.
Although the commit bar from feature-1 will show as older than baz from
feature-2, it has actually been tested (pipeline run #3) against code which
has already introduced baz.
This would have been the git history had we updated our branch with a rebase,
and then merged with a rebase and merge strategy.
Much easier to follow!
Conclusion
So by leveraging GitHub Actions' prepared refs and requiring a linear history, we are sure to not be surprised as code lands in mainline all the while reflecting the testing into the commit history!