ci: add DerivedData cache and PR success summary (build time + cache … #18
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: [ dev, feature/**, bugfix/**, chore/** ] | |
pull_request: | |
branches: [ dev ] | |
permissions: | |
contents: read | |
pull-requests: write | |
jobs: | |
build: | |
runs-on: macos-14 | |
env: | |
SCHEME: Avro Keyboard # Default equals target name in this repo | |
TARGET: Avro Keyboard # Fallback target if no shared scheme | |
CONFIG: Debug | |
LOG_DIR: ci-logs # Directory for CI logs and summaries | |
steps: | |
- name: Checkout | |
uses: actions/checkout@v4 | |
- name: Xcode version | |
run: | | |
xcodebuild -version | |
sw_vers | |
- name: Prepare logs directory | |
run: | | |
rm -rf "$LOG_DIR" && mkdir -p "$LOG_DIR" | |
- name: Enable Xcode problem matcher | |
run: echo "::add-matcher::.github/problem-matchers/xcodebuild.json" | |
- name: Enable Linker problem matcher | |
run: echo "::add-matcher::.github/problem-matchers/linker.json" | |
- name: Install xcpretty | |
run: | | |
gem install xcpretty --no-document | |
- name: Install CocoaPods | |
run: | | |
gem install cocoapods --no-document | |
- name: Install Subversion (for RegexKitLite) | |
run: | | |
brew update | |
brew install subversion | |
- name: Cache CocoaPods | |
id: pods_cache | |
uses: actions/cache@v4 | |
with: | |
path: | | |
Pods | |
~/.cocoapods | |
~/Library/Caches/CocoaPods | |
key: ${{ runner.os }}-pods-${{ hashFiles('Podfile.lock') }} | |
restore-keys: | | |
${{ runner.os }}-pods- | |
- name: Cache Xcode DerivedData | |
id: derived_cache | |
uses: actions/cache@v4 | |
with: | |
path: | | |
~/Library/Developer/Xcode/DerivedData | |
key: ${{ runner.os }}-derived-${{ hashFiles('AvroKeyboard.xcodeproj/project.pbxproj', 'Podfile.lock') }}-${{ env.SCHEME }} | |
restore-keys: | | |
${{ runner.os }}-derived- | |
- name: Pod install | |
run: | | |
pod install --repo-update --silent || pod install | |
- name: Detect project or workspace | |
shell: bash | |
run: | | |
set -euo pipefail | |
ROOT=$(pwd -P) | |
WS="AvroKeyboard.xcworkspace" | |
PRJ="AvroKeyboard.xcodeproj" | |
if [[ -d "$WS" && -d "Pods" && -f "Pods/Pods.xcodeproj/project.pbxproj" ]]; then | |
echo "KIND=workspace" >> "$GITHUB_ENV" | |
echo "WORKSPACE=$ROOT/$WS" >> "$GITHUB_ENV" | |
elif [[ -d "$PRJ" ]]; then | |
echo "KIND=project" >> "$GITHUB_ENV" | |
echo "PROJECT=$ROOT/$PRJ" >> "$GITHUB_ENV" | |
else | |
echo "No Xcode workspace or project found"; ls -la; exit 1 | |
fi | |
- name: Resolve scheme | |
shell: bash | |
run: | | |
set -euo pipefail | |
export LC_ALL=C | |
WANT_SCHEME="${SCHEME:-}" | |
list_cmd() { | |
if [[ "${KIND:-}" == "workspace" ]]; then | |
xcodebuild -list -workspace "${WORKSPACE}" 2>/dev/null | |
else | |
xcodebuild -list -project "${PROJECT}" 2>/dev/null | |
fi | |
} | |
LIST_TRIMMED=$(list_cmd \ | |
| sed -n '/Schemes:/,$p' \ | |
| tail -n +2 \ | |
| sed 's/\r$//' \ | |
| awk '{gsub(/^[ \t]+|[ \t]+$/,""); if (length) print}') | |
echo "Available schemes:"; printf '%s\n' "$LIST_TRIMMED" | |
SEL="" | |
WANT_SCHEME_TRIM=$(printf '%s' "$WANT_SCHEME" | awk '{gsub(/^[ \t]+|[ \t]+$/,""); print}') | |
if [[ -n "$WANT_SCHEME_TRIM" ]] && printf '%s\n' "$LIST_TRIMMED" | grep -Fxq "$WANT_SCHEME_TRIM"; then | |
SEL="$WANT_SCHEME_TRIM" | |
else | |
SEL=$(printf '%s\n' "$LIST_TRIMMED" | head -n1) | |
fi | |
if [[ -z "$SEL" ]]; then | |
echo "No shared schemes found"; exit 1 | |
fi | |
echo "Resolved scheme: '$SEL'" | |
echo "SCHEME_RESOLVED=$SEL" >> "$GITHUB_ENV" | |
- name: Record build start time | |
run: echo "BUILD_START=$(date +%s)" >> "$GITHUB_ENV" | |
- name: Build | |
shell: bash | |
run: | | |
set -euo pipefail | |
if [[ "${KIND:-}" == "workspace" ]]; then | |
xcodebuild \ | |
-workspace "${WORKSPACE}" \ | |
-scheme "${SCHEME_RESOLVED}" \ | |
-configuration "$CONFIG" \ | |
-destination 'platform=macOS' \ | |
CODE_SIGNING_ALLOWED=NO \ | |
build 2>&1 | tee "$LOG_DIR/build.log" | xcpretty | |
else | |
xcodebuild \ | |
-project "${PROJECT}" \ | |
-scheme "${SCHEME_RESOLVED}" \ | |
-configuration "$CONFIG" \ | |
-destination 'platform=macOS' \ | |
CODE_SIGNING_ALLOWED=NO \ | |
build 2>&1 | tee "$LOG_DIR/build.log" | xcpretty || { | |
echo "Scheme build failed; retrying with target '$TARGET'"; | |
xcodebuild \ | |
-project "${PROJECT}" \ | |
-target "$TARGET" \ | |
-configuration "$CONFIG" \ | |
-destination 'platform=macOS' \ | |
CODE_SIGNING_ALLOWED=NO \ | |
build 2>&1 | tee -a "$LOG_DIR/build.log" | xcpretty; | |
} | |
fi | |
env: | |
NSUnbufferedIO: "YES" | |
continue-on-error: false | |
- name: Run unit tests (if any) | |
shell: bash | |
run: | | |
set -euo pipefail | |
HAS_TESTS=$( { grep -R --include='*.m' --include='*.swift' -n "XCTestCase" . || true; } | wc -l | tr -d '[:space:]') | |
if [[ "$HAS_TESTS" -gt "0" ]]; then | |
if [[ "${KIND:-}" == "workspace" ]]; then | |
# Only run tests with a scheme; if no shared scheme, skip. | |
if xcodebuild -list -workspace "${WORKSPACE}" | grep -q "Schemes:"; then | |
xcodebuild \ | |
-workspace "${WORKSPACE}" \ | |
-scheme "${SCHEME_RESOLVED}" \ | |
-configuration "$CONFIG" \ | |
-destination 'platform=macOS' \ | |
CODE_SIGNING_ALLOWED=NO \ | |
test 2>&1 | tee "$LOG_DIR/test.log" | xcpretty | |
else | |
echo "No shared scheme detected; skipping XCTest run." | |
fi | |
else | |
if xcodebuild -list -project "${PROJECT}" | grep -q "Schemes:"; then | |
xcodebuild \ | |
-project "${PROJECT}" \ | |
-scheme "${SCHEME_RESOLVED}" \ | |
-configuration "$CONFIG" \ | |
-destination 'platform=macOS' \ | |
CODE_SIGNING_ALLOWED=NO \ | |
test 2>&1 | tee "$LOG_DIR/test.log" | xcpretty | |
else | |
echo "No shared scheme detected; skipping XCTest run." | |
fi | |
fi | |
else | |
echo "No XCTest targets detected; skipping test step." | |
fi | |
- name: Summarize failure (Option B) | |
if: failure() | |
shell: bash | |
run: | | |
set -euo pipefail | |
SUMMARY="$LOG_DIR/ci_summary.txt" | |
{ | |
echo "CI Failure Summary" | |
echo "Kind: ${KIND:-unknown}" | |
echo "Workspace: ${WORKSPACE:-n/a}" | |
echo "Project: ${PROJECT:-n/a}" | |
echo "Scheme: ${SCHEME_RESOLVED:-n/a}" | |
echo | |
if [[ -f "$LOG_DIR/build.log" ]]; then | |
echo "=== Build Errors (top hits) ===" | |
grep -nE "xcodebuild: error:|\\berror:|Undefined symbols|Command PhaseScriptExecution failed|exit code 65|ld: error" "$LOG_DIR/build.log" | head -n 100 || true | |
echo | |
echo "=== Build Log Tail (200 lines) ===" | |
tail -n 200 "$LOG_DIR/build.log" || true | |
echo | |
else | |
echo "No build.log captured." | |
fi | |
if [[ -f "$LOG_DIR/test.log" ]]; then | |
echo "=== Test Errors (top hits) ===" | |
grep -nE "\\berror:|failing|Assertion failed|Test Failed|Failures:" "$LOG_DIR/test.log" | head -n 100 || true | |
echo | |
echo "=== Test Log Tail (200 lines) ===" | |
tail -n 200 "$LOG_DIR/test.log" || true | |
echo | |
fi | |
} > "$SUMMARY" | |
echo "## CI Failure Summary" >> "$GITHUB_STEP_SUMMARY" | |
echo "" >> "$GITHUB_STEP_SUMMARY" | |
echo '\`\`\`' >> "$GITHUB_STEP_SUMMARY" | |
tail -n 200 "$SUMMARY" >> "$GITHUB_STEP_SUMMARY" || true | |
echo '\`\`\`' >> "$GITHUB_STEP_SUMMARY" | |
- name: Upload logs (Option A fallback) | |
if: failure() | |
uses: actions/upload-artifact@v4 | |
with: | |
name: ci-logs | |
retention-days: 30 | |
path: | | |
ci-logs/** | |
- name: Record build end time | |
if: success() | |
run: echo "BUILD_END=$(date +%s)" >> "$GITHUB_ENV" | |
- name: Comment success summary on PR | |
if: success() && github.event_name == 'pull_request' | |
uses: actions/github-script@v7 | |
with: | |
script: | | |
function fmtSeconds(sec) { | |
sec = Number(sec || 0); | |
const m = Math.floor(sec / 60); | |
const s = sec % 60; | |
return (m > 0 ? `${m}m ` : '') + `${s}s`; | |
} | |
const start = Number(process.env.BUILD_START || 0); | |
const end = Number(process.env.BUILD_END || 0); | |
const duration = (end && start) ? (end - start) : 0; | |
const podsCache = core.getInput('podsHit') || (process.env.PODS_HIT || ''); | |
const derivedCache = core.getInput('derivedHit') || (process.env.DERIVED_HIT || ''); | |
const podsHit = '${{ steps.pods_cache.outputs.cache-hit }}' === 'true'; | |
const derivedHit = '${{ steps.derived_cache.outputs.cache-hit }}' === 'true'; | |
const pr = context.payload.pull_request; | |
const body = [ | |
`CI Success Summary (workflow: ${process.env.GITHUB_WORKFLOW})`, | |
'', | |
`• Build time: ${fmtSeconds(duration)}`, | |
`• CocoaPods cache hit: ${podsHit}`, | |
`• DerivedData cache hit: ${derivedHit}`, | |
].join('\n'); | |
await github.rest.issues.createComment({ | |
owner: context.repo.owner, | |
repo: context.repo.repo, | |
issue_number: pr.number, | |
body | |
}); | |
- name: Comment failure summary on PR | |
if: failure() && github.event_name == 'pull_request' | |
uses: actions/github-script@v7 | |
with: | |
script: | | |
const fs = require('fs'); | |
const path = 'ci-logs/ci_summary.txt'; | |
let body = 'CI failure, but no summary was generated.'; | |
try { body = fs.readFileSync(path, 'utf8'); } catch (e) {} | |
const header = `CI Failure Summary (workflow: ${process.env.GITHUB_WORKFLOW})\n\n`; | |
const pr = context.payload.pull_request; | |
await github.rest.issues.createComment({ | |
owner: context.repo.owner, | |
repo: context.repo.repo, | |
issue_number: pr.number, | |
body: '```\n' + header + body + '\n```' | |
}); | |
- name: Regression tests (optional; skip if file missing) | |
shell: bash | |
run: | | |
if [[ -f "Tests/Regression/phrases.tsv" && -x "Tools/run_regression.sh" ]]; then | |
Tools/run_regression.sh Tests/Regression/phrases.tsv | |
else | |
echo "Regression harness not present yet; skipping." | |
fi |