Agent Validation #4127
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |