-
Notifications
You must be signed in to change notification settings - Fork 3
Provide a Swift package - Workflows to generate iOS builds #21
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 5 commits
b0ea648
66ebe0f
96820dd
79e9d3d
07d9098
5f35d6d
8d01700
ff8cfdc
8ee0828
afc91a3
33e334a
da9ce3d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,30 +1,158 @@ | ||
| name: Gradle CI | ||
| name: Build | ||
|
|
||
| on: | ||
| push: | ||
| branches: [ main ] | ||
| branches: [main] | ||
| pull_request: | ||
| branches: [ main ] | ||
| branches: [main] | ||
|
|
||
| concurrency: | ||
| group: build-${{ github.ref }} | ||
| cancel-in-progress: true # Cancel previous builds | ||
|
|
||
| jobs: | ||
| build: | ||
| test: | ||
| runs-on: macos-latest | ||
|
|
||
| steps: | ||
| - uses: actions/checkout@v5 | ||
|
|
||
| - name: Set up JDK 17 | ||
| uses: actions/setup-java@v5 | ||
| with: | ||
| java-version: '17' | ||
| distribution: 'temurin' | ||
| cache: gradle | ||
|
|
||
| - name: Setup Gradle | ||
| uses: gradle/actions/setup-gradle@v4 | ||
| - uses: actions/checkout@v4 | ||
|
|
||
| - name: Clean project | ||
| run: ./gradlew clean | ||
|
|
||
| - name: Run tests | ||
| run: ./gradlew allTests | ||
| - name: Setup Java | ||
| uses: actions/setup-java@v4 | ||
| with: | ||
| java-version: '17' | ||
| distribution: 'temurin' | ||
| cache: gradle | ||
|
|
||
| - name: Setup Gradle | ||
| uses: gradle/actions/setup-gradle@v4 | ||
|
|
||
| - name: Run Tests | ||
| run: ./gradlew allTests | ||
|
|
||
| build-dev: | ||
| needs: test | ||
| runs-on: macos-latest | ||
| if: github.ref == 'refs/heads/main' | ||
| outputs: | ||
| build-version: ${{ steps.version.outputs.version }} | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| with: | ||
| fetch-depth: 0 | ||
|
|
||
| - name: Setup Java | ||
| uses: actions/setup-java@v4 | ||
| with: | ||
| java-version: '17' | ||
| distribution: 'temurin' | ||
| cache: gradle | ||
|
|
||
| - name: Setup Gradle | ||
| uses: gradle/actions/setup-gradle@v4 | ||
|
|
||
| - name: Generate Build Version | ||
| id: version | ||
| run: | | ||
| # Use timestamp + short SHA for dev builds | ||
| TIMESTAMP=$(date +%Y%m%d-%H%M%S) | ||
| SHORT_SHA=${GITHUB_SHA:0:7} | ||
| BUILD_VERSION="dev-$TIMESTAMP-$SHORT_SHA" | ||
| echo "version=$BUILD_VERSION" >> $GITHUB_OUTPUT | ||
| echo "📦 Build version: $BUILD_VERSION" | ||
|
|
||
| - name: Build XCFramework | ||
| run: ./gradlew :umbrella:createXCFramework | ||
|
|
||
| - name: Prepare Distribution | ||
| run: | | ||
| cd umbrella/build/XCFrameworks/release | ||
| zip -r QuranSyncUmbrella-${{ steps.version.outputs.version }}.xcframework.zip QuranSyncUmbrella.xcframework | ||
|
|
||
| - name: Calculate Checksum | ||
| id: checksum | ||
| run: | | ||
| CHECKSUM=$(swift package compute-checksum umbrella/build/XCFrameworks/release/QuranSyncUmbrella-${{ steps.version.outputs.version }}.xcframework.zip) | ||
| echo "checksum=$CHECKSUM" >> $GITHUB_OUTPUT | ||
|
|
||
| - name: Update Package.swift for Dev Build | ||
| run: | | ||
| # Update Package.swift directly with dev build info | ||
| sed -i '' "s/{VERSION}/${{ steps.version.outputs.version }}/g" Package.swift | ||
| sed -i '' "s/{CHECKSUM_TO_BE_REPLACED_BY_CI}/${{ steps.checksum.outputs.checksum }}/g" Package.swift | ||
|
|
||
| - name: Commit Package.swift for Dev Build | ||
| run: | | ||
| git config user.name "github-actions[bot]" | ||
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | ||
| git add Package.swift | ||
| git commit -m "chore: update Package.swift for dev build ${{ steps.version.outputs.version }}" | ||
| git push origin main | ||
|
|
||
| - name: Create Development Release | ||
| uses: softprops/action-gh-release@v1 | ||
| with: | ||
| tag_name: ${{ steps.version.outputs.version }} | ||
| name: "Development Build ${{ steps.version.outputs.version }}" | ||
| prerelease: true | ||
| files: | | ||
| umbrella/build/XCFrameworks/release/QuranSyncUmbrella-${{ steps.version.outputs.version }}.xcframework.zip | ||
| body: | | ||
| ## 🚧 Development Build | ||
|
|
||
| **Commit:** ${{ github.sha }} | ||
| **Branch:** ${{ github.ref_name }} | ||
| **Checksum:** `${{ steps.checksum.outputs.checksum }}` | ||
|
|
||
| ### Usage (SPM) | ||
| Package.swift has been automatically updated for this dev build: | ||
| ```swift | ||
| .package(url: "https://github.com/quran/mobile-sync", exact: "${{ steps.version.outputs.version }}") | ||
| ``` | ||
|
|
||
| ### Alternative: Manual Binary Target | ||
| If you prefer manual control: | ||
| ```swift | ||
| .binaryTarget( | ||
| name: "QuranSyncUmbrella", | ||
| url: "https://github.com/quran/mobile-sync/releases/download/${{ steps.version.outputs.version }}/QuranSyncUmbrella-${{ steps.version.outputs.version }}.xcframework.zip", | ||
| checksum: "${{ steps.checksum.outputs.checksum }}" | ||
| ) | ||
| ``` | ||
|
|
||
| ### ⚠️ Important Warnings | ||
| - **This is a development build** - use stable releases for production | ||
| - **Dev builds may be deleted without notice** - we only keep the 5 most recent | ||
| - **Package.swift will be overwritten** by the next dev build or release | ||
| - **No stability guarantees** - APIs may change between dev builds | ||
| - **For testing only** - not recommended for App Store submissions | ||
| env: | ||
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
|
|
||
| cleanup-old-dev-builds: | ||
| needs: build-dev | ||
| runs-on: ubuntu-latest | ||
| if: github.ref == 'refs/heads/main' | ||
| steps: | ||
| - name: Cleanup Old Dev Builds | ||
| uses: actions/github-script@v7 | ||
| with: | ||
| script: | | ||
| const { data: releases } = await github.rest.repos.listReleases({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| }); | ||
|
|
||
| // Keep last 5 dev builds, delete older ones | ||
| const devBuilds = releases | ||
| .filter(release => release.tag_name.startsWith('dev-')) | ||
| .sort((a, b) => new Date(b.created_at) - new Date(a.created_at)) | ||
| .slice(5); // Skip first 5 (keep them) | ||
|
|
||
| for (const release of devBuilds) { | ||
| console.log(`Deleting old dev build: ${release.tag_name}`); | ||
| await github.rest.repos.deleteRelease({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| release_id: release.id, | ||
| }); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,177 @@ | ||
| name: Release | ||
|
|
||
| on: | ||
| workflow_dispatch: | ||
| inputs: | ||
| version_bump: | ||
| description: 'Version bump type' | ||
| required: true | ||
| default: 'patch' | ||
| type: choice | ||
| options: | ||
| - patch | ||
| - minor | ||
| - major | ||
| prerelease: | ||
| description: 'Is this a prerelease?' | ||
| required: false | ||
| default: false | ||
| type: boolean | ||
|
|
||
| concurrency: | ||
| group: release | ||
| cancel-in-progress: false # Don't cancel releases | ||
|
|
||
| jobs: | ||
| release: | ||
| runs-on: macos-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| with: | ||
| fetch-depth: 0 | ||
| token: ${{ secrets.GITHUB_TOKEN }} | ||
|
|
||
| - name: Setup Java | ||
| uses: actions/setup-java@v4 | ||
| with: | ||
| java-version: '17' | ||
| distribution: 'temurin' | ||
| cache: gradle | ||
|
|
||
| - name: Setup Gradle | ||
| uses: gradle/actions/setup-gradle@v4 | ||
|
|
||
| - name: Configure Git | ||
| run: | | ||
| git config user.name "github-actions[bot]" | ||
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | ||
|
|
||
| - name: Get Current Version | ||
| id: current_version | ||
| run: | | ||
| # Get latest tag, default to 0.0.0 if none exists | ||
| LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0") | ||
| CURRENT_VERSION=${LATEST_TAG#v} # Remove 'v' prefix | ||
| echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT | ||
| echo "📋 Current version: $CURRENT_VERSION" | ||
|
|
||
| - name: Calculate Next Version | ||
| id: next_version | ||
| run: | | ||
| CURRENT="${{ steps.current_version.outputs.version }}" | ||
| BUMP="${{ github.event.inputs.version_bump }}" | ||
|
|
||
| # Split version into parts | ||
| IFS='.' read -ra VERSION_PARTS <<< "$CURRENT" | ||
| MAJOR=${VERSION_PARTS[0]:-0} | ||
| MINOR=${VERSION_PARTS[1]:-0} | ||
| PATCH=${VERSION_PARTS[2]:-0} | ||
|
|
||
| # Calculate new version based on bump type | ||
| case $BUMP 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" | ||
| echo "version=$NEW_VERSION" >> $GITHUB_OUTPUT | ||
| echo "🚀 Next version: $NEW_VERSION" | ||
|
|
||
| - name: Run Tests | ||
| run: ./gradlew allTests | ||
|
|
||
| - name: Build XCFramework | ||
| run: ./gradlew :umbrella:createXCFramework | ||
|
|
||
| - name: Prepare Distribution | ||
| run: | | ||
| cd umbrella/build/XCFrameworks/release | ||
| # XCFramework is already named correctly: QuranSyncUmbrella.xcframework | ||
| zip -r QuranSyncUmbrella.xcframework.zip QuranSyncUmbrella.xcframework | ||
|
|
||
| - name: Calculate Checksum | ||
| id: checksum | ||
| run: | | ||
| CHECKSUM=$(swift package compute-checksum umbrella/build/XCFrameworks/release/QuranSyncUmbrella.xcframework.zip) | ||
| echo "checksum=$CHECKSUM" >> $GITHUB_OUTPUT | ||
|
|
||
| - name: Update Package.swift | ||
| run: | | ||
| sed -i '' "s/{VERSION}/v${{ steps.next_version.outputs.version }}/g" Package.swift | ||
| sed -i '' "s/{CHECKSUM_TO_BE_REPLACED_BY_CI}/${{ steps.checksum.outputs.checksum }}/g" Package.swift | ||
|
|
||
| - name: Generate Release Notes | ||
| id: release_notes | ||
| run: | | ||
| # Get commits since last release | ||
| LAST_TAG="${{ steps.current_version.outputs.version }}" | ||
| if [ "$LAST_TAG" = "0.0.0" ]; then | ||
| COMMITS=$(git log --pretty=format:"- %s" --reverse) | ||
| else | ||
| COMMITS=$(git log v$LAST_TAG..HEAD --pretty=format:"- %s" --reverse) | ||
| fi | ||
|
|
||
| # Create release notes | ||
| cat > release_notes.md << EOF | ||
| ## What's Changed | ||
|
|
||
| $COMMITS | ||
|
|
||
| ## Installation | ||
|
|
||
| ### Swift Package Manager | ||
| Add to your \`Package.swift\`: | ||
| \`\`\`swift | ||
| dependencies: [ | ||
| .package(url: "https://github.com/quran/mobile-sync", from: "${{ steps.next_version.outputs.version }}") | ||
| ] | ||
| \`\`\` | ||
|
|
||
| ### Xcode | ||
| 1. File → Add Package Dependencies | ||
| 2. Enter: \`https://github.com/quran/mobile-sync\` | ||
| 3. Select version: \`${{ steps.next_version.outputs.version }}\` | ||
| EOF | ||
|
|
||
| - name: Commit Package.swift Updates | ||
| run: | | ||
| git add Package.swift | ||
| git commit -m "chore: update Package.swift for v${{ steps.next_version.outputs.version }}" || exit 0 | ||
|
|
||
| - name: Create Tag | ||
| run: | | ||
| git tag -a "v${{ steps.next_version.outputs.version }}" -m "Release v${{ steps.next_version.outputs.version }}" | ||
|
|
||
| - name: Push Changes | ||
| run: | | ||
| git push origin main | ||
| git push origin "v${{ steps.next_version.outputs.version }}" | ||
|
|
||
| - name: Create Release | ||
| uses: softprops/action-gh-release@v1 | ||
| with: | ||
| tag_name: "v${{ steps.next_version.outputs.version }}" | ||
| name: "QuranSync v${{ steps.next_version.outputs.version }}" | ||
| prerelease: ${{ github.event.inputs.prerelease }} | ||
| files: | | ||
| umbrella/build/XCFrameworks/release/QuranSyncUmbrella.xcframework.zip | ||
| body_path: release_notes.md | ||
| env: | ||
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
|
|
||
| - name: Summary | ||
| run: | | ||
| echo "✅ Released QuranSyncUmbrella v${{ steps.next_version.outputs.version }}" | ||
| echo "🏷️ Tag: v${{ steps.next_version.outputs.version }}" | ||
| echo "📦 Asset: QuranSyncUmbrella.xcframework.zip" | ||
| echo "🔗 Release: https://github.com/quran/mobile-sync/releases/tag/v${{ steps.next_version.outputs.version }}" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| // swift-tools-version:5.5 | ||
| import PackageDescription | ||
|
|
||
| // Set to true for local development | ||
| let useLocalBuild = false | ||
|
|
||
| let package = Package( | ||
| name: "QuranSyncUmbrella", | ||
mohannad-hassan marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| platforms: [ | ||
| .iOS(.v13) | ||
| ], | ||
| products: [ | ||
| .library( | ||
| name: "QuranSyncUmbrella", | ||
| targets: ["QuranSyncUmbrella"] | ||
| ) | ||
| ], | ||
| targets: useLocalBuild ? [ | ||
| .binaryTarget( | ||
| name: "QuranSyncUmbrella", | ||
| path: "umbrella/build/XCFrameworks/release/QuranSyncUmbrella.xcframework" | ||
| ) | ||
| ] : [ | ||
| .binaryTarget( | ||
| name: "QuranSyncUmbrella", | ||
| url: "https://github.com/quran/mobile-sync/releases/download/{VERSION}/QuranSyncUmbrella.xcframework.zip", | ||
| checksum: "{CHECKSUM_TO_BE_REPLACED_BY_CI}" | ||
| ) | ||
| ] | ||
| ) | ||
|
Comment on lines
+24
to
+30
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As per our discussion and https://www.jetbrains.com/help/kotlin-multiplatform-dev/multiplatform-spm-export.html#set-up-remote-integration, we need a separate repo to host the package.swift.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I fail to see why we need to separate the Swift's package definition and generated XCFramework hosting to a separate repo. JetBrains mentions the approach used here as the second approach, and the drawback mentioned is:
This is actually one of the reasons I resorted to keeping both in the same repo. Whenever the library's components the exported library will have to be updated, so the Swift package will follow the KMP packages versions. We may have some problems if we ever reach the point that the Kotlin packages will need to have different versions. Let me know what you think.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This PR is running into the exact issue described in the approach: "SPM uses Git tags for versioning packages, which can conflict with tags used for your project." To work around it, the code introduces a non-standard workflow: creating a release and then immediately committing another change to bump This approach is problematic because it deviates from standard git practices, clutters the commit history, and distorts the meaning of releases. My suggestion is to go with the recommended approach for creating a separate repo for the package.swift, please.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What I understood from that is when the Swift package's versioning would collide with the KMP project's versioning, which we don't have. I see that would happen if the main repo needed to handle other concerns that those exposed for iOS, e.g. if it did contain the final app code, etc... As for the separate version commit, I believe we will have to do them whenever we handle versioning for the modules here, as well. It would be common to set the version in the gradle files, and edit change logs, so it'd be x.y.z version bump for both gradle and Package.swift, and hence any other needed artifacts. We could see this behavior in some of the Kotlin dependencies we're using: ktor-3.2.1, ktor-3.3.0, kermit-2.0.8 and sqldelight-2.1.0. These don't provide a Swift package for a native KMP package, but it shows they're following the same_-ish_ versioning technique we might be following, and since we're not developing anything else here, then it follows that versioning for the Swift package and framework can be de done here and track the same versions.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The main drawback I see will be if we had to drive versioning of the data repositories and the sync agent separately. If that's the case we will need to get the Edit: even if we have to provide different versions for different libraries, we could go with a prefixed tag strategy. Maybe then I'd see the need for a separate repo for the Swift package as it'd have a new version each time any of the individual modules gets pumped to a new version. But I don't expect we'd reach that stage, at least soon.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Why would that be the case? Creating a new release will build the needed artifacts, creates the Github release, pumps the version values and commits. We don't currently put the version in Gradle files, but it'd be the same logic. I depended on Claude to generate the workflows and script files, but I looked it over multiple times and it seems alright. Am I missing something here?
I meant the value of continuous integration. If we have the responsibility of building the framework in the same repo, then we can have that check executed on each PR alongside tests. I don't expect this to be a frequent problem, but it's just an example of what we lose when we separate Package.swift and XCFramework generation in a separate repo.
As things are set up now, quran-sync/Package.swift should not be changed when building locally. The env-var set from the host project (i.e. quran-ios) will control if it points to the local package or the remote XCFramework, so the change will be made from the importing project. The point is, as I see things now, importing the project should be a bit simpler. Still the main point is what I said about my interpretation of jetbrains' recommendation. It's recommended within the context of exposing few modules from an existing bigger project to an iOS framework. This is my opinion, so I can't find references for that. My clues for that: 1. Jetbrains did mention my approach here as the second recommendation, 2. and the drawback they mentioned does not exist in our case, as I see it. Moreover, if our usage or the internal structure change, we can easily migrate inshaa Allah.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Salam alikom, Mohannad, let's look at the steps. Let's assume we will not create a new release on each commit because this will leave the repo with just automated releases and commits and make it extremely hard to work with. Let's assume we can manually trigger the
The problem here is that, package.swift was created and pushed to the repo before xcframework was uploaded. Now, that means we could potentially be in a situation where package.swift point to a non-existent xcframework which is problematic. This is the main major issue, with multiple repos we upload xcframework first and then update package.swift. There are other issues like the automated commits and automated releases in the mobile-sync repo which is supposed to be not just about releasing new package.swift.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's not what happens now. The workflow is either triggered from Github, or triggered from scripts/release which does the same thing, so we won't be doing the steps manually, if that's the point.
I've tested that out on a fork. See the commit and the release. If we needed to update other artifacts with version numbers, we should be able to do that in the same workflow and add it to the same commit.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can't create a github release before creating a tag.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Again, the problem here is not that the approach works or not. The problem here is the approach is brittle and we can easily be in an invalid state. At this point, I feel we have gone in circles long enough and I don't think is more to say here. Let's revisit this in the team meeting tomorrow. |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't tried this myself but we probably could overwrite this with an env var and have it default to false. See https://gist.github.com/Sorix/21e61347f478ae2e83ef4d8a92d933af
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's clear from mobile-sync end.
From quran-ios's side, we will need to point to the local path as well. I've experimented with this and found that the best way to control both Package.swift files is through setting the evn var from Terminal before opening Xcode. Otherwise, setting it through the scheme's variables won't reflect in package resolution.
It will look something like this:
If that feels too awkward, the other solution I'd think of to
setenvfrom quran-ios's Package.swift. This will be inputted to quran-ios's package by reading it from a side file or editting it manually.@mohamede1945 Let me know what you think.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That sounds good to me. Thank you!