Skip to content

Simplify release process for Swift build #190

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

Merged
merged 9 commits into from
May 7, 2025
Merged
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
110 changes: 91 additions & 19 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -1,15 +1,35 @@
name: Deploy to Sonatype
name: Release

on: workflow_dispatch

permissions:
contents: read

jobs:
test:
uses: ./.github/workflows/test.yml
deploy:
needs: [test]
draft_release:
permissions:
contents: write
name: Create Draft Release on GitHub
runs-on: ubuntu-latest
outputs:
tag: ${{ steps.tag.outputs.tag }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Set tag name
id: tag
run: |
tag=$(basename "${{ github.ref }}")
echo "tag=$tag" >> $GITHUB_OUTPUT
- name: Create Release
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
run: |
tag="${{ steps.tag.outputs.tag }}"
body="Pending release for XCFramework, $tag"
gh release create --draft "$tag" --title "$tag" --notes "$body"

maven_publish:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -38,17 +58,69 @@ jobs:
-Ppowersync.binaries.allPlatforms="true" \
publishAllPublicationsToSonatypeRepository
shell: bash
# This will change Package.swift in Github packages to direct to new maven central KMMBridge zip file
call-kmmbridge-publish:
needs: deploy

build_xcframeworks:
name: Build XCFrameworks
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v1
- uses: actions/cache@v3
with:
path: ~/.konan
key: ${{ runner.os }}-${{ hashFiles('**/.lock') }}
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v4
- name: Build frameworks
run: "./gradlew PowerSyncKotlin:buildRelease"

- uses: actions/upload-artifact@v4
with:
name: XCFramework
retention-days: 1 # Only used temporarily
compression-level: 0 # We're already uploading a compressed file
path: PowerSyncKotlin/build/FrameworkArchives/PowersyncKotlinRelease.zip
if-no-files-found: error

add_assets:
permissions:
contents: write
packages: write
uses: touchlab/KMMBridgeGithubWorkflow/.github/workflows/[email protected]
with:
jvmVersion: 17
versionBaseProperty: SWIFT_LIBRARY_VERSION
publishTask: kmmBridgePublish
secrets:
gradle_params: -PsigningInMemoryKey="${{ secrets.SIGNING_KEY }}" -PsigningInMemoryKeyId="${{ secrets.SIGNING_KEY_ID }}" -PsigningInMemoryKeyPassword="${{ secrets.SIGNING_PASSWORD }}"
needs: [draft_release, build_xcframeworks]
name: Add assets to pending release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/download-artifact@v4
with:
merge-multiple: true
- run: "ls -al"
- name: Upload XCFramework
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
run: |
gh release upload "${{ needs.draft_release.outputs.tag }}" PowersyncKotlinRelease.zip

- name: "Update release description"
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
shell: bash
run: |
checksums=$(sha256sum PowersyncKotlinRelease.zip)
cat > RELEASE_NOTES <<- NOTES_END
File hashes:
\`\`\`
$checksums
\`\`\`
NOTES_END

gh release edit "${{ needs.draft_release.outputs.tag }}" -F RELEASE_NOTES
14 changes: 7 additions & 7 deletions Package.swift → PowerSyncKotlin/Package.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// swift-tools-version:5.3
import PackageDescription
// swift-tools-version: 5.7

// NOTE! This is never released, we're only using this to support local Kotlin SDK builds for the
// Swift SDK.
import PackageDescription
let packageName = "PowerSyncKotlin"

let package = Package(
Expand All @@ -12,14 +14,12 @@ let package = Package(
products: [
.library(
name: packageName,
targets: [packageName]
),
targets: [packageName]),
],
targets: [
.binaryTarget(
name: packageName,
path: "./PowerSyncKotlin/build/XCFrameworks/debug/\(packageName).xcframework"
path: "build/XCFrameworks/debug/PowerSyncKotlin.xcframework"
)
,
]
)
)
200 changes: 22 additions & 178 deletions PowerSyncKotlin/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
import co.touchlab.faktory.KmmBridgeExtension
import co.touchlab.faktory.artifactmanager.ArtifactManager
import co.touchlab.faktory.capitalized
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
import java.security.MessageDigest
import org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFramework

plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.kmmbridge)
alias(libs.plugins.skie)
alias(libs.plugins.mavenPublishPlugin)
alias(libs.plugins.kotlinter)
id("com.powersync.plugins.sonatype")
}

kotlin {
val xcf = XCFramework()

listOf(
iosX64(),
iosArm64(),
Expand All @@ -22,8 +18,14 @@ kotlin {
macosX64(),
).forEach {
it.binaries.framework {
baseName = "PowerSyncKotlin"
xcf.add(this)

export(project(":core"))
isStatic = true

binaryOption("bundleId", "PowerSyncKotlin")
binaryOption("bundleVersion", project.version.toString())
}
}

Expand Down Expand Up @@ -64,180 +66,22 @@ configurations.all {
}
}

kmmbridge {
artifactManager.set(SonatypePortalPublishArtifactManager(project, repositoryName = null))
artifactManager.finalizeValue()
spm()
}

// We need this so that when a user includes the package in XCode they are able to
// import the package using Github
if (System.getenv().containsKey("CI")) {
// Setup github publishing based on GitHub action variables
addGithubPackagesRepository()
}

// This is required for KMMBridge zip to be uploaded to Sonatype (Maven Central)
// Since this will only ever be used in this build file it does not make sense to make a
// plugin to use this.
class SonatypePortalPublishArtifactManager(
val project: Project,
private val publicationName: String = "KMMBridgeFramework",
artifactSuffix: String = "kmmbridge",
private val repositoryName: String?
) : ArtifactManager {
private val group: String = project.group.toString().replace(".", "/")
private val kmmbridgeArtifactId =
"${project.name}-$artifactSuffix"
private val zipName = "powersync-$artifactSuffix"
private val LIBRARY_VERSION: String by project

// This is the URL that will be added to Package.swift in Github package so that
// KMMBridge is downloaded when a user includes the package in XCode
private val MAVEN_CENTRAL_PACKAGE_ZIP_URL = "https://repo1.maven.org/maven2/com/powersync/${zipName}/${LIBRARY_VERSION}/${zipName}-${LIBRARY_VERSION}.zip"

override fun deployArtifact(
project: Project,
zipFilePath: File,
version: String
): String = MAVEN_CENTRAL_PACKAGE_ZIP_URL

override fun configure(
project: Project,
version: String,
uploadTask: TaskProvider<Task>,
kmmPublishTask: TaskProvider<Task>
) {
val zipXCFramework = project.tasks.named<Zip>("zipXCFramework")
zipXCFramework.configure {
// KMMBridge uses the Gradle Zip tasks to create XCFramework archives, but Gradle
// doesn't support symlinks. XCFrameworks for macOS need to use symlinks though, so we
// patch the task to generate zip files properly.
doLast {
val bridge = project.extensions.getByName<KmmBridgeExtension>("kmmbridge")
val source = project.layout.buildDirectory.map { it.dir("XCFrameworks/${bridge.buildType.get().name}") }.get().asFile

val out = archiveFile.get().asFile
out.delete()
listOf("Debug", "Release").forEach { buildType ->
tasks.register<Exec>("build$buildType") {
group = "build"
description = "Create an XCFramework archive for $buildType"

providers.exec {
executable = "zip"
args("-r", "--symlinks", out.absolutePath, "PowerSyncKotlin.xcframework")
workingDir(source)
}.result.get().assertNormalExitValue()
}
}

project.extensions.getByType<PublishingExtension>().publications.create(
publicationName,
MavenPublication::class.java,
) {
this.version = version
val archiveProvider = zipXCFramework.flatMap {
it.archiveFile
}
artifact(archiveProvider) {
extension = "zip"
}
artifactId = kmmbridgeArtifactId
}

// Register the task
project.tasks.register<UpdatePackageSwiftChecksumTask>("updatePackageSwiftChecksum") {
artifactId.set(kmmbridgeArtifactId)
zipUrl.set(MAVEN_CENTRAL_PACKAGE_ZIP_URL)
dependsOn("updatePackageSwift")
}

// Make sure this task runs after updatePackageSwift
project.tasks.named("kmmBridgePublish") {
dependsOn("updatePackageSwiftChecksum")
}
val originalFramework = tasks.getByName("assemblePowerSyncKotlin${buildType}XCFramework")
dependsOn(originalFramework)

publishingTasks().forEach {
uploadTask.configure {
dependsOn(it)
}
}
try {
project.tasks.named("publish").also { task ->
task.configure {
dependsOn(kmmPublishTask)
}
}
} catch (_: UnknownTaskException) {
project.logger.warn("Gradle publish task not found")
}
}

private fun publishingTasks(): List<TaskProvider<Task>> {
val publishingExtension = project.extensions.getByType<PublishingExtension>()

// Either the user has supplied a correct name, or we use the default. If neither is found, fail.
val publicationNameCap =
publishingExtension.publications
.getByName(
publicationName,
).name
.capitalized()

return publishingExtension.repositories
.filterIsInstance<MavenArtifactRepository>()
.map { repo ->
val repositoryName = repo.name.capitalized()
val publishTaskName =
"publish${publicationNameCap}PublicationTo${repositoryName}Repository"
// Verify that the "publish" task exists before collecting
project.tasks.named(publishTaskName)
}
}
}

// This task is used to update Package.swift with the checksum of the zip file
// located on maven central.
abstract class UpdatePackageSwiftChecksumTask : DefaultTask() {
@get:Input
abstract val artifactId: Property<String>

@get:Input
abstract val zipUrl: Property<String>

@TaskAction
fun updateChecksum() {
val LIBRARY_VERSION: String by project

val zipFile = project.file("${project.layout.buildDirectory.get()}/tmp/${artifactId.get().lowercase()}-$LIBRARY_VERSION.zip")

// Download the zip file
zipFile.parentFile.mkdirs()
project.uri(zipUrl.get()).toURL().openStream().use { input ->
zipFile.outputStream().use { output ->
input.copyTo(output)
}
}

// Compute the checksum
val checksum =
zipFile.inputStream().use { input ->
val digest = MessageDigest.getInstance("SHA-256")
val buffer = ByteArray(8192)
var bytes = input.read(buffer)
while (bytes >= 0) {
digest.update(buffer, 0, bytes)
bytes = input.read(buffer)
}
digest.digest().joinToString("") { "%02x".format(it) }
}
val source = project.layout.buildDirectory.map { it.dir("XCFrameworks/${buildType.lowercase()}") }.get().asFile
val archiveFile = project.layout.buildDirectory.map { it.file("FrameworkArchives/PowersyncKotlin$buildType.zip") }.get().asFile

// Update Package.swift
val packageSwiftFile = project.rootProject.file("Package.swift")
val updatedContent =
packageSwiftFile.readText().replace(
Regex("let remoteKotlinChecksum = \"[a-f0-9]+\""),
"let remoteKotlinChecksum = \"$checksum\"",
)
packageSwiftFile.writeText(updatedContent)
archiveFile.parentFile.mkdirs()
archiveFile.delete()

println("Updated Package.swift with new checksum: $checksum")
executable = "zip"
args("-r", "--symlinks", archiveFile.absolutePath, "PowerSyncKotlin.xcframework")
workingDir(source)
}
}
2 changes: 0 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ plugins {
alias(libs.plugins.androidApplication) apply false
alias(libs.plugins.androidLibrary) apply false
alias(libs.plugins.kotlinMultiplatform) apply false
alias(libs.plugins.cocoapods) apply false
alias(libs.plugins.kmmbridge) apply false
alias(libs.plugins.skie) apply false
alias(libs.plugins.kotlin.jvm) apply false
alias(libs.plugins.kotlin.android) apply false
Expand Down
Loading