Kotlin Multiplatform GitHub Actions Automated Pull Request Labels
This article is a continuation of my previous Kotlin Multiplatform CI article the code can be found in the repository and changes for this article are in this Pull Request.
Reminder
The Code Review workflow runs the jobs based on the Pull Request labels, so the Android label only runs Android related Gradle tasks and the KMP label runs everything.
jobs:
SetUp:
runs-on: ubuntu-latest
steps:
- id: setVariables
name: Set variables
run: ...
outputs:
shouldRunKmp: ${{ steps.setVariables.outputs.shouldRunKmp }}
shouldRunAndroid: ${{ steps.setVariables.outputs.shouldRunAndroid }}
shouldRunIos: ${{ steps.setVariables.outputs.shouldRunIos }}
shouldRunDesktop: ${{ steps.setVariables.outputs.shouldRunDesktop }}
Build:
needs: SetUp
uses: ./.github/workflows/build.yml
with:
shouldRunKmp: ${{ needs.SetUp.outputs.shouldRunKmp }}
shouldRunAndroid: ${{ needs.SetUp.outputs.shouldRunAndroid }}
shouldRunIos: ${{ needs.SetUp.outputs.shouldRunIos }}
shouldRunDesktop: ${{ needs.SetUp.outputs.shouldRunDesktop }}
UnitTests:
needs: SetUp
uses: ./.github/workflows/test.yml
with:
shouldRunKmp: ${{ needs.SetUp.outputs.shouldRunKmp }}
shouldRunAndroid: ${{ needs.SetUp.outputs.shouldRunAndroid }}
shouldRunIos: ${{ needs.SetUp.outputs.shouldRunIos }}
shouldRunDesktop: ${{ needs.SetUp.outputs.shouldRunDesktop }}
AllowMerge:
...
Thanks to this, the CI only builds things which are needed and allows for skipping unrelated jobs. There's no point in running the iOS build if there are only Android changes.
AllowMerge, will pass if the dependent jobs passed or were skipped, in case of failure it will fail, thus blocking merging. Additionally, when the PR is opened without any label, then it will also fail.
AllowMerge:
if: always()
runs-on: ubuntu-latest
needs: [ Build, UnitTests ]
steps:
- run: |
if [ ${{ github.event_name }} == pull_request ] && [ ${{ join(github.event.pull_request.labels.*.name) == '' }} == true ]; then
exit 1
elif [ ${{ (contains(needs.Build.result, 'failure')) }} == true ] || [ ${{ (contains(needs.UnitTests.result, 'failure')) }} == true ]; then
exit 1
The above logic will prevent a broken main branch, however, the Pull Request author needs to be diligent and apply the correct labels, which might be problematic if the changes start including other platforms and the author forgets to update them.
The workflow above only executes builds and tests based on what labels were applied. Thanks to this, the CI uses less resources when checking Pull Requests.
Automatic Labeling
The action for labeling is Labeler v5, which is configured as follows:
The repository for this project is a fork, and because of this Labeler requires additional permissions, to work correctly:
jobs:
SetUp:
permissions:
contents: read
pull-requests: write
...
The set-up outputs, which control which platform is run, are now based on the labeler output:
SetUp:
...
steps:
- id: labeler
if: ${{ !contains(github.event.pull_request.labels.*.name, 'Ignore') }}
uses: actions/labeler@v5
- id: setVariables
name: Set variables
run: |
...
hasKmpLabel=${{ contains(steps.labeler.outputs.all-labels, 'KMP') }}
...
outputs:
shouldRunKmp: ${{ steps.setVariables.outputs.shouldRunKmp }}
shouldRunAndroid: ${{ steps.setVariables.outputs.shouldRunAndroid }}
shouldRunIos: ${{ steps.setVariables.outputs.shouldRunIos }}
shouldRunDesktop: ${{ steps.setVariables.outputs.shouldRunDesktop }}
all-labels: ${{ steps.labeler.outputs.all-labels }}
Additionally, if the PR already has a label called 'Ignore' then the labeler does not apply any labels, allowing the PR to be merged without any checks which might be useful for Tooling, Documentation, Codeowner etc. changes. (More on this in the next section)
The AllowMerge job has one additional dependency to SetUp to access the labeler output:
AllowMerge:
if: always()
runs-on: ubuntu-latest
needs: [ SetUp, Build, UnitTests ]
steps:
- run: |
if [ ${{ github.event_name }} == pull_request ] && [ ${{ join(github.event.pull_request.labels.*.name) == '' }} == true ] && [ ${{ join(needs.SetUp.outputs.all-labels) == '' }} == true ]; then
exit 1
elif [ ${{ (contains(needs.Build.result, 'failure')) }} == true ] || [ ${{ (contains(needs.UnitTests.result, 'failure')) }} == true ]; then
exit 1
else
exit 0
fi
This output is used in the empty label condition to ensure that labels which were added automatically are also checked, preventing false positives.
Thanks to this automation, the Pull Request author doesn't need to worry about applying the correct labels. All that needs to be done is to create the Pull Request either a draft or open.
How it works
When the PR is created, then the labeler adds the corresponding labels, which then dictate what jobs are run:
Ignoring the labeling
There are some cases where there are no code changes, like tooling or documentation updates. In such cases, opening the PR with no matching directory changes will fail, because there are no labels.
However, the workflow logic has a special case for the Ignore label, which prevents labeler from being run (and accidentally adding labels when it shouldn't):
SetUp:
...
steps:
- id: labeler
if: ${{ !contains(github.event.pull_request.labels.*.name, 'Ignore') }}
uses: actions/labeler@v5
...
AllowMerge will pass because there is the Ignore label, allowing the PR to merge.
Summary
Having automated labels makes the Code Review process much safer by preventing merges for broken Pull Requests, ensuring that the main branch is stable (The only exception being adding the Ignore label by accident).
Additionally, the PR authors don't need to remember which labels to add or even add them at all, labeler takes care of this when creating a Pull Request.