diff --git a/.github/release-contract/java-ui.json b/.github/release-contract/java-ui.json
index 44cc54dd..2f7af555 100644
--- a/.github/release-contract/java-ui.json
+++ b/.github/release-contract/java-ui.json
@@ -2,7 +2,7 @@
"schema": 1,
"coordinates": "ly.count.sdk:java-ui",
"pomDependencies": [
- "ly.count.sdk:java:{version}:compile",
+ "ly.count.sdk:java:26.8.0:compile",
"org.json:json:20250517:runtime",
"org.openjfx:javafx-controls:21.0.5:runtime",
"org.openjfx:javafx-web:21.0.5:runtime"
diff --git a/.github/release/config.json b/.github/release/config.json
index 05b08de2..d7bc32b7 100644
--- a/.github/release/config.json
+++ b/.github/release/config.json
@@ -6,7 +6,8 @@
"platformSpecificGroups": ["org.openjfx"],
"requiredChecks": ["Build and Test", "Test on ubuntu-latest", "Test on windows-latest", "OSV scan (all modules)", "Analyze (java)"],
"tagRules": [
- { "tag": "{version}", "branch": "staging", "artifacts": ["java", "java-ui"] }
+ { "tag": "{version}", "branch": "staging", "artifacts": ["java"] },
+ { "tag": "ui-{version}", "branch": "staging", "artifacts": ["java-ui"] }
],
"artifacts": {
"java": {
@@ -23,11 +24,9 @@
"group": "ly.count.sdk", "artifact": "java-ui", "module": ":sdk-java-ui",
"contract": ".github/release-contract/java-ui.json", "consumerJava": 17,
"versionSources": [
- { "file": "gradle.properties", "regex": "^VERSION_NAME=(.+)$" },
- { "file": "build.gradle", "regex": "ext\\.CLY_VERSION = \"([^\"]+)\"" },
- { "file": "sdk-java/src/main/java/ly/count/sdk/java/Config.java", "regex": "protected String sdkVersion = \"([^\"]+)\"" }
+ { "file": "sdk-java-ui/gradle.properties", "regex": "^VERSION_NAME=(.+)$" }
],
- "changelog": "CHANGELOG.md"
+ "changelog": "sdk-java-ui/CHANGELOG.md"
}
}
}
diff --git a/.github/release/consumers.py b/.github/release/consumers.py
index 5f480c9f..69eb7151 100644
--- a/.github/release/consumers.py
+++ b/.github/release/consumers.py
@@ -9,20 +9,26 @@
REPOSITORY_ID = "countly"
-def gradle_command(gradlew, repository, artifact, version):
- """Builds the Gradle consumer against one artifact version and installs it with its runtime classpath."""
+def gradle_command(gradlew, repository, public_repository, plan, artifact):
+ """Builds the Gradle consumer against one artifact of the plan and installs it with its runtime classpath. Only the
+ plan's artifacts come from the staging folder; any other Countly package they need, such as the core SDK of a UI
+ release, comes from the public repository or Maven Central, as it does for an integrator."""
+ staged = ",".join(item.coordinates for item in plan.artifacts)
return [
gradlew, "-p", GRADLE_PROJECT, *GRADLE_FLAGS, "clean", "installDist",
- f"-PcountlyRepository={repository}", f"-PcountlyDependency={artifact.coordinates}:{version}",
+ f"-PcountlyRepository={repository}", f"-PcountlyPublicRepository={public_repository}", f"-PcountlyStaged={staged}",
+ f"-PcountlyDependency={artifact.coordinates}:{plan.version}",
f"-PcountlyRelease={artifact.consumer_java}", f"-PcountlyProbe={artifact.artifact}",
]
-def maven_command(mvn, repository, artifact, version, local_repository):
- """Compiles the Maven consumer against one artifact version, with an empty local repository so nothing is reused."""
+def maven_command(mvn, repository, public_repository, artifact, version, local_repository):
+ """Compiles the Maven consumer against one artifact version, with an empty local repository so nothing is reused.
+ Maven looks in the staging folder first, then in the public repository, then on Maven Central."""
return [
mvn, "-B", "-f", MAVEN_POM, "clean", "compile",
- f"-Dcountly.repository={repository}", f"-Dcountly.artifact={artifact.artifact}", f"-Dcountly.version={version}",
+ f"-Dcountly.repository={repository}", f"-Dcountly.publicRepository={public_repository}",
+ f"-Dcountly.artifact={artifact.artifact}", f"-Dcountly.version={version}",
f"-Dcountly.release={artifact.consumer_java}", f"-Dmaven.repo.local={local_repository}",
]
diff --git a/.github/release/consumers/gradle/settings.gradle b/.github/release/consumers/gradle/settings.gradle
index b663fe4e..aeac43f8 100644
--- a/.github/release/consumers/gradle/settings.gradle
+++ b/.github/release/consumers/gradle/settings.gradle
@@ -1,4 +1,6 @@
def countlyRepository = gradle.startParameter.projectProperties['countlyRepository']
+def countlyPublicRepository = gradle.startParameter.projectProperties['countlyPublicRepository']
+def countlyStaged = gradle.startParameter.projectProperties['countlyStaged'].split(',')
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
@@ -7,9 +9,18 @@ dependencyResolutionManagement {
forRepository {
maven { url = countlyRepository }
}
- filter { includeGroupByRegex 'ly\\.count\\..*' }
+ filter {
+ countlyStaged.each { coordinates ->
+ def (group, name) = coordinates.split(':')
+ includeModule(group, name)
+ }
+ }
}
mavenCentral()
+ maven {
+ url = countlyPublicRepository
+ content { includeGroupByRegex 'ly\\.count\\..*' }
+ }
}
}
diff --git a/.github/release/consumers/maven/pom.xml b/.github/release/consumers/maven/pom.xml
index 931a459c..66e339ad 100644
--- a/.github/release/consumers/maven/pom.xml
+++ b/.github/release/consumers/maven/pom.xml
@@ -16,6 +16,10 @@
countly
${countly.repository}
+
+ countly-public
+ ${countly.publicRepository}
+
diff --git a/.github/release/contract.py b/.github/release/contract.py
index 26afb471..4dc3e2ab 100644
--- a/.github/release/contract.py
+++ b/.github/release/contract.py
@@ -8,7 +8,6 @@
POM_NAMESPACE = {"m": "http://maven.apache.org/POM/4.0.0"}
JVM_VERSION = "org.gradle.jvm.version"
-RELEASE_VERSION = "{version}"
def _child(element, tag, default=""):
@@ -24,18 +23,6 @@ def pom_dependencies(pom):
)
-def release_dependencies(dependencies, group, version):
- """Dependencies with the version of the release itself written as {version} for artifacts of the same group (java-ui
- depends on java of its own release), so a contract holds for every release."""
- shown = []
- for dependency in dependencies:
- parts = dependency.split(":")
- if parts[0] == group and parts[2] == version:
- parts[2] = RELEASE_VERSION
- shown.append(":".join(parts))
- return shown
-
-
def module_variants(module):
"""Library variants (not sources or javadoc) of a Gradle module file."""
return [variant for variant in json.loads(module).get("variants", []) if variant.get("attributes", {}).get("org.gradle.category") == "library"]
@@ -79,7 +66,7 @@ def actual_contract(staging_dir, artifact, version):
return {
"schema": 1,
"coordinates": artifact.coordinates,
- "pomDependencies": release_dependencies(pom_dependencies((folder / f"{base}.pom").read_bytes()), artifact.group, version),
+ "pomDependencies": pom_dependencies((folder / f"{base}.pom").read_bytes()),
"moduleVariantAttributes": {JVM_VERSION: _common(module_variants((folder / f"{base}.module").read_bytes()), JVM_VERSION)},
"maxClassFileMajor": max_class_major((folder / f"{base}.jar").read_bytes()),
}
diff --git a/.github/release/release.py b/.github/release/release.py
index 3e98d00c..46a0ce26 100644
--- a/.github/release/release.py
+++ b/.github/release/release.py
@@ -276,15 +276,16 @@ def cmd_verify_public(args):
def cmd_consumer(args):
"""Builds a clean Gradle project and a clean Maven project against every artifact of the staging folder, used as a
local repository, and runs the Java 8 artifacts on a Java 8 runtime, so a release that a clean project cannot build
- with stops before the approval."""
- _, plan = read_plan(args.plan)
+ with stops before the approval. Countly packages outside the release come from the target's public repository."""
+ config, plan = read_plan(args.plan)
repository = Path(args.staging).resolve().as_uri()
+ public_repository = config["publicBaseUrl"][args.target].rstrip("/")
gradlew = str(REPO_ROOT / ("gradlew.bat" if os.name == "nt" else "gradlew"))
problems = []
lines = []
for artifact in plan.artifacts:
name = f"{artifact.coordinates}:{plan.version}"
- if subprocess.run(consumers.gradle_command(gradlew, repository, artifact, plan.version), cwd=REPO_ROOT).returncode != 0:
+ if subprocess.run(consumers.gradle_command(gradlew, repository, public_repository, plan, artifact), cwd=REPO_ROOT).returncode != 0:
problems.append(f"a clean Gradle project could not build with {name}")
elif artifact.consumer_java == 8:
smoke = subprocess.run(consumers.smoke_command(args.java8_home, REPO_ROOT), cwd=REPO_ROOT, capture_output=True, text=True)
@@ -295,7 +296,7 @@ def cmd_consumer(args):
lines.append(f"- Java 8 run of {artifact.coordinates}: {smoke.stdout.strip()}")
local_repository = Path(args.work) / f"m2-{artifact.artifact}"
shutil.rmtree(local_repository, ignore_errors=True)
- if subprocess.run(consumers.maven_command(args.mvn, repository, artifact, plan.version, local_repository.resolve()), cwd=REPO_ROOT).returncode != 0:
+ if subprocess.run(consumers.maven_command(args.mvn, repository, public_repository, artifact, plan.version, local_repository.resolve()), cwd=REPO_ROOT).returncode != 0:
problems.append(f"a clean Maven project could not build with {name}")
else:
problems += consumers.maven_source_problems(local_repository, plan, artifact)
@@ -392,6 +393,7 @@ def main(argv=None):
command = commands.add_parser("consumer")
command.add_argument("--plan", required=True)
command.add_argument("--staging", required=True)
+ command.add_argument("--target", choices=TARGETS, required=True)
command.add_argument("--java8-home", required=True)
command.add_argument("--mvn", default="mvn")
command.add_argument("--work", required=True)
diff --git a/.github/release/tests/test_consumers.py b/.github/release/tests/test_consumers.py
index 09e6f274..b1c75d14 100644
--- a/.github/release/tests/test_consumers.py
+++ b/.github/release/tests/test_consumers.py
@@ -6,23 +6,30 @@
from plan import plan_for_tag
from tests.support import repository_config
+PUBLIC = "https://maven.countly.com"
+
class ConsumersTest(unittest.TestCase):
def setUp(self):
- self.plan = plan_for_tag(repository_config(), "26.8.1")
- self.java, self.ui = self.plan.artifacts
-
- def test_gradle_builds_each_artifact_at_its_java_level(self):
- command = gradle_command("/repo/gradlew", "file:///staging", self.ui, "26.8.1")
+ config = repository_config()
+ self.core_plan = plan_for_tag(config, "26.9.0")
+ self.ui_plan = plan_for_tag(config, "ui-26.8.1")
+ self.java = self.core_plan.artifacts[0]
+ self.ui = self.ui_plan.artifacts[0]
+
+ def test_gradle_takes_only_the_staged_artifacts_from_the_staging_folder(self):
+ command = gradle_command("/repo/gradlew", "file:///staging", PUBLIC, self.ui_plan, self.ui)
self.assertEqual(command[:3], ["/repo/gradlew", "-p", ".github/release/consumers/gradle"])
- for part in ["installDist", "-PcountlyRepository=file:///staging", "-PcountlyDependency=ly.count.sdk:java-ui:26.8.1", "-PcountlyRelease=17", "-PcountlyProbe=java-ui"]:
+ for part in ["installDist", "-PcountlyRepository=file:///staging", f"-PcountlyPublicRepository={PUBLIC}", "-PcountlyStaged=ly.count.sdk:java-ui", "-PcountlyDependency=ly.count.sdk:java-ui:26.8.1", "-PcountlyRelease=17", "-PcountlyProbe=java-ui"]:
self.assertIn(part, command)
- self.assertIn("-PcountlyRelease=8", gradle_command("/repo/gradlew", "file:///staging", self.java, "26.8.1"))
+ core = gradle_command("/repo/gradlew", "file:///staging", PUBLIC, self.core_plan, self.java)
+ for part in ["-PcountlyStaged=ly.count.sdk:java", "-PcountlyDependency=ly.count.sdk:java:26.9.0", "-PcountlyRelease=8"]:
+ self.assertIn(part, core)
- def test_maven_uses_an_empty_local_repository(self):
- command = maven_command("mvn", "file:///staging", self.java, "26.8.1", "/work/m2-java")
+ def test_maven_uses_an_empty_local_repository_and_the_public_repository(self):
+ command = maven_command("mvn", "file:///staging", PUBLIC, self.java, "26.9.0", "/work/m2-java")
self.assertEqual(command[:4], ["mvn", "-B", "-f", ".github/release/consumers/maven/pom.xml"])
- for part in ["-Dcountly.artifact=java", "-Dcountly.version=26.8.1", "-Dcountly.release=8", "-Dmaven.repo.local=/work/m2-java"]:
+ for part in [f"-Dcountly.publicRepository={PUBLIC}", "-Dcountly.artifact=java", "-Dcountly.version=26.9.0", "-Dcountly.release=8", "-Dmaven.repo.local=/work/m2-java"]:
self.assertIn(part, command)
def test_smoke_run_uses_the_given_runtime_and_the_installed_classpath(self):
@@ -31,19 +38,25 @@ def test_smoke_run_uses_the_given_runtime_and_the_installed_classpath(self):
self.assertEqual(Path(command[2]), Path("/repo/.github/release/consumers/gradle/build/install/countly-consumer/lib/*"))
self.assertEqual(command[3], "ly.count.consumer.Probe")
- def write_record(self, local, artifact, text):
+ def write_record(self, local, artifact, version, text):
"""Writes Maven's download record for one artifact version into the local repository."""
- folder = Path(local) / artifact.folder("26.8.1")
+ folder = Path(local) / artifact.folder(version)
folder.mkdir(parents=True, exist_ok=True)
(folder / "_remote.repositories").write_text(text, encoding="utf-8")
- def test_maven_must_resolve_from_the_staged_release(self):
+ def test_maven_must_resolve_the_release_from_the_staged_folder(self):
+ local = tempfile.mkdtemp()
+ self.assertEqual(maven_source_problems(local, self.core_plan, self.java), ["Maven did not download ly.count.sdk:java:26.9.0"])
+ self.write_record(local, self.java, "26.9.0", "#NOTE: This is a Maven Resolver internal implementation file\njava-26.9.0.jar>countly=\njava-26.9.0.pom>countly=\n")
+ self.assertEqual(maven_source_problems(local, self.core_plan, self.java), [])
+ self.write_record(local, self.ui, "26.8.1", "java-ui-26.8.1.jar>central=\njava-ui-26.8.1.pom>countly=\n")
+ self.assertEqual(maven_source_problems(local, self.ui_plan, self.ui), ["Maven resolved ly.count.sdk:java-ui:26.8.1 from another repository than the staged release"])
+
+ def test_the_core_of_a_ui_release_may_come_from_anywhere(self):
local = tempfile.mkdtemp()
- self.assertEqual(maven_source_problems(local, self.plan, self.java), ["Maven did not download ly.count.sdk:java:26.8.1"])
- self.write_record(local, self.java, "#NOTE: This is a Maven Resolver internal implementation file\njava-26.8.1.jar>countly=\njava-26.8.1.pom>countly=\n")
- self.assertEqual(maven_source_problems(local, self.plan, self.java), [])
- self.write_record(local, self.ui, "java-ui-26.8.1.jar>central=\njava-ui-26.8.1.pom>countly=\n")
- self.assertEqual(maven_source_problems(local, self.plan, self.ui), ["Maven resolved ly.count.sdk:java-ui:26.8.1 from another repository than the staged release"])
+ self.write_record(local, self.ui, "26.8.1", "java-ui-26.8.1.jar>countly=\njava-ui-26.8.1.pom>countly=\n")
+ self.write_record(local, self.java, "26.8.0", "java-26.8.0.jar>central=\njava-26.8.0.pom>central=\n")
+ self.assertEqual(maven_source_problems(local, self.ui_plan, self.ui), [])
if __name__ == "__main__":
diff --git a/.github/release/tests/test_contract.py b/.github/release/tests/test_contract.py
index e984c6fd..7a82f9e0 100644
--- a/.github/release/tests/test_contract.py
+++ b/.github/release/tests/test_contract.py
@@ -50,11 +50,13 @@ def test_dependency_only_in_the_module_file_is_reported(self):
f"the .module declares ['{JSR305}', '{JSON}', 'org.slf4j:slf4j-api:2.0.17'] but the POM declares ['{JSR305}', '{JSON}']",
])
- def test_own_release_version_is_a_placeholder(self):
- ui = plan_for_tag(self.config, "26.8.1").artifacts[1]
- pom = pom_bytes(["ly.count.sdk:java:26.8.1:compile", "ly.count.sdk:java:26.8.0:compile", JSON + ":runtime"])
- write_artifact(self.staging, ui, "26.8.1", pom=pom, module=module_bytes(17, ["ly.count.sdk:java:26.8.1", JSON]))
- self.assertEqual(actual_contract(self.staging, ui, "26.8.1")["pomDependencies"], ["ly.count.sdk:java:26.8.0:compile", "ly.count.sdk:java:{version}:compile", JSON + ":runtime"])
+ def test_the_core_version_of_the_ui_is_compared_exactly(self):
+ ui = plan_for_tag(self.config, "ui-26.8.1").artifacts[0]
+ contract = {"schema": 1, "coordinates": "ly.count.sdk:java-ui", "pomDependencies": ["ly.count.sdk:java:26.8.0:compile", JSON + ":runtime"], "moduleVariantAttributes": {"org.gradle.jvm.version": 17}, "maxClassFileMajor": 61}
+ write_artifact(self.staging, ui, "26.8.1", main=jar_bytes([61]), pom=pom_bytes(["ly.count.sdk:java:26.8.1:compile", JSON + ":runtime"]), module=module_bytes(17, ["ly.count.sdk:java:26.8.1", JSON]))
+ actual = actual_contract(self.staging, ui, "26.8.1")
+ self.assertEqual(actual["pomDependencies"], ["ly.count.sdk:java:26.8.1:compile", JSON + ":runtime"])
+ self.assertEqual(compare(contract, actual), [f"POM dependencies are ['ly.count.sdk:java:26.8.1:compile', '{JSON}:runtime'], the contract says ['ly.count.sdk:java:26.8.0:compile', '{JSON}:runtime']"])
def test_highest_class_version_wins(self):
self.assertEqual(max_class_major(jar_bytes([52, 50])), 52)
diff --git a/.github/release/tests/test_layout.py b/.github/release/tests/test_layout.py
index b66a1c72..1264e0e1 100644
--- a/.github/release/tests/test_layout.py
+++ b/.github/release/tests/test_layout.py
@@ -13,9 +13,10 @@ def setUp(self):
self.config = repository_config()
self.staging = Path(tempfile.mkdtemp())
self.plan = plan_for_tag(self.config, "26.8.1")
+ self.ui_plan = plan_for_tag(self.config, "ui-26.8.1")
def test_expected_files_of_a_jar(self):
- ui = self.plan.artifacts[1]
+ ui = self.ui_plan.artifacts[0]
self.assertEqual(primary_files(ui, "26.8.1"), ["java-ui-26.8.1.jar", "java-ui-26.8.1.pom", "java-ui-26.8.1.module", "java-ui-26.8.1-sources.jar", "java-ui-26.8.1-javadoc.jar", "java-ui-26.8.1-cyclonedx.json"])
self.assertEqual(len(expected_files(ui, "26.8.1")), 30)
@@ -27,19 +28,22 @@ def test_checksums_are_plain_hex(self):
self.assertEqual((self.staging / "a.txt.sha1").read_bytes(), b"a9993e364706816aba3e25717850c26c9cd0d89d")
def test_complete_staging_passes(self):
- for artifact in self.plan.artifacts:
- write_artifact(self.staging, artifact, self.plan.version)
- self.assertEqual(check_staging(self.staging, self.plan), [])
+ for plan in (self.plan, self.ui_plan):
+ with self.subTest(tag=plan.tag):
+ staging = Path(tempfile.mkdtemp())
+ for artifact in plan.artifacts:
+ write_artifact(staging, artifact, plan.version)
+ self.assertEqual(check_staging(staging, plan), [])
- def test_both_artifacts_are_required(self):
+ def test_a_tag_stages_only_its_own_artifact(self):
+ write_artifact(self.staging, self.ui_plan.artifacts[0], self.ui_plan.version)
write_artifact(self.staging, self.plan.artifacts[0], self.plan.version)
- problems = check_staging(self.staging, self.plan)
+ problems = check_staging(self.staging, self.ui_plan)
self.assertEqual(len(problems), 30)
- self.assertIn("missing ly/count/sdk/java-ui/26.8.1/java-ui-26.8.1.jar", problems)
+ self.assertIn("unexpected ly/count/sdk/java/26.8.1/java-26.8.1.jar", problems)
def test_missing_unexpected_and_wrong_files_are_reported(self):
folder = write_artifact(self.staging, self.plan.artifacts[0], self.plan.version)
- write_artifact(self.staging, self.plan.artifacts[1], self.plan.version)
(folder / "java-26.8.1-javadoc.jar").unlink()
(folder / "notes.txt").write_bytes(b"x")
(folder / "java-26.8.1.pom.sha1").write_bytes(b"0" * 40)
diff --git a/.github/release/tests/test_plan.py b/.github/release/tests/test_plan.py
index 9fab6c50..9ca31e6e 100644
--- a/.github/release/tests/test_plan.py
+++ b/.github/release/tests/test_plan.py
@@ -5,7 +5,8 @@
from plan import PlanError, checkout_tags, plan_for_tag, plan_from_json
from tests.support import repository_config
-BOTH = ["ly.count.sdk:java", "ly.count.sdk:java-ui"]
+JAVA = ["ly.count.sdk:java"]
+UI = ["ly.count.sdk:java-ui"]
class PlanTest(unittest.TestCase):
@@ -14,49 +15,66 @@ def setUp(self):
def test_tags(self):
cases = [
- ("26.8.1", "26.8.1", True),
- ("26.8.1-rc1", "26.8.1-rc1", False),
- ("0.0.1", "0.0.1", True),
+ ("26.8.1", JAVA, "26.8.1", True),
+ ("26.8.1-rc1", JAVA, "26.8.1-rc1", False),
+ ("0.0.1", JAVA, "0.0.1", True),
+ ("ui-26.8.1", UI, "26.8.1", True),
+ ("ui-26.8.1-rc1", UI, "26.8.1-rc1", False),
+ ("ui-0.0.1", UI, "0.0.1", True),
]
- for tag, version, listed in cases:
+ for tag, artifacts, version, listed in cases:
with self.subTest(tag=tag):
plan = plan_for_tag(self.config, tag)
self.assertEqual(plan.branch, "staging")
- self.assertEqual([artifact.coordinates for artifact in plan.artifacts], BOTH)
+ self.assertEqual([artifact.coordinates for artifact in plan.artifacts], artifacts)
self.assertEqual(plan.version, version)
self.assertEqual(plan.listed, listed)
self.assertEqual(plan.prerelease, not listed)
def test_refused_tags(self):
- refused = ["v26.8.1", "26.8", "26.8.1-RC1", "26.8.1-rc.1", " 26.8.1", "26.8.1-nw", "native-26.8.1", "plugin-26.8.1", "26.8.1/../x", "java-26.8.1", "٢٦.8.1"]
+ refused = [
+ "v26.8.1", "26.8", "26.8.1-RC1", "26.8.1-rc.1", " 26.8.1", "26.8.1-nw", "native-26.8.1", "plugin-26.8.1",
+ "26.8.1/../x", "java-26.8.1", "٢٦.8.1", "ui-26.8", "ui-v26.8.1", "UI-26.8.1", "ui26.8.1",
+ "java-ui-26.8.1", "26.8.1-ui", "ui-ui-26.8.1",
+ ]
for tag in refused:
with self.subTest(tag=tag):
with self.assertRaises(PlanError):
plan_for_tag(self.config, tag)
def test_modules_and_round_trip(self):
- plan = plan_for_tag(self.config, "26.8.1")
- self.assertEqual(plan.modules, [":sdk-java", ":sdk-java-ui"])
- self.assertEqual(plan.artifacts[1].module_dir, "sdk-java-ui")
- self.assertEqual(plan.artifacts[1].folder("26.8.1"), "ly/count/sdk/java-ui/26.8.1")
- self.assertEqual(plan_from_json(self.config, plan.to_json()), plan)
+ core = plan_for_tag(self.config, "26.8.1")
+ ui = plan_for_tag(self.config, "ui-26.8.1")
+ self.assertEqual(core.modules, [":sdk-java"])
+ self.assertEqual(ui.modules, [":sdk-java-ui"])
+ self.assertEqual(ui.artifacts[0].module_dir, "sdk-java-ui")
+ self.assertEqual(ui.artifacts[0].folder("26.8.1"), "ly/count/sdk/java-ui/26.8.1")
+ for plan in (core, ui):
+ self.assertEqual(plan_from_json(self.config, plan.to_json()), plan)
def test_artifact_details(self):
- java, ui = plan_for_tag(self.config, "26.8.1").artifacts
+ java = plan_for_tag(self.config, "26.8.1").artifacts[0]
+ ui = plan_for_tag(self.config, "ui-26.8.1").artifacts[0]
self.assertEqual((java.contract, java.consumer_java), (".github/release-contract/java.json", 8))
self.assertEqual((ui.contract, ui.consumer_java), (".github/release-contract/java-ui.json", 17))
- def test_checkout_tags_follow_the_branch(self):
+ def test_checkout_tags_follow_each_version_file(self):
root = Path(tempfile.mkdtemp())
- (root / "gradle.properties").write_bytes(b"VERSION_NAME=26.8.1-rc1\r\nGROUP=ly.count.sdk\r\n")
- self.assertEqual(checkout_tags(self.config, root, "staging"), ["26.8.1-rc1"])
+ (root / "gradle.properties").write_bytes(b"VERSION_NAME=26.9.0-rc1\r\nGROUP=ly.count.sdk\r\n")
+ (root / "sdk-java-ui").mkdir()
+ (root / "sdk-java-ui/gradle.properties").write_bytes(b"POM_ARTIFACT_ID=java-ui\r\nVERSION_NAME=26.8.0\r\n")
+ self.assertEqual(checkout_tags(self.config, root, "staging"), ["26.9.0-rc1", "ui-26.8.0"])
self.assertEqual(checkout_tags(self.config, root, "master"), [])
def test_checkout_tags_need_a_readable_version(self):
root = Path(tempfile.mkdtemp())
with self.assertRaises(PlanError):
checkout_tags(self.config, root, "staging")
- (root / "gradle.properties").write_bytes(b"GROUP=ly.count.sdk\n")
+ (root / "gradle.properties").write_bytes(b"VERSION_NAME=26.9.0\n")
+ with self.assertRaises(PlanError):
+ checkout_tags(self.config, root, "staging")
+ (root / "sdk-java-ui").mkdir()
+ (root / "sdk-java-ui/gradle.properties").write_bytes(b"POM_ARTIFACT_ID=java-ui\n")
with self.assertRaises(PlanError):
checkout_tags(self.config, root, "staging")
diff --git a/.github/release/tests/test_release_cli.py b/.github/release/tests/test_release_cli.py
index f9cec37e..4c5cd063 100644
--- a/.github/release/tests/test_release_cli.py
+++ b/.github/release/tests/test_release_cli.py
@@ -20,17 +20,23 @@ def setUp(self):
self.folder = Path(tempfile.mkdtemp())
def test_plan_writes_plan_json_and_the_task_lists(self):
- out = self.folder / "plan.json"
- output = self.folder / "output"
- output.write_bytes(b"")
- result = run_cli("plan", "--tag", "26.8.1", "--target", "test", "--out", str(out), output=str(output))
- self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
- self.assertEqual(json.loads(out.read_text(encoding="utf-8"))["artifacts"], ["java", "java-ui"])
- lines = dict(line.split("=", 1) for line in output.read_text(encoding="utf-8").splitlines())
- self.assertEqual(lines["environment"], "maven-test")
- self.assertEqual(lines["check_tasks"], ":sdk-java:check :sdk-java-ui:check")
- self.assertEqual(lines["published_modules"], ":sdk-java,:sdk-java-ui")
- self.assertIn(":sdk-java-ui:publishAllPublicationsToReleaseStagingRepository :sdk-java-ui:cyclonedxDirectBom", lines["publish_tasks"])
+ cases = [
+ ("26.8.1", ["java"], ":sdk-java:check", ":sdk-java:publishAllPublicationsToReleaseStagingRepository :sdk-java:cyclonedxDirectBom"),
+ ("ui-26.8.1", ["java-ui"], ":sdk-java-ui:check", ":sdk-java-ui:publishAllPublicationsToReleaseStagingRepository :sdk-java-ui:cyclonedxDirectBom"),
+ ]
+ for tag, artifacts, check_tasks, publish_tasks in cases:
+ with self.subTest(tag=tag):
+ out = self.folder / "plan.json"
+ output = self.folder / "output"
+ output.write_bytes(b"")
+ result = run_cli("plan", "--tag", tag, "--target", "test", "--out", str(out), output=str(output))
+ self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
+ self.assertEqual(json.loads(out.read_text(encoding="utf-8"))["artifacts"], artifacts)
+ lines = dict(line.split("=", 1) for line in output.read_text(encoding="utf-8").splitlines())
+ self.assertEqual(lines["environment"], "maven-test")
+ self.assertEqual(lines["check_tasks"], check_tasks)
+ self.assertEqual(lines["publish_tasks"], publish_tasks)
+ self.assertEqual(lines["published_modules"], ":sdk-java,:sdk-java-ui")
def test_plan_refuses_an_unknown_tag(self):
result = run_cli("plan", "--tag", "v1", "--target", "test", "--out", str(self.folder / "plan.json"))
@@ -44,7 +50,9 @@ def test_checkout_tags_are_a_json_list_the_matrix_can_read(self):
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
line = output.read_text(encoding="utf-8").strip()
self.assertTrue(line.startswith("tags="), line)
- self.assertEqual(len(json.loads(line[len("tags="):])), 1)
+ tags = json.loads(line[len("tags="):])
+ self.assertEqual(len(tags), 2)
+ self.assertTrue(tags[1].startswith("ui-"), tags)
def test_a_branch_without_releases_gets_the_empty_list_the_workflow_skips_on(self):
output = self.folder / "output"
diff --git a/.github/release/tests/test_sbom.py b/.github/release/tests/test_sbom.py
index 4d757ef5..5b884089 100644
--- a/.github/release/tests/test_sbom.py
+++ b/.github/release/tests/test_sbom.py
@@ -25,7 +25,8 @@
class SbomTest(unittest.TestCase):
def setUp(self):
self.config = repository_config()
- self.java, self.ui = plan_for_tag(self.config, "26.8.1").artifacts
+ self.java = plan_for_tag(self.config, "26.8.1").artifacts[0]
+ self.ui = plan_for_tag(self.config, "ui-26.8.1").artifacts[0]
def test_normalize_sets_the_published_coordinates(self):
bom = normalize(copy.deepcopy(PLUGIN_BOM), self.ui, "26.8.1", "2026-10-27T10:15:00+03:00", "https://maven.countly.com/")
diff --git a/.github/release/tests/test_sources.py b/.github/release/tests/test_sources.py
index 58ea2cad..a8c8f243 100644
--- a/.github/release/tests/test_sources.py
+++ b/.github/release/tests/test_sources.py
@@ -50,6 +50,21 @@ def test_windows_line_endings_are_accepted(self):
self.write_sdk("26.8.1\r", "26.8.1", "26.8.1", "## 26.8.1\r\n")
self.assertEqual(check_version_sources(self.root, plan_for_tag(self.config, "26.8.1"), self.config), [])
+ def test_ui_uses_its_own_version_and_changelog(self):
+ self.write_sdk("26.9.0", "26.9.0", "26.9.0", "## 26.9.0\n")
+ self.write("sdk-java-ui/gradle.properties", "POM_ARTIFACT_ID=java-ui\nVERSION_NAME=26.8.1\n")
+ self.write("sdk-java-ui/CHANGELOG.md", "## 26.8.1\n* Fixed the survey card.\n")
+ self.assertEqual(check_version_sources(self.root, plan_for_tag(self.config, "ui-26.8.1"), self.config), [])
+ self.assertEqual(check_version_sources(self.root, plan_for_tag(self.config, "26.9.0"), self.config), [])
+
+ def test_ui_mismatches_are_reported(self):
+ self.write("sdk-java-ui/gradle.properties", "POM_ARTIFACT_ID=java-ui\nVERSION_NAME=26.8.0\n")
+ self.write("sdk-java-ui/CHANGELOG.md", "## 26.8.0\n")
+ self.assertEqual(check_version_sources(self.root, plan_for_tag(self.config, "ui-26.8.1"), self.config), [
+ "sdk-java-ui/gradle.properties: version is 26.8.0, the tag says 26.8.1",
+ "sdk-java-ui/CHANGELOG.md: no '## 26.8.1' heading",
+ ])
+
def test_every_version_source_is_found_in_this_checkout(self):
root = Path(__file__).resolve().parents[3]
for spec in self.config["artifacts"].values():
diff --git a/.github/release/tests/test_upload.py b/.github/release/tests/test_upload.py
index 45740ef9..df667346 100644
--- a/.github/release/tests/test_upload.py
+++ b/.github/release/tests/test_upload.py
@@ -24,9 +24,8 @@ def setUp(self):
self.plan = plan_for_tag(self.config, "26.8.1")
def stage(self):
- """Stages both artifacts of the tag and returns the folder of java."""
- folders = [write_artifact(self.staging, artifact, self.plan.version) for artifact in self.plan.artifacts]
- return folders[0]
+ """Stages the artifact of the tag and returns its folder."""
+ return write_artifact(self.staging, self.plan.artifacts[0], self.plan.version)
def test_pom_family_goes_last(self):
names = ["a.pom", "a.pom.asc", "a.pom.sha1", "a.jar", "a.jar.sha1", "a.module"]
@@ -37,9 +36,8 @@ def test_upload_creates_then_skips_identical_files(self):
bucket = FakeBucket()
upload_release(bucket, self.staging, self.plan, log=quiet)
stored = dict(bucket.objects)
- java_calls = [key for key in bucket.calls if key.startswith("ly/count/sdk/java/")]
- self.assertEqual(java_calls[-1], POM)
- self.assertEqual(bucket.calls[-1], "ly/count/sdk/java-ui/26.8.1/java-ui-26.8.1.pom")
+ self.assertEqual(bucket.calls[-1], POM)
+ self.assertEqual(len(bucket.calls), 30)
self.assertEqual(stored[POM][2], IMMUTABLE)
self.assertEqual(stored[JAR][1], "application/java-archive")
upload_release(bucket, self.staging, self.plan, log=quiet)
diff --git a/.github/release/tests/test_verify.py b/.github/release/tests/test_verify.py
index 1840bc23..62aef12f 100644
--- a/.github/release/tests/test_verify.py
+++ b/.github/release/tests/test_verify.py
@@ -26,7 +26,6 @@ def setUp(self):
BASE + POM + ".sha1": b"sha1",
BASE + POM + ".asc": b"signature",
BASE + "ly/count/sdk/java/maven-metadata.xml": b"26.8.1",
- BASE + "ly/count/sdk/java-ui/maven-metadata.xml": b"26.8.1",
})
def test_published_release_passes_and_keeps_the_signatures(self):
@@ -37,13 +36,19 @@ def test_published_release_passes_and_keeps_the_signatures(self):
def test_problems_are_reported(self):
self.web.pages[BASE + POM] = b"changed"
del self.web.pages[BASE + POM + ".asc"]
- self.web.pages[BASE + "ly/count/sdk/java-ui/maven-metadata.xml"] = b"26.8.0"
+ self.web.pages[BASE + "ly/count/sdk/java/maven-metadata.xml"] = b"26.8.0"
self.assertEqual(verify_public(self.manifest, self.plan, BASE, "1", self.work, fetcher=self.web), [
f"changed {POM}",
f"missing {POM}.asc",
- "ly.count.sdk:java-ui index does not list 26.8.1",
+ "ly.count.sdk:java index does not list 26.8.1",
])
+ def test_a_ui_release_checks_only_the_ui_index(self):
+ plan = plan_for_tag(self.config, "ui-26.8.1")
+ web = FakeWeb({BASE + "ly/count/sdk/java-ui/maven-metadata.xml": b"26.8.1"})
+ self.assertEqual(verify_public({"files": []}, plan, BASE, "1", self.work, fetcher=web), [])
+ self.assertEqual(web.requests, [BASE + "ly/count/sdk/java-ui/maven-metadata.xml?nocache=1"])
+
def test_candidate_must_stay_unlisted(self):
plan = plan_for_tag(self.config, "26.8.1-rc1")
web = FakeWeb({BASE + "ly/count/sdk/java/maven-metadata.xml": b"26.8.1-rc1"})
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index d95216c3..ca1f397f 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -121,7 +121,7 @@ jobs:
- name: Check what the release promises integrators
run: python3 .github/release/release.py contract --plan plan.json --staging build/release-staging
- name: Clean projects build against the staged release
- run: python3 .github/release/release.py consumer --plan plan.json --staging build/release-staging --java8-home "$JAVA_HOME_8_X64" --work "$RUNNER_TEMP/consumers"
+ run: python3 .github/release/release.py consumer --plan plan.json --staging build/release-staging --target "$TARGET" --java8-home "$JAVA_HOME_8_X64" --work "$RUNNER_TEMP/consumers"
- name: Scan the published dependencies
env:
OSV_FAIL_ON: high
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 81a5fe71..ce785af5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,6 +19,7 @@
```
Versions released before this one stay available on Maven Central. The files are signed with the same key as before (`FC8DB0DE234A273BA45E562FB8C83A079A5BBD0C`).
+* ! Minor breaking change ! `ly.count.sdk:java-ui` now has its own version numbers and is released only when it changes; see its own [CHANGELOG](sdk-java-ui/CHANGELOG.md). Use the latest version of each package instead of one shared number.
## 26.8.0
* Added support for the Content feature, accessible through the "Countly.instance().content()" interface:
diff --git a/app-javafx/build.gradle b/app-javafx/build.gradle
index fe048f2d..3a67df46 100644
--- a/app-javafx/build.gradle
+++ b/app-javafx/build.gradle
@@ -29,6 +29,15 @@ dependencies {
implementation 'org.json:json:20250517'
}
+// sdk-java-ui depends on a released ly.count.sdk:java. The demo runs everything on
+// the local sdk-java module instead, so two copies of the core never meet on its
+// classpath.
+configurations.configureEach {
+ resolutionStrategy.dependencySubstitution {
+ substitute module('ly.count.sdk:java') using project(':sdk-java')
+ }
+}
+
application {
mainClass = 'ly.count.javafx.demo.Main'
}
diff --git a/build.gradle b/build.gradle
index 7f224fc9..dea50f88 100644
--- a/build.gradle
+++ b/build.gradle
@@ -58,6 +58,11 @@ allprojects {
repositories {
google()
mavenCentral()
+ // Core SDK releases published after the move from Maven Central, which sdk-java-ui can depend on.
+ maven {
+ url = 'https://maven.countly.com'
+ content { includeGroupByRegex 'ly\\.count\\..*' }
+ }
}
}
diff --git a/sdk-java-ui/CHANGELOG.md b/sdk-java-ui/CHANGELOG.md
new file mode 100644
index 00000000..26be802d
--- /dev/null
+++ b/sdk-java-ui/CHANGELOG.md
@@ -0,0 +1,5 @@
+## XX.XX.XX
+* ! Minor breaking change ! `ly.count.sdk:java-ui` now has its own version numbers and is released only when it changes, so its version no longer follows the core SDK `ly.count.sdk:java`. It names the core version it needs, currently 26.8.0; if your project also declares `ly.count.sdk:java`, use that version or a newer one.
+
+## 26.8.0
+* First release, together with the Java SDK 26.8.0. See the Java SDK's [CHANGELOG](../CHANGELOG.md) for what it displays.
diff --git a/sdk-java-ui/build.gradle b/sdk-java-ui/build.gradle
index 5d0a2ad9..613e3463 100644
--- a/sdk-java-ui/build.gradle
+++ b/sdk-java-ui/build.gradle
@@ -59,8 +59,11 @@ javafx {
dependencies {
// 'api' so a consumer of this artifact also gets the core SDK, whose types
- // appear in this package's public signatures.
- api project(':sdk-java')
+ // appear in this package's public signatures. A released core, not the sdk-java
+ // project: this artifact is released on its own, so what it is built and tested
+ // against is exactly what its POM tells integrators to use. Raise it only to a
+ // core version that is already published.
+ api 'ly.count.sdk:java:26.8.0'
implementation 'org.json:json:20250517'
diff --git a/sdk-java-ui/gradle.properties b/sdk-java-ui/gradle.properties
index 980570f3..6e431c5e 100644
--- a/sdk-java-ui/gradle.properties
+++ b/sdk-java-ui/gradle.properties
@@ -1,5 +1,7 @@
#RELEASE FIELDS
POM_ARTIFACT_ID=java-ui
+# Released on its own with tags ui-X.Y.Z, so it overrides the root VERSION_NAME of the core SDK.
+VERSION_NAME=26.8.0
POM_NAME=Countly Java SDK UI
POM_DESCRIPTION=JavaFX user interface for the Countly Java SDK, displaying feedback widgets and content