# Save money when using GitHub Actions for iOS CI/CD

In this blog post, I share tips on how to use GitHub actions most efficiently and avoid unnecessary execution time and money spending.

I assume you are familiar with GitHub Actions and used them already but here is a quick recap:

* GitHub Actions is the built-in CI/CD tooling that allows defining workflows and jobs that get triggered on certain GitHub events (e.g. `push` or `pull_request`) for your repository.
    
* You need to define the type of machine that runs a job. This can be a
    
    * self-hosted runner (e.g. a local Mac Mini in your home) or
        
    * GitHub-hosted runner.
        

There are multiple GitHub-hosted runner types to choose from but often you will need a macOS-based runner image.

So what's the problem?

## GitHub Actions on macOS can be expensive

The good news is that using GitHub Actions is free from public repositories.

**But beware if you use a private repository!**

For private repositories, each GitHub account receives a certain amount of free minutes and storage for use with GitHub-hosted runners, depending on the product used with the account. Any usage beyond the included amounts is controlled by spending limits.

MacOS-based runner images are expensive for GitHub and hence GitHub applies a minute multiplier.

| Operating system | Minute multiplier |
| --- | --- |
| Linux | 1 |
| macOS | 10 |

Using 1,000 macOS minutes would consume 10,000 minutes included in your account. Once you used all your budget then paying for macOS runners gets expensive because of this multiplier.

| Operating system | Cores | Per-minute rate (USD) |
| --- | --- | --- |
| Linux | 2 | $0.008 |
| macOS | 3 | $0.08 |

<mark>It's ten times more expensive to use a macOS-based runner image than a Linux-based one.</mark>

Source: [About billing for GitHub Actions](https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions)

## Tips to save money

### Use a Linux-based runner whenever possible

This is a no-brainer but still worth mentioning.

If you want to lint the pull request then you can use a Linux-based runner for that job. `ubuntu-latest` has Swift installed.

```yaml
jobs:
  SwiftLint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v1
      - name: GitHub Action for SwiftLint
        uses: norio-nomura/action-swiftlint@3.2.1
```

Tools like SwiftLint or SwiftFormat do run on Linux as they don't have dependencies on Apple-specific frameworks.

FYI: in January 2023 `ubuntu-latest` points to the [ubuntu-22.04](https://github.com/actions/runner-images/blob/main/images/linux/Ubuntu2204-Readme.md) runner image which comes with Swift 5.7.3 pre-installed.

### Run a job at the single time

I don't mean to avoid parallel jobs because it is great that you can define a matrix strategy to automatically create multiple job runs that are based on the combinations of the variables.

Here is an example of building a Swift package on iOS for different Swift versions:

* Swift 5.7.1 that comes with Xcode 14.1
    
* Swift 5.5 that comes with Xcode 13
    

```yaml
jobs:
  build-macOS:
    name: Xcode ${{ matrix.xcode }}
    runs-on: ${{ matrix.runner-image }}
    env:
      DEVELOPER_DIR: "/Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer"
    strategy:
      fail-fast: false
      matrix:
        # https://github.com/actions/runner-images/blob/main/images/macos/macos-12-Readme.md#xcode
        # https://github.com/actions/runner-images/blob/main/images/macos/macos-11-Readme.md#xcode
        include:
          - xcode: "14.1"
            runner-image: macOS-12
            name: "macOS 12, Xcode 14.1, Swift 5.7.1"
          - xcode: "13.0"
            runner-image: macOS-11
            name: "macOS 11, Xcode 13.0, Swift 5.5.0"
    steps:
    - uses: actions/checkout@v3
    - name: Build (${{ matrix.name }})
      run: set -o pipefail && xcodebuild -project -scheme YourPackageName-Package -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 11' clean test | xcpretty
```

Back to the topic. I mean that you shall avoid the same job, belonging logically to the same group, being executed concurrently. Let me explain with an example:

* I create a pull request and a workflow gets triggered.
    
* You noticed that you forgot to add a file so you push it and add it to your pull request.
    
* The previous workflow is still running and a new workflow gets triggered =&gt; <mark>This is an example of an unnecessary concurrent workflow/job.</mark>
    

For the given example it would be best if GitHub immediately cancels the previous workflow execution.

This is possible by using `jobs.<job_id>.concurrency` to ensure that only a single job or workflow using the same concurrency group will run simultaneously. A concurrency group can be any string or expression.

Example of using concurrency to cancel any in-progress job or run:

```yaml
concurrency:
  group: ${{ github.head_ref || github.run_id }}
  cancel-in-progress: true
```

The popular [XcodeProj](https://github.com/tuist/XcodeProj) from Tuist is using the feature for example.

```yaml
# https://github.com/tuist/XcodeProj/blob/8.8.0/.github/workflows/xcodeproj.yml

concurrency:
  group: xcodeproj-${{ github.head_ref }}
  cancel-in-progress: true
```

You can find more information in GitHub's documentation [Using Concurrency](https://docs.github.com/en/actions/using-jobs/using-concurrency).

### Avoid workflow execution with filters

Do you need to build your Swift source code if you only updated your README file?

No! Use the `paths` filter when you want to consider file path patterns.

I like to define the following workflow in `.github/workflows/ci-build.yml`

```yaml
name: ci
on:
  push:
    paths:
      - ".github/workflows/**"
      - "**/*.swift"
  pull_request:
    paths:
      - ".github/workflows/**"
      - "**/*.swift"
```

Source code-related changes (as well as workflow-related changes) will trigger the workflow but files like README will not trigger the workflow.

### Attention: required status checks and skipping workflows

You might have configured status checks so that a workflow has to be successful before a pull request gets merged.

But if a workflow is skipped due to [path filtering](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#onpushpull_requestpull_request_targetpathspaths-ignore) (or [branch filtering](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#onpull_requestpull_request_targetbranchesbranches-ignore) or [commit message](https://docs.github.com/en/actions/managing-workflow-runs/skipping-workflow-runs)) then checks associated with that workflow will remain "Pending" and the pull request will be blocked from merging.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1674423934026/81fcb339-41fe-4310-bfe5-c4a3d9985944.png align="center")

One solution would be [Using conditions to control job execution](https://docs.github.com/en/actions/using-jobs/using-conditions-to-control-job-execution) but I believe GitHub has a better solution.

Let's create an additional workflow defined in `.github/workflows/ci-build-skipped.yml`

```yaml
name: ci
on:
  push:
    paths-ignore:
      - ".github/workflows/**"
      - "**/*.swift"
  pull_request:
    paths-ignore:
      - ".github/workflows/**"
      - "**/*.swift" 
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - run: 'echo "No build required"'
```

By defining an additional workflow with

* the identical workflow and job name and
    
* using the `paths-ignore` as counterpart
    

we can ensure that a minimal workflow/job is executed and therefore the status check passes.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1674423955279/99cde410-19c5-4c17-aa03-b958191db89b.png align="center")

> Make sure that the name key and required job name in both workflow files are the same.

More information in GitHub documentation [Handling skipped but required checks](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/troubleshooting-required-status-checks).
