Skip to content

Optimize CI: dynamic matrix, concurrency, path filtering, and move CodeQL to Linux #249

@leogdion

Description

@leogdion

Summary

Reduce CI waste on a public repo (free minutes, but still consumes queue time and GitHub-hosted macOS runners at gross cost). Main wins: dynamic matrix limits feature branch builds to ~3 jobs, and moving CodeQL from macOS to Linux eliminates $4.40/month in gross macOS minutes.

Current gross cost: $7.60/month ($6.57 macOS mostly from CodeQL, $0.55 Linux, $0.48 Windows). Net $0 as a public repo, but macOS minutes are wasteful.

Changes to make

1. Replace .github/workflows/MistKit.yml

Replace the entire file with the content below. Key changes from the current workflow:

  • Concurrency group with cancel-in-progress: true
  • paths-ignore on both push and pull_request triggers
  • pull_request trigger added (original only triggered on push)
  • New configure job outputting full-matrix flag + dynamic Ubuntu matrix values
  • build-ubuntu uses dynamic matrix from configure
  • build-windows gated to full-matrix == 'true'
  • build-android gated to full-matrix == 'true'
  • build-macos split into two jobs:
    • build-macos — always runs (SPM + iOS on macos-26)
    • build-macos-platforms — full-matrix only (extra Xcode versions, macOS, watchOS, tvOS, visionOS, older iOS)
  • lint job uses !cancelled() && !failure() and includes build-macos-platforms in needs

Replacement file content:

name: MistKit
on:
  push:
    branches-ignore:
      - '*WIP'
    paths-ignore:
      - '**.md'
      - 'docs/**'
      - 'LICENSE'
      - '.github/ISSUE_TEMPLATE/**'
  pull_request:
    paths-ignore:
      - '**.md'
      - 'docs/**'
      - 'LICENSE'
      - '.github/ISSUE_TEMPLATE/**'

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

env:
  PACKAGE_NAME: MistKit

jobs:
  configure:
    name: Configure Matrix
    runs-on: ubuntu-latest
    outputs:
      full-matrix: ${{ steps.check.outputs.full }}
      ubuntu-os: ${{ steps.matrix.outputs.ubuntu-os }}
      ubuntu-swift: ${{ steps.matrix.outputs.ubuntu-swift }}
      ubuntu-type: ${{ steps.matrix.outputs.ubuntu-type }}
    steps:
      - id: check
        name: Determine matrix scope
        run: |
          FULL=false
          REF="${{ github.ref }}"
          EVENT="${{ github.event_name }}"
          BASE_REF="${{ github.base_ref }}"

          # Full matrix on main
          if [[ "$REF" == "refs/heads/main" ]]; then
            FULL=true
          # Full matrix on semver branches (v1.0.0, 1.2.3-alpha.1, etc.)
          elif [[ "$REF" =~ ^refs/heads/v?[0-9]+\.[0-9]+\.[0-9]+ ]]; then
            FULL=true
          # Full matrix on PRs targeting main or semver branches
          elif [[ "$EVENT" == "pull_request" ]]; then
            if [[ "$BASE_REF" == "main" || "$BASE_REF" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+ ]]; then
              FULL=true
            fi
          fi

          echo "full=$FULL" >> "$GITHUB_OUTPUT"
          echo "Full matrix: $FULL (ref=$REF, event=$EVENT, base_ref=$BASE_REF)"

      - id: matrix
        name: Build matrix values
        run: |
          if [[ "${{ steps.check.outputs.full }}" == "true" ]]; then
            echo 'ubuntu-os=["noble","jammy"]' >> "$GITHUB_OUTPUT"
            echo 'ubuntu-swift=[{"version":"6.1"},{"version":"6.2"},{"version":"6.3","nightly":true}]' >> "$GITHUB_OUTPUT"
            echo 'ubuntu-type=["","wasm","wasm-embedded"]' >> "$GITHUB_OUTPUT"
          else
            echo 'ubuntu-os=["noble"]' >> "$GITHUB_OUTPUT"
            echo 'ubuntu-swift=[{"version":"6.2"}]' >> "$GITHUB_OUTPUT"
            echo 'ubuntu-type=[""]' >> "$GITHUB_OUTPUT"
          fi

  build-ubuntu:
    name: Build on Ubuntu
    needs: configure
    runs-on: ubuntu-latest
    container: ${{ matrix.swift.nightly && format('swiftlang/swift:nightly-{0}-{1}', matrix.swift.version, matrix.os) || format('swift:{0}-{1}', matrix.swift.version, matrix.os) }}
    if: ${{ !contains(github.event.head_commit.message, 'ci skip') }}
    strategy:
      fail-fast: false
      matrix:
        os: ${{ fromJSON(needs.configure.outputs.ubuntu-os) }}
        swift: ${{ fromJSON(needs.configure.outputs.ubuntu-swift) }}
        type: ${{ fromJSON(needs.configure.outputs.ubuntu-type) }}
        exclude:
          # Exclude Swift 6.1 from wasm builds
          - swift: { version: "6.1" }
            type: "wasm"
          - swift: { version: "6.1" }
            type: "wasm-embedded"
          # Exclude Swift 6.3 from wasm builds
          - swift: { version: "6.3", nightly: true }
            type: "wasm"
          - swift: { version: "6.3", nightly: true }
            type: "wasm-embedded"
    steps:
      - uses: actions/checkout@v4
      - uses: brightdigit/swift-build@v1.5.0
        id: build
        with:
          type: ${{ matrix.type }}
          wasmtime-version: "40.0.2"
          wasm-swift-flags: >-
            -Xcc -D_WASI_EMULATED_SIGNAL
            -Xcc -D_WASI_EMULATED_MMAN
            -Xlinker -lwasi-emulated-signal
            -Xlinker -lwasi-emulated-mman
      - uses: sersoft-gmbh/swift-coverage-action@v4
        if: steps.build.outputs.contains-code-coverage == 'true'
        id: coverage-files
        with:
          fail-on-empty-output: true
      - name: Upload coverage to Codecov
        if: steps.build.outputs.contains-code-coverage == 'true'
        uses: codecov/codecov-action@v4
        with:
          fail_ci_if_error: true
          flags: swift-${{ matrix.swift.version }}-${{ matrix.os }}${{ matrix.swift.nightly && 'nightly' || '' }}
          verbose: true
          token: ${{ secrets.CODECOV_TOKEN }}
          files: ${{ join(fromJSON(steps.coverage-files.outputs.files), ',') }}

  build-windows:
    name: Build on Windows
    needs: configure
    runs-on: ${{ matrix.runs-on }}
    if: ${{ needs.configure.outputs.full-matrix == 'true' && !contains(github.event.head_commit.message, 'ci skip') }}
    strategy:
      fail-fast: false
      matrix:
        runs-on: [windows-2022, windows-2025]
        swift:
          - version: swift-6.1-release
            build: 6.1-RELEASE
          - version: swift-6.2-release
            build: 6.2-RELEASE
    steps:
      - uses: actions/checkout@v4
      - uses: brightdigit/swift-build@v1.5.0
        id: build
        with:
          windows-swift-version: ${{ matrix.swift.version }}
          windows-swift-build: ${{ matrix.swift.build }}
      - name: Upload coverage to Codecov
        if: steps.build.outputs.contains-code-coverage == 'true'
        uses: codecov/codecov-action@v5
        with:
          fail_ci_if_error: true
          flags: swift-${{ matrix.swift.version }},windows
          verbose: true
          token: ${{ secrets.CODECOV_TOKEN }}
          os: windows
          swift_project: MistKit

  build-android:
    name: Build on Android
    needs: configure
    runs-on: ubuntu-latest
    if: ${{ needs.configure.outputs.full-matrix == 'true' && !contains(github.event.head_commit.message, 'ci skip') }}
    strategy:
      fail-fast: false
      matrix:
        swift:
          - version: "6.1"
          - version: "6.2"
        android-api-level: [33, 34]
    steps:
      - uses: actions/checkout@v4
      - name: Free disk space
        if: matrix.build-only == false
        uses: jlumbroso/free-disk-space@main
        with:
          tool-cache: false
          android: false
          dotnet: true
          haskell: true
          large-packages: true
          docker-images: true
          swap-storage: true
      - uses: brightdigit/swift-build@v1.5.0
        with:
          scheme: ${{ env.PACKAGE_NAME }}
          type: android
          android-swift-version: ${{ matrix.swift.version }}
          android-api-level: ${{ matrix.android-api-level }}
          android-run-tests: true

  # Minimal macOS builds — always runs (SPM + iOS)
  build-macos:
    name: Build on macOS
    runs-on: macos-26
    if: ${{ !contains(github.event.head_commit.message, 'ci skip') }}
    strategy:
      fail-fast: false
      matrix:
        include:
          # SPM build
          - xcode: "/Applications/Xcode_26.2.app"

          # iOS build
          - type: ios
            xcode: "/Applications/Xcode_26.2.app"
            deviceName: "iPhone 17 Pro"
            osVersion: "26.2"
            download-platform: true
    steps:
      - uses: actions/checkout@v4
      - name: Build and Test
        id: build
        uses: brightdigit/swift-build@v1.5.0
        with:
          scheme: ${{ env.PACKAGE_NAME }}
          type: ${{ matrix.type }}
          xcode: ${{ matrix.xcode }}
          deviceName: ${{ matrix.deviceName }}
          osVersion: ${{ matrix.osVersion }}
          download-platform: ${{ matrix.download-platform }}
      - name: Process Coverage
        if: steps.build.outputs.contains-code-coverage == 'true'
        uses: sersoft-gmbh/swift-coverage-action@v4
      - name: Upload Coverage
        if: steps.build.outputs.contains-code-coverage == 'true'
        uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}
          flags: ${{ matrix.type && format('{0}{1}', matrix.type, matrix.osVersion) || 'spm' }}

  # Full macOS platform builds — only on main, semver branches, and PRs targeting them
  build-macos-platforms:
    name: Build on macOS (Platforms)
    needs: configure
    runs-on: ${{ matrix.runs-on }}
    if: ${{ needs.configure.outputs.full-matrix == 'true' && !contains(github.event.head_commit.message, 'ci skip') }}
    strategy:
      fail-fast: false
      matrix:
        include:
          # Additional SPM Xcode versions
          - runs-on: macos-15
            xcode: "/Applications/Xcode_16.4.app"
          - runs-on: macos-15
            xcode: "/Applications/Xcode_16.3.app"

          # macOS
          - type: macos
            runs-on: macos-26
            xcode: "/Applications/Xcode_26.2.app"

          # iOS — older Xcode
          - type: ios
            runs-on: macos-15
            xcode: "/Applications/Xcode_16.3.app"
            deviceName: "iPhone 16"
            osVersion: "18.4"
            download-platform: true

          # watchOS
          - type: watchos
            runs-on: macos-26
            xcode: "/Applications/Xcode_26.2.app"
            deviceName: "Apple Watch Ultra 3 (49mm)"
            osVersion: "26.2"
            download-platform: true

          # tvOS
          - type: tvos
            runs-on: macos-26
            xcode: "/Applications/Xcode_26.2.app"
            deviceName: "Apple TV"
            osVersion: "26.2"
            download-platform: true

          # visionOS
          - type: visionos
            runs-on: macos-26
            xcode: "/Applications/Xcode_26.2.app"
            deviceName: "Apple Vision Pro"
            osVersion: "26.2"
            download-platform: true
    steps:
      - uses: actions/checkout@v4
      - name: Build and Test
        id: build
        uses: brightdigit/swift-build@v1.5.0
        with:
          scheme: ${{ env.PACKAGE_NAME }}
          type: ${{ matrix.type }}
          xcode: ${{ matrix.xcode }}
          deviceName: ${{ matrix.deviceName }}
          osVersion: ${{ matrix.osVersion }}
          download-platform: ${{ matrix.download-platform }}
      - name: Process Coverage
        if: steps.build.outputs.contains-code-coverage == 'true'
        uses: sersoft-gmbh/swift-coverage-action@v4
      - name: Upload Coverage
        if: steps.build.outputs.contains-code-coverage == 'true'
        uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}
          flags: ${{ matrix.type && format('{0}{1}', matrix.type, matrix.osVersion) || 'spm' }}

  lint:
    name: Linting
    runs-on: ubuntu-latest
    if: ${{ !cancelled() && !failure() && !contains(github.event.head_commit.message, 'ci skip') }}
    needs: [build-ubuntu, build-macos, build-macos-platforms, build-windows, build-android]
    env:
      MINT_PATH: .mint/lib
      MINT_LINK_PATH: .mint/bin
    steps:
      - uses: actions/checkout@v4
      - name: Cache mint
        id: cache-mint
        uses: actions/cache@v4
        env:
          cache-name: cache
        with:
          path: |
            .mint
            Mint
          key: ${{ runner.os }}-mint-${{ hashFiles('**/Mintfile') }}
          restore-keys: |
            ${{ runner.os }}-mint-
      - name: Install mint
        if: steps.cache-mint.outputs.cache-hit == ''
        run: |
          git clone https://github.com/yonaskolb/Mint.git
          cd Mint
          swift run mint install yonaskolb/mint
      - name: Lint
        run: |
          set -e
          ./Scripts/lint.sh

2. Move CodeQL to Linux

In .github/workflows/codeql.yml, find the runs-on field and change it from macos-* (or self-hosted) to ubuntu-latest. CodeQL is static analysis and does not need macOS — this eliminates ~$4.40/month in gross macOS minutes.

3. Add .github/workflows/cleanup-caches.yml

Create this new file:

name: Cleanup Branch Caches
on:
  delete:

jobs:
  cleanup:
    runs-on: ubuntu-latest
    permissions:
      actions: write
    steps:
      - name: Cleanup caches for deleted branch
        uses: actions/github-script@v7
        with:
          script: |
            const ref = `refs/heads/${context.payload.ref}`;
            const caches = await github.rest.actions.getActionsCacheList({
              owner: context.repo.owner,
              repo: context.repo.repo,
              ref: ref,
            });
            for (const cache of caches.data.actions_caches) {
              console.log(`Deleting cache: ${cache.key}`);
              await github.rest.actions.deleteActionsCacheById({
                owner: context.repo.owner,
                repo: context.repo.repo,
                cache_id: cache.id,
              });
            }
            console.log(`Deleted ${caches.data.actions_caches.length} cache(s) for ${ref}`);

Impact

  • Feature branch pushes drop from ~20+ jobs to ~3 (configure + 1 Ubuntu + 2 macOS)
  • Windows/Android gated to full-matrix only
  • CodeQL on Linux eliminates ~$4.40/month in gross macOS minutes
  • Full CI coverage preserved for main, semver branches, and PRs targeting them

Checklist

  • Replace .github/workflows/MistKit.yml with the content above
  • In .github/workflows/codeql.yml, change runs-on to ubuntu-latest
  • Create .github/workflows/cleanup-caches.yml with the content above
  • Push to a feature branch and verify ~3 jobs run
  • Open a PR to main and verify the full matrix fires

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions