Skip to content
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
9 changes: 7 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ jobs:
run: chmod +x gradlew

- name: Build and Test with Coverage
run: ./gradlew clean build koverXmlReport --stacktrace --continue
run: |
./gradlew -p tooling :plugins:test :plugins:ktlintCheck --stacktrace
./gradlew clean build koverXmlReport --stacktrace --continue

- name: Upload Coverage to Codecov
# Secrets (including CODECOV_TOKEN) are not exposed to fork PRs, so the
Expand Down Expand Up @@ -83,6 +85,9 @@ jobs:
run: |
echo "VERSION_NAME=$(grep -E '^store[[:space:]]*=' gradle/libs.versions.toml | head -1 | cut -d'"' -f2)" >> $GITHUB_ENV

- name: Verify publication versions
run: ./gradlew verifyPublicationVersions --stacktrace

- name: Publish to Maven Central (Central Portal)
env:
ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_USERNAME }}
Expand All @@ -94,4 +99,4 @@ jobs:
./gradlew publishToMavenCentral
else
./gradlew publishAndReleaseToMavenCentral
fi
fi
18 changes: 17 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@

### Thank you to all our wonderful contributors and users

## [5.1.0-beta01] (2026-09-08)

**Bug Fixes**

* Fix MutableStore synchronization so a successful server update acknowledges only its captured batch, preserving newer pending writes and excluding failed local writes [#762](https://github.com/MobileNativeFoundation/Store/pull/762)
* Propagate cancellation, retain admitted pending writes in memory until acknowledgement, and unblock SourceOfTruth readers after a cancelled write [#762](https://github.com/MobileNativeFoundation/Store/pull/762)
* Run success callbacks after releasing synchronization locks so they can reenter the store, and continue delivering callbacks if an earlier callback fails [#762](https://github.com/MobileNativeFoundation/Store/pull/762)
* Detect recursive adapter calls into the same store and key through inherited coroutine context and fail before reentry can deadlock [#762](https://github.com/MobileNativeFoundation/Store/pull/762)

**Improvements**

* Serialize MutableStore updater calls per key while allowing newer writes to persist locally during an earlier update [#762](https://github.com/MobileNativeFoundation/Store/pull/762)
* Align the Rx2 artifact's publication version with the shared Store release version

## [5.1.0-alpha11] (2026-08-28)

**Bug Fixes**
Expand Down Expand Up @@ -395,7 +409,9 @@ This is a first alpha release of Store ported to RxJava 2.
* The change log for Store version 1.x can be
found [here](https://github.com/NYTimes/Store/blob/develop/CHANGELOG.md).

[Unreleased]: https://github.com/MobileNativeFoundation/Store/compare/5.1.0-alpha11...HEAD
[Unreleased]: https://github.com/MobileNativeFoundation/Store/compare/5.1.0-beta01...HEAD

[5.1.0-beta01]: https://github.com/MobileNativeFoundation/Store/releases/tag/5.1.0-beta01

[5.1.0-alpha11]: https://github.com/MobileNativeFoundation/Store/releases/tag/5.1.0-alpha11

Expand Down
61 changes: 34 additions & 27 deletions RELEASING.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,35 @@
Releasing
========
# Releasing

1. Change the version in top level `gradle.properties` to a non-SNAPSHOT version.
2. Update the `cocoapods` version in `build.gradle.kts` in `:store`.
3. Modify `create_swift_package.yml` workflow.
* https://github.com/MobileNativeFoundation/Store/blob/e526400cdf51aa2f78b6b7e9e87f4a6845e6dcea/.github/workflows/create_swift_package.yml
4. Update the `CHANGELOG.md` for the impending release.
5. Update the `README.md` with the new version.
6. `git commit -sam "Prepare for release X.Y.Z."` (where X.Y.Z is the new version)
7. `git tag -a X.Y.X -m "Version X.Y.Z"` (where X.Y.Z is the new version)
* Run `git tag` to verify it.
8. `git push && git push --tags`
* This should be pushed to your fork.
9. Create a PR with this commit and merge it.
10. Update the top level `build.gradle` to the next SNAPSHOT version.
11. Modify `create_swift_package.yml` workflow to only run manually.
* https://github.com/MobileNativeFoundation/Store/blob/de9ed1764408eeaafe5e58fe602205c875a8b0b0/.github/workflows/create_swift_package.yml
12. `git commit -am "Prepare next development version."`
13. Create a PR with this commit and merge it.
14. Login to Sonatype to promote the artifacts https://central.sonatype.org/pages/releasing-the-deployment.html
* This part is automated. If it fails in CI, follow the steps below.
* Click on Staging Repositories under Build Promotion
* Select all the Repositories that contain the content you want to release
* Click on Close and refresh until the Release button is active
* Click Release and submit
15. Update the sample module's `build.gradle` to point to the newly released version. (It may take ~2 hours for artifact to be available after release)

1. Set `store` in `gradle/libs.versions.toml` to the new version. The Android and
Kotlin Multiplatform conventions use it for Maven publications, and the
Multiplatform convention also uses it for CocoaPods.
2. Update `CHANGELOG.md`, including the release link and `Unreleased` comparison.
3. Validate the build tooling and generated publication metadata before opening
the release PR:

```sh
./gradlew -p tooling :plugins:test :plugins:ktlintCheck
./gradlew verifyPublicationVersions
```

The metadata check validates generated POMs and Gradle module metadata,
including internal dependency versions and target redirects. Host-disabled
metadata generators can skip execution. Validate on macOS as well as Linux
to cover Apple publications. Existing files from skipped tasks are not checked.
4. Commit with a DCO sign-off and open a PR. Require passing tests, API and
formatting checks, and CI on the release commit. Record JVM and Native test
execution separately.
5. Merge the approved release PR. A successful push build on `main` automatically
publishes to Maven Central. Non-SNAPSHOT versions use
`publishAndReleaseToMavenCentral`; SNAPSHOT versions use `publishToMavenCentral`.
The macOS publication job verifies metadata before uploading artifacts.
6. Verify that consumers can resolve the new Maven coordinates and their internal
dependencies after publication completes.
7. For Swift distribution, run the **KMMBridge-Publish** workflow for the tested
release commit. Its reusable workflow reads the same catalog version, creates
or finds the GitHub release, publishes XCFrameworks, and updates the release tag.
Mark alpha and beta GitHub releases as prereleases and verify the Swift package
resolves the intended artifacts.

Do not reuse an already-published version. Prepare any subsequent development
version through another PR, accounting for automatic publication on `main`.
4 changes: 2 additions & 2 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ org.gradle.jvmargs=-XX:MaxMetaspaceSize=2G
# POM file
GROUP=org.mobilenativefoundation.store
# Project version lives in gradle/libs.versions.toml (store) and is applied to
# project.version by KotlinMultiplatformConventionPlugin.
# project.version by the Kotlin Multiplatform and Android convention plugins.
POM_PACKAGING=pom
POM_DESCRIPTION = Store5 is a Kotlin Multiplatform network-resilient repository layer

Expand All @@ -26,4 +26,4 @@ POM_DEVELOPER_NAME=Dropbox
kotlinx.atomicfu.enableJvmIrTransformation=false
kotlinx.atomicfu.enableJsIrTransformation=false

org.jetbrains.compose.experimental.uikit.enabled=true
org.jetbrains.compose.experimental.uikit.enabled=true
4 changes: 2 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ kmmBridge = "1.2.1"
ktlint = "1.8.0"
kover = "0.9.9"
#noinspection UnusedVersionCatalogEntry
store = "5.1.0-alpha12"
store = "5.1.0-beta01"
truth = "1.4.5"
turbine = "1.2.1"
binary-compatibility-validator = "0.18.1"
Expand Down Expand Up @@ -69,4 +69,4 @@ vanniktech-maven-publish = { id = "com.vanniktech.maven.publish", version.ref =
dokka = { id = "org.jetbrains.dokka" , version.ref = "dokkaGradlePlugin"}
atomicfu = { id = "org.jetbrains.kotlinx.atomicfu", version.ref = "atomicFu"}
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "baseKotlin"}
kotlin-cocoapods = { id = "org.jetbrains.kotlin.native.cocoapods", version.ref = "baseKotlin"}
kotlin-cocoapods = { id = "org.jetbrains.kotlin.native.cocoapods", version.ref = "baseKotlin"}
2 changes: 2 additions & 0 deletions tooling/plugins/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ kotlin {
}

dependencies {
implementation(localGroovy())
testImplementation(kotlin("test"))
compileOnly(libs.android.gradle.plugin)
compileOnly(libs.kotlin.gradle.plugin)
compileOnly(libs.dokka.gradle.plugin)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension

class AndroidConventionPlugin : Plugin<Project> {
override fun apply(project: Project) = with(project) {
version = versionCatalog.store

with(pluginManager) {
apply("com.android.library")
apply("com.vanniktech.maven.publish")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,11 +207,13 @@ fun KotlinMultiplatformExtension.android(configure: Action<KotlinMultiplatformAn

private fun Project.java(action: JavaPluginExtension.() -> Unit) = extensions.configure<JavaPluginExtension>(action)

fun Project.configureMavenPublishing() =
fun Project.configureMavenPublishing() {
configurePublicationVersionChecks()
extensions.configure<MavenPublishBaseExtension> {
publishToMavenCentral(automaticRelease = true)
signAllPublications()
}
}

fun Project.configureKmmBridge() =
extensions.configure<KmmBridgeExtension> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package org.mobilenativefoundation.store.tooling.plugins

import groovy.json.JsonSlurper
import org.gradle.api.Project
import org.gradle.api.publish.maven.tasks.GenerateMavenPom
import org.gradle.api.publish.tasks.GenerateModuleMetadata
import org.gradle.kotlin.dsl.withType
import org.w3c.dom.Element
import java.io.File
import java.net.URI
import javax.xml.parsers.DocumentBuilderFactory

fun Project.configurePublicationVersionChecks() {
val expectedVersion = versionCatalog.store
val expectedGroup = providers.gradleProperty("GROUP").get()
val pomTasks = tasks.withType<GenerateMavenPom>()
val metadataTasks = tasks.withType<GenerateModuleMetadata>()

pomTasks.configureEach {
inputs.property("storePublicationVersion", expectedVersion)
inputs.property("storePublicationGroup", expectedGroup)
doLast {
verifyPomVersions((this as GenerateMavenPom).destination, expectedGroup, expectedVersion)
}
}
metadataTasks.configureEach {
inputs.property("storePublicationVersion", expectedVersion)
inputs.property("storePublicationGroup", expectedGroup)
doLast {
verifyModuleVersions((this as GenerateModuleMetadata).outputFile.get().asFile, expectedGroup, expectedVersion)
}
}

val verification = tasks.register("verifyPublicationVersions") {
group = "verification"
description = "Generates and checks publication versions against the Store version catalog."
dependsOn(pomTasks, metadataTasks)
}
tasks.matching { it.name == "check" }.configureEach { dependsOn(verification) }
}

internal fun verifyPomVersions(file: File, expectedGroup: String, expectedVersion: String) {
val factory = DocumentBuilderFactory.newInstance().apply {
isNamespaceAware = true
setFeature("http://apache.org/xml/features/disallow-doctype-decl", true)
}
val project = factory.newDocumentBuilder().parse(file).documentElement
require(project.childText("groupId") == expectedGroup) { "${file.path}: unexpected publication group" }
requireVersion(project.childText("version"), expectedVersion, "${file.path}: project")

for (tag in listOf("dependency", "parent")) {
val references = project.getElementsByTagNameNS("*", tag)
for (index in 0 until references.length) {
val reference = references.item(index) as Element
if (reference.childText("groupId") == expectedGroup) {
requireVersion(reference.childText("version"), expectedVersion, "${file.path}: $tag ${reference.childText("artifactId")}")
}
}
}
}

internal fun verifyModuleVersions(file: File, expectedGroup: String, expectedVersion: String) {
val metadata = JsonSlurper().parse(file) as Map<*, *>
val component = metadata["component"] as Map<*, *>
require(component["group"] == expectedGroup) { "${file.path}: unexpected component group" }
requireVersion(component["version"], expectedVersion, "${file.path}: component")
component["url"]?.let { verifyRedirect(it as String, component["module"] as String, expectedVersion, file) }

for (variant in metadata["variants"] as List<*>) {
variant as Map<*, *>
val location = "${file.path}: variant ${variant["name"]}"
for (kind in listOf("dependencies", "dependencyConstraints")) {
for (reference in variant[kind] as? List<*> ?: emptyList<Any>()) {
reference as Map<*, *>
if (reference["group"] == expectedGroup) {
val version = reference["version"] as? Map<*, *> ?: emptyMap<Any, Any>()
val selectors = listOf("requires", "strictly", "prefers").mapNotNull { version[it] }
require(selectors.isNotEmpty()) { "$location: missing version for ${reference["module"]}" }
selectors.forEach { requireVersion(it, expectedVersion, "$location: ${reference["module"]}") }
require(expectedVersion !in (version["rejects"] as? List<*> ?: emptyList<Any>())) {
"$location: ${reference["module"]} rejects $expectedVersion"
}
}
}
}
val redirect = variant["available-at"] as? Map<*, *>
if (redirect != null && redirect["group"] == expectedGroup) {
requireVersion(redirect["version"], expectedVersion, "$location: available-at")
verifyRedirect(redirect["url"] as String, redirect["module"] as String, expectedVersion, file)
}
}
}

private fun Element.childText(name: String): String? =
(0 until childNodes.length)
.map { childNodes.item(it) }
.filterIsInstance<Element>()
.firstOrNull { it.localName == name }
?.textContent
?.trim()

private fun requireVersion(actual: Any?, expected: String, location: String) {
require(actual == expected) { "$location has version '$actual'; expected '$expected' from the Store version catalog" }
}

private fun verifyRedirect(url: String, module: String, expectedVersion: String, file: File) {
val path = URI(url).normalize().path.orEmpty()
val target = "$module/$expectedVersion/$module-$expectedVersion.module"
require(path == target || path.endsWith("/$target")) {
"${file.path}: metadata redirect '$url' does not reference '$target'"
}
}
Loading