feat(file-provider): log FP errors and mutating-op outcomes (#488) #1241
Workflow file for this run
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: CI | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| branches: [main] | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: ci-${{ github.ref }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | |
| jobs: | |
| build: | |
| name: Build + smoke tests | |
| runs-on: macos-15 | |
| timeout-minutes: 30 | |
| steps: | |
| - uses: actions/checkout@v7.0.0 | |
| - name: Load pinned Xcode version | |
| # Single source of truth for the Xcode version, shared with | |
| # codeql.yml and release.yml — see .github/xcode-version. Exporting | |
| # DEVELOPER_DIR here (instead of a hardcoded job-level env:) means | |
| # the three workflows can never drift to different literals again. | |
| id: xcode | |
| run: | | |
| set -euo pipefail | |
| version="$(cat .github/xcode-version)" | |
| echo "version=$version" >> "$GITHUB_OUTPUT" | |
| echo "DEVELOPER_DIR=/Applications/Xcode_${version}.app/Contents/Developer" >> "$GITHUB_ENV" | |
| - name: Assert Xcode ${{ steps.xcode.outputs.version }} and Swift 6.1.2 are present | |
| # Fail fast if the runner image no longer ships the pinned Xcode | |
| # rather than silently falling back to a different Xcode version. | |
| # periphery 3.7.4 requires Swift >= 6.1.2 (Xcode 16.4); the build, | |
| # tests, and periphery scan all share this single Xcode so their | |
| # index stores are compatible. | |
| run: | | |
| set -euo pipefail | |
| xcode_app="/Applications/Xcode_${{ steps.xcode.outputs.version }}.app" | |
| if [ ! -d "$xcode_app" ]; then | |
| echo "ERROR: ${xcode_app} not found (pinned in .github/xcode-version)" >&2 | |
| echo "Available Xcode installations:" >&2 | |
| ls /Applications/Xcode*.app 2>/dev/null || echo " (none found)" >&2 | |
| exit 1 | |
| fi | |
| xcodebuild -version | |
| swift --version | |
| - name: Check bundle-ID consistency | |
| # Asserts bundle IDs, team prefix, and app-group are consistent across | |
| # OfemPaths.swift, project.yml, the cask template, and release.yml. | |
| # Fails fast if they drift so regressions surface at PR time, not at | |
| # notarization. The full single-source xcconfig refactor is deferred to | |
| # a dedicated signed-release-validated PR; this guard covers the gap. | |
| run: bash scripts/check-bundle-ids.sh | |
| - name: Cache Homebrew downloads | |
| uses: actions/cache@v6.1.0 | |
| with: | |
| path: | | |
| ~/Library/Caches/Homebrew/downloads | |
| ~/Library/Caches/Homebrew/api | |
| key: brew-${{ runner.os }}-xcodegen-periphery-3.7.4-${{ hashFiles('.github/workflows/ci.yml') }} | |
| restore-keys: | | |
| brew-${{ runner.os }}-xcodegen- | |
| - name: Cache SwiftPM build artifacts | |
| # Cache .build and the Xcode DerivedData SourcePackages folder keyed on | |
| # the lockfile so MSAL, GRDB, and TOMLKit are not re-resolved/recompiled | |
| # on every run. The restore-key falls back to a prior run's cache when | |
| # Package.resolved has not changed, accepting a slightly stale build | |
| # artifact cache that re-increments on the next full build. | |
| # "spm-build-" is this job's own prefix — the ofemkit-tests job below | |
| # and release.yml cache a different path set (no DerivedData here) and | |
| # must not share a key, or whichever job saves first silently wins and | |
| # the others restore/overwrite the wrong contents. | |
| uses: actions/cache@v6.1.0 | |
| with: | |
| path: | | |
| Packages/OfemKit/.build | |
| DerivedData/SourcePackages | |
| key: spm-build-${{ runner.os }}-${{ hashFiles('Packages/OfemKit/Package.resolved') }} | |
| restore-keys: | | |
| spm-build-${{ runner.os }}- | |
| - name: Install xcodegen and periphery | |
| run: brew install xcodegen periphery | |
| - name: Assert periphery 3.7.4 | |
| # Fail fast if Homebrew installs a different version than expected. | |
| # This catches unexpected Homebrew formula updates before they silently | |
| # change rule behaviour. Update this assertion when intentionally | |
| # upgrading periphery. | |
| run: | | |
| set -euo pipefail | |
| got=$(periphery version) | |
| want="3.7.4" | |
| if [ "$got" != "$want" ]; then | |
| echo "ERROR: periphery version is $got, expected $want" >&2 | |
| exit 1 | |
| fi | |
| echo "periphery $got" | |
| - name: Bootstrap local xcconfig | |
| # CI has no signing identity; the sample file's placeholder | |
| # DEVELOPMENT_TEAM is harmless because every xcodebuild invocation | |
| # below disables code signing (CODE_SIGNING_ALLOWED=NO). | |
| run: make bootstrap | |
| - name: Generate Xcode project | |
| run: make gen | |
| - name: Verify project.pbxproj exists | |
| run: test -f OneLake.xcodeproj/project.pbxproj | |
| - name: Build for testing (single compile) | |
| # One xcodebuild pass compiles the app, the File Provider .appex, and | |
| # both test bundles (OneLakeHostTests + OneLakeFileProviderTests) — the | |
| # OneLake scheme lists both test targets in its test: action. The | |
| # DerivedData/Index.noindex/DataStore written here is also consumed by | |
| # the periphery scan below, so the entire job shares a single compile. | |
| # -enableCodeCoverage YES instruments the test bundles at build time so | |
| # the resulting xcresult files contain coverage data that xcresultparser | |
| # can export; without this flag, test-without-building produces an | |
| # xcresult with no coverage archive and xcresultparser exits non-zero. | |
| run: | | |
| xcodebuild build-for-testing \ | |
| -project OneLake.xcodeproj \ | |
| -scheme OneLake \ | |
| -configuration Debug \ | |
| -destination 'platform=macOS,arch=arm64' \ | |
| -derivedDataPath DerivedData \ | |
| -enableCodeCoverage YES \ | |
| CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO CODE_SIGN_IDENTITY="" | |
| - name: Run host-app tests (no rebuild) | |
| # test-without-building reuses the OneLake_*.xctestrun produced by | |
| # build-for-testing above — no second compile. -only-testing scopes | |
| # execution to OneLakeHostTests while keeping the OneLake scheme (which | |
| # owns the .xctestrun file). A separate result bundle per suite | |
| # preserves the host/fpe Codecov flag split. | |
| run: | | |
| rm -rf DerivedData/HostTests.xcresult | |
| xcodebuild test-without-building \ | |
| -project OneLake.xcodeproj \ | |
| -scheme OneLake \ | |
| -destination 'platform=macOS,arch=arm64' \ | |
| -derivedDataPath DerivedData \ | |
| -only-testing:OneLakeHostTests \ | |
| -enableCodeCoverage YES \ | |
| -resultBundlePath DerivedData/HostTests.xcresult | |
| - name: Run FPE tests (no rebuild) | |
| # Same pattern as the host-app step: OneLake scheme + -only-testing | |
| # to select the FPE bundle, separate .xcresult for the fpe Codecov flag. | |
| run: | | |
| rm -rf DerivedData/FPETests.xcresult | |
| xcodebuild test-without-building \ | |
| -project OneLake.xcodeproj \ | |
| -scheme OneLake \ | |
| -destination 'platform=macOS,arch=arm64' \ | |
| -derivedDataPath DerivedData \ | |
| -only-testing:OneLakeFileProviderTests \ | |
| -enableCodeCoverage YES \ | |
| -resultBundlePath DerivedData/FPETests.xcresult | |
| - name: Export host-app coverage | |
| run: | | |
| set -euo pipefail | |
| brew install xcresultparser | |
| xrp="$(brew --prefix xcresultparser)/bin/xcresultparser" | |
| "$xrp" --output-format cobertura \ | |
| DerivedData/HostTests.xcresult > host-coverage.xml | |
| # xcresultparser emits absolute filename paths; strip the workspace | |
| # prefix so they are repo-relative (e.g. OneLake/Foo.swift), which is | |
| # what Codecov needs to map coverage onto the tree. | |
| sed "s|${GITHUB_WORKSPACE}/||g" host-coverage.xml > host-coverage.xml.tmp | |
| mv host-coverage.xml.tmp host-coverage.xml | |
| echo "host-coverage.xml: $(wc -l < host-coverage.xml) lines" | |
| - name: Upload host coverage to Codecov | |
| uses: codecov/codecov-action@v7.0.0 | |
| with: | |
| files: host-coverage.xml | |
| flags: host | |
| disable_search: true | |
| # Never fail the build on a Codecov hiccup or a fork PR with no token. | |
| fail_ci_if_error: false | |
| - name: Export host-app test results (JUnit) | |
| # Reuse the same .xcresult already produced by the test run — no | |
| # second compile or test pass. The !cancelled() guard ensures this | |
| # step runs even when tests fail so failing runs are captured by | |
| # Codecov Test Analytics (that is the point of the feature). | |
| if: ${{ !cancelled() }} | |
| run: | | |
| set -uo pipefail | |
| xrp="$(brew --prefix xcresultparser)/bin/xcresultparser" | |
| "$xrp" --output-format junit \ | |
| DerivedData/HostTests.xcresult > host-junit.xml | |
| count=$(grep -c '<testcase' host-junit.xml || echo 0) | |
| echo "host-junit.xml: $count testcase(s)" | |
| if [ "$count" -eq 0 ]; then | |
| echo "ERROR: host JUnit has no testcases" >&2 | |
| exit 1 | |
| fi | |
| - name: Upload host test results to Codecov | |
| if: ${{ !cancelled() }} | |
| uses: codecov/test-results-action@v1.2.1 | |
| with: | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| files: host-junit.xml | |
| flags: host | |
| disable_search: true | |
| fail_ci_if_error: false | |
| - name: Export FPE coverage | |
| run: | | |
| set -euo pipefail | |
| xrp="$(brew --prefix xcresultparser)/bin/xcresultparser" | |
| "$xrp" --output-format cobertura \ | |
| DerivedData/FPETests.xcresult > fpe-coverage.xml | |
| sed "s|${GITHUB_WORKSPACE}/||g" fpe-coverage.xml > fpe-coverage.xml.tmp | |
| mv fpe-coverage.xml.tmp fpe-coverage.xml | |
| echo "fpe-coverage.xml: $(wc -l < fpe-coverage.xml) lines" | |
| - name: Upload FPE coverage to Codecov | |
| uses: codecov/codecov-action@v7.0.0 | |
| with: | |
| files: fpe-coverage.xml | |
| flags: fpe | |
| disable_search: true | |
| fail_ci_if_error: false | |
| - name: Export FPE test results (JUnit) | |
| # Reuse the same .xcresult already produced by the test run — no | |
| # second compile or test pass. The !cancelled() guard ensures this | |
| # step runs even when tests fail so failing runs are captured by | |
| # Codecov Test Analytics. | |
| if: ${{ !cancelled() }} | |
| run: | | |
| set -uo pipefail | |
| xrp="$(brew --prefix xcresultparser)/bin/xcresultparser" | |
| "$xrp" --output-format junit \ | |
| DerivedData/FPETests.xcresult > fpe-junit.xml | |
| count=$(grep -c '<testcase' fpe-junit.xml || echo 0) | |
| echo "fpe-junit.xml: $count testcase(s)" | |
| if [ "$count" -eq 0 ]; then | |
| echo "ERROR: fpe JUnit has no testcases" >&2 | |
| exit 1 | |
| fi | |
| - name: Upload FPE test results to Codecov | |
| if: ${{ !cancelled() }} | |
| uses: codecov/test-results-action@v1.2.1 | |
| with: | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| files: fpe-junit.xml | |
| flags: fpe | |
| disable_search: true | |
| fail_ci_if_error: false | |
| - name: Verify index store exists | |
| # Fail fast with a clear message if Xcode changes its DerivedData layout | |
| # in a future version, rather than surfacing a cryptic periphery error. | |
| run: | | |
| test -d DerivedData/Index.noindex/DataStore || { | |
| echo "ERROR: index store not found at DerivedData/Index.noindex/DataStore" >&2 | |
| echo " Did build-for-testing run with -derivedDataPath DerivedData?" >&2 | |
| exit 1 | |
| } | |
| - name: Dead-code scan (periphery, no rebuild) | |
| # --skip-build reuses the index store written by the build-for-testing | |
| # step above (DerivedData/Index.noindex/DataStore). No second compile. | |
| # strict: true in .periphery.yml — any new finding fails the build. | |
| # To intentionally keep a symbol, annotate it with // periphery:ignore. | |
| run: | | |
| periphery scan \ | |
| --skip-build \ | |
| --index-store-path DerivedData/Index.noindex/DataStore \ | |
| --format xcode | |
| ofemkit-tests: | |
| name: OfemKit package tests | |
| runs-on: macos-15 | |
| timeout-minutes: 20 | |
| steps: | |
| - uses: actions/checkout@v7.0.0 | |
| - name: Load pinned Xcode version | |
| # Single source of truth for the Xcode version — see .github/xcode-version. | |
| id: xcode | |
| run: | | |
| set -euo pipefail | |
| version="$(cat .github/xcode-version)" | |
| echo "version=$version" >> "$GITHUB_OUTPUT" | |
| echo "DEVELOPER_DIR=/Applications/Xcode_${version}.app/Contents/Developer" >> "$GITHUB_ENV" | |
| - name: Assert Xcode ${{ steps.xcode.outputs.version }} and Swift 6.1.2 are present | |
| run: | | |
| set -euo pipefail | |
| xcode_app="/Applications/Xcode_${{ steps.xcode.outputs.version }}.app" | |
| if [ ! -d "$xcode_app" ]; then | |
| echo "ERROR: ${xcode_app} not found (pinned in .github/xcode-version)" >&2 | |
| ls /Applications/Xcode*.app 2>/dev/null || echo " (none found)" >&2 | |
| exit 1 | |
| fi | |
| xcodebuild -version | |
| swift --version | |
| - name: Cache SwiftPM build artifacts | |
| # "spm-ofemkit-" is this job's own prefix — it caches only | |
| # Packages/OfemKit/.build (no DerivedData/SourcePackages), which is a | |
| # different path set than the build job above; sharing a key would let | |
| # the first job to save silently overwrite the cache the other reads. | |
| uses: actions/cache@v6.1.0 | |
| with: | |
| path: | | |
| Packages/OfemKit/.build | |
| key: spm-ofemkit-${{ runner.os }}-${{ hashFiles('Packages/OfemKit/Package.resolved') }} | |
| restore-keys: | | |
| spm-ofemkit-${{ runner.os }}- | |
| - name: Validate Package.resolved is in sync | |
| # Ensures the committed lockfile matches Package.swift before running | |
| # tests, so CI surfaces drift early rather than at release time. | |
| working-directory: Packages/OfemKit | |
| run: | | |
| set -euo pipefail | |
| swift package resolve --only-use-versions-from-resolved-file 2>/dev/null || \ | |
| swift package resolve | |
| if ! git diff --quiet -- Package.resolved; then | |
| echo "ERROR: Package.resolved drifted from Package.swift — commit the updated lockfile." >&2 | |
| git diff -- Package.resolved >&2 | |
| exit 1 | |
| fi | |
| - name: Run OfemKit tests | |
| # The engine package owns all sync/auth/cache logic. No xcodegen | |
| # is needed — `swift test` resolves from Package.resolved directly. | |
| # --enable-code-coverage emits LLVM profiles for the Codecov upload below. | |
| # --xunit-output emits JUnit XML for Codecov Test Analytics; on Swift | |
| # 6.1.2 this includes swift-testing results alongside XCTest. | |
| working-directory: Packages/OfemKit | |
| run: swift test --enable-code-coverage --xunit-output ofemkit-junit.xml | |
| - name: Export coverage to lcov | |
| working-directory: Packages/OfemKit | |
| run: | | |
| set -euo pipefail | |
| bin=$(swift build --show-bin-path) | |
| profdata="$bin/codecov/default.profdata" | |
| # The test bundle's executable lives inside the .xctest on macOS. | |
| xctest=$(find "$bin" -path '*.xctest/Contents/MacOS/*' -type f -name 'OfemKitPackageTests' | head -1) | |
| test -n "$xctest" || { echo "OfemKit test bundle not found under $bin" >&2; exit 1; } | |
| xcrun llvm-cov export -format=lcov \ | |
| -instr-profile "$profdata" "$xctest" \ | |
| -ignore-filename-regex='(/Tests/|/\.build/)' > coverage.lcov | |
| # llvm-cov emits absolute compile paths; strip the workspace prefix so | |
| # the SF: paths are repo-relative (e.g. Packages/OfemKit/Sources/...), | |
| # which is what Codecov needs to map coverage onto the tree. | |
| sed "s|${GITHUB_WORKSPACE}/||g" coverage.lcov > coverage.lcov.tmp | |
| mv coverage.lcov.tmp coverage.lcov | |
| echo "wrote coverage.lcov ($(wc -l < coverage.lcov) lines)" | |
| - name: Upload coverage to Codecov | |
| uses: codecov/codecov-action@v7.0.0 | |
| with: | |
| files: Packages/OfemKit/coverage.lcov | |
| flags: ofemkit | |
| disable_search: true | |
| # Never fail the build on a Codecov hiccup or a fork PR with no token. | |
| fail_ci_if_error: false | |
| - name: Verify OfemKit JUnit output | |
| # swift test --xunit-output writes XCTest results to ofemkit-junit.xml | |
| # and Swift Testing results to the sibling ofemkit-junit-swift-testing.xml. | |
| # OfemKit is 100% Swift Testing, so the primary file is always empty; | |
| # count <testcase> across all matching files to cover both split and | |
| # combined toolchain behaviour. Fail fast on zero so a toolchain | |
| # regression doesn't silently upload an empty file. | |
| if: ${{ !cancelled() }} | |
| working-directory: Packages/OfemKit | |
| run: | | |
| set -uo pipefail | |
| ls -la ofemkit-junit*.xml || true | |
| count=$(cat ofemkit-junit*.xml 2>/dev/null | grep -c '<testcase' || echo 0) | |
| echo "ofemkit testcases (all JUnit files): $count" | |
| if [ "$count" -eq 0 ]; then | |
| echo "ERROR: no <testcase> elements found across ofemkit-junit*.xml" >&2 | |
| echo " --xunit-output may not be capturing swift-testing results on this toolchain." >&2 | |
| exit 1 | |
| fi | |
| - name: Upload OfemKit test results to Codecov | |
| # Upload both the XCTest file (empty but harmless) and the Swift Testing | |
| # file that carries all the actual test cases. | |
| if: ${{ !cancelled() }} | |
| uses: codecov/test-results-action@v1.2.1 | |
| with: | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| files: Packages/OfemKit/ofemkit-junit.xml,Packages/OfemKit/ofemkit-junit-swift-testing.xml | |
| flags: ofemkit | |
| disable_search: true | |
| fail_ci_if_error: false | |
| commitlint: | |
| name: Commitlint | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| if: github.event_name == 'pull_request' | |
| steps: | |
| - uses: actions/checkout@v7.0.0 | |
| with: | |
| fetch-depth: 0 | |
| - name: Set up Node | |
| uses: actions/setup-node@v6.4.0 | |
| with: | |
| node-version: "22" | |
| - name: Install commitlint | |
| run: npm install --no-save @commitlint/cli@19.8.1 @commitlint/config-conventional@19.8.1 | |
| - name: Validate PR commits | |
| # Use the merge-base rather than base.sha so commitlint validates only | |
| # the commits on this PR branch, not stale base commits that diverged | |
| # after a force-push or after main advanced since the branch was cut. | |
| run: | | |
| npx commitlint \ | |
| --from "$(git merge-base origin/${{ github.event.pull_request.base.ref }} HEAD)" \ | |
| --to HEAD \ | |
| --verbose |