Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
227 changes: 227 additions & 0 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
name: iOS Build & Test

on:
workflow_dispatch:
push:
branches:
- main
- 'devin/*'
pull_request:
branches:
- main

jobs:
swiftlint:
name: SwiftLint
runs-on: ubuntu-latest
timeout-minutes: 5

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Install SwiftLint
run: |
SWIFTLINT_VERSION="0.55.1"
curl -sL "https://github.com/realm/SwiftLint/releases/download/${SWIFTLINT_VERSION}/swiftlint_linux.zip" -o swiftlint.zip
unzip -o swiftlint.zip
chmod +x swiftlint
sudo mv swiftlint /usr/local/bin/
swiftlint version

- name: Run SwiftLint
run: swiftlint lint --reporter github-actions-logging

build:
name: Build & Screenshot
runs-on: macos-15
timeout-minutes: 30

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Select latest Xcode
run: |
LATEST_XCODE=$(ls -d /Applications/Xcode*.app 2>/dev/null | sort -V | tail -1)
if [ -z "$LATEST_XCODE" ]; then
echo "No Xcode found!"
exit 1
fi
echo "Using Xcode: $LATEST_XCODE"
sudo xcode-select -s "$LATEST_XCODE"
xcodebuild -version
xcrun simctl list runtimes

- name: Create Xcode project wrapper
run: |
# Since this is a Swift Package, we create a temporary Xcode project
# to enable building and testing on iOS simulator
cat > project.yml << 'EOF'
name: PocketApp
options:
bundleIdPrefix: com.pocket
deploymentTarget:
iOS: "17.0"
targets:
PocketApp:
type: application
platform: iOS
sources:
- path: Pocket/Sources
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: com.pocket.app
INFOPLIST_FILE: ""
GENERATE_INFOPLIST_FILE: YES
CODE_SIGNING_ALLOWED: NO
CODE_SIGN_IDENTITY: "-"
SWIFT_VERSION: "5.9"
EOF

# Install XcodeGen if available, otherwise use xcodebuild directly
brew install xcodegen 2>/dev/null || true

if command -v xcodegen &> /dev/null; then
xcodegen generate
echo "Xcode project generated successfully"
else
echo "XcodeGen not available, will use swift build"
fi

- name: Find available simulator
id: simulator
run: |
RESULT=$(xcrun simctl list devices available -j | python3 -c "
import json, sys, re
data = json.load(sys.stdin)
preferred = ['iPhone 16 Pro', 'iPhone 15 Pro', 'iPhone 16', 'iPhone 15', 'iPhone 14 Pro', 'iPhone 14']
ios_runtimes = []
for runtime in data.get('devices', {}):
if 'iOS' in runtime or 'ios' in runtime.lower():
m = re.search(r'(\d+)[-.](\d+)', runtime.split('iOS')[-1])
if m:
ver = (int(m.group(1)), int(m.group(2)))
ios_runtimes.append((ver, runtime))
ios_runtimes.sort(reverse=True)
for ver, runtime in ios_runtimes:
devices = data['devices'][runtime]
for pref in preferred:
for d in devices:
if d['name'] == pref and d.get('isAvailable', False):
print(f\"{d['udid']}|{pref}|{runtime}\")
sys.exit(0)
for ver, runtime in ios_runtimes:
for d in data['devices'][runtime]:
if 'iPhone' in d['name'] and d.get('isAvailable', False):
print(f\"{d['udid']}|{d['name']}|{runtime}\")
sys.exit(0)
" 2>/dev/null || true)

DEVICE_ID=$(echo "$RESULT" | cut -d'|' -f1)
DEVICE_NAME=$(echo "$RESULT" | cut -d'|' -f2)

if [ -n "$DEVICE_ID" ]; then
echo "Found device: $DEVICE_NAME ($DEVICE_ID)"
echo "device_id=$DEVICE_ID" >> $GITHUB_OUTPUT
echo "device_name=$DEVICE_NAME" >> $GITHUB_OUTPUT
else
echo "No suitable iPhone simulator found"
exit 1
fi

- name: Build for simulator
run: |
set -o pipefail

if [ -f "PocketApp.xcodeproj/project.pbxproj" ]; then
echo "Building with Xcode project..."
xcodebuild build \
-project PocketApp.xcodeproj \
-scheme PocketApp \
-destination "id=${{ steps.simulator.outputs.device_id }}" \
-derivedDataPath DerivedData \
-allowProvisioningUpdates \
CODE_SIGNING_ALLOWED=NO \
CODE_SIGN_IDENTITY="-" \
2>&1 | tee build_log.txt
else
echo "Building with xcodebuild (workspace)..."
# Try swift build for syntax check
swift build 2>&1 || echo "Swift build (Linux-style) not applicable for iOS target"

# Use xcodebuild with package
xcodebuild build \
-scheme Pocket \
-destination "id=${{ steps.simulator.outputs.device_id }}" \
-derivedDataPath DerivedData \
CODE_SIGNING_ALLOWED=NO \
CODE_SIGN_IDENTITY="-" \
2>&1 | tee build_log.txt || true
fi

echo "BUILD COMPLETED"

- name: Boot simulator and capture screenshots
if: success()
run: |
DEVICE_ID="${{ steps.simulator.outputs.device_id }}"
mkdir -p screenshots-ci

# Boot simulator
xcrun simctl boot "$DEVICE_ID" || true
xcrun simctl bootstatus "$DEVICE_ID" -b

# Set dark appearance (app uses dark theme)
xcrun simctl ui "$DEVICE_ID" appearance dark 2>/dev/null || true
sleep 2

# Find and install the app
APP_PATH=$(find DerivedData -name "*.app" -type d -not -path "*/SourcePackages/*" | grep -v "Intermediates" | head -1)
if [ -n "$APP_PATH" ]; then
echo "Installing app: $APP_PATH"
xcrun simctl install "$DEVICE_ID" "$APP_PATH"

# Get bundle ID
BUNDLE_ID=$(defaults read "$APP_PATH/Info.plist" CFBundleIdentifier 2>/dev/null || echo "com.pocket.app")
echo "Bundle ID: $BUNDLE_ID"

# Launch app
xcrun simctl launch "$DEVICE_ID" "$BUNDLE_ID" || true
sleep 5

# Take screenshots
echo "Taking screenshot: Welcome screen"
xcrun simctl io "$DEVICE_ID" screenshot screenshots-ci/01_welcome_screen.png
sleep 3

# Second screenshot after carousel auto-scroll
echo "Taking screenshot: After carousel scroll"
xcrun simctl io "$DEVICE_ID" screenshot screenshots-ci/02_carousel_card.png
sleep 4

# Third screenshot after another auto-scroll
echo "Taking screenshot: Stock chart card"
xcrun simctl io "$DEVICE_ID" screenshot screenshots-ci/03_stock_chart_card.png
else
echo "No app bundle found - skipping screenshots"
fi

echo "=== Screenshots captured ==="
ls -la screenshots-ci/ 2>/dev/null || echo "No screenshots"

- name: Upload screenshots
uses: actions/upload-artifact@v4
if: always()
with:
name: app-screenshots
path: screenshots-ci/
retention-days: 30

- name: Upload build log
uses: actions/upload-artifact@v4
if: always()
with:
name: build-logs
path: build_log.txt
retention-days: 7
92 changes: 92 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Xcode
#
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore

## User settings
xcuserdata/

## compatibility with Xcode 8 and earlier (Xcode 9 introduced Run Destination for profiles)
*.xcscheme

## Xcode 8+ workspace settings
*.xcworkspacedata

## Build generated
build/
DerivedData/

## Various settings
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
!default.xcworkspace

## Other
*.moved-aside
*.xcuserstate
*.xcscmblueprint

## Obj-C/Swift specific
*.hmap

## App packaging
*.ipa
*.dSYM.zip
*.dSYM

# Swift Package Manager
.build/
.swiftpm/
Package.resolved
Packages/

# CocoaPods
Pods/

# Carthage
Carthage/Build/
Carthage/Checkouts/

# fastlane
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots/**/*.png
fastlane/test_output

# Code Injection
iOSInjectionProject/

# macOS
.DS_Store
.AppleDouble
.LSOverride
Icon
._*
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent

# VS Code
.vscode/

# SwiftLint
.swiftlint.yml.bak

# Environment
.env
*.xcconfig
!*.xcconfig.template

# Test artifacts
*.xcresult
test-results/
screenshots-ci/
66 changes: 66 additions & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# SwiftLint Configuration for Pocket App

included:
- Pocket/Sources

excluded:
- .build
- DerivedData
- Pods
- Carthage

# Rule Configuration
disabled_rules:
- trailing_whitespace
- line_length

opt_in_rules:
- empty_count
- closure_spacing
- contains_over_filter_count
- contains_over_first_not_nil
- empty_string
- first_where
- force_unwrapping
- implicit_return
- last_where
- modifier_order
- overridden_super_call
- private_action
- private_outlet
- redundant_nil_coalescing
- sorted_first_last

# Configurable Rules
type_body_length:
warning: 300
error: 500

file_length:
warning: 500
error: 1000

function_body_length:
warning: 60
error: 100

identifier_name:
min_length:
warning: 2
error: 1
max_length:
warning: 60
error: 80
excluded:
- id
- x
- y
- i

nesting:
type_level:
warning: 3
function_level:
warning: 5

reporter: "xcode"
Loading
Loading