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
10 changes: 8 additions & 2 deletions .github/release/checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import subprocess

CHECKS_APP = "github-actions"


def branch_problems(commit, branch, repo_root, runner=subprocess.run):
"""Problems unless the commit is an ancestor of origin/<branch>."""
Expand All @@ -10,6 +12,10 @@ def branch_problems(commit, branch, repo_root, runner=subprocess.run):


def missing_checks(check_runs, required):
"""Required check names without a successful run on the commit, from the GitHub check-runs API response."""
succeeded = {run["name"] for run in check_runs.get("check_runs", []) if run.get("conclusion") == "success"}
"""Required check names without a successful run on the commit, from the GitHub check-runs API response. Only runs
the repository's own workflows created count: any app allowed to write checks could create one with the same name."""
succeeded = {
run["name"] for run in check_runs.get("check_runs", [])
if run.get("conclusion") == "success" and (run.get("app") or {}).get("slug") == CHECKS_APP
}
return [name for name in required if name not in succeeded]
3 changes: 2 additions & 1 deletion .github/release/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"publishedModules": ":sdk,:sdk-native,:upload-plugin",
"requiredChecks": ["Build and Test the SDK", "OSV scan (all modules)", "Analyze (java)", "Analyze (cpp)"],
"consumerPairs": [
{ "name": "AGP 7.4.2 on Gradle 7.5", "gradle": "gradle", "agp": "7.4.2", "compileSdk": "24", "buildTools": "30.0.3" },
{ "name": "AGP 7.4.2 on Gradle 7.5", "gradle": "{env:GRADLE_7}", "agp": "7.4.2", "compileSdk": "24", "buildTools": "30.0.3" },
{ "name": "AGP 8.7.3 on Gradle 8.9", "gradle": "{env:GRADLE_8}", "agp": "8.7.3", "compileSdk": "35", "buildTools": "35.0.0" },
{ "name": "the checkout's AGP on the wrapper's Gradle", "gradle": "{gradlew}", "agp": "{project}", "compileSdk": "37", "buildTools": "37.0.0" }
],
"tagRules": [
Expand Down
22 changes: 20 additions & 2 deletions .github/release/consumers.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,39 @@
"""Gradle commands for the clean app in .github/release/consumers that builds against a staged release."""

import os
import re

ANDROID_PROJECT = ".github/release/consumers/android"
FLAGS = ["--no-daemon", "--no-configuration-cache", "--stacktrace"]
AGP_CLASSPATH = re.compile(r"""com\.android\.tools\.build:gradle:([^'"\s]+)""")
ENV_GRADLE = re.compile(r"\{env:(?P<name>[A-Za-z0-9_]+)\}")


def project_agp(build_gradle):
"""The AGP version the checkout's root build.gradle builds with."""
return AGP_CLASSPATH.search(build_gradle).group(1)


def android_command(pair, gradlew, repository, artifact, version, checkout_agp):
def gradle_executable(pair, gradlew, environ=None):
"""The Gradle one pair builds with: the checkout's wrapper for "{gradlew}", the executable an environment variable
holds for "{env:NAME}" (the release workflow installs each Gradle version and records its path there), else the
command as written. Raises ValueError when the named variable is not set."""
environ = os.environ if environ is None else environ
if pair["gradle"] == "{gradlew}":
return gradlew
match = ENV_GRADLE.fullmatch(pair["gradle"])
if match is None:
return pair["gradle"]
path = environ.get(match.group("name"))
if not path:
raise ValueError(f"{pair['name']}: {match.group('name')} does not name a Gradle executable in this environment")
return path


def android_command(pair, gradlew, repository, artifact, version, checkout_agp, environ=None):
"""Builds the consumer app with one AGP/Gradle pair against one artifact version: libraries are depended on, the
plugin is applied. A pair whose AGP is "{project}" uses the checkout's own AGP, which its Gradle wrapper supports."""
gradle = gradlew if pair["gradle"] == "{gradlew}" else pair["gradle"]
gradle = gradle_executable(pair, gradlew, environ)
agp = checkout_agp if pair["agp"] == "{project}" else pair["agp"]
command = [
gradle, "-p", ANDROID_PROJECT, *FLAGS, "assembleDebug",
Expand Down
14 changes: 13 additions & 1 deletion .github/release/release.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ def cmd_plan(args):
summary(lines + [f"- {artifact.coordinates}" for artifact in plan.artifacts])
problems = []
if args.target == "production":
if plan.version.split(".")[0] == "0":
problems.append(f"{plan.version} is a dry run version, a production release needs 1.0.0 or above")
if args.prerelease not in ("true", "false"):
problems.append("a production release needs a published GitHub release for this tag")
elif (args.prerelease == "true") != plan.prerelease:
Expand Down Expand Up @@ -291,7 +293,17 @@ def cmd_consumer(args):
if not artifact.consumer_probe:
continue
for pair in config["consumerPairs"]:
if subprocess.run(consumers.android_command(pair, gradlew, repository, artifact, plan.version, checkout_agp), cwd=REPO_ROOT).returncode != 0:
try:
command = consumers.android_command(pair, gradlew, repository, artifact, plan.version, checkout_agp)
except ValueError as error:
problems.append(str(error))
continue
try:
returncode = subprocess.run(command, cwd=REPO_ROOT).returncode
except OSError as error:
problems.append(f"{pair['name']}: could not run {command[0]}: {error}")
continue
if returncode != 0:
problems.append(f"{pair['name']}: a clean app could not build with {artifact.coordinates}:{plan.version}")
finish(problems, "Consumer builds")

Expand Down
12 changes: 11 additions & 1 deletion .github/release/tests/test_checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,20 @@

class ChecksTest(unittest.TestCase):
def test_reports_missing_and_failed_checks(self):
runs = {"check_runs": [{"name": "Build and Test the SDK", "conclusion": "success"}, {"name": "Analyze (java)", "conclusion": "failure"}]}
actions = {"slug": "github-actions"}
runs = {"check_runs": [{"name": "Build and Test the SDK", "conclusion": "success", "app": actions}, {"name": "Analyze (java)", "conclusion": "failure", "app": actions}]}
required = ["Build and Test the SDK", "Analyze (java)", "OSV scan (all modules)"]
self.assertEqual(missing_checks(runs, required), ["Analyze (java)", "OSV scan (all modules)"])

def test_only_runs_of_the_repository_workflows_count(self):
runs = {"check_runs": [
{"name": "Build and Test the SDK", "conclusion": "success", "app": {"slug": "some-other-app"}},
{"name": "Analyze (java)", "conclusion": "success"},
{"name": "OSV scan (all modules)", "conclusion": "success", "app": {"slug": "github-actions"}},
]}
required = ["Build and Test the SDK", "Analyze (java)", "OSV scan (all modules)"]
self.assertEqual(missing_checks(runs, required), ["Build and Test the SDK", "Analyze (java)"])

def test_tag_commit_must_be_on_the_release_branch(self):
calls = []

Expand Down
19 changes: 14 additions & 5 deletions .github/release/tests/test_consumers.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import unittest

from consumers import android_command, project_agp
from consumers import android_command, gradle_executable, project_agp
from plan import plan_for_tag
from tests.support import android_config

FLOOR = {"name": "AGP 7.4.2 on Gradle 7.5", "gradle": "gradle", "agp": "7.4.2", "compileSdk": "24", "buildTools": "30.0.3"}
FLOOR = {"name": "AGP 7.4.2 on Gradle 7.5", "gradle": "{env:GRADLE_7}", "agp": "7.4.2", "compileSdk": "24", "buildTools": "30.0.3"}
MIDDLE = {"name": "AGP 8.7.3 on Gradle 8.9", "gradle": "{env:GRADLE_8}", "agp": "8.7.3", "compileSdk": "35", "buildTools": "35.0.0"}
ENVIRON = {"GRADLE_7": "/gradle-7.5/bin/gradle", "GRADLE_8": "/gradle-8.9/bin/gradle"}
CURRENT = {"name": "the checkout's AGP on the wrapper's Gradle", "gradle": "{gradlew}", "agp": "{project}", "compileSdk": "37", "buildTools": "37.0.0"}


Expand All @@ -13,12 +15,12 @@ def setUp(self):
self.config = android_config()

def test_config_pairs(self):
self.assertEqual(self.config["consumerPairs"], [FLOOR, CURRENT])
self.assertEqual(self.config["consumerPairs"], [FLOOR, MIDDLE, CURRENT])

def test_sdk_on_the_floor_pair(self):
sdk = plan_for_tag(self.config, "26.2.0").artifacts[0]
command = android_command(FLOOR, "/repo/gradlew", "https://maven.test/", sdk, "26.2.0", "9.3.2")
self.assertEqual(command[:3], ["gradle", "-p", ".github/release/consumers/android"])
command = android_command(FLOOR, "/repo/gradlew", "https://maven.test/", sdk, "26.2.0", "9.3.2", ENVIRON)
self.assertEqual(command[:3], ["/gradle-7.5/bin/gradle", "-p", ".github/release/consumers/android"])
for part in ["assembleDebug", "-PagpVersion=7.4.2", "-PconsumerCompileSdk=24", "-PconsumerBuildTools=30.0.3", "-PcountlyRepository=https://maven.test/", "-PcountlyDependency=ly.count.android:sdk:26.2.0", "-PcountlyProbe=sdk"]:
self.assertIn(part, command)

Expand All @@ -31,6 +33,13 @@ def test_plugin_on_the_current_pair_uses_the_checkout_agp(self):
self.assertIn("-PcountlyProbe=none", command)
self.assertFalse(any(part.startswith("-PcountlyDependency=") for part in command))

def test_each_pair_resolves_its_own_gradle(self):
self.assertEqual(gradle_executable(FLOOR, "/repo/gradlew", ENVIRON), "/gradle-7.5/bin/gradle")
self.assertEqual(gradle_executable(MIDDLE, "/repo/gradlew", ENVIRON), "/gradle-8.9/bin/gradle")
self.assertEqual(gradle_executable(CURRENT, "/repo/gradlew", ENVIRON), "/repo/gradlew")
with self.assertRaisesRegex(ValueError, "GRADLE_8"):
gradle_executable(MIDDLE, "/repo/gradlew", {})

def test_reads_the_agp_version_of_the_checkout(self):
build_gradle = "buildscript {\n dependencies {\n classpath 'com.android.tools.build:gradle:9.2.0'\n classpath 'com.google.gms:google-services:4.4.3'\n }\n}\n"
self.assertEqual(project_agp(build_gradle), "9.2.0")
Expand Down
9 changes: 9 additions & 0 deletions .github/release/tests/test_release_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,15 @@ def test_prerelease_flag_must_match(self):
result = run_cli("plan", "--tag", tag, "--target", "production", "--prerelease", prerelease, "--out", str(self.folder / "plan.json"))
self.assertEqual(result.returncode, code, result.stdout)

def test_production_refuses_a_dry_run_version(self):
cases = [("0.0.3", "production", 1), ("native-0.0.1", "production", 1), ("plugin-0.1.0", "production", 1), ("0.0.3", "test", 0), ("26.2.0", "production", 0)]
for tag, target, code in cases:
with self.subTest(tag=tag, target=target):
result = run_cli("plan", "--tag", tag, "--target", target, "--prerelease", "false", "--out", str(self.folder / "plan.json"))
self.assertEqual(result.returncode, code, result.stdout)
if code:
self.assertIn("dry run version", result.stdout)


if __name__ == "__main__":
unittest.main()
35 changes: 27 additions & 8 deletions .github/workflows/build_and_test_sdk.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,18 @@ jobs:
run: docker pull ${{ env.EMULATOR_REPO }}

- name: Checkout SDK code
uses: actions/checkout@v4
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false

- name: Setup Java
uses: actions/setup-java@v3
uses: actions/setup-java@e9fbacdec3bb3b6036605a3e6f7995d66773a8c6 # v3.14.2
with:
java-version: ${{ env.JAVA_V }}
distribution: ${{ env.JAVA_DIST }}

- name: Setup Android SDK
uses: android-actions/setup-android@v3
uses: android-actions/setup-android@9fc6c4e9069bf8d3d10b2204b1fb8f6ef7065407 # v3.2.2
with:
# The action's default is "tools platform-tools", but Google has removed the
# obsolete "tools" package from the SDK repository, so the default now fails.
Expand Down Expand Up @@ -82,7 +84,8 @@ jobs:
run: ./gradlew sdk:connectedAndroidTest

- name: Publish Test Results to PR
uses: EnricoMi/publish-unit-test-result-action@v2
# the composite linux variant runs from the pinned source; the root action pulls a mutable Docker tag
uses: EnricoMi/publish-unit-test-result-action/linux@d0a4676d0e0b938bc201470d88276b7c74c712b3 # v2.24.0
if: always()
with:
check_name: "Unit Test Results :rocket:"
Expand Down Expand Up @@ -115,9 +118,25 @@ jobs:
upload-plugin/build/reports/jacoco/test/jacocoTestReport.xml 2> /dev/null | paste -sd, -)
echo "reports=$reports" >> "$GITHUB_OUTPUT"

# a pinned, checksum-verified reporter binary, and best effort, so a Codacy outage does not fail this build
- name: Upload Coverage to Codacy
if: always() && steps.coverage.outputs.reports != ''
uses: codacy/codacy-coverage-reporter-action@89d6c85cfafaec52c72b6c5e8b2878d33104c699 # v1.3.0
with:
project-token: ${{ secrets.CODACY_PROJECT_TOKEN }}
coverage-reports: ${{ steps.coverage.outputs.reports }}
continue-on-error: true
env:
CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }}
CODACY_REPORTER_VERSION: 14.1.3
CODACY_REPORTER_SHA512: 73a6c93fc5db509fd0423d6e545759c3f61d8360ee12054e32a9277daab6add0da6c6639248f6ed6c0042366440293ebc0b6144325aef52c5d3d4b230d65eb17
COVERAGE_REPORTS: ${{ steps.coverage.outputs.reports }}
run: |
curl -fsSL -o codacy-coverage-reporter "https://github.com/codacy/codacy-coverage-reporter/releases/download/${CODACY_REPORTER_VERSION}/codacy-coverage-reporter-linux"
echo "${CODACY_REPORTER_SHA512} codacy-coverage-reporter" | sha512sum -c -
chmod +x codacy-coverage-reporter
report_params=()
IFS=',' read -ra reports <<< "$COVERAGE_REPORTS"
for report in "${reports[@]}"; do
if [ -n "$report" ]; then
report_params+=(-r "$report")
fi
done
./codacy-coverage-reporter report --project-token "$CODACY_PROJECT_TOKEN" "${report_params[@]}" --partial
./codacy-coverage-reporter final --project-token "$CODACY_PROJECT_TOKEN"
6 changes: 3 additions & 3 deletions .github/workflows/codeql-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,11 @@ jobs:

steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0

# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
uses: github/codeql-action/init@9f759ee644a3e7c15c1390abf49868036c00067b # v3.38.3
with:
languages: ${{ matrix.language }}
# Autobuild fails for both languages here: the native code is prebuilt with scripts CodeQL cannot find, and the
Expand All @@ -55,4 +55,4 @@ jobs:
# queries: ./path/to/local/query, your-org/your-repo/queries@main

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
uses: github/codeql-action/analyze@9f759ee644a3e7c15c1390abf49868036c00067b # v3.38.3
24 changes: 12 additions & 12 deletions .github/workflows/dependency-security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,20 +42,20 @@ jobs:
pull-requests: write
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0

- name: Set up JDK 17
uses: actions/setup-java@v4
uses: actions/setup-java@cf277c60eb25467037889841efdb72551f06f6c3 # v4.9.1
with:
java-version: '17'
distribution: 'corretto'

- name: Set up Gradle
uses: gradle/actions/setup-gradle@v4
uses: gradle/actions/setup-gradle@ed408507eac070d1f99cc633dbcf757c94c7933a # v4.4.3

# The Android Gradle Plugin cannot configure the projects without an SDK present.
- name: Set up Android SDK
uses: android-actions/setup-android@v3
uses: android-actions/setup-android@9fc6c4e9069bf8d3d10b2204b1fb8f6ef7065407 # v3.2.2
with:
# The action's default is "tools platform-tools", but Google has removed the
# obsolete "tools" package from the SDK repository, so the default now fails.
Expand Down Expand Up @@ -88,7 +88,7 @@ jobs:
# instead of adding a new one on every push.
- name: Comment scan result on the pull request
if: always() && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
uses: actions/github-script@v7
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
with:
script: |
const fs = require('fs');
Expand Down Expand Up @@ -137,7 +137,7 @@ jobs:

- name: Upload dependency list
if: always()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: resolved-dependencies
path: |
Expand All @@ -158,16 +158,16 @@ jobs:
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-java@cf277c60eb25467037889841efdb72551f06f6c3 # v4.9.1
with:
java-version: '17'
distribution: 'corretto'
- uses: android-actions/setup-android@v3
- uses: android-actions/setup-android@9fc6c4e9069bf8d3d10b2204b1fb8f6ef7065407 # v3.2.2
with:
packages: platform-tools
- name: Submit resolved dependency graph to GitHub
uses: gradle/actions/dependency-submission@v4
uses: gradle/actions/dependency-submission@ed408507eac070d1f99cc633dbcf757c94c7933a # v4.4.3
env:
# Submit only the classpaths we actually declare — the same closed list the OSV
# report uses. Without this the graph also carries AGP's tooling configurations
Expand All @@ -194,9 +194,9 @@ jobs:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- name: Fail the PR on newly introduced vulnerable dependencies
uses: actions/dependency-review-action@v4
uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48 # v4.9.0
with:
fail-on-severity: ${{ env.SEVERITY_THRESHOLD }}
# The osv-scan job owns the single PR comment. A second bot comment here just
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/gradle_validator.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,5 @@ jobs:
name: "Validate Gradle Wrapper SHA-256 checksum"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: gradle/wrapper-validation-action@v2
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: gradle/wrapper-validation-action@b5418f5a58f5fd2eb486dd7efb368fe7be7eae45 # v2.1.3
Loading
Loading