Skip to content

Instantly share code, notes, and snippets.

@g-cqd
Last active December 29, 2025 16:10
Show Gist options
  • Select an option

  • Save g-cqd/c35b4bf8521b31718573a73676f94cbd to your computer and use it in GitHub Desktop.

Select an option

Save g-cqd/c35b4bf8521b31718573a73676f94cbd to your computer and use it in GitHub Desktop.
GitHub Actions Optimization Guide - Comprehensive reference for CI/CD optimization

GitHub Actions Optimization Guide for Swift Projects

A comprehensive guide for optimizing GitHub Actions workflows, with special focus on Swift/Xcode projects.

Table of Contents

  1. Cost Optimization
  2. Caching Strategies
  3. Job Dependencies & Artifacts
  4. Conditional Execution
  5. Concurrency & Timeouts
  6. CodeQL Security Analysis
  7. Swift Build Plugin Issues
  8. Reusable Workflows
  9. Complete Example Workflow

Cost Optimization

Runner Economics

Runner Cost Multiplier Use Cases
ubuntu-latest 1x (baseline) Changelog, docs deploy, simple scripts
macos-15 10x Swift builds, Xcode projects
macos-15-xlarge 20x Heavy parallel builds only

Key Strategies

  1. Move cheap jobs to Linux: Changelog generation, GitHub Pages deployment, and simple scripts should use ubuntu-latest
  2. Single build, multiple consumers: Build once and share artifacts between jobs
  3. Cancel redundant runs: Use concurrency groups with cancel-in-progress: true for PRs
  4. Skip unnecessary work: Use path filters and conditional execution

Caching Strategies

SPM Dependencies

- name: Cache SPM Dependencies
  uses: actions/cache@v4
  with:
    path: |
      .build
      ~/Library/Developer/Xcode/DerivedData
    key: spm-${{ runner.os }}-${{ hashFiles('Package.resolved') }}
    restore-keys: |
      spm-${{ runner.os }}-

Best Practices

  • Use Package.resolved hash for cache key (not Package.swift)
  • Include restore-keys for partial cache hits
  • Cache DerivedData for Xcode builds
  • Use save-always: true to preserve cache even on failure

Job Dependencies & Artifacts

Sharing Build Artifacts

jobs:
  build:
    runs-on: macos-15
    steps:
      - name: Build
        run: swift build -c release
      
      - name: Upload Artifact
        uses: actions/upload-artifact@v4
        with:
          name: build-release
          path: .build/release/your-binary
          retention-days: 1  # Minimize storage costs

  test:
    needs: build
    runs-on: macos-15
    steps:
      - name: Download Artifact
        uses: actions/download-artifact@v4
        with:
          name: build-release
          path: .build/release
      
      - name: Make Executable
        run: chmod +x .build/release/your-binary

Passing Data Between Jobs

jobs:
  prepare:
    outputs:
      version: ${{ steps.version.outputs.version }}
    steps:
      - id: version
        run: echo "version=1.0.0" >> $GITHUB_OUTPUT

  release:
    needs: prepare
    steps:
      - run: echo "Version is ${{ needs.prepare.outputs.version }}"

Conditional Execution

Job-Level Conditions

jobs:
  release:
    if: startsWith(github.ref, 'refs/tags/v')
    
  docs:
    if: github.ref == 'refs/heads/main'
    
  pr-only:
    if: github.event_name == 'pull_request'

Path Filters (Workflow Level)

on:
  push:
    paths:
      - 'Sources/**'
      - 'Tests/**'
      - 'Package.swift'
    paths-ignore:
      - '**.md'
      - 'docs/**'

Skip CI Markers

if: "!contains(github.event.head_commit.message, '[skip ci]')"

Concurrency & Timeouts

Concurrency Groups

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: ${{ github.event_name == 'pull_request' }}
  • PRs: Cancel previous runs (save resources)
  • Main/Tags: Let runs complete (ensure releases finish)

Job Timeouts

jobs:
  build:
    timeout-minutes: 30
    steps:
      - name: Long Step
        timeout-minutes: 10
        run: ...

CodeQL Security Analysis

Important: CodeQL Action v3 will be deprecated in December 2026. Use v4.

Swift Projects with Modern Xcode

CodeQL requires manual build mode for Swift projects using Xcode 16+:

codeql:
  name: CodeQL Analysis
  runs-on: macos-15
  permissions:
    security-events: write
    actions: read
    contents: read
  steps:
    - uses: actions/checkout@v4

    - name: Select Xcode
      run: sudo xcode-select -s /Applications/Xcode_26.1.1.app

    - name: Ensure Linting Tools
      run: |
        command -v swiftlint >/dev/null 2>&1 || brew install swiftlint
        command -v swiftformat >/dev/null 2>&1 || brew install swiftformat

    - name: Initialize CodeQL
      uses: github/codeql-action/init@v4
      with:
        languages: swift
        build-mode: manual

    - name: Build for CodeQL
      run: swift build -c release --arch arm64

    - name: Perform CodeQL Analysis
      uses: github/codeql-action/analyze@v4
      with:
        category: "/language:swift"

Key Points

  • Swift 6.2 support: Requires CodeQL 2.23.5+ (November 2025)
  • Xcode 26 support: Requires CodeQL 2.23.5+
  • Single architecture: Use --arch arm64 for faster builds
  • Manual build mode: Required for modern Xcode versions
  • Use v4: CodeQL Action v3 deprecated December 2026

Swift Build Plugin Issues

Problem 1: "SwiftLint/SwiftFormat not available"

Build plugins may fail to download tools in CI due to:

  • Network restrictions in sandboxed environments
  • DNS resolution failures
  • Download server unavailability

Solution: Smart Tool Installation

Check if tools exist before installing to avoid redundant messages:

- name: Ensure Linting Tools
  run: |
    command -v swiftlint >/dev/null 2>&1 || brew install swiftlint
    command -v swiftformat >/dev/null 2>&1 || brew install swiftformat

Why this works:

  • command -v checks if executable exists in PATH
  • Only runs brew install if tool is missing
  • No redundant "already installed" messages
  • Silent success if tools exist

Problem 2: Cache Permission Errors

SwiftLint may fail in CI with errors like:

warning: Cannot create cache: You don't have permission to save the file...
Error: The folder ... doesn't exist.
Error: Process completed with exit code 1.

This happens because SwiftLint tries to write cache files in sandboxed environments where it lacks permissions.

Solution: Disable Cache in Plugins

Add --no-cache and --force-exclude flags to SwiftLint arguments:

// In your build plugin
var arguments = [
    "lint",
    "--quiet",
    "--no-cache",        // Prevents cache permission errors in CI
    "--force-exclude",   // Ensures exclusions work with direct file paths
    "--reporter", "xcode"
]

Why these flags matter:

  • --no-cache: Prevents SwiftLint from writing to cache directories (which may be read-only in CI sandboxes)
  • --force-exclude: Ensures .swiftlint.yml exclusions are respected even when files are passed directly as arguments

Plugin-Level PATH Lookup

For build plugins that download binaries, check PATH first:

private func findInPath(_ executable: String) -> URL? {
    let searchPaths = [
        "/opt/homebrew/bin",
        "/usr/local/bin",
        "/usr/bin",
    ]

    let envPath = ProcessInfo.processInfo.environment["PATH"] ?? ""
    let allPaths = searchPaths + envPath.split(separator: ":").map(String.init)

    for dir in allPaths {
        let fullPath = URL(fileURLWithPath: dir).appendingPathComponent(executable)
        if FileManager.default.isExecutableFile(atPath: fullPath.path) {
            return fullPath
        }
    }
    return nil
}

Recommended Tool Versions (December 2025)

  • SwiftLint: 0.62.0 (requires Swift 6+ compiler to build)
  • SwiftFormat: 0.58.7

Reusable Workflows

Defining a Reusable Workflow

# .github/workflows/reusable-build.yml
name: Reusable Build

on:
  workflow_call:
    inputs:
      configuration:
        type: string
        default: 'release'
    secrets:
      DEPLOY_KEY:
        required: false

jobs:
  build:
    runs-on: macos-15
    steps:
      - uses: actions/checkout@v4
      - run: swift build -c ${{ inputs.configuration }}

Using a Reusable Workflow

jobs:
  call-build:
    uses: ./.github/workflows/reusable-build.yml
    with:
      configuration: release
    secrets: inherit

Complete Example Workflow

A unified CI/CD workflow for Swift projects:

name: CI/CD

on:
  push:
    branches: [main]
    tags: ['v*']
  pull_request:
    branches: [main]
  workflow_dispatch:
    inputs:
      release:
        type: boolean
        default: false

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: ${{ github.event_name == 'pull_request' }}

permissions:
  contents: write
  pages: write
  id-token: write
  security-events: write

env:
  XCODE_PATH: /Applications/Xcode_26.1.1.app

jobs:
  # Stage 1: Build (single build, shared artifacts)
  build:
    runs-on: macos-15
    steps:
      - uses: actions/checkout@v4
      - run: sudo xcode-select -s ${{ env.XCODE_PATH }}
      - name: Ensure Linting Tools
        run: |
          command -v swiftlint >/dev/null 2>&1 || brew install swiftlint
          command -v swiftformat >/dev/null 2>&1 || brew install swiftformat
      - uses: actions/cache@v4
        with:
          path: |
            .build
            ~/Library/Developer/Xcode/DerivedData
          key: spm-${{ runner.os }}-${{ hashFiles('Package.resolved') }}
      - run: swift build -c release
      - uses: actions/upload-artifact@v4
        with:
          name: build
          path: .build/release/
          retention-days: 1

  # Stage 2: Parallel validation (all depend on build)
  test:
    needs: build
    runs-on: macos-15
    steps:
      - uses: actions/checkout@v4
      - run: sudo xcode-select -s ${{ env.XCODE_PATH }}
      - name: Ensure Linting Tools
        run: |
          command -v swiftlint >/dev/null 2>&1 || brew install swiftlint
          command -v swiftformat >/dev/null 2>&1 || brew install swiftformat
      - uses: actions/cache@v4
        with:
          path: .build
          key: spm-${{ runner.os }}-${{ hashFiles('Package.resolved') }}
      - run: swift test --parallel

  codeql:
    needs: build
    runs-on: macos-15
    permissions:
      security-events: write
    steps:
      - uses: actions/checkout@v4
      - run: sudo xcode-select -s ${{ env.XCODE_PATH }}
      - name: Ensure Linting Tools
        run: |
          command -v swiftlint >/dev/null 2>&1 || brew install swiftlint
          command -v swiftformat >/dev/null 2>&1 || brew install swiftformat
      - uses: github/codeql-action/init@v4
        with:
          languages: swift
          build-mode: manual
      - run: swift build -c release --arch arm64
      - uses: github/codeql-action/analyze@v4

  # Stage 3: Release (only on tags)
  release:
    needs: [test, codeql]
    if: startsWith(github.ref, 'refs/tags/v')
    runs-on: macos-15
    steps:
      - uses: actions/checkout@v4
      - uses: actions/download-artifact@v4
        with:
          name: build
      - uses: softprops/action-gh-release@v2
        with:
          files: '*.tar.gz'

Quick Reference

Goal Solution
Reduce costs Use ubuntu-latest for non-Swift jobs
Faster builds Cache SPM dependencies with Package.resolved hash
Security scanning Use CodeQL v4 with build-mode: manual for Swift 6+
Avoid duplicate work Share artifacts between jobs via actions/upload-artifact
Cancel stale runs Use concurrency groups with cancel-in-progress
Skip docs-only changes Use paths and paths-ignore filters
Fix plugin download issues Use command -v check before brew install
Avoid redundant installs command -v tool >/dev/null 2>&1 || brew install tool
Fix SwiftLint cache errors Use --no-cache --force-exclude flags in plugins

Last updated: December 2025 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment