Skip to content

Scaffold android-cli Gradle plugin#1

Merged
ClankerGuru merged 2 commits intomainfrom
initial-scaffold
Apr 17, 2026
Merged

Scaffold android-cli Gradle plugin#1
ClankerGuru merged 2 commits intomainfrom
initial-scaffold

Conversation

@ClankerGuru
Copy link
Copy Markdown
Owner

@ClankerGuru ClankerGuru commented Apr 17, 2026

Summary

Initial scaffold for zone.clanker.gradle.android-cli — a Gradle settings plugin wrapping Google's agent-first android CLI.

Structure

clkx convention plugins (conventions / library / module / publish / ktlint / detekt / konsist / cover / toolchain), same as clkx-agents.

Exec-based tasks grouped by CLI subcommand:

zone/clanker/gradle/androidcli/
├── AndroidCli.kt                     (data object: constants, SettingsExtension, SettingsPlugin)
└── task/
    ├── AndroidExecTask.kt            (abstract base: PATH check)
    ├── AndroidVersionTask.kt
    ├── AndroidDescribeTask.kt
    ├── AndroidRunTask.kt
    ├── screen/AndroidScreenCaptureTask.kt
    └── skills/{List,Find,Add,Remove}Task.kt

Quality gates

  • 24 tests, 93% kover coverage (no exclusions)
  • ktlint, detekt, konsist all green
  • Konsist checks forbid println, System.out/err, wildcard imports in main

CI

  • build.yml — runs ./gradlew build on push/PR to main
  • release.yml — publishes to Maven Central on GitHub release (Vanniktech, CENTRAL_PORTAL)

Test plan

  • Build workflow passes on this PR
  • Verify plugin publishes to mavenLocal: ./gradlew :plugin:publishToMavenLocal
  • Sanity-check task list: ./gradlew tasks --group android-cli

Summary by CodeRabbit

  • New Features

    • Android CLI Gradle plugin now available with integrated tasks for version queries, device screen capture, skill discovery and management, and direct CLI execution.
  • Chores

    • Automated build and release pipelines configured.
    • Build infrastructure and quality assurance tooling established.

Settings plugin wrapping Google's agent-first 'android' CLI.

- Plugin ID: zone.clanker.gradle.android-cli
- Artifact: zone.clanker:android-cli
- clkx convention plugins (conventions/library/module/publish/ktlint/detekt/konsist/cover/toolchain)
- Exec-based tasks grouped by CLI subcommand:
  - task/: AndroidExecTask (base), version, describe, run
  - task/screen/: screen capture
  - task/skills/: list, find, add, remove
- Gradle property forwarding for skill name, query, args, JSON flag
- 24 tests, 93% kover coverage (no exclusions)
- Konsist forbidden-pattern checks on main source
- CI: build on push/PR to main, release to Maven Central on published release
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 17, 2026

Warning

Rate limit exceeded

@ClankerGuru has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 35 minutes and 52 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 35 minutes and 52 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ab4e16d0-3115-4234-87dc-5b51186b8d3b

📥 Commits

Reviewing files that changed from the base of the PR and between 42beb8e and 3844adf.

📒 Files selected for processing (2)
  • config/detekt.yml
  • plugin/src/test/kotlin/zone/clanker/gradle/androidcli/AndroidCliPluginTest.kt
📝 Walkthrough

Walkthrough

This pull request establishes a complete Gradle plugin project infrastructure for wrapping Google's Android CLI tool. It introduces CI/CD workflows, build logic conventions, Maven publishing configuration, a Gradle settings plugin that registers CLI wrapper tasks, and comprehensive test coverage.

Changes

Cohort / File(s) Summary
GitHub Actions Workflows
.github/workflows/build.yml, .github/workflows/release.yml
New CI/CD pipelines: build workflow on push/PR to main; release workflow on GitHub release publish with Maven Central signing and publishing.
Gradle Wrapper & Root Configuration
gradle/wrapper/gradle-wrapper.properties, gradlew, gradlew.bat, gradle.properties, settings.gradle.kts
Gradle 9.4.1 wrapper setup with Maven publishing metadata (group, version, POM fields, SCM/developer info, Sonatype Central Portal).
Build Logic Convention Plugins
build-logic/settings.gradle.kts, build-logic/build.gradle.kts, build-logic/src/main/kotlin/clkx-*.gradle.kts
Gradle convention plugin suite: module (Java/Kotlin/group/Gradle API), toolchain (Java 17 JetBrains), testing (Kotest/JUnit5/GradleTestKit), coverage (Kover 90\% threshold), konsist (slop tests), detekt (analysis), ktlint (formatting), library/plugin/publish (Maven config).
Gradle Settings Plugin Implementation
build-logic/src/main/kotlin/zone/clanker/gradle/conventions/ClkxSettingsPlugin.kt
Settings plugin enforcing centralized Maven/Gradle repositories, disallowing project-level repo declarations, auto-resolving toolchains via Foojay.
Code Quality Configuration
config/detekt.yml
Detekt rules: complexity thresholds (cyclomatic, class size, method length), naming conventions, exception/performance/style checks, magic number exclusions.
Plugin Module Configuration
plugin/build.gradle.kts, plugin/gradle.properties
Plugin module applies conventions, registers Gradle plugin zone.clanker.gradle.android-cli mapping to AndroidCli$SettingsPlugin entrypoint.
Plugin Entrypoint & Task Base
plugin/src/main/kotlin/zone/clanker/gradle/androidcli/AndroidCli.kt, plugin/src/main/kotlin/zone/clanker/gradle/androidcli/task/AndroidExecTask.kt
AndroidCli data object defines constants/task names/properties; SettingsPlugin registers CLI wrapper tasks on root project; SettingsExtension configures binary name and extra CLI args; AndroidExecTask base class verifies binary availability via which before execution.
CLI Task Implementations
plugin/src/main/kotlin/zone/clanker/gradle/androidcli/task/Android{Version,Describe,Run}Task.kt, plugin/src/main/kotlin/zone/clanker/gradle/androidcli/task/screen/AndroidScreenCaptureTask.kt, plugin/src/main/kotlin/zone/clanker/gradle/androidcli/task/skills/AndroidSkills{List,Find,Add,Remove}Task.kt
Eight tasks extending AndroidExecTask, each building CLI command arguments: version/describe/run read optional properties (PROP_JSON/PROP_ARGS); screen-capture fixed args; skills tasks read PROP_SKILL/PROP_QUERY with validation.
Test Suite
plugin/src/test/kotlin/zone/clanker/gradle/androidcli/AndroidCliPluginTest.kt, plugin/src/test/kotlin/zone/clanker/gradle/androidcli/AndroidTasksTest.kt
AndroidCliPluginTest validates constants, extension configuration, task registration, wiring; AndroidTasksTest verifies buildArgs() for all tasks, property parsing, missing-property error handling, binary-not-found error cases.
Structural Testing
plugin/src/slopTest/kotlin/zone/clanker/gradle/androidcli/ForbiddenPatternTest.kt
Konsist test forbidding println(, System.err/out, and wildcard imports in production code.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Poem

🐰 A gradle warren now stands tall and bright,
With plugins, tasks, and workflows in sight—
Convention stones stack neat and wise,
While Android CLI's wrappers arise,
Build-logic builds our burrow just right! 🏗️✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Scaffold android-cli Gradle plugin' directly and clearly describes the primary change—setting up the initial structure for a Gradle plugin. It accurately summarizes the main objective of the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch initial-scaffold

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (13)
build-logic/src/main/kotlin/clkx-testing.gradle.kts (1)

5-10: Consider extracting Kotest version into a version catalog.

Three Kotest coordinates are duplicated with the same literal 5.9.1. If/when a libs.versions.toml is added, consolidate these to a single kotest version key so upgrades touch one place. Not blocking for a scaffold PR.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@build-logic/src/main/kotlin/clkx-testing.gradle.kts` around lines 5 - 10,
Extract the repeated Kotest version "5.9.1" into the version catalog and
reference that single key from the dependencies block: add a kotest = "5.9.1"
entry in libs.versions.toml (or the project version catalog) and then replace
the three literals in the dependencies block referencing
"io.kotest:kotest-runner-junit5", "io.kotest:kotest-assertions-core", and
"io.kotest:kotest-framework-datatest" with the catalog version reference (e.g.,
use the libs.versions.kotest entry) so upgrades only change the single kotest
version key.
build-logic/src/main/kotlin/clkx-cover.gradle.kts (1)

5-21: Exclusion filters are report-wide; consider scoping to verify only.

Excluding @TaskAction methods and *SettingsPlugin classes under reports.filters removes them from the HTML/XML reports as well as from verification. If you want the HTML report to still show these classes (useful for spotting that they exist and are uncovered) while keeping the 90% gate focused on testable code, move the excludes under verify { rule { ... } } instead of the top-level filters. Otherwise, this looks consistent with the 93% coverage claim in the PR description.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@build-logic/src/main/kotlin/clkx-cover.gradle.kts` around lines 5 - 21, The
current kover configuration places excludes under reports.filters which removes
them from both HTML/XML reports and verification; move the
annotatedBy("org.gradle.api.tasks.TaskAction") and classes("*$SettingsPlugin",
"*$SettingsPlugin$*") exclusion entries out of reports { filters { ... } } and
instead add them under verify { rule { ... } } so the minBound(90) verification
uses those excludes while the reports still show the unexcluded classes; update
the kover block (kover -> reports -> filters -> remove those excludes; kover ->
reports -> verify -> rule -> include the excludes alongside minBound(90)) to
implement this scoping change.
.github/workflows/build.yml (1)

12-27: Consider adding concurrency cancellation for PR pushes.

Rapid pushes to a PR will queue redundant builds. A small concurrency block saves CI time and Gradle cache contention:

♻️ Suggested addition
 permissions:
   contents: read

+concurrency:
+  group: build-${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: ${{ github.event_name == 'pull_request' }}
+
 jobs:
   build:
     runs-on: ubuntu-latest

Also optional: pin third-party actions (gradle/actions/setup-gradle@v4) by commit SHA for supply-chain hardening — not required for a scaffold.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/build.yml around lines 12 - 27, Add a GitHub Actions
concurrency block to the workflow's jobs.build section to cancel in-progress
runs for the same PR on new pushes (use the concurrency key with a name like
"build-${{ github.ref }}" and cancel-in-progress: true) so redundant Gradle
builds started by rapid PR pushes are canceled; optionally pin external actions
like actions/checkout@v4 and gradle/actions/setup-gradle@v4 to specific commit
SHAs for supply-chain hardening.
build-logic/src/main/kotlin/clkx-library.gradle.kts (1)

4-17: Verify clkx-library and clkx-conventions are never co-applied.

clkx-conventions applies clkx-publish, which calls mavenPublishing { configure(GradlePlugin(...)) }, while clkx-library calls configure(JavaLibrary(...)). If any module ever applies both, the later configure(...) call silently overrides the earlier one, changing the publication type. Currently, only the :plugin module applies clkx-conventions and no modules apply clkx-library, so this is safe today.

Consider either:

  • Dropping com.vanniktech.maven.publish from clkx-publish and letting clkx-library and clkx-conventions each own their configure(...) calls, or
  • Adding a check (or README note) that clkx-library and clkx-conventions are mutually exclusive.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@build-logic/src/main/kotlin/clkx-library.gradle.kts` around lines 4 - 17, The
mavenPublishing configure call in clkx-library can silently override the
configure call from clkx-conventions (which applies clkx-publish/GradlePlugin),
so add a guard to prevent both from being applied: in the clkx-library
plugin/application code (where you apply
id("clkx-library")/mavenPublishing/JavaLibrary) check for the presence of
clkx-conventions/clkx-publish (e.g.,
project.plugins.hasPlugin("clkx-conventions") or
pluginManager.withPlugin("clkx-conventions") ) and throw a clear GradleException
(or fail the configuration) if both are present; alternatively, if you prefer
ownership changes, remove the com.vanniktech.maven.publish application from
clkx-publish and let each module call mavenPublishing.configure(...) for its own
publication type (JavaLibrary vs GradlePlugin) and update README to state they
are mutually exclusive.
build-logic/src/main/kotlin/clkx-konsist.gradle.kts (1)

7-13: slopTest classpath doesn't include the test source set output.

compileClasspath/runtimeClasspath only inherit from main, and the configurations extend testImplementation/testRuntimeOnly (dependencies), not sourceSets["test"].output (compiled test classes). That's likely intentional here since the Konsist check only needs production scope — just flagging so future slop tests that want to reuse test helpers/fixtures don't silently compile against a narrower classpath than expected.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@build-logic/src/main/kotlin/clkx-konsist.gradle.kts` around lines 7 - 13, The
slopTest source set currently only adds sourceSets["main"].output to its
compileClasspath and runtimeClasspath, so compiled test classes
(sourceSets["test"].output) are not available; update the slopTest configuration
(the val slopTest block) to also include sourceSets["test"].output in
compileClasspath and runtimeClasspath so slop tests can compile/run against test
helpers/fixtures, and keep the existing configuration extensions from
testImplementation/testRuntimeOnly as-is.
plugin/src/slopTest/kotlin/zone/clanker/gradle/androidcli/ForbiddenPatternTest.kt (1)

12-28: Substring checks are brittle and will false-positive on strings/comments.

it.text.contains("println(") / "System.err" / "System.out" match anywhere in the file, including doc comments, string literals, and unrelated identifiers (e.g., a future System.out_of_memory_handler wouldn't, but "// prefer logging over println(...)" in a KDoc would). Konsist provides stricter AST-based checks — scope.functions()/files.functions filtered by name, and scope.imports/file property access — that avoid this. Consider something like:

scope.imports.assertFalse { it.name == "java.lang.System.out" || it.name == "java.lang.System.err" }
scope.functions(includeNested = true).assertFalse { it.name == "println" } // for calls, use Konsist's call APIs

Not blocking, but today a stray doc-comment mentioning println( would break the build.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugin/src/slopTest/kotlin/zone/clanker/gradle/androidcli/ForbiddenPatternTest.kt`
around lines 12 - 28, Replace brittle substring checks that inspect raw file
text (scope.files.assertFalse { it.text.contains("...") }) with AST-aware
checks: use scope.functions(includeNested = true).assertFalse { it.name ==
"println" } to detect actual println function declarations/calls and use
scope.imports.assertFalse { it.name == "java.lang.System.out" || it.name ==
"java.lang.System.err" } (or equivalent file-level import/property accessors) to
detect direct System.out/System.err usage; update the three then-blocks that
reference println(, System.err and System.out to use these Konsist APIs
(scope.functions and scope.imports) so comments and string literals no longer
trigger false positives.
build-logic/build.gradle.kts (1)

10-17: Consider centralizing plugin versions in the version catalog.

Hard-coding versions here duplicates what a libs.versions.toml typically owns and makes bumps (e.g., Detekt, Kover) noisier. Given the scaffold is still small this is purely optional.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@build-logic/build.gradle.kts` around lines 10 - 17, The dependencies block
currently hardcodes plugin versions (e.g.,
"org.jetbrains.kotlinx:kover-gradle-plugin:0.9.1",
"io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.23.8",
"org.jlleitschuh.gradle:ktlint-gradle:12.2.0",
"com.vanniktech:gradle-maven-publish-plugin:0.32.0",
"org.gradle.toolchains:foojay-resolver:1.0.0"); centralize these by adding
entries in the version catalog (libs.versions.toml) for each plugin coordinate
and version, then update the dependencies block in build.gradle.kts to use the
catalog references (e.g., libs.plugins.detekt, libs.plugins.kover, etc.) or the
typed accessors (libs.xxx) instead of hardcoded :version strings; ensure the
version keys are named clearly (kover, detekt, ktlint, mavenPublish, foojay) and
that any build logic that expects the previous strings is adjusted to use the
catalog symbols.
.github/workflows/release.yml (1)

31-34: Redundant ./gradlew build before publish.

publishAndReleaseToMavenCentral already depends on assemble/check through the publication and sign chains, so the preceding build step roughly doubles wall-clock time for releases. If you want a pre-publish safety net, consider ./gradlew check publishAndReleaseToMavenCentral in a single invocation so Gradle can reuse the task graph and up-to-date checks.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/release.yml around lines 31 - 34, Replace the separate
"Build and test" and "Publish to Maven Central" steps with a single Gradle
invocation so Gradle can reuse the task graph; specifically, update the step(s)
referencing the run commands in the steps named "Build and test" and "Publish to
Maven Central" to invoke ./gradlew check publishAndReleaseToMavenCentral
-PVERSION_NAME=${{ steps.version.outputs.version }} (instead of running
./gradlew build and then publishAndReleaseToMavenCentral separately).
plugin/src/main/kotlin/zone/clanker/gradle/androidcli/task/screen/AndroidScreenCaptureTask.kt (1)

1-13: Consider exposing screenshot output path as a task input.

Users invoking android-screen-capture typically want to control where the screenshot is written. Relying solely on extension.extraArgs (which is global across all tasks) makes per-invocation output path awkward. Consider a task-level @Input/@Option or a dedicated -P property akin to PROP_SKILL/PROP_ARGS, forwarded as the CLI's output flag. Non-blocking for the scaffold.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugin/src/main/kotlin/zone/clanker/gradle/androidcli/task/screen/AndroidScreenCaptureTask.kt`
around lines 1 - 13, The task currently hardcodes CLI args via
extension.extraArgs and doesn't expose a per-task output path; add a task-level
property (e.g., an `@Input` and `@Option` annotated var named outputPath or
screenshotPath) on AndroidScreenCaptureTask and include its flag in buildArgs()
so doFirst uses it when present; update buildArgs() to merge
listOf("screen","capture") + optional output flag (from the new outputPath
property) + extension.extraArgs, and document that outputPath overrides/augments
the global extraArgs.
plugin/src/main/kotlin/zone/clanker/gradle/androidcli/task/AndroidDescribeTask.kt (1)

12-19: Minor: toBoolean() silently accepts any non-"true" value as false.

"yes", "1", or a typo like "ture" all evaluate to false, which may surprise users who expect -Pzone.clanker.android-cli.json=1 to work. Acceptable for a scaffold; consider documenting the accepted values or using toBooleanStrictOrNull() to surface misuse. Non-blocking.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugin/src/main/kotlin/zone/clanker/gradle/androidcli/task/AndroidDescribeTask.kt`
around lines 12 - 19, The JSON flag parsing in buildArgs (function buildArgs,
property AndroidCli.PROP_JSON) uses String.toBoolean() which silently treats any
non-"true" value as false; change to String.toBooleanStrictOrNull() and handle
the null case so malformed values are surfaced: if the property is absent keep
the existing default (false), but if the property is present and
toBooleanStrictOrNull() returns null, throw a clear GradleException or log an
error indicating acceptable values ("true"/"false") so callers using
"1"/"yes"/typos get immediate feedback; leave the rest of buildArgs (adding
"describe", "--json", and extension.extraArgs) unchanged.
plugin/src/main/kotlin/zone/clanker/gradle/androidcli/AndroidCli.kt (2)

43-47: Nit: explicit Action { } wrapper is unnecessary.

Gradle.rootProject accepts a SAM Action<? super Project>, so a lambda works directly:

-            settings.gradle.rootProject(
-                Action { rootProject ->
-                    registerTasks(rootProject, extension)
-                },
-            )
+            settings.gradle.rootProject { rootProject ->
+                registerTasks(rootProject, extension)
+            }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugin/src/main/kotlin/zone/clanker/gradle/androidcli/AndroidCli.kt` around
lines 43 - 47, The call uses an unnecessary explicit Action wrapper; replace the
Action { rootProject -> registerTasks(rootProject, extension) } with a direct
lambda passed to settings.gradle.rootProject so that settings.gradle.rootProject
{ rootProject -> registerTasks(rootProject, extension) } (i.e., remove the
explicit Action wrapper around the lambda used to invoke registerTasks).

51-95: Optional: extract task-registration boilerplate.

The 9 register blocks all repeat it.group = GROUP; it.description = ...; it.extension = extension. A small helper would shrink the surface and make adding new tasks a one-liner.

♻️ Sketch
private inline fun <reified T : AndroidExecTask> Project.registerAndroidTask(
    name: String,
    description: String,
    extension: SettingsExtension,
) = tasks.register(name, T::class.java) {
    it.group = GROUP
    it.description = description
    it.extension = extension
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugin/src/main/kotlin/zone/clanker/gradle/androidcli/AndroidCli.kt` around
lines 51 - 95, Extract the repeated task-registration boilerplate inside
registerTasks by adding a small helper (e.g. a private inline fun
registerAndroidTask<T : AndroidExecTask>(name: String, description: String,
extension: SettingsExtension)) and then replace the nine tasks registered in
registerTasks (TASK_VERSION, TASK_DESCRIBE, TASK_SCREEN_CAPTURE,
TASK_SKILLS_LIST, TASK_SKILLS_FIND, TASK_SKILLS_ADD, TASK_SKILLS_REMOVE,
TASK_RUN) with single-line calls to that helper that pass the corresponding
AndroidVersionTask/AndroidDescribeTask/AndroidScreenCaptureTask/AndroidSkillsListTask/AndroidSkillsFindTask/AndroidSkillsAddTask/AndroidSkillsRemoveTask/AndroidRunTask
classes, the description strings, and the extension; the helper should set
it.group = GROUP, it.description = description and it.extension = extension so
behavior is unchanged.
plugin/src/test/kotlin/zone/clanker/gradle/androidcli/AndroidTasksTest.kt (1)

30-77: Test scoping caveat with BehaviorSpec.

Project/extension creation lives in the given/when body rather than inside then. With Kotest's default isolation, that body is executed when the spec is constructed and shared across the contained then blocks. Each given/when currently has only a single then, so behavior is correct today, but adding a second then to any block would silently leak mutations (e.g., ext.extraArgs at Line 70, extraProperties set at Line 52/61) into sibling tests.

Consider moving setup into then blocks (or using beforeTest) to keep tests independent as coverage grows.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugin/src/test/kotlin/zone/clanker/gradle/androidcli/AndroidTasksTest.kt`
around lines 30 - 77, The tests use Kotest BehaviorSpec with project/extension
setup (newProject(),
project.extensions.extraProperties.set(AndroidCli.PROP_JSON, ...), and
ext.extraArgs) inside given/when blocks which run at spec construction and thus
share state across their then blocks; move the creation of the Project and
extension and any mutations to inside each then block (or use a per-test
lifecycle like beforeTest) so AndroidDescribeTask/AndroidVersionTask tests
(referencing AndroidCli.TASK_DESCRIBE, AndroidCli.TASK_VERSION,
AndroidDescribeTask, AndroidCli.PROP_JSON and ext.extraArgs) get a fresh
newProject() per assertion and cannot leak state between tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/release.yml:
- Around line 20-25: The step "Extract version from tag" currently interpolates
github.event.release.tag_name directly in the shell; instead set an environment
variable (e.g., RELEASE_TAG) to ${{ github.event.release.tag_name }} for that
step and reference "$RELEASE_TAG" inside the run block, then perform the strip
operation (VERSION="${RELEASE_TAG#v}") and echo to GITHUB_OUTPUT; ensure the
variable is double-quoted when used to safely handle special characters.

In `@build-logic/src/main/kotlin/clkx-plugin.gradle.kts`:
- Around line 1-5: Add a mavenPublishing configuration that registers this
Gradle plugin artifact the same way the library convention does: call
mavenPublishing.configure(GradlePlugin(...)) for the plugin artifact and set its
JavaLibrary publishing options to JavaLibrary(javadocJar = JavadocJar.Empty())
so the published plugin gets the empty Javadoc jar and consistent POM metadata;
locate the plugins block in clkx-plugin.gradle.kts and add a
mavenPublishing.configure block referencing GradlePlugin and
JavaLibrary/JavadocJar.Empty to mirror the sibling
clkx-library.gradle.kts/clkx-publish.gradle.kts setup.

In `@config/detekt.yml`:
- Line 106: The allowedPatterns value is over-escaped so Detekt sees literal
backslashes and the exemption never matches TODO(owner): comments; update the
allowedPatterns entry (allowedPatterns) to use single backslashes in the regex
(e.g., change the current 'TODO\\(.*\\):' to use single backslashes like
'TODO\(.*\):' so Detekt receives the intended pattern and the ForbiddenComment
exemption for TODO(name): works correctly.

In `@gradle/wrapper/gradle-wrapper.properties`:
- Line 3: Upgrade the incompatible Gradle plugins to versions that support
Gradle 9: update the ktlint-gradle plugin (org.jlleitschuh.gradle.ktlint) from
12.2.0 to at least 13.1.0 (recommend 14.2.0) and update the detekt Gradle plugin
(io.gitlab.arturbosch.detekt / detekt-gradle-plugin) from 1.23.8 to a Detekt 2.x
release (2.0.0-alpha or preferably a stable 2.x) in your plugin declarations
(build.gradle / build.gradle.kts or pluginManagement block) and refresh the
Gradle wrapper to use the existing distributionUrl (Gradle 9.4.1) so those
plugin versions are compatible.

In
`@plugin/src/main/kotlin/zone/clanker/gradle/androidcli/task/AndroidExecTask.kt`:
- Around line 17-23: checkBinaryInstalled() currently invokes "which" which is
not portable and fails on Windows and when extension.binary is an
absolute/path-containing value; change the logic to first treat extension.binary
as a Path and if it contains a file separator or is absolute use
java.nio.file.Path/Files.exists + Files.isExecutable (or File.canExecute()) to
validate it, otherwise perform a PATH search by splitting System.getenv("PATH")
and checking each entry for an executable named extension.binary (or, on
Windows, prefer calling "where" only as a fallback), and throw the same error
when no executable is found; update the method to reference extension.binary and
checkBinaryInstalled() accordingly.

In
`@plugin/src/main/kotlin/zone/clanker/gradle/androidcli/task/AndroidRunTask.kt`:
- Around line 12-16: The buildArgs() function naively splits
AndroidCli.PROP_ARGS with raw.split(' '), which breaks quoted arguments and
doesn't collapse whitespace; replace this with a shell-like tokenizer so quoted
strings are preserved (e.g. use a small lexer such as Apache Commons Exec
CommandLine.parse or implement a minimal parser that splits on runs of
whitespace and respects single/double quotes), keep appending
extension.extraArgs as before, and ensure empty input still yields an empty list
rather than spurious tokens.

In
`@plugin/src/test/kotlin/zone/clanker/gradle/androidcli/AndroidCliPluginTest.kt`:
- Around line 47-69: The test shares a mutable AndroidCli.SettingsExtension
instance (ext) across multiple when/then leaves which makes the tests
order-dependent; instead create a fresh SettingsExtension inside each when or
each then so assertions don't rely on state mutated by other leaves—replace the
shared val ext = AndroidCli.SettingsExtension() at the given scope with
instantiations inside the individual `when` or `then` blocks that reference
AndroidCli.SettingsExtension and ext so each assertion (defaults and mutability
checks) uses its own new instance.

In `@plugin/src/test/kotlin/zone/clanker/gradle/androidcli/AndroidTasksTest.kt`:
- Around line 200-209: The test currently relies on an external "which" binary;
instead create a temporary executable in the test and prepend its directory to
PATH so the test does not depend on system image. In the
AndroidExecTask.checkBinaryInstalled test (where ext.binary is set and
AndroidCli.TASK_VERSION/AndroidVersionTask used), create a temp file executable
(a small shell script or platform-appropriate no-op), mark it executable, add
its parent directory to the PATH environment for the test process, set
ext.binary to the temp filename, then call task.checkBinaryInstalled(); finally
clean up the temp file and restore PATH.

---

Nitpick comments:
In @.github/workflows/build.yml:
- Around line 12-27: Add a GitHub Actions concurrency block to the workflow's
jobs.build section to cancel in-progress runs for the same PR on new pushes (use
the concurrency key with a name like "build-${{ github.ref }}" and
cancel-in-progress: true) so redundant Gradle builds started by rapid PR pushes
are canceled; optionally pin external actions like actions/checkout@v4 and
gradle/actions/setup-gradle@v4 to specific commit SHAs for supply-chain
hardening.

In @.github/workflows/release.yml:
- Around line 31-34: Replace the separate "Build and test" and "Publish to Maven
Central" steps with a single Gradle invocation so Gradle can reuse the task
graph; specifically, update the step(s) referencing the run commands in the
steps named "Build and test" and "Publish to Maven Central" to invoke ./gradlew
check publishAndReleaseToMavenCentral -PVERSION_NAME=${{
steps.version.outputs.version }} (instead of running ./gradlew build and then
publishAndReleaseToMavenCentral separately).

In `@build-logic/build.gradle.kts`:
- Around line 10-17: The dependencies block currently hardcodes plugin versions
(e.g., "org.jetbrains.kotlinx:kover-gradle-plugin:0.9.1",
"io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.23.8",
"org.jlleitschuh.gradle:ktlint-gradle:12.2.0",
"com.vanniktech:gradle-maven-publish-plugin:0.32.0",
"org.gradle.toolchains:foojay-resolver:1.0.0"); centralize these by adding
entries in the version catalog (libs.versions.toml) for each plugin coordinate
and version, then update the dependencies block in build.gradle.kts to use the
catalog references (e.g., libs.plugins.detekt, libs.plugins.kover, etc.) or the
typed accessors (libs.xxx) instead of hardcoded :version strings; ensure the
version keys are named clearly (kover, detekt, ktlint, mavenPublish, foojay) and
that any build logic that expects the previous strings is adjusted to use the
catalog symbols.

In `@build-logic/src/main/kotlin/clkx-cover.gradle.kts`:
- Around line 5-21: The current kover configuration places excludes under
reports.filters which removes them from both HTML/XML reports and verification;
move the annotatedBy("org.gradle.api.tasks.TaskAction") and
classes("*$SettingsPlugin", "*$SettingsPlugin$*") exclusion entries out of
reports { filters { ... } } and instead add them under verify { rule { ... } }
so the minBound(90) verification uses those excludes while the reports still
show the unexcluded classes; update the kover block (kover -> reports -> filters
-> remove those excludes; kover -> reports -> verify -> rule -> include the
excludes alongside minBound(90)) to implement this scoping change.

In `@build-logic/src/main/kotlin/clkx-konsist.gradle.kts`:
- Around line 7-13: The slopTest source set currently only adds
sourceSets["main"].output to its compileClasspath and runtimeClasspath, so
compiled test classes (sourceSets["test"].output) are not available; update the
slopTest configuration (the val slopTest block) to also include
sourceSets["test"].output in compileClasspath and runtimeClasspath so slop tests
can compile/run against test helpers/fixtures, and keep the existing
configuration extensions from testImplementation/testRuntimeOnly as-is.

In `@build-logic/src/main/kotlin/clkx-library.gradle.kts`:
- Around line 4-17: The mavenPublishing configure call in clkx-library can
silently override the configure call from clkx-conventions (which applies
clkx-publish/GradlePlugin), so add a guard to prevent both from being applied:
in the clkx-library plugin/application code (where you apply
id("clkx-library")/mavenPublishing/JavaLibrary) check for the presence of
clkx-conventions/clkx-publish (e.g.,
project.plugins.hasPlugin("clkx-conventions") or
pluginManager.withPlugin("clkx-conventions") ) and throw a clear GradleException
(or fail the configuration) if both are present; alternatively, if you prefer
ownership changes, remove the com.vanniktech.maven.publish application from
clkx-publish and let each module call mavenPublishing.configure(...) for its own
publication type (JavaLibrary vs GradlePlugin) and update README to state they
are mutually exclusive.

In `@build-logic/src/main/kotlin/clkx-testing.gradle.kts`:
- Around line 5-10: Extract the repeated Kotest version "5.9.1" into the version
catalog and reference that single key from the dependencies block: add a kotest
= "5.9.1" entry in libs.versions.toml (or the project version catalog) and then
replace the three literals in the dependencies block referencing
"io.kotest:kotest-runner-junit5", "io.kotest:kotest-assertions-core", and
"io.kotest:kotest-framework-datatest" with the catalog version reference (e.g.,
use the libs.versions.kotest entry) so upgrades only change the single kotest
version key.

In `@plugin/src/main/kotlin/zone/clanker/gradle/androidcli/AndroidCli.kt`:
- Around line 43-47: The call uses an unnecessary explicit Action wrapper;
replace the Action { rootProject -> registerTasks(rootProject, extension) } with
a direct lambda passed to settings.gradle.rootProject so that
settings.gradle.rootProject { rootProject -> registerTasks(rootProject,
extension) } (i.e., remove the explicit Action wrapper around the lambda used to
invoke registerTasks).
- Around line 51-95: Extract the repeated task-registration boilerplate inside
registerTasks by adding a small helper (e.g. a private inline fun
registerAndroidTask<T : AndroidExecTask>(name: String, description: String,
extension: SettingsExtension)) and then replace the nine tasks registered in
registerTasks (TASK_VERSION, TASK_DESCRIBE, TASK_SCREEN_CAPTURE,
TASK_SKILLS_LIST, TASK_SKILLS_FIND, TASK_SKILLS_ADD, TASK_SKILLS_REMOVE,
TASK_RUN) with single-line calls to that helper that pass the corresponding
AndroidVersionTask/AndroidDescribeTask/AndroidScreenCaptureTask/AndroidSkillsListTask/AndroidSkillsFindTask/AndroidSkillsAddTask/AndroidSkillsRemoveTask/AndroidRunTask
classes, the description strings, and the extension; the helper should set
it.group = GROUP, it.description = description and it.extension = extension so
behavior is unchanged.

In
`@plugin/src/main/kotlin/zone/clanker/gradle/androidcli/task/AndroidDescribeTask.kt`:
- Around line 12-19: The JSON flag parsing in buildArgs (function buildArgs,
property AndroidCli.PROP_JSON) uses String.toBoolean() which silently treats any
non-"true" value as false; change to String.toBooleanStrictOrNull() and handle
the null case so malformed values are surfaced: if the property is absent keep
the existing default (false), but if the property is present and
toBooleanStrictOrNull() returns null, throw a clear GradleException or log an
error indicating acceptable values ("true"/"false") so callers using
"1"/"yes"/typos get immediate feedback; leave the rest of buildArgs (adding
"describe", "--json", and extension.extraArgs) unchanged.

In
`@plugin/src/main/kotlin/zone/clanker/gradle/androidcli/task/screen/AndroidScreenCaptureTask.kt`:
- Around line 1-13: The task currently hardcodes CLI args via
extension.extraArgs and doesn't expose a per-task output path; add a task-level
property (e.g., an `@Input` and `@Option` annotated var named outputPath or
screenshotPath) on AndroidScreenCaptureTask and include its flag in buildArgs()
so doFirst uses it when present; update buildArgs() to merge
listOf("screen","capture") + optional output flag (from the new outputPath
property) + extension.extraArgs, and document that outputPath overrides/augments
the global extraArgs.

In
`@plugin/src/slopTest/kotlin/zone/clanker/gradle/androidcli/ForbiddenPatternTest.kt`:
- Around line 12-28: Replace brittle substring checks that inspect raw file text
(scope.files.assertFalse { it.text.contains("...") }) with AST-aware checks: use
scope.functions(includeNested = true).assertFalse { it.name == "println" } to
detect actual println function declarations/calls and use
scope.imports.assertFalse { it.name == "java.lang.System.out" || it.name ==
"java.lang.System.err" } (or equivalent file-level import/property accessors) to
detect direct System.out/System.err usage; update the three then-blocks that
reference println(, System.err and System.out to use these Konsist APIs
(scope.functions and scope.imports) so comments and string literals no longer
trigger false positives.

In `@plugin/src/test/kotlin/zone/clanker/gradle/androidcli/AndroidTasksTest.kt`:
- Around line 30-77: The tests use Kotest BehaviorSpec with project/extension
setup (newProject(),
project.extensions.extraProperties.set(AndroidCli.PROP_JSON, ...), and
ext.extraArgs) inside given/when blocks which run at spec construction and thus
share state across their then blocks; move the creation of the Project and
extension and any mutations to inside each then block (or use a per-test
lifecycle like beforeTest) so AndroidDescribeTask/AndroidVersionTask tests
(referencing AndroidCli.TASK_DESCRIBE, AndroidCli.TASK_VERSION,
AndroidDescribeTask, AndroidCli.PROP_JSON and ext.extraArgs) get a fresh
newProject() per assertion and cannot leak state between tests.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5fb2082a-3551-45b4-81f9-5f98356f9404

📥 Commits

Reviewing files that changed from the base of the PR and between 57f762a and 42beb8e.

⛔ Files ignored due to path filters (1)
  • gradle/wrapper/gradle-wrapper.jar is excluded by !**/*.jar
📒 Files selected for processing (37)
  • .github/workflows/build.yml
  • .github/workflows/release.yml
  • build-logic/build.gradle.kts
  • build-logic/settings.gradle.kts
  • build-logic/src/main/kotlin/clkx-conventions.gradle.kts
  • build-logic/src/main/kotlin/clkx-cover.gradle.kts
  • build-logic/src/main/kotlin/clkx-detekt.gradle.kts
  • build-logic/src/main/kotlin/clkx-konsist.gradle.kts
  • build-logic/src/main/kotlin/clkx-ktlint.gradle.kts
  • build-logic/src/main/kotlin/clkx-library.gradle.kts
  • build-logic/src/main/kotlin/clkx-module.gradle.kts
  • build-logic/src/main/kotlin/clkx-plugin.gradle.kts
  • build-logic/src/main/kotlin/clkx-publish.gradle.kts
  • build-logic/src/main/kotlin/clkx-testing.gradle.kts
  • build-logic/src/main/kotlin/clkx-toolchain.gradle.kts
  • build-logic/src/main/kotlin/zone/clanker/gradle/conventions/ClkxSettingsPlugin.kt
  • config/detekt.yml
  • gradle.properties
  • gradle/wrapper/gradle-wrapper.properties
  • gradlew
  • gradlew.bat
  • plugin/build.gradle.kts
  • plugin/gradle.properties
  • plugin/src/main/kotlin/zone/clanker/gradle/androidcli/AndroidCli.kt
  • plugin/src/main/kotlin/zone/clanker/gradle/androidcli/task/AndroidDescribeTask.kt
  • plugin/src/main/kotlin/zone/clanker/gradle/androidcli/task/AndroidExecTask.kt
  • plugin/src/main/kotlin/zone/clanker/gradle/androidcli/task/AndroidRunTask.kt
  • plugin/src/main/kotlin/zone/clanker/gradle/androidcli/task/AndroidVersionTask.kt
  • plugin/src/main/kotlin/zone/clanker/gradle/androidcli/task/screen/AndroidScreenCaptureTask.kt
  • plugin/src/main/kotlin/zone/clanker/gradle/androidcli/task/skills/AndroidSkillsAddTask.kt
  • plugin/src/main/kotlin/zone/clanker/gradle/androidcli/task/skills/AndroidSkillsFindTask.kt
  • plugin/src/main/kotlin/zone/clanker/gradle/androidcli/task/skills/AndroidSkillsListTask.kt
  • plugin/src/main/kotlin/zone/clanker/gradle/androidcli/task/skills/AndroidSkillsRemoveTask.kt
  • plugin/src/slopTest/kotlin/zone/clanker/gradle/androidcli/ForbiddenPatternTest.kt
  • plugin/src/test/kotlin/zone/clanker/gradle/androidcli/AndroidCliPluginTest.kt
  • plugin/src/test/kotlin/zone/clanker/gradle/androidcli/AndroidTasksTest.kt
  • settings.gradle.kts

Comment on lines +20 to +25
- name: Extract version from tag
id: version
run: |
VERSION="${{ github.event.release.tag_name }}"
VERSION="${VERSION#v}"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

GitHub Actions script injection hardening github.event context run step

💡 Result:

Script injection in GitHub Actions occurs when untrusted inputs from the github.event context, such as github.event.pull_request.title or github.event.issue.body, are interpolated into 'run' steps, allowing attackers to execute arbitrary shell commands. This is particularly risky in workflows triggered by pull_request_target, workflow_run, or similar high-privilege events where attackers control parts of github.event. Vulnerable example: - name: Check PR title run: | title="${{ github.event.pull_request.title }}" if [[ $title =~ ^octocat ]]; then echo "Valid" fi An attacker sets PR title to 'a"; ls $GITHUB_WORKSPACE"' , breaking out of the quotes and executing 'ls'. Hardening practices from official GitHub docs: 1. Use actions instead of inline scripts (recommended): Pass untrusted input as 'with' parameter to a JS action, which stringifies it safely. uses: myaction/check@v1 with: title: ${{ github.event.pull_request.title }} 2. Use intermediate environment variables in inline scripts: - name: Check PR title env: TITLE: ${{ github.event.pull_request.title }} run: | if [[ "$TITLE" =~ ^octocat ]]; then echo "Valid" fi The env var prevents shell interpretation. Untrusted github.event fields (treat as user-controlled): those ending in body, default_branch, email, head_ref, label, message, name, page_name, ref, title (e.g., github.event.issue.title, github.event.pull_request.body). Also branch names, emails. Additional mitigations: - Set workflow permissions to read-only: permissions: { contents: read, pull-requests: read } - Pin actions to commit SHAs. - Use tools like step-security/harden-runner for runtime monitoring. - Avoid pull_request_target unless necessary; prefer pull_request. No major changes in 2025-2026; core advice remains from docs.github.com/en/actions/concepts/security/script-injections and /en/actions/security-guides/security-hardening-for-github-actions.

Citations:


🌐 Web query:

GitHub Actions github.event.release.tag_name untrusted field security

💡 Result:

github.event.release.tag_name in GitHub Actions is generally considered a trusted field and not listed among the explicitly untrusted inputs by official GitHub documentation or security advisories. Official GitHub docs on script injections and untrusted inputs (e.g., issue titles, PR bodies, comments) do not include github.event.release.tag_name or other release event fields in their lists of potentially attacker-controlled data. The tag_name corresponds to a Git tag ref created by users with write access to the repository, making it controlled by trusted actors rather than external contributors like in pull_request events. However, general best practices for GitHub Actions security recommend treating many github.event fields cautiously to prevent script injection when passed directly into shell commands or expressions. For tag_name specifically: - It can contain special characters allowed in Git refs (e.g., zzz";echo${IFS}"hello";#), which could lead to command injection if unsanitized in run: steps. - Release events are triggered by repository collaborators, so the risk is lower than for fork PRs or issues, but still present if workflows use elevated permissions. Mitigation: - Use environment variables: env: TAG_NAME: ${{ github.event.release.tag_name }} then run: echo $TAG_NAME - Validate/sanitize before use (e.g., regex check for expected semver format). - Prefer github.ref_name for tag-triggered workflows, as it's equivalent and sometimes more reliable. No known CVEs or breaches specifically exploiting github.event.release.tag_name as of 2026-04-17. Primary risks in release workflows stem from other factors like untrusted actions or improper GITHUB_TOKEN permissions.

Citations:


🌐 Web query:

GitHub Actions release event context security sensitive fields

💡 Result:

In GitHub Actions workflows triggered by the 'release' event (e.g., on: release: types: [published]), the github.event.release context provides the full release object from the webhook payload, which matches the GitHub REST API Release schema. This includes fields like id, tag_name, target_commitish, name, body, draft, prerelease, author (user details), assets array (with download URLs, sizes), and timestamps (created_at, published_at). No fields contain secrets or tokens by default. The only known sensitive field in the broader github context is github.token, but it is not part of github.event.release. Security considerations: github.event data, including release fields like body or asset names, should be treated as potentially untrusted if the release creator has write access, as it could enable script injection if interpolated into commands without sanitization. Use safe handling practices like passing to JS actions or env vars with quoting. GitHub redacts secrets from logs but does not mask event data. Release workflows typically run with full GITHUB_TOKEN access, so minimize permissions and avoid untrusted code execution.

Citations:


Best practice: Use environment variables for release tag names instead of direct interpolation.

While github.event.release.tag_name is controlled by repository maintainers and is not listed in GitHub's official untrusted inputs, passing it through an environment variable before use in shell commands aligns with GitHub's security hardening guidance and prevents potential issues if special characters in the tag name are mishandled.

Recommended fix
       - name: Extract version from tag
         id: version
+        env:
+          RAW_TAG: ${{ github.event.release.tag_name }}
         run: |
-          VERSION="${{ github.event.release.tag_name }}"
-          VERSION="${VERSION#v}"
+          VERSION="${RAW_TAG#v}"
           echo "version=$VERSION" >> "$GITHUB_OUTPUT"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Extract version from tag
id: version
run: |
VERSION="${{ github.event.release.tag_name }}"
VERSION="${VERSION#v}"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Extract version from tag
id: version
env:
RAW_TAG: ${{ github.event.release.tag_name }}
run: |
VERSION="${RAW_TAG#v}"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/release.yml around lines 20 - 25, The step "Extract
version from tag" currently interpolates github.event.release.tag_name directly
in the shell; instead set an environment variable (e.g., RELEASE_TAG) to ${{
github.event.release.tag_name }} for that step and reference "$RELEASE_TAG"
inside the run block, then perform the strip operation
(VERSION="${RELEASE_TAG#v}") and echo to GITHUB_OUTPUT; ensure the variable is
double-quoted when used to safely handle special characters.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

    __
   (-.-)
   /   \   matching siblings, not
  (  /\  )  changing just this repo
   \_/\_/

Sensible in isolation, but this release.yml is a copy of our sibling Clanker repos (clkx-agents, wrkx, srcx, opsx) — they all use the same ${{ github.event.release.tag_name }} interpolation. tag_name comes from an event that already requires write access to trigger, so the blast radius is low. If we tighten this we'll do it in a sweep across all repos at once, not one-off here.

Parking as known-and-intentional for now. 🐰

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ClankerGuru — Understood, that's a reasonable call. The event already requires write access, so the risk is bounded, and keeping the pattern consistent across the sibling repos makes a coordinated sweep much cleaner. I'll note this for future reviews. 🐰


✏️ Learnings added
Learnt from: ClankerGuru
Repo: ClankerGuru/android-cli PR: 1
File: .github/workflows/release.yml:20-25
Timestamp: 2026-04-17T20:11:42.419Z
Learning: In the ClankerGuru family of repos (android-cli, clkx-agents, wrkx, srcx, opsx), direct interpolation of `${{ github.event.release.tag_name }}` in `release.yml` run steps is intentional and consistent across all sibling repos. The pattern is considered acceptable because the `release: published` event requires repository write access to trigger (low blast radius). Any hardening (e.g., moving to an env var) will be done as a coordinated sweep across all repos, not one-off. Do not flag this pattern in individual PR reviews for these repos.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: ClankerGuru
Repo: ClankerGuru/android-cli PR: 1
File: build-logic/src/main/kotlin/clkx-plugin.gradle.kts:1-5
Timestamp: 2026-04-17T20:11:24.722Z
Learning: In the android-cli Gradle project (build-logic/src/main/kotlin/), the Vanniktech Maven publish configuration is intentionally split across two convention scripts:
- `clkx-plugin.gradle.kts` applies only the `java-gradle-plugin` and `com.vanniktech.maven.publish` plugin IDs.
- `clkx-publish.gradle.kts` owns the `mavenPublishing { configure(GradlePlugin(javadocJar = JavadocJar.Empty())) }` block.
Both are composed together by `clkx-conventions.gradle.kts` (which applies `clkx-plugin` and `clkx-publish`), so every plugin module gets the full publishing config. The split is intentional: `clkx-publish` is reused for non-plugin library artifacts as well (via `clkx-library.gradle.kts`, which uses `JavaLibrary` instead of `GradlePlugin`). Do not flag the absence of a `mavenPublishing` block in `clkx-plugin.gradle.kts` as a missing configuration.

Comment on lines +1 to +5
plugins {
id("clkx-module")
`java-gradle-plugin`
id("com.vanniktech.maven.publish")
}
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing mavenPublishing configuration for the Gradle plugin artifact.

Unlike clkx-library.gradle.kts (which configures JavaLibrary(javadocJar = JavadocJar.Empty())), this script applies com.vanniktech.maven.publish without any mavenPublishing.configure(GradlePlugin(...)) block. The sibling clkx-publish.gradle.kts has that configuration but does not appear to be applied here. As-is, the published plugin artifact won't have the intended empty-Javadoc behavior and POM configuration may be inconsistent with the library convention.

Proposed fix
 plugins {
     id("clkx-module")
     `java-gradle-plugin`
-    id("com.vanniktech.maven.publish")
+    id("clkx-publish")
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
plugins {
id("clkx-module")
`java-gradle-plugin`
id("com.vanniktech.maven.publish")
}
plugins {
id("clkx-module")
`java-gradle-plugin`
id("clkx-publish")
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@build-logic/src/main/kotlin/clkx-plugin.gradle.kts` around lines 1 - 5, Add a
mavenPublishing configuration that registers this Gradle plugin artifact the
same way the library convention does: call
mavenPublishing.configure(GradlePlugin(...)) for the plugin artifact and set its
JavaLibrary publishing options to JavaLibrary(javadocJar = JavadocJar.Empty())
so the published plugin gets the empty Javadoc jar and consistent POM metadata;
locate the plugins block in clkx-plugin.gradle.kts and add a
mavenPublishing.configure block referencing GradlePlugin and
JavaLibrary/JavadocJar.Empty to mirror the sibling
clkx-library.gradle.kts/clkx-publish.gradle.kts setup.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

   __
  /  \
 (  o.o )  Actually — we DO use Vanniktech.
  >  ^  <      You caught one half of a split config.

clkx-plugin.gradle.kts only applies the java-gradle-plugin + com.vanniktech.maven.publish plugin IDs. The mavenPublishing { configure(GradlePlugin(javadocJar = JavadocJar.Empty())) } block lives in the sibling clkx-publish.gradle.kts:

// clkx-publish.gradle.kts
import com.vanniktech.maven.publish.GradlePlugin
import com.vanniktech.maven.publish.JavadocJar

plugins {
    id("com.vanniktech.maven.publish")
}

mavenPublishing {
    configure(GradlePlugin(javadocJar = JavadocJar.Empty()))
}

Both are applied together by clkx-conventions, so every plugin module gets the full publishing config:

// clkx-conventions.gradle.kts
plugins {
    id("clkx-module")
    id("clkx-toolchain")
    id("clkx-plugin")    // java-gradle-plugin + vanniktech ID
    id("clkx-publish")   // vanniktech config block
    ...
}

Same result as your diff — just split across two convention plugins on purpose, so we can reuse clkx-publish for non-plugin library artifacts too (see clkx-library.gradle.kts which uses JavaLibrary instead of GradlePlugin).

🐰

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ClankerGuru, thanks for the clear walkthrough — you're absolutely right, and I apologize for the noise. The split is a clean design: clkx-plugin registers the plugin IDs, clkx-publish owns the mavenPublishing configuration block, and clkx-conventions composes both so every plugin module gets the full setup. The reuse of clkx-publish with JavaLibrary vs GradlePlugin depending on context makes the intent clear. I'll remember this pattern for future reviews.


✏️ Learnings added
Learnt from: ClankerGuru
Repo: ClankerGuru/android-cli PR: 1
File: build-logic/src/main/kotlin/clkx-plugin.gradle.kts:1-5
Timestamp: 2026-04-17T20:11:24.722Z
Learning: In the android-cli Gradle project (build-logic/src/main/kotlin/), the Vanniktech Maven publish configuration is intentionally split across two convention scripts:
- `clkx-plugin.gradle.kts` applies only the `java-gradle-plugin` and `com.vanniktech.maven.publish` plugin IDs.
- `clkx-publish.gradle.kts` owns the `mavenPublishing { configure(GradlePlugin(javadocJar = JavadocJar.Empty())) }` block.
Both are composed together by `clkx-conventions.gradle.kts` (which applies `clkx-plugin` and `clkx-publish`), so every plugin module gets the full publishing config. The split is intentional: `clkx-publish` is reused for non-plugin library artifacts as well (via `clkx-library.gradle.kts`, which uses `JavaLibrary` instead of `GradlePlugin`). Do not flag the absence of a `mavenPublishing` block in `clkx-plugin.gradle.kts` as a missing configuration.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Comment thread config/detekt.yml Outdated
@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
curl -sI https://services.gradle.org/distributions/gradle-9.4.1-bin.zip | head -n 1

Repository: ClankerGuru/android-cli

Length of output: 98


🌐 Web query:

ktlint gradle plugin org.jlleitschuh.gradle.ktlint Gradle 9 compatibility

💡 Result:

Yes, the org.jlleitschuh.gradle.ktlint Gradle plugin is compatible with Gradle 9. Use version 14.2.0 (latest as of 2026-03-12) or newer. Key evidence: - Version 13.1.0 (2025-08-21): Added Gradle 9 support (PR #937). - Version 14.0.1 (2025-11-10): Updated build for Gradle 9.1 and Java 25 (PR #962). - Latest versions 14.1.0+ (2026) include further improvements like AGP 9 / built-in Kotlin support and isolated projects compatibility. - Minimum supported Gradle version is 7.4; no upper limit specified, and CI/build explicitly tested/built against Gradle 9.x. - Available on Gradle Plugin Portal: plugins { id("org.jlleitschuh.gradle.ktlint") version "14.2.0" } Note: Some users reported issues with AGP 9.0 + built-in Kotlin (issue #1035, Feb 2026), resolved in 14.1.0+ (#1016).

Citations:


🏁 Script executed:

#!/bin/bash
# Find build.gradle or build.gradle.kts files
find . -maxdepth 3 -type f \( -name "build.gradle" -o -name "build.gradle.kts" \) | head -20

Repository: ClankerGuru/android-cli

Length of output: 123


🏁 Script executed:

#!/bin/bash
# Also check if there's a settings.gradle or settings.gradle.kts
find . -maxdepth 3 -type f \( -name "settings.gradle" -o -name "settings.gradle.kts" \)

Repository: ClankerGuru/android-cli

Length of output: 122


🏁 Script executed:

#!/bin/bash
# Read the main build.gradle.kts file
wc -l ./plugin/build.gradle.kts ./build-logic/build.gradle.kts

Repository: ClankerGuru/android-cli

Length of output: 144


🏁 Script executed:

#!/bin/bash
# Read plugin/build.gradle.kts to see plugin declarations
cat -n ./plugin/build.gradle.kts

Repository: ClankerGuru/android-cli

Length of output: 544


🏁 Script executed:

#!/bin/bash
# Read build-logic/build.gradle.kts to see the actual plugin declarations
cat -n ./build-logic/build.gradle.kts

Repository: ClankerGuru/android-cli

Length of output: 1002


🏁 Script executed:

#!/bin/bash
# Also check settings.gradle.kts to see pluginManagement configurations
cat -n ./settings.gradle.kts

Repository: ClankerGuru/android-cli

Length of output: 324


🌐 Web query:

com.vanniktech gradle-maven-publish-plugin 0.32.0 Gradle 9 compatibility

💡 Result:

Yes, com.vanniktech gradle-maven-publish-plugin version 0.32.0 is compatible with Gradle 9. The 0.32.0 release (May 2025) explicitly includes "Fix compatibility with Gradle 9.0" and lists compatibility tested up to Gradle 9.0-milestone-6. Minimum supported Gradle version is 8.5. Later versions like 0.36.0 (Jan 2026) raise the minimum to Gradle 9.0.0.

Citations:


🌐 Web query:

detekt-gradle-plugin 1.23.8 Gradle 9 compatibility

💡 Result:

No, detekt-gradle-plugin 1.23.8 is not officially compatible with Gradle 9. It was compiled and tested against Gradle 8.12.1. Official docs recommend aligning Gradle versions with those listed in the compatibility table, which shows Gradle 8.x for 1.23.8. While some users run it with Gradle 9.x (e.g., 9.2.1), issues like deprecations exist, and full support requires Detekt 2.0.0-alpha (built against Gradle 9.x). Detekt 1.23.8 docs state it requires Gradle 6.8.3+, but recommend the table version for best support.

Citations:


🌐 Web query:

ktlint-gradle 12.2.0 Gradle 9 compatibility

💡 Result:

No, ktlint-gradle 12.2.0 is not compatible with Gradle 9. Official documentation states the minimum supported Gradle version is 7.4, but full Gradle 9 support was added in version 13.1.0 (August 2025). Version 12.2.0 (February 2025) updated testing to newer Gradle versions but predates explicit Gradle 9 support. Issues with Gradle 9, such as configuration cache problems and lint detection failures (especially with built-in Kotlin DSL in Gradle 9), exist even in later versions like 14.x. Upgrade to at least 13.1.0 or the latest 14.2.0 for Gradle 9 compatibility.

Citations:


Upgrade ktlint-gradle and detekt to support Gradle 9.4.1.

Gradle 9.4.1 distribution is released and available. However, two plugins in the build are incompatible with Gradle 9:

  • ktlint-gradle 12.2.0: Lacks Gradle 9 support (added in 13.1.0, August 2025). Upgrade to 13.1.0 or later (recommend 14.2.0).
  • detekt-gradle-plugin 1.23.8: Tested only against Gradle 8.12.1. Gradle 9 support requires Detekt 2.0.0-alpha or later.

The vanniktech maven-publish-plugin (0.32.0) is compatible with Gradle 9. Foojay resolver (1.0.0) is an official Gradle tool.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@gradle/wrapper/gradle-wrapper.properties` at line 3, Upgrade the incompatible
Gradle plugins to versions that support Gradle 9: update the ktlint-gradle
plugin (org.jlleitschuh.gradle.ktlint) from 12.2.0 to at least 13.1.0 (recommend
14.2.0) and update the detekt Gradle plugin (io.gitlab.arturbosch.detekt /
detekt-gradle-plugin) from 1.23.8 to a Detekt 2.x release (2.0.0-alpha or
preferably a stable 2.x) in your plugin declarations (build.gradle /
build.gradle.kts or pluginManagement block) and refresh the Gradle wrapper to
use the existing distributionUrl (Gradle 9.4.1) so those plugin versions are
compatible.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

     __
    (o.o)   hop hop hop
   o(")(") 9.4.1 green

Builds cleanly on Gradle 9.4.1 locally and in CI — this exact plugin-version matrix (ktlint-gradle 12.2.0, detekt 1.23.8, vanniktech 0.32.0, Foojay 1.0.0) is what powers clkx-agents, wrkx, srcx, and opsx today. No deprecation warnings or test failures that matter for us.

Plugin version bumps are owned by the shared clkx-* convention plugins and will land as a coordinated sweep across all Clanker repos. Not a per-PR bump. 🐰

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ClankerGuru, understood — thanks for the context! If it's green end-to-end across the whole Clanker fleet at those versions, the concern doesn't apply here. I'll note the governance model for future reviews.


✏️ Learnings added
Learnt from: ClankerGuru
Repo: ClankerGuru/android-cli PR: 1
File: gradle/wrapper/gradle-wrapper.properties:3-3
Timestamp: 2026-04-17T20:11:50.118Z
Learning: In ClankerGuru/android-cli (and across all Clanker repos: clkx-agents, wrkx, srcx, opsx), the plugin-version matrix for the shared `clkx-*` convention plugins — ktlint-gradle 12.2.0 (`org.jlleitschuh.gradle.ktlint`), detekt 1.23.8 (`io.gitlab.arturbosch.detekt`), vanniktech maven-publish 0.32.0, and Foojay resolver 1.0.0 — is intentionally pinned and verified to build cleanly on Gradle 9.4.1 (locally and in CI). Plugin version upgrades are managed as a coordinated sweep across all Clanker repos via the shared `clkx-*` convention plugins, not bumped on a per-PR basis. Do not flag these plugin versions as incompatible with Gradle 9 in any Clanker repo review.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: ClankerGuru
Repo: ClankerGuru/android-cli PR: 1
File: build-logic/src/main/kotlin/clkx-plugin.gradle.kts:1-5
Timestamp: 2026-04-17T20:11:24.722Z
Learning: In the android-cli Gradle project (build-logic/src/main/kotlin/), the Vanniktech Maven publish configuration is intentionally split across two convention scripts:
- `clkx-plugin.gradle.kts` applies only the `java-gradle-plugin` and `com.vanniktech.maven.publish` plugin IDs.
- `clkx-publish.gradle.kts` owns the `mavenPublishing { configure(GradlePlugin(javadocJar = JavadocJar.Empty())) }` block.
Both are composed together by `clkx-conventions.gradle.kts` (which applies `clkx-plugin` and `clkx-publish`), so every plugin module gets the full publishing config. The split is intentional: `clkx-publish` is reused for non-plugin library artifacts as well (via `clkx-library.gradle.kts`, which uses `JavaLibrary` instead of `GradlePlugin`). Do not flag the absence of a `mavenPublishing` block in `clkx-plugin.gradle.kts` as a missing configuration.

Comment on lines +17 to +23
internal fun checkBinaryInstalled() {
val check = ProcessBuilder("which", extension.binary).redirectErrorStream(true).start()
check.waitFor()
if (check.exitValue() != 0) {
error("'${extension.binary}' not found on PATH. Install Google's Android CLI first.")
}
}
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Is Google's "android" agent CLI (developer.android.com/tools/agents/android-cli) distributed for Windows, or Unix-only?

💡 Result:

Google's Android CLI agent (at developer.android.com/tools/agents/android-cli) is not Unix-only; it is distributed for Windows as well, though the android emulator command is currently disabled on Windows. Official documentation confirms Windows support with a known issue for emulator functionality, and installation instructions like which android or command -v android imply Unix-like environments (including WSL on Windows), but the explicit mention of Windows-specific issues indicates a Windows binary or compatibility exists.

Citations:


🏁 Script executed:

# First, find the AndroidExecTask.kt file
find . -name "AndroidExecTask.kt" -type f

Repository: ClankerGuru/android-cli

Length of output: 146


🏁 Script executed:

# Once found, examine the file to verify the code at lines 17-23
head -30 ./plugin/src/main/kotlin/zone/clanker/gradle/androidcli/task/AndroidExecTask.kt | tail -20

Repository: ClankerGuru/android-cli

Length of output: 699


🏁 Script executed:

# Search for test files that might reference line 61 or the android path
find . -path "*/test*" -name "*.kt" | head -10

Repository: ClankerGuru/android-cli

Length of output: 222


🏁 Script executed:

# Check AndroidCliPluginTest.kt for line 61 and absolute path usage
wc -l ./plugin/src/test/kotlin/zone/clanker/gradle/androidcli/AndroidCliPluginTest.kt

Repository: ClankerGuru/android-cli

Length of output: 150


🏁 Script executed:

# Check both test files for absolute path patterns
rg "opt/android|extension\.binary|checkBinaryInstalled" ./plugin/src/test/kotlin/zone/clanker/gradle/androidcli/

Repository: ClankerGuru/android-cli

Length of output: 781


🏁 Script executed:

# Check if extension.binary is set with absolute paths or relative paths elsewhere
rg "binary\s*=" ./plugin/src/main/kotlin/ -A 2

Repository: ClankerGuru/android-cli

Length of output: 49


🏁 Script executed:

# Check the test context around the absolute path setting
rg -B 5 -A 5 "/opt/android/bin/android" ./plugin/src/test/kotlin/zone/clanker/gradle/androidcli/AndroidCliPluginTest.kt

Repository: ClankerGuru/android-cli

Length of output: 515


🏁 Script executed:

# Check AndroidTasksTest.kt to see test cases for checkBinaryInstalled
cat -n ./plugin/src/test/kotlin/zone/clanker/gradle/androidcli/AndroidTasksTest.kt | grep -A 20 "checkBinaryInstalled"

Repository: ClankerGuru/android-cli

Length of output: 1235


🏁 Script executed:

# Check the AndroidCli.SettingsExtension to understand extension.binary
rg "class.*SettingsExtension|var binary|val binary" ./plugin/src/main/kotlin/ -B 2 -A 2

Repository: ClankerGuru/android-cli

Length of output: 672


which is not portable — breaks the plugin on Windows.

ProcessBuilder("which", ...) will fail with IOException: Cannot run program "which" on Windows agents because which isn't a builtin or on PATH; Windows uses where.exe. Since this is a Gradle plugin published to Maven Central, Windows consumers will hit this on every task invocation. Official documentation confirms the Android CLI agent is distributed for Windows, making this a critical compatibility gap.

Additionally, extension.binary can be set to an absolute path (tests show /opt/android/bin/android), but which only works with command names, not paths. The check will fail for absolute paths even on Unix.

Suggested approach: use java.nio.file.Path / File.canExecute() for absolute paths or paths with separators, otherwise probe each entry on PATH (or use where on Windows). A minimal cross-platform fix:

🛠️ Proposed fix
-    internal fun checkBinaryInstalled() {
-        val check = ProcessBuilder("which", extension.binary).redirectErrorStream(true).start()
-        check.waitFor()
-        if (check.exitValue() != 0) {
-            error("'${extension.binary}' not found on PATH. Install Google's Android CLI first.")
-        }
-    }
+    internal fun checkBinaryInstalled() {
+        val binary = extension.binary
+        val asFile = java.io.File(binary)
+        val found = if (asFile.isAbsolute || binary.contains(java.io.File.separatorChar)) {
+            asFile.canExecute()
+        } else {
+            val pathEnv = System.getenv("PATH").orEmpty()
+            val exts = if (org.gradle.internal.os.OperatingSystem.current().isWindows) {
+                (System.getenv("PATHEXT") ?: ".EXE;.BAT;.CMD").split(';')
+            } else {
+                listOf("")
+            }
+            pathEnv.split(java.io.File.pathSeparatorChar).any { dir ->
+                exts.any { ext -> java.io.File(dir, binary + ext).canExecute() }
+            }
+        }
+        if (!found) {
+            error("'$binary' not found on PATH. Install Google's Android CLI first.")
+        }
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugin/src/main/kotlin/zone/clanker/gradle/androidcli/task/AndroidExecTask.kt`
around lines 17 - 23, checkBinaryInstalled() currently invokes "which" which is
not portable and fails on Windows and when extension.binary is an
absolute/path-containing value; change the logic to first treat extension.binary
as a Path and if it contains a file separator or is absolute use
java.nio.file.Path/Files.exists + Files.isExecutable (or File.canExecute()) to
validate it, otherwise perform a PATH search by splitting System.getenv("PATH")
and checking each entry for an executable named extension.binary (or, on
Windows, prefer calling "where" only as a fallback), and throw the same error
when no executable is found; update the method to reference extension.binary and
checkBinaryInstalled() accordingly.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

      __
    (-.-)
    /   \   macOS + Linux only
   (  \"/  )  (like the CLI itself)
    \___/

Intentional scope limitation. Upstream Google's agent-first android CLI is distributed primarily for macOS/Linux (the emulator subcommand is explicitly disabled on Windows per the upstream docs you linked), and every known consumer of this plugin runs in a Clanker workspace on macOS or Linux CI.

We'd rather fail with a readable "not found on PATH" message than invent cross-platform probe logic for a platform the underlying CLI doesn't fully support. If/when upstream ships first-class Windows support we'll revisit — likely pushing the PATH probe into a shared utility in build-logic so all Clanker CLI-wrapping plugins benefit at once.

Known tradeoff, documented. 🐰

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ClankerGuru — fair point, and that's a clean rationale. 🐧 The macOS/Linux scope matches upstream, the error message is readable enough for any accidental Windows attempt, and deferring the PATH-probe utility to a coordinated build-logic sweep (if/when Google ships full Windows support) is the right call. Standing down on this one.


✏️ Learnings added
Learnt from: ClankerGuru
Repo: ClankerGuru/android-cli PR: 1
File: plugin/src/main/kotlin/zone/clanker/gradle/androidcli/task/AndroidExecTask.kt:17-23
Timestamp: 2026-04-17T20:12:01.148Z
Learning: In the android-cli plugin (`plugin/src/main/kotlin/zone/clanker/gradle/androidcli/task/AndroidExecTask.kt`), `checkBinaryInstalled()` intentionally uses `ProcessBuilder("which", ...)` because the plugin targets macOS and Linux only — mirroring the upstream Google `android` agent CLI, which does not have full Windows support (e.g., the `emulator` subcommand is explicitly disabled on Windows). Do not flag the use of `which` as a portability issue for this plugin. If/when upstream ships first-class Windows support, the plan is to move a cross-platform PATH-probe utility into `build-logic` so all Clanker CLI-wrapping plugins benefit at once.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: ClankerGuru
Repo: ClankerGuru/android-cli PR: 1
File: .github/workflows/release.yml:20-25
Timestamp: 2026-04-17T20:11:42.419Z
Learning: In the ClankerGuru family of repos (android-cli, clkx-agents, wrkx, srcx, opsx), direct interpolation of `${{ github.event.release.tag_name }}` in `release.yml` run steps is intentional and consistent across all sibling repos. The pattern is considered acceptable because the `release: published` event requires repository write access to trigger (low blast radius). Any hardening (e.g., moving to an env var) will be done as a coordinated sweep across all repos, not one-off. Do not flag this pattern in individual PR reviews for these repos.

Learnt from: ClankerGuru
Repo: ClankerGuru/android-cli PR: 1
File: build-logic/src/main/kotlin/clkx-plugin.gradle.kts:1-5
Timestamp: 2026-04-17T20:11:24.722Z
Learning: In the android-cli Gradle project (build-logic/src/main/kotlin/), the Vanniktech Maven publish configuration is intentionally split across two convention scripts:
- `clkx-plugin.gradle.kts` applies only the `java-gradle-plugin` and `com.vanniktech.maven.publish` plugin IDs.
- `clkx-publish.gradle.kts` owns the `mavenPublishing { configure(GradlePlugin(javadocJar = JavadocJar.Empty())) }` block.
Both are composed together by `clkx-conventions.gradle.kts` (which applies `clkx-plugin` and `clkx-publish`), so every plugin module gets the full publishing config. The split is intentional: `clkx-publish` is reused for non-plugin library artifacts as well (via `clkx-library.gradle.kts`, which uses `JavaLibrary` instead of `GradlePlugin`). Do not flag the absence of a `mavenPublishing` block in `clkx-plugin.gradle.kts` as a missing configuration.

Comment on lines +12 to +16
internal fun buildArgs(): List<String> {
val raw = project.findProperty(AndroidCli.PROP_ARGS)?.toString().orEmpty()
val parsed = raw.split(' ').filter { it.isNotBlank() }
return parsed + extension.extraArgs
}
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Naive tokenization of PROP_ARGS breaks quoted arguments.

raw.split(' ') splits on every single space, so users can't pass arguments containing spaces even with quotes, e.g. -Pzone.clanker.android-cli.args="run --message 'hello world'" becomes ["run", "--message", "'hello", "world'"]. It also doesn't collapse runs of whitespace (tabs, multiple spaces) — split(Regex("\\s+")) would be slightly better, but for shell-like parsing you really want a tokenizer.

Recommend either:

  • Document that PROP_ARGS is whitespace-split and doesn't support quoting, and rely on extension.extraArgs (typed List<String>) for anything non-trivial; or
  • Use a small shell-lexer (e.g. Apache Commons Exec's CommandLine.parse, or a minimal hand-rolled one) to respect quotes.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugin/src/main/kotlin/zone/clanker/gradle/androidcli/task/AndroidRunTask.kt`
around lines 12 - 16, The buildArgs() function naively splits
AndroidCli.PROP_ARGS with raw.split(' '), which breaks quoted arguments and
doesn't collapse whitespace; replace this with a shell-like tokenizer so quoted
strings are preserved (e.g. use a small lexer such as Apache Commons Exec
CommandLine.parse or implement a minimal parser that splits on runs of
whitespace and respects single/double quotes), keep appending
extension.extraArgs as before, and ensure empty input still yields an empty list
rather than spurious tokens.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

      __
    (o_o)
   /     \  extraArgs for real work
   \_____/  PROP_ARGS for quickies

Intentional. PROP_ARGS is a shell-convenience knob for one-shot invocations like -Pzone.clanker.android-cli.args="version --json" where the user is typing at a terminal. Anything that needs quoting should go through extension.extraArgs in settings.gradle.kts, which is already typed List<String> and sidesteps the parsing question entirely:

androidCli {
    extraArgs = listOf("run", "--message", "hello world")
}

Adding a shell lexer would trade one class of bugs (no quoting) for a different class (surprises in how we tokenize vs sh vs bash). Whitespace-split + typed extraArgs escape hatch is the right tradeoff for us.

Will add a README note making the tokenization rule explicit. 🐰

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ClankerGuru — the split-then-extraArgs two-tier design makes sense. Simple terminal convenience via -P + a fully typed escape hatch in settings.gradle.kts is a clean tradeoff, and avoiding a shell lexer removes an entire class of platform-specific parsing surprises. Noted — thanks for the clear rationale and the ASCII rabbit. 🐇


✏️ Learnings added
Learnt from: ClankerGuru
Repo: ClankerGuru/android-cli PR: 1
File: plugin/src/main/kotlin/zone/clanker/gradle/androidcli/task/AndroidRunTask.kt:12-16
Timestamp: 2026-04-17T20:12:13.246Z
Learning: In `ClankerGuru/android-cli`, `AndroidRunTask.buildArgs()` intentionally uses `raw.split(' ')` for the `PROP_ARGS` Gradle property. `PROP_ARGS` is a shell-convenience knob for simple one-shot terminal invocations (e.g. `-Pzone.clanker.android-cli.args="version --json"`). Arguments that require quoting or contain spaces must use `extension.extraArgs: List<String>` in `settings.gradle.kts` instead. A shell lexer was deliberately rejected to avoid platform-specific tokenization surprises. Do not flag this split as a bug.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: ClankerGuru
Repo: ClankerGuru/android-cli PR: 1
File: .github/workflows/release.yml:20-25
Timestamp: 2026-04-17T20:11:42.419Z
Learning: In the ClankerGuru family of repos (android-cli, clkx-agents, wrkx, srcx, opsx), direct interpolation of `${{ github.event.release.tag_name }}` in `release.yml` run steps is intentional and consistent across all sibling repos. The pattern is considered acceptable because the `release: published` event requires repository write access to trigger (low blast radius). Any hardening (e.g., moving to an env var) will be done as a coordinated sweep across all repos, not one-off. Do not flag this pattern in individual PR reviews for these repos.

Learnt from: ClankerGuru
Repo: ClankerGuru/android-cli PR: 1
File: gradle/wrapper/gradle-wrapper.properties:3-3
Timestamp: 2026-04-17T20:11:50.118Z
Learning: In ClankerGuru/android-cli (and across all Clanker repos: clkx-agents, wrkx, srcx, opsx), the plugin-version matrix for the shared `clkx-*` convention plugins — ktlint-gradle 12.2.0 (`org.jlleitschuh.gradle.ktlint`), detekt 1.23.8 (`io.gitlab.arturbosch.detekt`), vanniktech maven-publish 0.32.0, and Foojay resolver 1.0.0 — is intentionally pinned and verified to build cleanly on Gradle 9.4.1 (locally and in CI). Plugin version upgrades are managed as a coordinated sweep across all Clanker repos via the shared `clkx-*` convention plugins, not bumped on a per-PR basis. Do not flag these plugin versions as incompatible with Gradle 9 in any Clanker repo review.

Learnt from: ClankerGuru
Repo: ClankerGuru/android-cli PR: 1
File: build-logic/src/main/kotlin/clkx-plugin.gradle.kts:1-5
Timestamp: 2026-04-17T20:11:24.722Z
Learning: In the android-cli Gradle project (build-logic/src/main/kotlin/), the Vanniktech Maven publish configuration is intentionally split across two convention scripts:
- `clkx-plugin.gradle.kts` applies only the `java-gradle-plugin` and `com.vanniktech.maven.publish` plugin IDs.
- `clkx-publish.gradle.kts` owns the `mavenPublishing { configure(GradlePlugin(javadocJar = JavadocJar.Empty())) }` block.
Both are composed together by `clkx-conventions.gradle.kts` (which applies `clkx-plugin` and `clkx-publish`), so every plugin module gets the full publishing config. The split is intentional: `clkx-publish` is reused for non-plugin library artifacts as well (via `clkx-library.gradle.kts`, which uses `JavaLibrary` instead of `GradlePlugin`). Do not flag the absence of a `mavenPublishing` block in `clkx-plugin.gradle.kts` as a missing configuration.

Comment on lines +200 to +209
given("AndroidExecTask.checkBinaryInstalled") {
`when`("binary exists on PATH") {
val (project, ext) = newProject()
ext.binary = "which"
val task = project.tasks.getByName(AndroidCli.TASK_VERSION) as AndroidVersionTask

then("it does not throw") {
task.checkBinaryInstalled()
}
}
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Minor: test depends on which being on $PATH.

ext.binary = "which" causes the production code to invoke ProcessBuilder("which", "which"). This is fine on standard Linux/macOS runners but would fail in a stripped container without which (e.g., distroless/Alpine without busybox-which). Acceptable for the current GitHub Actions setup; flagging in case the CI base image changes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugin/src/test/kotlin/zone/clanker/gradle/androidcli/AndroidTasksTest.kt`
around lines 200 - 209, The test currently relies on an external "which" binary;
instead create a temporary executable in the test and prepend its directory to
PATH so the test does not depend on system image. In the
AndroidExecTask.checkBinaryInstalled test (where ext.binary is set and
AndroidCli.TASK_VERSION/AndroidVersionTask used), create a temp file executable
(a small shell script or platform-appropriate no-op), mark it executable, add
its parent directory to the PATH environment for the test process, set
ext.binary to the temp filename, then call task.checkBinaryInstalled(); finally
clean up the temp file and restore PATH.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

      __
    (◕‿◕)
   o(")(")   ubuntu + macOS have which

Acknowledged — accepted tradeoff. ubuntu-latest and macos-latest GitHub runners both ship which, and that's the only environment this test runs in. If we ever move to a distroless/stripped base image we'll swap in a temp-executable-on-PATH approach as you suggest.

Parking as known. 🐰

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ClankerGuru — fair enough, noted! ubuntu-latest and macos-latest both carry which, so the current setup is solid. I'll remember the agreed-upon migration path (temp-executable-on-PATH) for if/when the CI base image changes. 🐰


✏️ Learnings added
Learnt from: ClankerGuru
Repo: ClankerGuru/android-cli PR: 1
File: plugin/src/test/kotlin/zone/clanker/gradle/androidcli/AndroidTasksTest.kt:200-209
Timestamp: 2026-04-17T20:12:19.277Z
Learning: In ClankerGuru/android-cli, the `AndroidExecTask.checkBinaryInstalled` test in `plugin/src/test/kotlin/zone/clanker/gradle/androidcli/AndroidTasksTest.kt` (around lines 200-209) intentionally uses `ext.binary = "which"` (invoking `ProcessBuilder("which", "which")`) as a binary-on-PATH probe. This dependency on the `which` binary is an accepted tradeoff: CI only runs on `ubuntu-latest` and `macos-latest` GitHub runners, both of which ship `which`. If the CI base image ever changes to a distroless/stripped image (e.g., Alpine without busybox-which), the agreed migration is to create a temp executable in the test and prepend its parent directory to `PATH`. Do not flag this pattern as a test portability issue in reviews for this repo unless the CI base image changes.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: ClankerGuru
Repo: ClankerGuru/android-cli PR: 1
File: .github/workflows/release.yml:20-25
Timestamp: 2026-04-17T20:11:42.419Z
Learning: In the ClankerGuru family of repos (android-cli, clkx-agents, wrkx, srcx, opsx), direct interpolation of `${{ github.event.release.tag_name }}` in `release.yml` run steps is intentional and consistent across all sibling repos. The pattern is considered acceptable because the `release: published` event requires repository write access to trigger (low blast radius). Any hardening (e.g., moving to an env var) will be done as a coordinated sweep across all repos, not one-off. Do not flag this pattern in individual PR reviews for these repos.

Learnt from: ClankerGuru
Repo: ClankerGuru/android-cli PR: 1
File: gradle/wrapper/gradle-wrapper.properties:3-3
Timestamp: 2026-04-17T20:11:50.118Z
Learning: In ClankerGuru/android-cli (and across all Clanker repos: clkx-agents, wrkx, srcx, opsx), the plugin-version matrix for the shared `clkx-*` convention plugins — ktlint-gradle 12.2.0 (`org.jlleitschuh.gradle.ktlint`), detekt 1.23.8 (`io.gitlab.arturbosch.detekt`), vanniktech maven-publish 0.32.0, and Foojay resolver 1.0.0 — is intentionally pinned and verified to build cleanly on Gradle 9.4.1 (locally and in CI). Plugin version upgrades are managed as a coordinated sweep across all Clanker repos via the shared `clkx-*` convention plugins, not bumped on a per-PR basis. Do not flag these plugin versions as incompatible with Gradle 9 in any Clanker repo review.

Learnt from: ClankerGuru
Repo: ClankerGuru/android-cli PR: 1
File: build-logic/src/main/kotlin/clkx-plugin.gradle.kts:1-5
Timestamp: 2026-04-17T20:11:24.722Z
Learning: In the android-cli Gradle project (build-logic/src/main/kotlin/), the Vanniktech Maven publish configuration is intentionally split across two convention scripts:
- `clkx-plugin.gradle.kts` applies only the `java-gradle-plugin` and `com.vanniktech.maven.publish` plugin IDs.
- `clkx-publish.gradle.kts` owns the `mavenPublishing { configure(GradlePlugin(javadocJar = JavadocJar.Empty())) }` block.
Both are composed together by `clkx-conventions.gradle.kts` (which applies `clkx-plugin` and `clkx-publish`), so every plugin module gets the full publishing config. The split is intentional: `clkx-publish` is reused for non-plugin library artifacts as well (via `clkx-library.gradle.kts`, which uses `JavaLibrary` instead of `GradlePlugin`). Do not flag the absence of a `mavenPublishing` block in `clkx-plugin.gradle.kts` as a missing configuration.

- config/detekt.yml: fix over-escaped ForbiddenComment allowedPatterns regex
  (YAML single-quotes don't process escapes; 'TODO\\(.*\\):' was matching
  literal backslashes instead of parentheses)
- AndroidCliPluginTest: construct fresh SettingsExtension in each when{} scope
  so defaults and mutation leaves don't share state
@ClankerGuru ClankerGuru merged commit 4fc3061 into main Apr 17, 2026
2 checks passed
@ClankerGuru ClankerGuru deleted the initial-scaffold branch April 17, 2026 20:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant