diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1c12e4f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,323 @@ +name: CI + +on: + pull_request: + branches: [main] + types: [opened, synchronize, reopened] + push: + branches: [main, develop] + workflow_dispatch: + +concurrency: + group: ci-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +permissions: + contents: read + pull-requests: write + issues: write + +env: + DEVELOPER_DIR: /Applications/Xcode.app/Contents/Developer + +jobs: + # ============================================ + # Quick validation checks (run first) + # ============================================ + + lint: + name: SwiftLint + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: SwiftLint + id: swiftlint + uses: norio-nomura/action-swiftlint@3.2.1 + with: + args: --strict + continue-on-error: true + + - name: Comment PR with SwiftLint results + uses: actions/github-script@v7 + if: github.event_name == 'pull_request' + with: + script: | + const output = '${{ steps.swiftlint.outputs.stdout }}'; + if (output) { + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: '## SwiftLint Results\n\n```\n' + output + '\n```' + }); + } + + swiftformat: + name: SwiftFormat Check + runs-on: macos-26 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install SwiftFormat + run: brew install swiftformat + + - name: Check code formatting + run: | + swiftformat --version + swiftformat . --lint --verbose + continue-on-error: true + + - name: Generate format diff + if: failure() + run: | + swiftformat . --dryrun > format-diff.txt + cat format-diff.txt + + - name: Upload format diff + if: failure() + uses: actions/upload-artifact@v4 + with: + name: format-diff + path: format-diff.txt + + pr-size: + name: PR Size Check + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Check PR size + uses: actions/github-script@v7 + with: + script: | + const pr = context.payload.pull_request; + const additions = pr.additions; + const deletions = pr.deletions; + const total = additions + deletions; + + let label = ''; + if (total < 10) label = 'size/XS'; + else if (total < 50) label = 'size/S'; + else if (total < 200) label = 'size/M'; + else if (total < 500) label = 'size/L'; + else if (total < 1000) label = 'size/XL'; + else label = 'size/XXL'; + + // Remove all size labels + const labels = await github.rest.issues.listLabelsOnIssue({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number + }); + + for (const label of labels.data) { + if (label.name.startsWith('size/')) { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + name: label.name + }); + } + } + + // Add new size label + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + labels: [label] + }); + + check-commits: + name: Check Commit Messages + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Check commits + uses: actions/github-script@v7 + with: + script: | + const commits = await github.rest.pulls.listCommits({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number + }); + + const conventionalCommitRegex = /^(feat|fix|docs|style|refactor|test|chore|perf|ci|build|revert)(\(.+\))?: .+/; + const invalidCommits = []; + + for (const commit of commits.data) { + const message = commit.commit.message.split('\n')[0]; + if (!conventionalCommitRegex.test(message)) { + invalidCommits.push(`- ${commit.sha.substring(0, 7)}: ${message}`); + } + } + + if (invalidCommits.length > 0) { + core.warning(`Found ${invalidCommits.length} commits without conventional format:\n${invalidCommits.join('\n')}`); + } + + # ============================================ + # Build and Test + # ============================================ + + build-and-test: + name: Build and Test + runs-on: macos-26 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Select Xcode version + run: sudo xcode-select -s /Applications/Xcode_26.0.1.app/Contents/Developer + + - name: Show Xcode version + run: xcodebuild -version + + - name: Install xcpretty + run: gem install xcpretty + + - name: Cache SPM packages + uses: actions/cache@v4 + with: + path: ~/Library/Developer/Xcode/DerivedData/**/SourcePackages + key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }} + restore-keys: | + ${{ runner.os }}-spm- + + - name: Resolve Swift packages + run: | + xcodebuild -resolvePackageDependencies \ + -project V2er.xcodeproj \ + -scheme V2er + + - name: Build for testing + run: | + set -o pipefail && xcodebuild build-for-testing \ + -project V2er.xcodeproj \ + -scheme V2er \ + -sdk iphonesimulator \ + -destination 'platform=iOS Simulator,name=iPhone 17' \ + ONLY_ACTIVE_ARCH=YES \ + CODE_SIGN_IDENTITY="" \ + CODE_SIGNING_REQUIRED=NO | xcpretty --color + + - name: Run tests + run: | + set -o pipefail && xcodebuild test-without-building \ + -project V2er.xcodeproj \ + -scheme V2er \ + -sdk iphonesimulator \ + -destination 'platform=iOS Simulator,name=iPhone 17' \ + ONLY_ACTIVE_ARCH=YES \ + CODE_SIGN_IDENTITY="" \ + CODE_SIGNING_REQUIRED=NO | xcpretty --color --test + + - name: Upload test results + uses: actions/upload-artifact@v4 + if: failure() + with: + name: test-results + path: | + ~/Library/Logs/DiagnosticReports/ + ~/Library/Developer/Xcode/DerivedData/**/Logs/Test/ + + # ============================================ + # Code Coverage + # ============================================ + + code-coverage: + name: Code Coverage + runs-on: macos-26 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Select Xcode version + run: sudo xcode-select -s /Applications/Xcode_26.0.1.app/Contents/Developer + + - name: Install xcpretty + run: gem install xcpretty + + - name: Build and test with coverage + run: | + xcodebuild test \ + -project V2er.xcodeproj \ + -scheme V2er \ + -sdk iphonesimulator \ + -destination 'platform=iOS Simulator,name=iPhone 17' \ + -enableCodeCoverage YES \ + -derivedDataPath build/DerivedData \ + CODE_SIGN_IDENTITY="" \ + CODE_SIGNING_REQUIRED=NO | xcpretty + + - name: Generate coverage report + run: | + cd build/DerivedData + # Find the xcresult bundle + RESULT_BUNDLE=$(find . -name '*.xcresult' -type d | head -n 1) + + if [ -z "$RESULT_BUNDLE" ]; then + echo "No test results found, setting coverage to 0%" + echo "coverage=0.00" >> $GITHUB_ENV + else + xcrun xccov view --report --json "$RESULT_BUNDLE" > coverage.json || echo '{}' > coverage.json + + # Extract coverage percentage with fallback + COVERAGE=$(cat coverage.json | jq -r '.lineCoverage // 0' | awk '{printf "%.2f", $1 * 100}') + echo "Code coverage: ${COVERAGE}%" + echo "coverage=${COVERAGE}" >> $GITHUB_ENV + fi + + - name: Create coverage badge + if: env.GIST_SECRET != '' + uses: schneegans/dynamic-badges-action@v1.6.0 + with: + auth: ${{ secrets.GIST_SECRET }} + gistID: ${{ secrets.GIST_ID }} + filename: v2er-ios-coverage.json + label: Coverage + message: ${{ env.coverage }}% + color: ${{ env.coverage > 80 && 'success' || env.coverage > 60 && 'yellow' || 'critical' }} + env: + GIST_SECRET: ${{ secrets.GIST_SECRET }} + + - name: Comment PR with coverage + if: github.event_name == 'pull_request' + uses: actions/github-script@v7 + with: + script: | + const coverage = parseFloat('${{ env.coverage }}'); + const emoji = coverage > 80 ? '✅' : coverage > 60 ? '⚠️' : '❌'; + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: `## Code Coverage Report ${emoji}\n\nCurrent coverage: **${coverage}%**` + }); diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml deleted file mode 100644 index 044dd0e..0000000 --- a/.github/workflows/code-quality.yml +++ /dev/null @@ -1,112 +0,0 @@ -name: Code Quality - -on: - push: - branches: [ main, develop ] - pull_request: - branches: [ main ] - -jobs: - swiftformat: - name: SwiftFormat Check - runs-on: macos-26 - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Install SwiftFormat - run: brew install swiftformat - - - name: Check code formatting - run: | - swiftformat --version - swiftformat . --lint --verbose - continue-on-error: true - - - name: Generate format diff - if: failure() - run: | - swiftformat . --dryrun > format-diff.txt - cat format-diff.txt - - - name: Upload format diff - if: failure() - uses: actions/upload-artifact@v4 - with: - name: format-diff - path: format-diff.txt - - code-coverage: - name: Code Coverage - runs-on: macos-26 - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - submodules: recursive - - - name: Select Xcode version - run: sudo xcode-select -s /Applications/Xcode_26.0.1.app/Contents/Developer - - - name: Install xcpretty - run: gem install xcpretty - - - name: Build and test with coverage - run: | - xcodebuild test \ - -project V2er.xcodeproj \ - -scheme V2er \ - -sdk iphonesimulator \ - -destination 'platform=iOS Simulator,name=iPhone 17' \ - -enableCodeCoverage YES \ - -derivedDataPath build/DerivedData \ - CODE_SIGN_IDENTITY="" \ - CODE_SIGNING_REQUIRED=NO | xcpretty - - - name: Generate coverage report - run: | - cd build/DerivedData - # Find the xcresult bundle - RESULT_BUNDLE=$(find . -name '*.xcresult' -type d | head -n 1) - - if [ -z "$RESULT_BUNDLE" ]; then - echo "No test results found, setting coverage to 0%" - echo "coverage=0.00" >> $GITHUB_ENV - else - xcrun xccov view --report --json "$RESULT_BUNDLE" > coverage.json || echo '{}' > coverage.json - - # Extract coverage percentage with fallback - COVERAGE=$(cat coverage.json | jq -r '.lineCoverage // 0' | awk '{printf "%.2f", $1 * 100}') - echo "Code coverage: ${COVERAGE}%" - echo "coverage=${COVERAGE}" >> $GITHUB_ENV - fi - - - name: Create coverage badge - if: env.GIST_SECRET != '' - uses: schneegans/dynamic-badges-action@v1.6.0 - with: - auth: ${{ secrets.GIST_SECRET }} - gistID: ${{ secrets.GIST_ID }} - filename: v2er-ios-coverage.json - label: Coverage - message: ${{ env.coverage }}% - color: ${{ env.coverage > 80 && 'success' || env.coverage > 60 && 'yellow' || 'critical' }} - env: - GIST_SECRET: ${{ secrets.GIST_SECRET }} - - - name: Comment PR with coverage - if: github.event_name == 'pull_request' - uses: actions/github-script@v7 - with: - script: | - const coverage = parseFloat('${{ env.coverage }}'); - const emoji = coverage > 80 ? '✅' : coverage > 60 ? '⚠️' : '❌'; - - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: `## Code Coverage Report ${emoji}\n\nCurrent coverage: **${coverage}%**` - }); \ No newline at end of file diff --git a/.github/workflows/ios-build-test.yml b/.github/workflows/ios-build-test.yml deleted file mode 100644 index ff5ec3b..0000000 --- a/.github/workflows/ios-build-test.yml +++ /dev/null @@ -1,76 +0,0 @@ -name: iOS Build and Test - -on: - push: - branches: [ main, develop ] - pull_request: - branches: [ main ] - workflow_dispatch: - -env: - DEVELOPER_DIR: /Applications/Xcode.app/Contents/Developer - -jobs: - build-and-test: - name: Build and Test - runs-on: macos-26 - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - submodules: recursive - - - name: Select Xcode version - run: sudo xcode-select -s /Applications/Xcode_26.0.1.app/Contents/Developer - - - name: Show Xcode version - run: xcodebuild -version - - - name: Install xcpretty - run: gem install xcpretty - - - name: Cache SPM packages - uses: actions/cache@v4 - with: - path: ~/Library/Developer/Xcode/DerivedData/**/SourcePackages - key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }} - restore-keys: | - ${{ runner.os }}-spm- - - - name: Resolve Swift packages - run: | - xcodebuild -resolvePackageDependencies \ - -project V2er.xcodeproj \ - -scheme V2er - - - name: Build for testing - run: | - set -o pipefail && xcodebuild build-for-testing \ - -project V2er.xcodeproj \ - -scheme V2er \ - -sdk iphonesimulator \ - -destination 'platform=iOS Simulator,name=iPhone 17' \ - ONLY_ACTIVE_ARCH=YES \ - CODE_SIGN_IDENTITY="" \ - CODE_SIGNING_REQUIRED=NO | xcpretty --color - - - name: Run tests - run: | - set -o pipefail && xcodebuild test-without-building \ - -project V2er.xcodeproj \ - -scheme V2er \ - -sdk iphonesimulator \ - -destination 'platform=iOS Simulator,name=iPhone 17' \ - ONLY_ACTIVE_ARCH=YES \ - CODE_SIGN_IDENTITY="" \ - CODE_SIGNING_REQUIRED=NO | xcpretty --color --test - - - name: Upload test results - uses: actions/upload-artifact@v4 - if: failure() - with: - name: test-results - path: | - ~/Library/Logs/DiagnosticReports/ - ~/Library/Developer/Xcode/DerivedData/**/Logs/Test/ \ No newline at end of file diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml deleted file mode 100644 index 3e8523c..0000000 --- a/.github/workflows/pr-validation.yml +++ /dev/null @@ -1,124 +0,0 @@ -name: PR Validation - -on: - pull_request: - types: [opened, synchronize, reopened] - -jobs: - lint: - name: SwiftLint - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: SwiftLint - uses: norio-nomura/action-swiftlint@3.2.1 - with: - args: --strict - continue-on-error: true - - - name: Comment PR with SwiftLint results - uses: actions/github-script@v7 - if: github.event_name == 'pull_request' - with: - script: | - const output = '${{ steps.swiftlint.outputs.stdout }}'; - if (output) { - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: '## SwiftLint Results\n\n```\n' + output + '\n```' - }); - } - - pr-size: - name: PR Size Check - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Check PR size - uses: actions/github-script@v7 - with: - script: | - const pr = context.payload.pull_request; - const additions = pr.additions; - const deletions = pr.deletions; - const total = additions + deletions; - - let label = ''; - if (total < 10) label = 'size/XS'; - else if (total < 50) label = 'size/S'; - else if (total < 200) label = 'size/M'; - else if (total < 500) label = 'size/L'; - else if (total < 1000) label = 'size/XL'; - else label = 'size/XXL'; - - // Remove all size labels - const labels = await github.rest.issues.listLabelsOnIssue({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: pr.number - }); - - for (const label of labels.data) { - if (label.name.startsWith('size/')) { - await github.rest.issues.removeLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: pr.number, - name: label.name - }); - } - } - - // Add new size label - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: pr.number, - labels: [label] - }); - - check-commits: - name: Check Commit Messages - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Check commits - uses: actions/github-script@v7 - with: - script: | - const commits = await github.rest.pulls.listCommits({ - owner: context.repo.owner, - repo: context.repo.repo, - pull_number: context.issue.number - }); - - const conventionalCommitRegex = /^(feat|fix|docs|style|refactor|test|chore|perf|ci|build|revert)(\(.+\))?: .+/; - const invalidCommits = []; - - for (const commit of commits.data) { - const message = commit.commit.message.split('\n')[0]; - if (!conventionalCommitRegex.test(message)) { - invalidCommits.push(`- ${commit.sha.substring(0, 7)}: ${message}`); - } - } - - if (invalidCommits.length > 0) { - core.warning(`Found ${invalidCommits.length} commits without conventional format:\n${invalidCommits.join('\n')}`); - } \ No newline at end of file