From 4a0da08b2244c5df958769e1e9b2e770e068874c Mon Sep 17 00:00:00 2001 From: Rawand Ahmed Shaswar Date: Mon, 4 Aug 2025 18:04:08 +0300 Subject: [PATCH] Add CI workflows --- .github/workflows/{ci.yml => linter.yml} | 2 +- .github/workflows/release.yml | 356 ++++++++++++++++++ .github/workflows/test.yml | 67 ---- .../DependencyContainer.swift | 2 - .../Home/ViewModel/RecapViewModel.swift | 2 - 5 files changed, 357 insertions(+), 72 deletions(-) rename .github/workflows/{ci.yml => linter.yml} (99%) create mode 100644 .github/workflows/release.yml delete mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/linter.yml similarity index 99% rename from .github/workflows/ci.yml rename to .github/workflows/linter.yml index 76db2cc..d7691f4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/linter.yml @@ -1,4 +1,4 @@ -name: CI +name: Run SwiftLint on: push: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..5d4792a --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,356 @@ +name: Release Build + +on: + push: + branches: [ main ] + workflow_dispatch: + inputs: + release_type: + description: 'Release type (patch, minor, major)' + required: false + default: 'patch' + type: choice + options: + - patch + - minor + - major + +permissions: + contents: write + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-and-release: + name: Build, Sign, Release + runs-on: macos-15 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Need full history for release notes + + - name: Setup Xcode + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: latest-stable + + - name: Get current version + id: get_version + run: | + VERSION=$(xcodebuild -project Recap.xcodeproj -target Recap -showBuildSettings 2>/dev/null | grep "MARKETING_VERSION" | head -1 | sed 's/.*= //' | tr -d ' ') + BUILD=$(xcodebuild -project Recap.xcodeproj -target Recap -showBuildSettings 2>/dev/null | grep "CURRENT_PROJECT_VERSION" | head -1 | sed 's/.*= //' | tr -d ' ') + + VERSION=${VERSION:-"1.0"} + BUILD=${BUILD:-"1"} + + if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + if [[ "$VERSION" =~ ^[0-9]+\.[0-9]+$ ]]; then + VERSION="${VERSION}.0" + else + VERSION="1.0.0" + fi + fi + + echo "current_version=$VERSION" >> $GITHUB_OUTPUT + echo "current_build=$BUILD" >> $GITHUB_OUTPUT + echo "Current version: $VERSION ($BUILD)" + + - name: Calculate next version + id: next_version + run: | + CURRENT="${{ steps.get_version.outputs.current_version }}" + TYPE="${{ github.event.inputs.release_type || 'patch' }}" + + IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT" + + case $TYPE in + major) + MAJOR=$((MAJOR + 1)) + MINOR=0 + PATCH=0 + ;; + minor) + MINOR=$((MINOR + 1)) + PATCH=0 + ;; + patch) + PATCH=$((PATCH + 1)) + ;; + esac + + NEW_VERSION="$MAJOR.$MINOR.$PATCH" + NEW_BUILD="${{ github.run_number }}" + + echo "version=$NEW_VERSION" >> $GITHUB_OUTPUT + echo "build=$NEW_BUILD" >> $GITHUB_OUTPUT + echo "Next version: $NEW_VERSION ($NEW_BUILD)" + + - name: Update version in project + run: | + sed -i '' "s/MARKETING_VERSION = [^;]*/MARKETING_VERSION = ${{ steps.next_version.outputs.version }}/g" Recap.xcodeproj/project.pbxproj + sed -i '' "s/CURRENT_PROJECT_VERSION = [^;]*/CURRENT_PROJECT_VERSION = ${{ steps.next_version.outputs.build }}/g" Recap.xcodeproj/project.pbxproj + + echo "Updated project version to ${{ steps.next_version.outputs.version }} (${{ steps.next_version.outputs.build }})" + + - name: Install Apple certificates + env: + BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }} + KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} + P12_PASSWORD: ${{ secrets.P12_PASSWORD }} + run: | + CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12 + KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db + + echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH + + security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + security set-keychain-settings -lut 21600 $KEYCHAIN_PATH + security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + + security import $CERTIFICATE_PATH -k $KEYCHAIN_PATH -P "$P12_PASSWORD" -T /usr/bin/codesign -T /usr/bin/security + + security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + + security list-keychain -d user -s $KEYCHAIN_PATH login.keychain + + echo "Certificates in new keychain:" + security find-identity -v -p codesigning $KEYCHAIN_PATH + + echo "✓ Certificates installed" + + - name: Cache Swift Package Manager + uses: actions/cache@v4 + with: + path: | + ~/Library/Developer/Xcode/DerivedData/*/SourcePackages + ~/Library/Caches/org.swift.swiftpm + key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }} + restore-keys: | + ${{ runner.os }}-spm- + + - name: Resolve Dependencies + run: | + xcodebuild -resolvePackageDependencies \ + -project Recap.xcodeproj \ + -scheme Recap \ + -configuration Release + + - name: Build and Archive + env: + DEVELOPMENT_TEAM: ${{ secrets.DEVELOPMENT_TEAM }} + CODE_SIGN_IDENTITY: ${{ secrets.CODE_SIGN_IDENTITY }} + run: | + xcodebuild archive \ + -project Recap.xcodeproj \ + -scheme Recap \ + -configuration Release \ + -archivePath $RUNNER_TEMP/Recap.xcarchive \ + -destination 'generic/platform=macOS' \ + -derivedDataPath $RUNNER_TEMP/DerivedData \ + -skipMacroValidation \ + -allowProvisioningUpdates \ + ONLY_ACTIVE_ARCH=YES \ + ARCHS=arm64 \ + VALID_ARCHS=arm64 \ + EXCLUDED_ARCHS=x86_64 \ + DEVELOPMENT_TEAM="$DEVELOPMENT_TEAM" \ + CODE_SIGN_STYLE="Manual" \ + CODE_SIGN_IDENTITY="$CODE_SIGN_IDENTITY" \ + PROVISIONING_PROFILE_SPECIFIER="" \ + CODE_SIGNING_REQUIRED=YES \ + CODE_SIGNING_ALLOWED=YES \ + SWIFT_VERSION=5.0 \ + SWIFT_TREAT_WARNINGS_AS_ERRORS=NO \ + GCC_TREAT_WARNINGS_AS_ERRORS=NO + + - name: Export Archive + env: + DEVELOPMENT_TEAM: ${{ secrets.DEVELOPMENT_TEAM }} + run: | + if [ ! -d "$RUNNER_TEMP/Recap.xcarchive" ]; then + echo "Archive not found at $RUNNER_TEMP/Recap.xcarchive" + exit 1 + fi + + echo "Available signing certificates:" + security find-identity -v -p codesigning + + cat > $RUNNER_TEMP/ExportOptions.plist < + + + + method + developer-id + teamID + $DEVELOPMENT_TEAM + signingStyle + automatic + uploadBitcode + + uploadSymbols + + + + EOF + + xcodebuild -exportArchive \ + -archivePath $RUNNER_TEMP/Recap.xcarchive \ + -exportPath $RUNNER_TEMP/export \ + -exportOptionsPlist $RUNNER_TEMP/ExportOptions.plist \ + -allowProvisioningUpdates + + - name: Notarize app + env: + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} + TEAM_ID: ${{ secrets.DEVELOPMENT_TEAM }} + run: | + if [ ! -d "$RUNNER_TEMP/export" ]; then + echo "Export directory not found at $RUNNER_TEMP/export" + echo "Contents of RUNNER_TEMP:" + ls -la $RUNNER_TEMP + exit 1 + fi + + xcrun notarytool store-credentials "notarytool-profile" \ + --apple-id "$APPLE_ID" \ + --password "$APPLE_PASSWORD" \ + --team-id "$TEAM_ID" + + cd $RUNNER_TEMP/export + + if [ ! -d "Recap.app" ]; then + echo "Recap.app not found in export directory" + echo "Contents of export directory:" + ls -la + exit 1 + fi + + ditto -c -k --keepParent "Recap.app" "Recap.zip" + + xcrun notarytool submit "Recap.zip" \ + --keychain-profile "notarytool-profile" \ + --wait \ + --timeout 30m + + xcrun stapler staple "Recap.app" + + - name: Create DMG + run: | + brew install create-dmg + + cd $RUNNER_TEMP + + ICON_PATH="" + if [ -f "$GITHUB_WORKSPACE/Recap/Assets.xcassets/AppIcon.appiconset/mac512.png" ]; then + ICON_PATH="$GITHUB_WORKSPACE/Recap/Assets.xcassets/AppIcon.appiconset/mac512.png" + elif [ -f "$GITHUB_WORKSPACE/Recap/Recap/Assets.xcassets/AppIcon.appiconset/mac512.png" ]; then + ICON_PATH="$GITHUB_WORKSPACE/Recap/Recap/Assets.xcassets/AppIcon.appiconset/mac512.png" + else + echo "Warning: mac512.png not found, using app's icon" + ICON_PATH="" + fi + + mkdir dmg_source + cp -R export/Recap.app dmg_source/ + + if [ -n "$ICON_PATH" ]; then + create-dmg \ + --volname "Recap" \ + --volicon "$ICON_PATH" \ + --window-pos 200 120 \ + --window-size 600 400 \ + --icon-size 100 \ + --icon "Recap.app" 150 180 \ + --hide-extension "Recap.app" \ + --app-drop-link 450 180 \ + --no-internet-enable \ + "Recap-${{ steps.next_version.outputs.version }}.dmg" \ + "dmg_source/" + else + create-dmg \ + --volname "Recap" \ + --window-pos 200 120 \ + --window-size 600 400 \ + --icon-size 100 \ + --icon "Recap.app" 150 180 \ + --hide-extension "Recap.app" \ + --app-drop-link 450 180 \ + --no-internet-enable \ + "Recap-${{ steps.next_version.outputs.version }}.dmg" \ + "dmg_source/" + fi + + xcrun notarytool submit "Recap-${{ steps.next_version.outputs.version }}.dmg" \ + --keychain-profile "notarytool-profile" \ + --wait \ + --timeout 30m + + xcrun stapler staple "Recap-${{ steps.next_version.outputs.version }}.dmg" + + - name: Generate Release Notes + id: release_notes + run: | + LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || git rev-list --max-parents=0 HEAD) + + cat > $RUNNER_TEMP/release_notes.md <> $RUNNER_TEMP/release_notes.md + + cat >> $RUNNER_TEMP/release_notes.md <> release_notes.md + echo "SHA256 (DMG): $(shasum -a 256 Recap-${{ steps.next_version.outputs.version }}.dmg | cut -d' ' -f1)" >> release_notes.md + echo '```' >> release_notes.md + + echo "RELEASE_NOTES<> $GITHUB_OUTPUT + cat release_notes.md >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + - name: Create GitHub Release + uses: softprops/action-gh-release@v1 + with: + tag_name: v${{ steps.next_version.outputs.version }} + name: Recap v${{ steps.next_version.outputs.version }} + body: ${{ steps.release_notes.outputs.RELEASE_NOTES }} + draft: false + prerelease: false + files: | + ${{ runner.temp }}/Recap-${{ steps.next_version.outputs.version }}.dmg + + - name: Upload Build Artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: build-artifacts + path: | + ${{ runner.temp }}/Recap.xcarchive + ${{ runner.temp }}/export/ + ${{ runner.temp }}/*.dmg + + - name: Clean up keychain + if: always() + run: | + security delete-keychain $RUNNER_TEMP/app-signing.keychain-db \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index 473bc41..0000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,67 +0,0 @@ -name: Test Suite - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - workflow_dispatch: - -jobs: - test: - name: Run Tests - runs-on: macos-15 - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Xcode - uses: maxim-lobanov/setup-xcode@v1 - with: - xcode-version: latest-stable - - - name: Cache DerivedData - uses: actions/cache@v4 - with: - path: ~/Library/Developer/Xcode/DerivedData - key: ${{ runner.os }}-deriveddata-${{ hashFiles('Recap.xcodeproj/project.pbxproj') }} - restore-keys: | - ${{ runner.os }}-deriveddata- - - - name: Resolve Package Dependencies - run: | - xcodebuild -resolvePackageDependencies \ - -project Recap.xcodeproj \ - -scheme Recap - - - name: Build and Test - run: | - xcodebuild test \ - -project Recap.xcodeproj \ - -scheme Recap \ - -destination 'platform=macOS' \ - -resultBundlePath TestResults.xcresult \ - -enableCodeCoverage YES \ - -only-testing:RecapTests \ - -skipMacroValidation \ - CODE_SIGNING_ALLOWED=NO - - - name: Upload Test Results - uses: actions/upload-artifact@v4 - if: always() - with: - name: test-results - path: TestResults.xcresult - - - name: Generate Coverage Report - run: | - xcrun xccov view --report --json TestResults.xcresult > coverage.json - - - name: Upload Coverage - uses: codecov/codecov-action@v5 - with: - file: coverage.json - flags: unittests - name: recap-coverage - fail_ci_if_error: false \ No newline at end of file diff --git a/Recap/Services/DependencyContainer/DependencyContainer.swift b/Recap/Services/DependencyContainer/DependencyContainer.swift index 68cb91f..8fb841a 100644 --- a/Recap/Services/DependencyContainer/DependencyContainer.swift +++ b/Recap/Services/DependencyContainer/DependencyContainer.swift @@ -86,7 +86,6 @@ extension DependencyContainer { } } -#if DEBUG extension DependencyContainer { static func createForPreview() -> DependencyContainer { DependencyContainer(inMemory: true) @@ -96,4 +95,3 @@ extension DependencyContainer { DependencyContainer(inMemory: inMemory) } } -#endif diff --git a/Recap/UseCases/Home/ViewModel/RecapViewModel.swift b/Recap/UseCases/Home/ViewModel/RecapViewModel.swift index e386f90..bafd460 100644 --- a/Recap/UseCases/Home/ViewModel/RecapViewModel.swift +++ b/Recap/UseCases/Home/ViewModel/RecapViewModel.swift @@ -198,11 +198,9 @@ extension RecapViewModel { } } -#if DEBUG extension RecapViewModel { static func createForPreview() -> RecapViewModel { let container = DependencyContainer.createForPreview() return container.createRecapViewModel() } } -#endif