Using AI for CI & Automation in iOS Projects

Nov 11 2026 · Swift 5, iOS 27, Xcode 27

Lesson 03: Interpreting Feedback, Deciding Value, and Understanding the Limits

Demo

Episode complete

Play next episode

Next

Written by the author, AI-editedAI-generated, author-verified How this module was made

Transcript

Welcome back. In the previous lessons we configured three agentic review agents and compared their comments on the same pull request. Today we turn those experiments into a sustainable production practice. We begin from the same sample iOS repository. The Starter folder remains the root of the repository and is committed as the main branch so that GitHub Actions discovers every workflow file we add or modify.

I will now open the Copilot review workflow. At the present it triggers on every pull_request of types opened and synchronize with no path filter and no draft exemption. I add three governance controls. First, a paths filter that limits the job to Swift, Objective-C, Xcode project files, the shared CODE_REVIEW.md and CI_GUIDANCE.md files, the review scripts, and the workflow file itself. Second, a conditional that skips the job when the pull request is still in draft. Third, I confirm that the concurrency group already cancels in-progress runs for the same pull-request number. The workflow already declares CI_GUIDANCE as a parameter and passes it into the shared review script; I leave that wiring in place so later feedback-loop edits stay in markdown rather than in shell. Existing checkout, Copilot, token-log and comment steps stay as they were.

name: AI Code Review
on:
  pull_request:
    types: [opened, synchronize, reopened]
    paths:
      - '**/*.swift'
      - '**/*.{m,h,mm}'
      - '**/*.xcodeproj/**'
      - '**/*.xcworkspace/**'
      - 'CODE_REVIEW.md'
      - 'CI_GUIDANCE.md'
      - '.github/scripts/**'
      - '.github/workflows/ai-code-review.yml'
env:
  CI_GUIDANCE: CI_GUIDANCE.md
concurrency:
  group: ai-review-${{ github.event.pull_request.number }}
  cancel-in-progress: true
jobs:
  review:
    if: github.event.pull_request.draft == false
    runs-on: ubuntu-latest
    timeout-minutes: 10
    permissions:
      contents: read
      pull-requests: write
    steps:
      - name: Checkout repository
        uses: actions/checkout@v7
        with:
          fetch-depth: 0
      - name: Set up Node.js
        uses: actions/setup-node@v7
        with:
          node-version: '24'
      - name: Install GitHub Copilot CLI
        run: npm install -g @github/copilot
      - name: Run Copilot review
        env:
          COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          COPILOT_OTEL_ENABLED: true
          COPILOT_OTEL_EXPORTER_TYPE: file
        run: |
          ./.github/scripts/ai-code-review.sh "${{ github.base_ref }}" \
            --review-file review.md \
            --otel-file "${{ runner.temp }}/copilot-otel.jsonl" \
            --guidance "${{ env.CI_GUIDANCE }}"
      - name: Upload token log
        uses: actions/upload-artifact@v7
        if: always()
        with:
          name: copilot-token-log
          path: ${{ runner.temp }}/copilot-otel.jsonl
          if-no-files-found: ignore
      - name: Post review comment
        if: success()
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          PR_NUMBER: ${{ github.event.pull_request.number }}
        run: ./.github/scripts/post-review-comment.sh review.md "AI Code Review"

I apply the same three controls—path filter, draft exemption and concurrency—to the Claude Code and Codex workflows so that all three agents obey identical token-usage and trigger rules. This keeps comparative measurements meaningful and prevents any single agent from becoming an uncontrolled source of token consumption.

name: Claude Code Review
on:
  pull_request:
    types: [opened, synchronize, reopened]
    paths:
      - '**/*.swift'
      - '**/*.{m,h,mm}'
      - '**/*.xcodeproj/**'
      - '**/*.xcworkspace/**'
      - 'CODE_REVIEW.md'
      - 'CI_GUIDANCE.md'
      - '.github/scripts/**'
      - '.github/workflows/claude-code-review.yml'
env:
  CI_GUIDANCE: CI_GUIDANCE.md
concurrency:
  group: claude-review-${{ github.event.pull_request.number }}
  cancel-in-progress: true
jobs:
  claude-review:
    if: github.event.pull_request.draft == false
    runs-on: ubuntu-latest
    timeout-minutes: 15
    permissions:
      contents: read
      pull-requests: write
      # Federated authentication for this agent only.
      id-token: write
    steps:
      - name: Checkout repository
        uses: actions/checkout@v7
        with:
          fetch-depth: 0
      - name: Build review prompt
        id: prompt
        run: |
          {
            echo 'text<<PROMPT_EOF'
            ./.github/scripts/build-review-prompt.sh claude \
              "${{ github.base_ref }}" \
              "${{ github.repository }}" \
              "${{ github.event.pull_request.number }}" \
              --guidance "${{ env.CI_GUIDANCE }}"
            echo 'PROMPT_EOF'
          } >> "$GITHUB_OUTPUT"
      - name: Run Claude Code review
        id: claude
        uses: anthropics/claude-code-action@v1
        with:
          anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
          github_token: ${{ secrets.GITHUB_TOKEN }}
          prompt: ${{ steps.prompt.outputs.text }}
          claude_args: |
            --model claude-sonnet-5
            --max-turns 20
            --allowedTools "Read,Glob,Grep,Bash(git *)"
      - name: Print token usage
        if: always() && steps.claude.outputs.execution_file != ''
        run: ./.github/scripts/claude-token-usage.sh "${{ steps.claude.outputs.execution_file }}"
      - name: Extract review text
        if: success() && steps.claude.outputs.execution_file != ''
        run: |
          jq -r '
            [.[] | select(.type == "result" and .subtype == "success")]
            | last
            | .result // empty
          ' "${{ steps.claude.outputs.execution_file }}" > claude-review.md
      - name: Post review comment
        if: success()
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          PR_NUMBER: ${{ github.event.pull_request.number }}
        run: ./.github/scripts/post-review-comment.sh claude-review.md "Claude Code Review"
name: Codex Review
on:
  pull_request:
    types: [opened, synchronize, reopened]
    paths:
      - '**/*.swift'
      - '**/*.{m,h,mm}'
      - '**/*.xcodeproj/**'
      - '**/*.xcworkspace/**'
      - 'CODE_REVIEW.md'
      - 'CI_GUIDANCE.md'
      - '.github/scripts/**'
      - '.github/workflows/codex-review.yml'
env:
  CI_GUIDANCE: CI_GUIDANCE.md
concurrency:
  group: codex-review-${{ github.event.pull_request.number }}
  cancel-in-progress: true
jobs:
  codex-review:
    if: github.event.pull_request.draft == false
    runs-on: ubuntu-latest
    timeout-minutes: 15
    permissions:
      contents: read
      pull-requests: write
    steps:
      - name: Checkout repository
        uses: actions/checkout@v7
        with:
          fetch-depth: 0
      - name: Build review prompt
        run: ./.github/scripts/build-review-prompt.sh codex "${{ github.base_ref }}" --guidance "${{ env.CI_GUIDANCE }}" > review-prompt.txt
      - name: Run Codex review
        id: codex
        uses: openai/codex-action@v1
        with:
          openai-api-key: ${{ secrets.OPENAI_API_KEY }}
          codex-home: ${{ runner.temp }}/codex-home
          prompt-file: review-prompt.txt
          output-file: codex-review.md
      - name: Print token usage
        if: always()
        run: ./.github/scripts/codex-token-usage.sh "${{ runner.temp }}/codex-home"
      - name: Post review comment
        if: success()
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          PR_NUMBER: ${{ github.event.pull_request.number }}
        run: ./.github/scripts/post-review-comment.sh codex-review.md "Codex Review"

Next I turn to the existing build-and-test workflow. I restructure it so that the agentic review jobs become peers rather than isolated pipelines. The review jobs continue to run on their own workflow files for clarity and independent concurrency, yet they now share the same path filters and draft exemption. Because the review comments are advisory, I deliberately do not make the review job a required status check; the build and test jobs remain the hard gates.

name: Token Dashboard CI
on:
  workflow_dispatch:
  pull_request:
    types: [opened, synchronize, reopened]
    branches:
      - main

jobs:
  build:
    name: Build
    runs-on: macos-latest
    timeout-minutes: 20
    steps:
      - name: Checkout repository
        uses: actions/checkout@v7
      - name: Build
        run: |
          cd TokensDashboard
          xcodebuild -project TokensDashboard.xcodeproj \
            -scheme TokensDashboard \
            -destination 'platform=iOS Simulator,name=iPhone 17' \
            build

  test:
    name: Test
    runs-on: macos-latest
    timeout-minutes: 20
    steps:
      - name: Checkout repository
        uses: actions/checkout@v7
      - name: Test
        run: |
          cd TokensDashboard
          xcodebuild -project TokensDashboard.xcodeproj \
            -scheme TokensDashboard \
            -destination 'generic/platform=iOS Simulator' \
            test

I also tighten the permissions block on every review job to the minimum required: contents read and pull-requests write. Where an agent needs federated authentication I retain id-token write for that job only. Provider API keys remain in repository secrets and are referenced only by the steps that need them; they never appear in the build or test jobs.

# Review jobs (Copilot and Codex) — no provider key in the CI workflow.
permissions:
  contents: read
  pull-requests: write

# Claude job only — keep id-token write for federated authentication.
permissions:
  contents: read
  pull-requests: write
  id-token: write

# Build and test jobs — no pull-request write, no provider secrets.
permissions:
  contents: read

After the changes are on the default branch I open a new, non-draft pull request that modifies a SwiftUI view and its view model—exactly the kind of change the path filter is intended to catch. The Actions tab shows the build job, the test job and the three review jobs all starting. Because the pull request is not a draft and the paths match, every review agent runs. Had the change been limited to a README, only the build and test jobs would have executed. A change to CODE_REVIEW.md or CI_GUIDANCE.md would still start the review jobs, because those files are part of the filter.

The review comments appear alongside the green build and test checks. There is no race that overwrites status, and the conversation remains readable. I note the wall-clock duration and the token summaries for later comparison.

We now close the feedback loop. I walk through the three comments that just landed, starting with Codex.

Codex reports three issues: UI state published off the main actor, a repeating timer that retains the view model and is never stopped, and a view that holds its model with the wrong SwiftUI property wrapper so refreshes never reach the UI.

Copilot agrees on the main-actor defect and splits the timer into a retain cycle plus a restart every time the view appears. It also mentions an unused force-unwrap, then marks that lower priority because this diff never calls it. The comment opens with a short process note that is not a finding.

Claude matches Copilot: off-main-thread UI mutation, the timer leak, appear without a stop, and the same unused force-unwrap as a lower-severity note. It does not mention the SwiftUI ownership issue that Codex raised.

Every agent flags the main-actor isolation and the leaked timer. Codex alone raises the SwiftUI ownership mistake. The unused helper and Copilot’s preamble sit outside the priorities in CODE_REVIEW.md. I open that file and make two concrete updates.

Under the priority list I elevate MainActor and actor-isolation correctness to the top, and I sharpen memory management so timer closures capture [weak self] and the timer is invalidated on stop or deinit. I also spell out that a view-owned class ObservableObject uses @StateObject, not @State. Under “Do not flag” I add unused helpers this diff does not call, and I keep the existing naming and formatting exclusions. Under preferred comment style I add the MainActor finding as the example of a useful comment. I commit the guidance change on its own branch and open a small pull request so that the change itself is reviewed.

I will not edit the shared review scripts. Those files stay immutable so Copilot, Claude and Codex keep building the same prompt shape. Instead, I open CI_GUIDANCE.md, which the three workflows already pass as the CI_GUIDANCE parameter into .github/scripts/build-review-prompt.sh and .github/scripts/ai-code-review.sh. The Starter file is a single sentence that only asks the agent to use CODE_REVIEW.md. I replace it with the tighter policy: treat that file as authoritative, follow the new priority order, suppress anything the “Do not flag” section now rules out, and match the preferred comment style. After this markdown change is merged I force a synchronise on a follow-up pull request that contains both an unused helper this change never calls and a deliberate MainActor issue.

# CI_GUIDANCE.md — Starter (before)

Treat the CODE_REVIEW.md contents below as the review criteria.

# CI_GUIDANCE.md — after the feedback-loop edit

Treat the CODE_REVIEW.md contents below as the authoritative review policy.
- Follow its priority order exactly. MainActor and actor-isolation correctness come first.
- Suppress any finding the "Do not flag" section marks as out of scope, including unused helpers or force-unwraps this diff does not call.
- Match the preferred comment style, including file:line references and a minimal fix. Do not include process preamble.
# Each review workflow already parameterises the file:
env:
  CI_GUIDANCE: CI_GUIDANCE.md

# Copilot
./.github/scripts/ai-code-review.sh "${{ github.base_ref }}" \
  --guidance "${{ env.CI_GUIDANCE }}"

# Claude / Codex
./.github/scripts/build-review-prompt.sh … --guidance "${{ env.CI_GUIDANCE }}"

After pushing changes and re-running the CI jobs, the new comments show the effect of the closed loop. The unused model(named:) remarks are absent; the MainActor finding appears promptly and with a concrete line reference. Token counts remain inside the expected band, confirming that the additional guidance did not inflate context size. This single cycle—capture, analyze, update CODE_REVIEW.md, update CI_GUIDANCE.md, re-measure is the mechanism that keeps the agentic channel improving rather than stagnating. The scripts did not change.

This completes the demonstration. We have applied path filters, draft exemptions and concurrency controls; we have integrated the review jobs as peers inside the wider iOS CI practice under least-privilege permissions; and finally we have executed one full feedback-loop cycle that turned these review comments into measurable improvement by editing CODE_REVIEW.md and CI_GUIDANCE.md while leaving the shared scripts unchanged. The resulting system is observable, aware of token usage, and under continuous team ownership. In subsequent modules these same governance and feedback principles will support broader uses of AI inside continuous integration and deployment.

See forum comments
Cinema mode Download course materials from Github
Previous: Instruction Next: Conclusion