Skip to content
Draft
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
File renamed without changes.
52 changes: 52 additions & 0 deletions .github/actions/build-xcframework/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: Build XCFramework
description: Build XCFramework for iOS and iOS Simulator

inputs:
project_name:
description: Name of the Xcode project or scheme
required: true

outputs:
xcframework_path:
description: Final path to the generated XCFramework
value: ${{ steps.build.outputs.xcframework_path }}

runs:
using: "composite"
steps:
- name: Build XCFramework
id: build
shell: bash
run: |
set -euo pipefail

PROJECT_NAME="${{ inputs.project_name }}"
BUILD_DIR="./build"

echo "🛠️ Building XCFramework..."

xcodebuild archive \
-scheme "$PROJECT_NAME" \
-configuration Release \
-destination 'generic/platform=iOS Simulator' \
-archivePath "$BUILD_DIR/${PROJECT_NAME}.framework-iphonesimulator.xcarchive" \
SKIP_INSTALL=NO \
BUILD_LIBRARIES_FOR_DISTRIBUTION=YES | xcbeautify

xcodebuild archive \
-scheme "$PROJECT_NAME" \
-configuration Release \
-destination 'generic/platform=iOS' \
-archivePath "$BUILD_DIR/${PROJECT_NAME}.framework-iphoneos.xcarchive" \
SKIP_INSTALL=NO \
BUILD_LIBRARIES_FOR_DISTRIBUTION=YES | xcbeautify

XCFRAMEWORK_PATH="$BUILD_DIR/${PROJECT_NAME}.xcframework"

xcodebuild -create-xcframework \
-framework "$BUILD_DIR/${PROJECT_NAME}.framework-iphonesimulator.xcarchive/Products/Library/Frameworks/${PROJECT_NAME}.framework" \
-framework "$BUILD_DIR/${PROJECT_NAME}.framework-iphoneos.xcarchive/Products/Library/Frameworks/${PROJECT_NAME}.framework" \
-output "$XCFRAMEWORK_PATH"

echo "✅ XCFramework built successfully"
echo "xcframework_path=$XCFRAMEWORK_PATH" >> "$GITHUB_OUTPUT"
45 changes: 45 additions & 0 deletions .github/actions/get-project-version/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Get Project Version
description: Extracts MARKETING_VERSION from a .pbxproj file

inputs:
project_name:
description: Name of the Xcode project (without .xcodeproj)
required: false
pbxproj_path:
description: Path to the project.pbxproj file (overrides project_name if provided)
required: false

outputs:
version:
description: The extracted project version
value: ${{ steps.extract.outputs.version }}

runs:
using: "composite"
steps:
- name: Extract current project version
id: extract
shell: bash
run: |
set -euo pipefail

# Determine the pbxproj path
if [[ -n "${{ inputs.pbxproj_path }}" ]]; then
PBXPROJ="${{ inputs.pbxproj_path }}"
elif [[ -n "${{ inputs.project_name }}" ]]; then
PBXPROJ="${{ inputs.project_name }}.xcodeproj/project.pbxproj"
else
echo "❌ Either 'project_name' or 'pbxproj_path' must be provided."
exit 1
fi

# Check if the pbxproj file exists
if [[ ! -f "$PBXPROJ" ]]; then
echo "❌ Project file not found: $PBXPROJ"
exit 1
fi

echo "📦 Extracting current MARKETING_VERSION from $PBXPROJ..."
VERSION=$(grep -m1 'MARKETING_VERSION =' "$PBXPROJ" | sed -E 's/.*MARKETING_VERSION = ([^;]+);/\1/' | xargs)
echo "🔢 Current version: $VERSION"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
50 changes: 50 additions & 0 deletions .github/actions/install-dependencies/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: Install Dependencies
description: Checks for Homebrew and installs any missing CLI tools and Ruby gems

inputs:
tools:
description: Space-separated list of tools to check and install via Homebrew
required: false
gems:
description: Space-separated list of gems to check and install via gem
required: false

runs:
using: "composite"
steps:
- name: Install Dependencies
shell: bash
run: |
set -euo pipefail

if [ -n "${{ inputs.tools }}" ]; then
echo "🔍 Checking for Homebrew..."
if ! command -v brew >/dev/null; then
echo "❌ Homebrew is required but not installed. Aborting."
exit 1
fi

echo "🔧 Installing missing brew tools..."
for tool in ${{ inputs.tools }}; do
if command -v "$tool" >/dev/null; then
echo "✅ $tool is already installed."
else
echo "📦 Installing $tool via brew..."
brew install "$tool"
fi
done
fi

if [ -n "${{ inputs.gems }}" ]; then
echo "🔧 Installing missing gems..."
for gem in ${{ inputs.gems }}; do
if gem list -i "$gem" >/dev/null; then
echo "✅ $gem gem is already installed."
else
echo "💎 Installing $gem via gem..."
gem install "$gem"
fi
done
fi

echo "✅ All dependencies are ready."
34 changes: 34 additions & 0 deletions .github/actions/package-xcframework/action.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Package XCFramework and LICENSE
description: Zips the built XCFramework and LICENSE into a versioned release artifact

inputs:
package_name:
description: The name of the package (e.g., MyLibrary-1.0.0)
required: true
xcframework_path:
description: The path to the built .xcframework
required: true
license_path:
description: The path to the LICENSE file
required: true

outputs:
zip_name:
description: The name of the created zip file
value: ${{ steps.package.outputs.zip_name }}

runs:
using: "composite"
steps:
- id: package
shell: bash
run: |
set -euo pipefail
ZIP_NAME="${{ inputs.package_name }}.zip"
mkdir -p release
cp -R "${{ inputs.xcframework_path }}" release/
cp "${{ inputs.license_path }}" release/
cd release
zip -r "../$ZIP_NAME" .
echo "✅ Packaged XCFramework and LICENSE into $ZIP_NAME"
echo "zip_name=$ZIP_NAME" >> "$GITHUB_OUTPUT"
18 changes: 18 additions & 0 deletions .github/actions/set-xcode-version/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: Set Xcode Version
description: Selects the desired Xcode version using xcode-select.
inputs:
xcode-version:
description: The Xcode version to select (e.g., 16.4)
required: true
runs:
using: 'composite'
steps:
- run: |
set -e
echo "Setting Xcode version to ${{ inputs.xcode-version }}..."
if ! sudo xcode-select -s /Applications/Xcode_${{ inputs.xcode-version }}.app/Contents/Developer; then
echo "❌ Failed to select Xcode ${{ inputs.xcode-version }}. Listing available Xcodes:"
ls /Applications | grep Xcode
exit 1
fi
shell: bash
File renamed without changes.
180 changes: 180 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
name: CI

on:
push:
branches:
- main
pull_request:
types: [opened, synchronize, reopened]

permissions:
contents: read
pull-requests: write # For PR comments

env:
PROJECT_NAME: OSBarcodeLib
SCHEME_NAME: OSBarcodeLib
XCODEPROJ_PATH: OSBarcodeLib.xcodeproj
XCODE_VERSION: 16.4
DESTINATION: 'platform=iOS Simulator,OS=latest,name=iPhone 16'
COVERAGE_TARGET_FILTER: OSBarcodeLib
BUILD_REPORTS_DIR: build/reports
SONAR_REPORTS_DIR: sonar-reports

jobs:
test:
name: Run Tests
runs-on: macos-15

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

- name: Install Dependencies
uses: ./.github/actions/install-dependencies
with:
tools: swiftlint xcbeautify

- name: Set Xcode version
uses: ./.github/actions/set-xcode-version
with:
xcode-version: ${{ env.XCODE_VERSION }}

- name: Run Unit Tests
id: unit_tests
env:
SCHEME_NAME: ${{ env.SCHEME_NAME }}
XCODEPROJ_PATH: ${{ env.XCODEPROJ_PATH }}
IOS_SIMULATOR_DEVICE: ${{ env.IOS_SIMULATOR_DEVICE }}
DESTINATION: ${{ env.DESTINATION }}
run: |
set -euo pipefail
XCRESULT_NAME="TestResults.xcresult"
mkdir -p "$BUILD_REPORTS_DIR"
xcodebuild test \
-project "$XCODEPROJ_PATH" \
-scheme "$SCHEME_NAME" \
-destination "$DESTINATION" \
-configuration Debug \
-enableCodeCoverage YES \
-resultBundlePath "$XCRESULT_NAME" \
SKIP_SCRIPT_PHASES=YES \
CODE_SIGNING_ALLOWED=NO | xcbeautify --report junit --report-path "$BUILD_REPORTS_DIR"
echo "xcresult_name=$XCRESULT_NAME" >> "$GITHUB_OUTPUT"

- name: Generate Code Coverage Report for SonarQube
continue-on-error: true
env:
XCRESULT_NAME: ${{ steps.unit_tests.outputs.xcresult_name }}
run: |
set -euo pipefail
echo "🔍 Generating SonarQube coverage report..."

if [ ! -d "$XCRESULT_NAME" ]; then
echo "⚠️ $XCRESULT_NAME not found. Skipping coverage report generation."
exit 0
fi

mkdir -p ${{ env.SONAR_REPORTS_DIR }}

echo "📦 Downloading coverage converter script..."
curl -sSL https://raw.githubusercontent.com/SonarSource/sonar-scanning-examples/master/swift-coverage/swift-coverage-example/xccov-to-sonarqube-generic.sh -o xccov-to-sonarqube-generic.sh
chmod +x xccov-to-sonarqube-generic.sh

echo "📝 Running coverage converter..."
./xccov-to-sonarqube-generic.sh TestResults.xcresult > ${{ env.SONAR_REPORTS_DIR }}/sonarqube-generic-coverage.xml
echo "✅ SonarQube coverage report generated successfully"

- name: Run SwiftLint for SonarQube
run: |
set -euo pipefail
echo "🔍 Running SwiftLint..."
mkdir -p ${{ env.SONAR_REPORTS_DIR }}
swiftlint --reporter checkstyle > "${{ env.SONAR_REPORTS_DIR }}/swiftlint.xml" || {
echo "⚠️ SwiftLint finished with issues."
exit 0
}
echo "✅ SwiftLint report generated successfully"

- name: Setup SonarQube Scanner
uses: warchant/setup-sonar-scanner@v8

- name: Send to SonarCloud
id: sonarcloud
continue-on-error: true
run: |
set -euo pipefail
if [ -z "${{ secrets.SONAR_TOKEN }}" ]; then
echo "⚠️ SONAR_TOKEN secret is not set. Skipping SonarCloud analysis."
exit 0
fi
if [ -f "sonar-project.properties" ]; then
echo "🔍 Sending results to SonarCloud..."
echo "📦 Commit: ${{ github.sha }}"
if [ "${{ github.ref_name }}" = "main" ]; then
echo "🌟 Analyzing main branch"
sonar-scanner
else
echo "🌿 Analyzing feature branch: ${{ github.ref_name }}"
sonar-scanner -Dsonar.branch.name="${{ github.ref_name }}"
fi
else
echo "⚠️ sonar-project.properties not found, skipping SonarCloud"
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

- name: Upload Test Results
uses: actions/upload-artifact@v4
if: always()
with:
name: test-results
path: |
${{ steps.unit_tests.outputs.xcresult_name }}
${{ env.SONAR_REPORTS_DIR }}
${{ env.BUILD_REPORTS_DIR }}

- name: Comment Test Results
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
env:
XCRESULT_NAME: ${{ steps.unit_tests.outputs.xcresult_name }}
COVERAGE_TARGET_FILTER: ${{ env.COVERAGE_TARGET_FILTER }}
with:
script: |
const { execSync } = require('child_process');
const fs = require('fs');

console.log('📝 Starting to comment test results...');
let coveragePercentage = 'N/A';
try {
const xcresultName = process.env.XCRESULT_NAME;
const coverageTarget = process.env.COVERAGE_TARGET_FILTER;
console.log(`Checking result file: ${xcresultName}`);
if (fs.existsSync(xcresultName)) {
console.log('Result file found. Calculating coverage...');
const output = execSync(`xcrun xccov view --report "${xcresultName}"`).toString();
const match = output.match(new RegExp(`${coverageTarget}.*?([0-9]+\\.[0-9]+%)`));
if (match && match[1]) {
coveragePercentage = match[1];
console.log(`Coverage found: ${coveragePercentage}`);
} else {
console.log('Coverage not found in report.');
}
} else {
console.log('Result file not found.');
}
} catch (e) {
console.error('Error calculating coverage:', e);
coveragePercentage = 'N/A';
}

console.log('Commenting on PR with test results and coverage...');
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `✅ **Tests**: All passed\n📊 **Coverage**: ${coveragePercentage}`
});
console.log('Comment sent successfully.');
Loading
Loading