Skip to content

feat(Phase A): scaffold TolerantDecoder + Ranking; wire into lookup w… #46

feat(Phase A): scaffold TolerantDecoder + Ranking; wire into lookup w…

feat(Phase A): scaffold TolerantDecoder + Ranking; wire into lookup w… #46

Workflow file for this run

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
# Optionally pin Xcode explicitly to reduce toolchain drift
# - Uses default Xcode on macOS-13 (15.2) if not set up below
env:
CACHE_VERSION: v1 # bump to v2 to invalidate old caches
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
DESTINATION: platform=macOS
XCODE_VERSION: '15.2'
NSUnbufferedIO: "YES"
COCOAPODS_VERSION: '1.16.2'
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Xcode version
run: |
xcodebuild -version
sw_vers
- name: Setup Xcode (optional pin)
if: ${{ always() }}
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: '${{ env.XCODE_VERSION }}'
- name: Prepare logs directory
run: |
rm -rf "$LOG_DIR" && mkdir -p "$LOG_DIR"
- name: Enforce Podfile.lock presence
shell: bash
run: |
if [[ -f Podfile.lock ]]; then
echo "Found Podfile.lock (enforced)."
else
echo "ERROR: Podfile.lock is missing. Run 'pod install' locally and commit the lockfile for reproducible builds." >&2
exit 1
fi
- 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 (pinned)
run: |
echo "Installing CocoaPods ${COCOAPODS_VERSION} via RubyGems"
gem install cocoapods -v "${COCOAPODS_VERSION}" --no-document
echo "CocoaPods version: $(pod --version)"
- name: Install Subversion (for RegexKitLite)
shell: bash
run: |
set -euo pipefail
if command -v svn >/dev/null 2>&1; then
echo "svn already installed: $(svn --version --quiet)"
else
echo "Installing Subversion via Homebrew (no auto-update)"
export HOMEBREW_NO_AUTO_UPDATE=1
brew install subversion
fi
- name: Cache CocoaPods
id: pods_cache
uses: actions/cache@v4
with:
path: |
Pods
~/.cocoapods
~/Library/Caches/CocoaPods
key: ${{ runner.os }}-${{ env.CACHE_VERSION }}-pods-${{ hashFiles('Podfile.lock') }}
restore-keys: |
${{ runner.os }}-${{ env.CACHE_VERSION }}-pods-
${{ runner.os }}-pods-
- name: Cache Xcode DerivedData
id: derived_cache
uses: actions/cache@v4
with:
path: |
~/Library/Developer/Xcode/DerivedData
key: ${{ runner.os }}-${{ env.CACHE_VERSION }}-derived-${{ hashFiles('AvroKeyboard.xcodeproj/project.pbxproj', 'Podfile.lock') }}-${{ env.SCHEME }}
restore-keys: |
${{ runner.os }}-${{ env.CACHE_VERSION }}-derived-
${{ runner.os }}-derived-
- name: Pod install (deployment mode)
shell: bash
env:
COCOAPODS_DISABLE_STATS: "true"
run: |
set -euo pipefail
echo "Using CocoaPods $(pod --version)"
mkdir -p "$LOG_DIR"
# Enforce lockfile resolution
if ! pod install --deployment 2>&1 | tee "$LOG_DIR/pod_install.log"; then
echo "pod install --deployment failed; retrying after pod repo update..."
pod repo update || true
pod install --deployment --verbose 2>&1 | tee -a "$LOG_DIR/pod_install.log"
fi
- name: Pod env (on failure)
if: failure()
run: pod env || true
- 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 "$DESTINATION" \
CODE_SIGNING_ALLOWED=NO \
-resultBundlePath "$LOG_DIR/build.xcresult" \
build 2>&1 | tee "$LOG_DIR/build.log" | xcpretty
else
xcodebuild \
-project "${PROJECT}" \
-scheme "${SCHEME_RESOLVED}" \
-configuration "$CONFIG" \
-destination "$DESTINATION" \
CODE_SIGNING_ALLOWED=NO \
-resultBundlePath "$LOG_DIR/build.xcresult" \
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 "$DESTINATION" \
CODE_SIGNING_ALLOWED=NO \
-resultBundlePath "$LOG_DIR/build.xcresult" \
build 2>&1 | tee -a "$LOG_DIR/build.log" | xcpretty;
}
fi
continue-on-error: false
- name: Record build end time
if: always()
run: echo "BUILD_END=$(date +%s)" >> "$GITHUB_ENV"
- 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
echo "TEST_START=$(date +%s)" >> "$GITHUB_ENV"
xcodebuild \
-workspace "${WORKSPACE}" \
-scheme "${SCHEME_RESOLVED}" \
-configuration "$CONFIG" \
-destination "$DESTINATION" \
CODE_SIGNING_ALLOWED=NO \
-resultBundlePath "$LOG_DIR/test.xcresult" \
test 2>&1 | tee "$LOG_DIR/test.log" | xcpretty
echo "TEST_END=$(date +%s)" >> "$GITHUB_ENV"
else
echo "No shared scheme detected; skipping XCTest run."
fi
else
if xcodebuild -list -project "${PROJECT}" | grep -q "Schemes:"; then
echo "TEST_START=$(date +%s)" >> "$GITHUB_ENV"
xcodebuild \
-project "${PROJECT}" \
-scheme "${SCHEME_RESOLVED}" \
-configuration "$CONFIG" \
-destination "$DESTINATION" \
CODE_SIGNING_ALLOWED=NO \
-resultBundlePath "$LOG_DIR/test.xcresult" \
test 2>&1 | tee "$LOG_DIR/test.log" | xcpretty
echo "TEST_END=$(date +%s)" >> "$GITHUB_ENV"
else
echo "No shared scheme detected; skipping XCTest run."
fi
fi
else
echo "No XCTest targets detected; skipping test step."
fi
- name: Write CI Summary
if: always()
env:
JOB_STATUS: ${{ job.status }}
shell: bash
run: |
set -euo pipefail
mkdir -p "$LOG_DIR"
BUILD_START_TS=${BUILD_START:-}
BUILD_END_TS=${BUILD_END:-}
TEST_START_TS=${TEST_START:-}
TEST_END_TS=${TEST_END:-}
dur() { perl -e '($s,$e)=@ARGV; if($s && $e){ printf("%ds", $e-$s) } else { print "n/a" }' "$1" "$2" 2>/dev/null || echo n/a; }
BUILD_DUR=$(dur "$BUILD_START_TS" "$BUILD_END_TS")
TEST_DUR=$(dur "$TEST_START_TS" "$TEST_END_TS")
{
echo "CI Summary"
echo "=========="
echo "Status: $JOB_STATUS"
echo "Kind: ${KIND:-unknown}"
echo "Scheme: ${SCHEME_RESOLVED:-${SCHEME:-unset}}"
echo "Config: ${CONFIG}"
echo "Destination: ${DESTINATION}"
echo "Xcode: ${XCODE_VERSION}"
echo "Build duration: ${BUILD_DUR} (start: ${BUILD_START_TS:-n/a}, end: ${BUILD_END_TS:-n/a})"
echo "Test duration: ${TEST_DUR} (start: ${TEST_START_TS:-n/a}, end: ${TEST_END_TS:-n/a})"
echo
echo "Logs:"
echo "- build: ${LOG_DIR}/build.log"
echo "- test: ${LOG_DIR}/test.log (if any)"
echo "- xcresult: ${LOG_DIR}/build.xcresult, ${LOG_DIR}/test.xcresult (if any)"
} | tee "${LOG_DIR}/ci_summary.txt"
{
echo "## CI Summary"
echo "- Status: $JOB_STATUS"
echo "- Kind: ${KIND:-unknown}"
echo "- Scheme: ${SCHEME_RESOLVED:-${SCHEME:-unset}}"
echo "- Config: ${CONFIG}"
echo "- Destination: ${DESTINATION}"
echo "- Xcode: ${XCODE_VERSION}"
echo "- Build duration: ${BUILD_DUR}"
echo "- Test duration: ${TEST_DUR}"
} >> "$GITHUB_STEP_SUMMARY"
- name: Upload CI logs and result bundles
if: always()
uses: actions/upload-artifact@v4
with:
name: ci-logs-${{ github.run_number }}
path: |
${{ env.LOG_DIR }}/
${{ env.LOG_DIR }}/build.xcresult
${{ env.LOG_DIR }}/test.xcresult
if-no-files-found: warn
retention-days: 7
- 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: Compress logs (failure)
if: failure()
shell: bash
run: |
set -euo pipefail
for f in "$LOG_DIR"/*.log; do
[[ -f "$f" ]] || continue
gzip -9f "$f" || true
done
- name: Upload logs (compressed)
if: always()
uses: actions/upload-artifact@v4
with:
name: ci-logs
retention-days: 30
path: |
ci-logs/**
- 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