Parallel execution for clone, pull, and checkout#2
Conversation
📝 WalkthroughWalkthroughThis PR introduces three new parallel Gradle tasks (clone, pull, checkout) to replace sequential per-repo lifecycle task aggregation, updates task registration to support these parallel implementations, and adds a GitHub Actions workflow for automated Maven Central release publishing with version validation. Changes
Sequence Diagram(s)sequenceDiagram
participant Gradle as Gradle<br/>(Client)
participant Task as Parallel Task<br/>(e.g., Clone/Pull)
participant ThreadPool as Thread Pool<br/>(4 threads)
participant Git as Git<br/>(Command Execution)
participant Repos as Repositories<br/>(Filesystem)
Gradle->>Task: Execute (cloneAll/pullAll/checkoutAll)
Task->>Task: Load repos from container
Task->>ThreadPool: Create fixed-size executor (4 threads)
loop For each repository
Task->>ThreadPool: Submit callable
end
par Parallel Execution
ThreadPool->>Git: Repo 1: git clone/pull/checkout
Git->>Repos: Access repo filesystem
Repos-->>Git: Output/status
ThreadPool->>Git: Repo 2: git clone/pull/checkout
Git->>Repos: Access repo filesystem
Repos-->>Git: Output/status
ThreadPool->>Git: Repo N: git clone/pull/checkout
Git->>Repos: Access repo filesystem
Repos-->>Git: Output/status
end
Task->>Task: Await all futures
Task->>Task: Aggregate results (success/failure)
Task->>Gradle: Log summary + throw if any failed
Estimated Code Review Effort🎯 4 (Complex) | ⏱️ ~45 minutes The diff introduces substantial new functionality across multiple parallel task implementations with non-trivial git operations, branch management logic, and parallel coordination; refactors existing task registration; adds release automation; and provides comprehensive test coverage. The heterogeneous mix of refactoring, new features, and tests across 10+ files with varying complexity patterns requires careful review of control flow, error handling, and integration with Gradle's task execution model. Possibly Related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 7
🤖 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 7-8: Change the workflow job permissions so the repository token
is read-only: replace the "contents: write" permission under the top-level
permissions block with "contents: read" (and do the same for the other
permissions occurrence that currently grants write). Update the "permissions"
entries so only "contents: read" is granted to avoid exposing a writable repo
credential to Gradle steps that only need to read source and publish externally.
In `@src/main/kotlin/zone/clanker/gradle/wrkx/task/ParallelCheckoutTask.kt`:
- Around line 110-116: The checkout branch creation currently assumes the
configured baseBranch exists locally; update the logic in
ParallelCheckoutTask.kt (the workingBranch handling block where baseBranch,
targetBranch and runGitCommand are used) to fall back to the remote ref if the
local branch is absent: first attempt to create with "git checkout -b
<targetBranch> <baseBranch>", and if that fails because <baseBranch> is not a
local branch, retry creating the branch from "origin/<baseBranch>" (or
explicitly check for the local branch with "git show-ref --verify
refs/heads/<baseBranch>" and use "origin/<baseBranch>" when absent), ensuring
runGitCommand is invoked with the chosen ref and error handling/log message is
updated accordingly.
- Around line 95-99: In checkoutRepo, you're reconstructing the repo path with
File(repoDir, repo.directoryName) instead of using the
WorkspaceRepository.clonePath property, so custom clone locations are ignored;
update checkoutRepo to use repo.clonePath (or its computed value) when building
the File and checking existence and in any returned messages so wrkx-checkout
operates on the actual clone location defined by WorkspaceRepository.
In `@src/main/kotlin/zone/clanker/gradle/wrkx/task/ParallelCloneTask.kt`:
- Around line 90-97: The cloneRepo function currently recomputes the target path
from repoDir and repo.directoryName which can diverge from the configured clone
location; change cloneRepo to use the WorkspaceRepository's resolved clonePath
property (e.g. repo.clonePath.get()/asFile) as the target File, ensure its
parent directories are created (target.parentFile?.mkdirs()), and perform the
exists check/return and subsequent clone logic against that target so it matches
includeEnabled() and other lifecycle tasks.
In `@src/main/kotlin/zone/clanker/gradle/wrkx/task/ParallelPullTask.kt`:
- Around line 102-128: The current logic fetches origin/$base but then merges
into whatever HEAD is checked out (mergeProcess), which can fast-forward the
wrong branch; fix by ensuring the local base branch is checked out before the
merge (use repo.baseBranch.get().value as the branch name), i.e. run a git
checkout <base> and verify it succeeds (like you do for fetch/merge) before
invoking the ProcessBuilder that creates mergeProcess, then proceed to merge
origin/$base into that checked-out branch and check its exit code/output
similarly.
- Around line 91-99: The pullRepo function currently builds the checkout
directory from repoDir and repo.directoryName which can be out of sync with
WorkspaceRepository.clonePath; update pullRepo to use repo.clonePath as the
source of truth when locating the repo directory and when calling hasRemote, so
replace the File(repoDir, repo.directoryName) usage with File(repo.clonePath)
(or equivalent accessors) and adjust the existence check and subsequent
hasRemote(dir) call to operate on that clonePath-derived File; ensure all
references inside pullRepo (e.g., the check message, hasRemote invocation, and
any other path uses) consistently use WorkspaceRepository.clonePath.
In `@src/test/kotlin/zone/clanker/gradle/wrkx/task/ParallelPullTaskTest.kt`:
- Around line 94-111: The test commits in pushDir rely on global git identity;
make the test hermetic by configuring a repo-local identity in pushDir before
the commit: run git config user.name and git config user.email (targeting
pushDir, e.g., via ProcessBuilder with "-C" and pushDir.absolutePath) prior to
calling git commit in the sequence shown in ParallelPullTaskTest.kt so the
commit no longer depends on machine-global git config.
🪄 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: 48f88fec-9a97-4a81-b24f-d6f6493bdbd4
📒 Files selected for processing (8)
.github/workflows/release.ymlsrc/main/kotlin/zone/clanker/gradle/wrkx/Wrkx.ktsrc/main/kotlin/zone/clanker/gradle/wrkx/task/ParallelCheckoutTask.ktsrc/main/kotlin/zone/clanker/gradle/wrkx/task/ParallelCloneTask.ktsrc/main/kotlin/zone/clanker/gradle/wrkx/task/ParallelPullTask.ktsrc/test/kotlin/zone/clanker/gradle/wrkx/task/ParallelCheckoutTaskTest.ktsrc/test/kotlin/zone/clanker/gradle/wrkx/task/ParallelCloneTaskTest.ktsrc/test/kotlin/zone/clanker/gradle/wrkx/task/ParallelPullTaskTest.kt
| permissions: | ||
| contents: write |
There was a problem hiding this comment.
Reduce the workflow token to read-only.
actions/checkout persists the job token in git config by default, so contents: write gives every Gradle step a writable repo credential even though this workflow only reads source and publishes externally.
Suggested change
permissions:
- contents: write
+ contents: readAlso applies to: 14-14
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.github/workflows/release.yml around lines 7 - 8, Change the workflow job
permissions so the repository token is read-only: replace the "contents: write"
permission under the top-level permissions block with "contents: read" (and do
the same for the other permissions occurrence that currently grants write).
Update the "permissions" entries so only "contents: read" is granted to avoid
exposing a writable repo credential to Gradle steps that only need to read
source and publish externally.
| private fun checkoutRepo(repo: WorkspaceRepository): String { | ||
| val dir = File(repoDir, repo.directoryName) | ||
| if (!dir.exists()) { | ||
| return "SKIP -- not cloned at ${dir.absolutePath}" | ||
| } |
There was a problem hiding this comment.
Use clonePath here as well.
Like the other new lifecycle tasks, Line 96 rebuilds the repo path instead of using WorkspaceRepository.clonePath. Any custom clone location will make wrkx-checkout operate on the wrong directory.
Suggested change
- val dir = File(repoDir, repo.directoryName)
+ val dir = repo.clonePath.get().asFile📝 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.
| private fun checkoutRepo(repo: WorkspaceRepository): String { | |
| val dir = File(repoDir, repo.directoryName) | |
| if (!dir.exists()) { | |
| return "SKIP -- not cloned at ${dir.absolutePath}" | |
| } | |
| private fun checkoutRepo(repo: WorkspaceRepository): String { | |
| val dir = repo.clonePath.get().asFile | |
| if (!dir.exists()) { | |
| return "SKIP -- not cloned at ${dir.absolutePath}" | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/main/kotlin/zone/clanker/gradle/wrkx/task/ParallelCheckoutTask.kt` around
lines 95 - 99, In checkoutRepo, you're reconstructing the repo path with
File(repoDir, repo.directoryName) instead of using the
WorkspaceRepository.clonePath property, so custom clone locations are ignored;
update checkoutRepo to use repo.clonePath (or its computed value) when building
the File and checking existence and in any returned messages so wrkx-checkout
operates on the actual clone location defined by WorkspaceRepository.
| if (workingBranch != null) { | ||
| val baseBranch = repo.baseBranch.get().value | ||
| val createResult = runGitCommand(dir, "git", "checkout", "-b", targetBranch, baseBranch) | ||
| check(createResult.exitCode == 0) { | ||
| "Failed to create branch '$targetBranch' from '$baseBranch': ${createResult.output}" | ||
| } | ||
| return "Created and checked out '$targetBranch' from '$baseBranch'" |
There was a problem hiding this comment.
Create the working branch from the remote base branch when needed.
Line 112 uses git checkout -b <target> <baseBranch>, which only works when <baseBranch> already exists locally. Fresh clones often only have origin/<baseBranch>, so this path can fail even though the configured base branch exists upstream.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/main/kotlin/zone/clanker/gradle/wrkx/task/ParallelCheckoutTask.kt` around
lines 110 - 116, The checkout branch creation currently assumes the configured
baseBranch exists locally; update the logic in ParallelCheckoutTask.kt (the
workingBranch handling block where baseBranch, targetBranch and runGitCommand
are used) to fall back to the remote ref if the local branch is absent: first
attempt to create with "git checkout -b <targetBranch> <baseBranch>", and if
that fails because <baseBranch> is not a local branch, retry creating the branch
from "origin/<baseBranch>" (or explicitly check for the local branch with "git
show-ref --verify refs/heads/<baseBranch>" and use "origin/<baseBranch>" when
absent), ensuring runGitCommand is invoked with the chosen ref and error
handling/log message is updated accordingly.
| private fun cloneRepo(repo: WorkspaceRepository): String { | ||
| val target = File(repoDir, repo.directoryName) | ||
| if (target.exists()) { | ||
| return "SKIP -- directory already exists at ${target.absolutePath}" | ||
| } | ||
|
|
||
| val url = repo.path.get().value | ||
| target.parentFile?.mkdirs() |
There was a problem hiding this comment.
Clone into the configured clonePath, not a recomputed path.
The plugin already resolves and stores each repo's clone location on WorkspaceRepository.clonePath. Rebuilding it from repoDir here can diverge from the path later used by includeEnabled() and the other lifecycle tasks.
Suggested change
- val target = File(repoDir, repo.directoryName)
+ val target = repo.clonePath.get().asFile📝 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.
| private fun cloneRepo(repo: WorkspaceRepository): String { | |
| val target = File(repoDir, repo.directoryName) | |
| if (target.exists()) { | |
| return "SKIP -- directory already exists at ${target.absolutePath}" | |
| } | |
| val url = repo.path.get().value | |
| target.parentFile?.mkdirs() | |
| private fun cloneRepo(repo: WorkspaceRepository): String { | |
| val target = repo.clonePath.get().asFile | |
| if (target.exists()) { | |
| return "SKIP -- directory already exists at ${target.absolutePath}" | |
| } | |
| val url = repo.path.get().value | |
| target.parentFile?.mkdirs() |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/main/kotlin/zone/clanker/gradle/wrkx/task/ParallelCloneTask.kt` around
lines 90 - 97, The cloneRepo function currently recomputes the target path from
repoDir and repo.directoryName which can diverge from the configured clone
location; change cloneRepo to use the WorkspaceRepository's resolved clonePath
property (e.g. repo.clonePath.get()/asFile) as the target File, ensure its
parent directories are created (target.parentFile?.mkdirs()), and perform the
exists check/return and subsequent clone logic against that target so it matches
includeEnabled() and other lifecycle tasks.
| private fun pullRepo(repo: WorkspaceRepository): String { | ||
| val dir = File(repoDir, repo.directoryName) | ||
| check(dir.exists()) { | ||
| "Cannot pull '${repo.repoName}' -- directory not found at ${dir.absolutePath}. " + | ||
| "Run './gradlew ${Wrkx.TASK_CLONE}-${repo.sanitizedBuildName}' to clone it first." | ||
| } | ||
|
|
||
| if (!hasRemote(dir)) { | ||
| return "SKIP -- no remote configured" |
There was a problem hiding this comment.
Use clonePath as the repo source of truth.
Line 92 reconstructs the checkout location from repoDir and directoryName, but WorkspaceRepository already carries clonePath, and src/main/kotlin/zone/clanker/gradle/wrkx/Wrkx.kt Lines 352-358 initializes it. If that property is ever overridden, wrkx-pull will run in the wrong directory.
Suggested change
- val dir = File(repoDir, repo.directoryName)
+ val dir = repo.clonePath.get().asFile📝 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.
| private fun pullRepo(repo: WorkspaceRepository): String { | |
| val dir = File(repoDir, repo.directoryName) | |
| check(dir.exists()) { | |
| "Cannot pull '${repo.repoName}' -- directory not found at ${dir.absolutePath}. " + | |
| "Run './gradlew ${Wrkx.TASK_CLONE}-${repo.sanitizedBuildName}' to clone it first." | |
| } | |
| if (!hasRemote(dir)) { | |
| return "SKIP -- no remote configured" | |
| private fun pullRepo(repo: WorkspaceRepository): String { | |
| val dir = repo.clonePath.get().asFile | |
| check(dir.exists()) { | |
| "Cannot pull '${repo.repoName}' -- directory not found at ${dir.absolutePath}. " + | |
| "Run './gradlew ${Wrkx.TASK_CLONE}-${repo.sanitizedBuildName}' to clone it first." | |
| } | |
| if (!hasRemote(dir)) { | |
| return "SKIP -- no remote configured" |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/main/kotlin/zone/clanker/gradle/wrkx/task/ParallelPullTask.kt` around
lines 91 - 99, The pullRepo function currently builds the checkout directory
from repoDir and repo.directoryName which can be out of sync with
WorkspaceRepository.clonePath; update pullRepo to use repo.clonePath as the
source of truth when locating the repo directory and when calling hasRemote, so
replace the File(repoDir, repo.directoryName) usage with File(repo.clonePath)
(or equivalent accessors) and adjust the existence check and subsequent
hasRemote(dir) call to operate on that clonePath-derived File; ensure all
references inside pullRepo (e.g., the check message, hasRemote invocation, and
any other path uses) consistently use WorkspaceRepository.clonePath.
| val base = repo.baseBranch.get().value | ||
|
|
||
| val fetchProcess = | ||
| ProcessBuilder("git", "fetch", "origin", base) | ||
| .directory(dir) | ||
| .redirectErrorStream(true) | ||
| .start() | ||
| val fetchOutput = | ||
| fetchProcess.inputStream | ||
| .bufferedReader() | ||
| .readText() | ||
| .trim() | ||
| val fetchExit = fetchProcess.waitFor() | ||
| check(fetchExit == 0) { "git fetch failed: $fetchOutput" } | ||
|
|
||
| val mergeProcess = | ||
| ProcessBuilder("git", "merge", "--ff-only", "origin/$base") | ||
| .directory(dir) | ||
| .redirectErrorStream(true) | ||
| .start() | ||
| val mergeOutput = | ||
| mergeProcess.inputStream | ||
| .bufferedReader() | ||
| .readText() | ||
| .trim() | ||
| val mergeExit = mergeProcess.waitFor() | ||
| check(mergeExit == 0) { "git merge --ff-only failed: $mergeOutput" } |
There was a problem hiding this comment.
wrkx-pull is updating whatever branch is currently checked out.
Line 118 merges origin/$base into HEAD, not into the local $base branch. If a repo is on feature/*, this either fast-forwards that feature branch or fails while leaving the base branch stale, which breaks the task contract.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/main/kotlin/zone/clanker/gradle/wrkx/task/ParallelPullTask.kt` around
lines 102 - 128, The current logic fetches origin/$base but then merges into
whatever HEAD is checked out (mergeProcess), which can fast-forward the wrong
branch; fix by ensuring the local base branch is checked out before the merge
(use repo.baseBranch.get().value as the branch name), i.e. run a git checkout
<base> and verify it succeeds (like you do for fetch/merge) before invoking the
ProcessBuilder that creates mergeProcess, then proceed to merge origin/$base
into that checked-out branch and check its exit code/output similarly.
| val pushDir = File(baseDir, "push-work") | ||
| ProcessBuilder("git", "clone", bareRepo1.absolutePath, pushDir.absolutePath) | ||
| .redirectErrorStream(true) | ||
| .start() | ||
| .waitFor() | ||
| File(pushDir, "new-file.txt").writeText("parallel pull content") | ||
| ProcessBuilder("git", "-C", pushDir.absolutePath, "add", ".") | ||
| .redirectErrorStream(true) | ||
| .start() | ||
| .waitFor() | ||
| ProcessBuilder("git", "-C", pushDir.absolutePath, "commit", "-m", "Add new file") | ||
| .redirectErrorStream(true) | ||
| .start() | ||
| .waitFor() | ||
| ProcessBuilder("git", "-C", pushDir.absolutePath, "push") | ||
| .redirectErrorStream(true) | ||
| .start() | ||
| .waitFor() |
There was a problem hiding this comment.
Make the test repo configure its own git identity.
The commit in this scenario depends on global user.name / user.email. This is one of the reasons the new workflow now mutates runner-wide git config. Set the identity on pushDir before git commit so the test stays hermetic on clean machines.
Suggested change
ProcessBuilder("git", "-C", pushDir.absolutePath, "add", ".")
.redirectErrorStream(true)
.start()
.waitFor()
+ ProcessBuilder("git", "-C", pushDir.absolutePath, "config", "user.name", "CI")
+ .redirectErrorStream(true)
+ .start()
+ .waitFor()
+ ProcessBuilder("git", "-C", pushDir.absolutePath, "config", "user.email", "ci@clanker.zone")
+ .redirectErrorStream(true)
+ .start()
+ .waitFor()
ProcessBuilder("git", "-C", pushDir.absolutePath, "commit", "-m", "Add new file")
.redirectErrorStream(true)
.start()
.waitFor()📝 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.
| val pushDir = File(baseDir, "push-work") | |
| ProcessBuilder("git", "clone", bareRepo1.absolutePath, pushDir.absolutePath) | |
| .redirectErrorStream(true) | |
| .start() | |
| .waitFor() | |
| File(pushDir, "new-file.txt").writeText("parallel pull content") | |
| ProcessBuilder("git", "-C", pushDir.absolutePath, "add", ".") | |
| .redirectErrorStream(true) | |
| .start() | |
| .waitFor() | |
| ProcessBuilder("git", "-C", pushDir.absolutePath, "commit", "-m", "Add new file") | |
| .redirectErrorStream(true) | |
| .start() | |
| .waitFor() | |
| ProcessBuilder("git", "-C", pushDir.absolutePath, "push") | |
| .redirectErrorStream(true) | |
| .start() | |
| .waitFor() | |
| val pushDir = File(baseDir, "push-work") | |
| ProcessBuilder("git", "clone", bareRepo1.absolutePath, pushDir.absolutePath) | |
| .redirectErrorStream(true) | |
| .start() | |
| .waitFor() | |
| File(pushDir, "new-file.txt").writeText("parallel pull content") | |
| ProcessBuilder("git", "-C", pushDir.absolutePath, "add", ".") | |
| .redirectErrorStream(true) | |
| .start() | |
| .waitFor() | |
| ProcessBuilder("git", "-C", pushDir.absolutePath, "config", "user.name", "CI") | |
| .redirectErrorStream(true) | |
| .start() | |
| .waitFor() | |
| ProcessBuilder("git", "-C", pushDir.absolutePath, "config", "user.email", "ci@clanker.zone") | |
| .redirectErrorStream(true) | |
| .start() | |
| .waitFor() | |
| ProcessBuilder("git", "-C", pushDir.absolutePath, "commit", "-m", "Add new file") | |
| .redirectErrorStream(true) | |
| .start() | |
| .waitFor() | |
| ProcessBuilder("git", "-C", pushDir.absolutePath, "push") | |
| .redirectErrorStream(true) | |
| .start() | |
| .waitFor() |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/test/kotlin/zone/clanker/gradle/wrkx/task/ParallelPullTaskTest.kt` around
lines 94 - 111, The test commits in pushDir rely on global git identity; make
the test hermetic by configuring a repo-local identity in pushDir before the
commit: run git config user.name and git config user.email (targeting pushDir,
e.g., via ProcessBuilder with "-C" and pushDir.absolutePath) prior to calling
git commit in the sequence shown in ParallelPullTaskTest.kt so the commit no
longer depends on machine-global git config.
Summary
Test plan
Summary by CodeRabbit
New Features
Chores
Tests