Skip to content
Open
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
2 changes: 1 addition & 1 deletion .github/release-contract/java-ui.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
9 changes: 4 additions & 5 deletions .github/release/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand All @@ -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"
}
}
}
18 changes: 12 additions & 6 deletions .github/release/consumers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}",
]

Expand Down
13 changes: 12 additions & 1 deletion .github/release/consumers/gradle/settings.gradle
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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\\..*' }
}
}
}

Expand Down
4 changes: 4 additions & 0 deletions .github/release/consumers/maven/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
<id>countly</id>
<url>${countly.repository}</url>
</repository>
<repository>
<id>countly-public</id>
<url>${countly.publicRepository}</url>
</repository>
</repositories>

<dependencies>
Expand Down
15 changes: 1 addition & 14 deletions .github/release/contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -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=""):
Expand All @@ -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"]
Expand Down Expand Up @@ -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()),
}
Expand Down
10 changes: 6 additions & 4 deletions .github/release/release.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
49 changes: 31 additions & 18 deletions .github/release/tests/test_consumers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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__":
Expand Down
12 changes: 7 additions & 5 deletions .github/release/tests/test_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
20 changes: 12 additions & 8 deletions .github/release/tests/test_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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)
Expand Down
Loading
Loading