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