Kotlin Multiplatform GitHub Actions Automated Pull Request Labels

Having automated labels makes the Code Review process much safer by preventing merges for broken Pull Requests, ensuring that the main branch is stable

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:

KMP:
- changed-files:
    - any-glob-to-any-file:
      - shared/**
      - konsist/**

Android:
- changed-files:
    - any-glob-to-any-file:
      - androidApp/**

iOS:
- changed-files:
    - any-glob-to-any-file:
      - iosApp/**

Desktop:
- changed-files:
    - any-glob-to-any-file:
      - desktopApp/**

The labels are set depending on what files were changed in this PR.

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:

https://github.com/AKJAW/kotlin-multiplatform-github-actions/pull/17
https://github.com/AKJAW/kotlin-multiplatform-github-actions/pull/19

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.

https://github.com/AKJAW/kotlin-multiplatform-github-actions/pull/21

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.

You've successfully subscribed to AKJAW
Great! Now you have full access to all members content.
Error! Could not sign up. invalid link.
Welcome back! You've successfully signed in.
Error! Could not sign in. Please try again.
Success! Your account is fully activated, you now have access to all content.
Error! Stripe checkout failed.
Success! Your billing info is updated.
Error! Billing info update failed.