Skip to content

feat(file-provider): log FP errors and mutating-op outcomes (#488) #1241

feat(file-provider): log FP errors and mutating-op outcomes (#488)

feat(file-provider): log FP errors and mutating-op outcomes (#488) #1241

Workflow file for this run

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