Skip to content

Agent Validation

Agent Validation #4121

name: Agent Validation
on:
pull_request:
branches: [develop]
paths-ignore:
- '*.md'
- '.github/ISSUE_TEMPLATE/**'
- '.github/PRD_TEMPLATE.md'
workflow_dispatch:
inputs:
branch:
description: 'Branch to validate (used after auto-rebase)'
required: false
type: string
pr_number:
description: 'PR number (for changed-file detection in lint/SPM jobs)'
required: false
type: string
triggered_by:
description: 'Trigger source: rebase | push (skips smoke-build when rebase)'
required: false
type: string
default: 'push'
concurrency:
group: agent-validation-${{ github.head_ref || github.event.inputs.branch }}
cancel-in-progress: true
jobs:
# Job 1: Fast lint on ubuntu (~1 min)
lint:
name: SwiftLint
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: false
fetch-depth: 1
- name: Get changed Swift files
id: changed
env:
GH_TOKEN: ${{ github.token }}
run: |
PR_NUMBER="${{ github.event.pull_request.number || github.event.inputs.pr_number }}"
if [ -z "$PR_NUMBER" ] || [ "$PR_NUMBER" = "0" ]; then
echo "No PR number available — skipping lint (no changed-file context)"
echo "skip=true" >> "$GITHUB_OUTPUT"
else
FILES=$(gh pr diff "$PR_NUMBER" --name-only | grep '\.swift$' || true)
if [ -z "$FILES" ]; then
echo "skip=true" >> "$GITHUB_OUTPUT"
else
echo "skip=false" >> "$GITHUB_OUTPUT"
echo "$FILES" > changed_files.txt
fi
fi
- name: Install SwiftLint
if: steps.changed.outputs.skip == 'false'
env:
GH_TOKEN: ${{ github.token }}
run: |
# Resolve the latest release tag and pick the right Linux artifact.
# SwiftLint 0.63+ ships arch-specific zips (swiftlint_linux_amd64.zip /
# swiftlint_linux_arm64.zip); older releases used swiftlint_linux.zip.
SWIFTLINT_VERSION=$(gh api repos/realm/SwiftLint/releases/latest \
--jq '.tag_name' | sed 's/^v//')
echo "Installing SwiftLint ${SWIFTLINT_VERSION}"
ARCH=$(uname -m)
case "$ARCH" in
x86_64) ARCH_SUFFIX="amd64" ;;
aarch64) ARCH_SUFFIX="arm64" ;;
*) ARCH_SUFFIX="$ARCH" ;;
esac
# Try arch-specific name first (0.63+), then fall back to generic name.
BASE_URL="https://github.com/realm/SwiftLint/releases/download/${SWIFTLINT_VERSION}"
if curl --fail -sSL "${BASE_URL}/swiftlint_linux_${ARCH_SUFFIX}.zip" -o swiftlint.zip 2>/dev/null; then
echo "Downloaded swiftlint_linux_${ARCH_SUFFIX}.zip"
else
curl --fail -sSL "${BASE_URL}/swiftlint_linux.zip" -o swiftlint.zip
echo "Downloaded swiftlint_linux.zip (legacy name)"
fi
unzip -q swiftlint.zip -d swiftlint_bin
chmod +x swiftlint_bin/swiftlint
sudo mv swiftlint_bin/swiftlint /usr/local/bin/swiftlint
swiftlint version
- name: Run SwiftLint on changed files
if: steps.changed.outputs.skip == 'false'
run: |
LINT_FAILED=0
while IFS= read -r file; do
if [ -f "$file" ]; then
swiftlint lint "$file" --config .swiftlint.yml --reporter github-actions-logging || LINT_FAILED=1
fi
done < changed_files.txt
exit $LINT_FAILED
# Job 2: SPM Build/Test for leaf modules (~5-10 min)
spm-test:
name: SPM Test (${{ matrix.module }})
runs-on: macos-15
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
module:
- PVLogging
- PVSettings
- PVHashing
- PVPlists
- PVFeatureFlags
- PVObjCUtils
- PVCheevos
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: false
fetch-depth: 1
- name: Check if module changed
id: check
env:
GH_TOKEN: ${{ github.token }}
run: |
PR_NUMBER="${{ github.event.pull_request.number || github.event.inputs.pr_number }}"
if [ -z "$PR_NUMBER" ] || [ "$PR_NUMBER" = "0" ]; then
# No PR number — run all modules unconditionally
echo "changed=true" >> "$GITHUB_OUTPUT"
else
CHANGED=$(gh pr diff "$PR_NUMBER" --name-only | grep -c "^${{ matrix.module }}/" || echo "0")
if [ "${CHANGED:-0}" -gt 0 ]; then
echo "changed=true" >> "$GITHUB_OUTPUT"
else
echo "changed=false" >> "$GITHUB_OUTPUT"
fi
fi
- name: Select Xcode
if: steps.check.outputs.changed == 'true'
run: sudo xcode-select -s /Applications/Xcode_16.2.app
- name: Build module
if: steps.check.outputs.changed == 'true'
run: |
cd ${{ matrix.module }}
swift build 2>&1 | tail -20
- name: Test module
if: steps.check.outputs.changed == 'true'
run: |
cd ${{ matrix.module }}
swift test 2>&1 | tail -40
# Job 3: Fast CI simulator build — validates that code compiles
# Uses Provenance-CI target (no emulator cores) for fastest possible build
# Skipped when the PR has build-ipa label: build.yml runs the full 5h archive build
# which already validates compilation (redundant smoke build wastes runner time).
# Skipped for rebase-triggered runs: a clean rebase doesn't change the PR's diff,
# so compilation is unchanged. Saves 30 min × N PRs on every develop advance.
smoke-build:
name: Build (Provenance-CI Simulator)
runs-on: macos-15
timeout-minutes: 30
if: |
!contains(github.event.pull_request.labels.*.name, 'build-ipa') &&
github.event.inputs.triggered_by != 'rebase'
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: false
fetch-depth: 1
- name: Select Xcode
run: sudo xcode-select -s /Applications/Xcode_16.2.app
# These Core dirs are top-level git submodules, empty with submodules: false.
# xcodebuild resolves ALL XCLocalSwiftPackageReference entries in the workspace
# (not just those in the CI scheme), so their Package.swift files must exist.
# ZipArchive uses a custom commit not pushed to the public remote; fall back to
# cloning the public repo (compatible Package.swift, same SSZipArchive structure).
- name: Init required Core submodules (shallow)
run: |
git submodule update --init --depth 1 \
Cores/4DO \
Cores/Bliss \
Cores/CrabEMU \
Cores/VirtualJaguar
# ZipArchive: custom commit not available on public remote — clone public instead
if ! git submodule update --init --depth 1 Dependencies/ZipArchive 2>/dev/null; then
echo "ZipArchive shallow init failed (custom commit). Cloning public remote..."
rm -rf Dependencies/ZipArchive
git clone --depth 1 https://github.com/ZipArchive/ZipArchive.git Dependencies/ZipArchive
fi
- name: Cache DerivedData
uses: actions/cache@v4
with:
path: ~/Library/Developer/Xcode/DerivedData
# Hash project/package files for cache key — avoid broad source globs that
# exceed hashFiles limits.
key: ${{ runner.os }}-ci-deriveddata-${{ hashFiles('**/*.xcodeproj/project.pbxproj', '**/Package.swift', '**/Package.resolved') }}
restore-keys: |
${{ runner.os }}-ci-deriveddata-
- name: Build Provenance-CI (iOS Simulator)
run: |
set -eo pipefail
start_time=$(date +%s)
xcodebuild build \
-workspace Provenance.xcworkspace \
-scheme "Provenance-CI" \
-destination "generic/platform=iOS Simulator" \
-skipPackagePluginValidation \
-skipMacroValidation \
CODE_SIGNING_ALLOWED=NO \
-quiet \
2>&1 | tail -100
build_exit=${PIPESTATUS[0]}
end_time=$(date +%s)
duration=$((end_time - start_time))
minutes=$((duration / 60))
seconds=$((duration % 60))
echo "### CI Build Time" >> $GITHUB_STEP_SUMMARY
echo "Duration: ${minutes}m ${seconds}s" >> $GITHUB_STEP_SUMMARY
exit $build_exit
# Job 4: Check PR dependency ordering (lightweight, ubuntu)
dependency-check:
name: Check PR dependencies
runs-on: ubuntu-latest
timeout-minutes: 5
# Only makes sense when we have a PR number
if: github.event.pull_request.number != 0 || github.event.inputs.pr_number != ''
steps:
- name: Check Depends-on links
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number || github.event.inputs.pr_number }}
REPO: ${{ github.repository }}
run: |
body=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json body --jq '.body // ""' 2>/dev/null || echo "")
deps=$(echo "$body" | grep -oP 'Depends on: #\K\d+' || true)
if [ -z "$deps" ]; then
echo "No 'Depends on: #N' dependencies found — OK"
exit 0
fi
blocked=false
for dep in $deps; do
state=$(gh pr view "$dep" --repo "$REPO" --json state --jq '.state' 2>/dev/null || echo "UNKNOWN")
echo "Dependency PR #$dep: $state"
if [ "$state" = "OPEN" ]; then
echo "::warning::PR #$dep is still open — this PR depends on it merging first"
blocked=true
fi
done
if [ "$blocked" = "true" ]; then
# Post a warning comment (idempotent — check for existing blocked comment)
EXISTING=$(gh api "repos/$REPO/issues/${PR_NUMBER}/comments" \
--jq '[.[] | select(.body | test("<!-- dep-blocked -->";""))] | length' \
2>/dev/null || echo "0")
if [ "${EXISTING:-0}" -eq 0 ]; then
gh pr comment "$PR_NUMBER" --repo "$REPO" \
--body "<!-- dep-blocked -->
⚠️ **Merge dependency not yet satisfied** — this PR has a \`Depends on: #N\` link to an open PR. Merge order matters to avoid conflicts. The dependency check is advisory only — merging is still allowed." \
2>/dev/null || true
fi
gh pr edit "$PR_NUMBER" --repo "$REPO" --add-label "blocked" 2>/dev/null || true
else
# All deps merged — remove blocked label if present
gh pr edit "$PR_NUMBER" --repo "$REPO" --remove-label "blocked" 2>/dev/null || true
fi