Skip to content

Commit be8a46a

Browse files
committed
commit
1 parent 58a3de9 commit be8a46a

11 files changed

Lines changed: 766 additions & 83 deletions

README.md

Lines changed: 67 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<div align="center">
2-
<h1>JAIPilot - Autogenerate High Coverage Java Unit Tests</h1>
3-
<p><strong>JAIPilot automatically writes high quality unit tests for your PR to achieve high coverage for Java codebases.</strong></p>
2+
<img src="docs/assets/jaipilot-logo.svg" alt="JAIPilot logo" width="320" />
3+
<h1>JAIPilot GitHub Action</h1>
4+
<p><strong>Automatically generate high-coverage Java unit tests on every pull request.</strong></p>
45
<p>
56
<a href="https://github.com/JAIPilot/jaipilot-cli/actions/workflows/ci.yml">
67
<img src="https://github.com/JAIPilot/jaipilot-cli/actions/workflows/ci.yml/badge.svg?branch=main" alt="CI">
@@ -13,95 +14,98 @@
1314
</a>
1415
</p>
1516
<p>
16-
<a href="#install"><strong>Install</strong></a>
17-
·
1817
<a href="#quick-start"><strong>Quick Start</strong></a>
1918
·
19+
<a href="#inputs"><strong>Inputs</strong></a>
20+
·
21+
<a href="#outputs"><strong>Outputs</strong></a>
22+
·
2023
<a href="#how-it-works"><strong>How It Works</strong></a>
2124
</p>
2225
</div>
2326

2427
<p align="center">
25-
JAIPilot automatically writes high quality unit tests for your PR to achieve high coverage for Java codebases.
28+
This repository is focused on the JAIPilot GitHub Action for PR automation.
2629
</p>
2730

2831
<hr />
2932

30-
JAIPilot automatically generates high quality high coverage unit tests for PRs for your Java codebase.
31-
32-
## Why JAIPilot
33-
34-
- Automatically generates high quality high coverage unit tests for PRs for your Java codebase
35-
- All generated tests are fully compilable, executable, and maximize line coverage
36-
- Analyzes changed Java code and context for every PR to generate high quality meaningful tests
37-
- Builds, executes, and maximizes line coverage
38-
39-
## GitHub Action (PR Automation)
40-
41-
To run JAIPilot automatically on pull requests with this action, you must provide your JAIPilot license key to the workflow.
42-
Get the key by logging in at `https://jaipilot.com` (free credits are available).
43-
44-
1. Go to your repository `Settings` -> `Secrets and variables` -> `Actions`.
45-
2. Create a repository secret (for example `JAIPILOT_LICENSE_KEY`) and paste your JAIPilot license key as the value.
46-
3. Reference that secret in your workflow input/env for the JAIPilot action.
47-
48-
Without a valid license key configured in the action, JAIPilot will not auto-execute on PRs.
49-
50-
## Install
33+
JAIPilot generates high-quality tests for changed Java production classes in pull requests and pushes the generated changes back to the PR branch.
5134

52-
Install with:
35+
## Why This Action
5336

54-
```sh
55-
curl -fsSL https://jaipilot.com/install.sh | bash
56-
```
57-
58-
That installs `jaipilot` into `~/.local/bin` by default, downloads the platform-specific release archive for your machine, and verifies the release archive SHA-256 checksum before unpacking it.
59-
60-
Bundled-runtime releases target:
61-
62-
- `linux-x64`
63-
- `linux-aarch64`
64-
- `macos-x64`
65-
- `macos-aarch64`
66-
67-
Make sure `~/.local/bin` is on your `PATH`.
37+
- Generates and updates tests for changed Java production classes in a PR
38+
- Commits generated tests back to the PR branch automatically
39+
- Supports Maven and Gradle repositories
40+
- Exposes processed and failed class counts as action outputs
6841

69-
## Usage Options
42+
## Prerequisites
7043

71-
You can use JAIPilot in either of these ways:
72-
73-
- JAIPilot CLI (run locally with `jaipilot` commands)
74-
- GitHub Action (run automatically on pull requests in GitHub)
44+
1. Get a JAIPilot license key from `https://jaipilot.com`.
45+
2. In the target repository, open `Settings -> Secrets and variables -> Actions`.
46+
3. Create a secret named `JAIPILOT_LICENSE_KEY`.
47+
4. Ensure your workflow job has `contents: write` permission so the action can push generated commits.
7548

7649
## Quick Start
7750

78-
Set your JAIPilot license key:
79-
80-
```sh
81-
export JAIPILOT_LICENSE_KEY="your-license-key"
51+
```yaml
52+
name: JAIPilot Generate Tests
53+
54+
on:
55+
pull_request:
56+
types: [opened, synchronize, reopened]
57+
58+
jobs:
59+
jaipilot:
60+
runs-on: ubuntu-latest
61+
permissions:
62+
contents: write
63+
pull-requests: write
64+
65+
steps:
66+
- name: Checkout
67+
uses: actions/checkout@v4
68+
with:
69+
ref: ${{ github.head_ref }}
70+
71+
- name: Run JAIPilot
72+
uses: JAIPilot/jaipilot-cli@action-v1
73+
with:
74+
jaipilot-license-key: ${{ secrets.JAIPILOT_LICENSE_KEY }}
8275
```
8376
84-
Get your license key by logging in at `https://jaipilot.com` (free credits are available).
85-
86-
Generate a JUnit test for a class:
87-
88-
```sh
89-
jaipilot generate src/main/java/org/example/CrashController.java
90-
```
77+
## Inputs
9178
92-
## Commands
79+
| Input | Required | Default | Description |
80+
| --- | --- | --- | --- |
81+
| `working-directory` | No | `.` | Project directory where the action runs. |
82+
| `java-version` | No | `21` | Temurin Java version to install before generation. |
83+
| `install-script-url` | No | `https://jaipilot.com/install.sh` | Installer script URL used by the action. |
84+
| `jaipilot-license-key` | Yes | - | JAIPilot license key used to authorize generation. |
85+
| `fail-on-generate-error` | No | `true` | Fail the workflow if one or more classes fail generation. |
86+
| `commit-message` | No | `chore: generate tests with JAIPilot` | Commit message for generated test changes. |
87+
| `git-user-name` | No | `github-actions[bot]` | Git author name for generated commit. |
88+
| `git-user-email` | No | `41898282+github-actions[bot]@users.noreply.github.com` | Git author email for generated commit. |
9389

94-
JAIPilot CLI exposes only one command:
90+
## Outputs
9591

96-
- `jaipilot generate <path-to-class>` generates or updates a corresponding test file.
92+
| Output | Description |
93+
| --- | --- |
94+
| `processed-classes` | Number of changed Java production classes processed. |
95+
| `failed-classes` | Number of classes for which generation failed. |
96+
| `commit-sha` | Commit SHA pushed by the action; empty when no changes were committed. |
9797

9898
## How It Works
9999

100-
`jaipilot generate` reads local source files, calls the backend generation API, polls for completion, writes the returned test file, and validates it with your build tool in three stages: compile, codebase rules, and targeted test execution (`test-compile`/`verify`/targeted `test` for Maven, `testClasses`/`check`/targeted `test --tests` for Gradle). Rule validation is run with full-suite test execution skipped because JAIPilot already runs targeted test validation separately.
100+
- Detects changed files from PR base branch (or previous commit for push events).
101+
- Filters to non-test `.java` production classes only.
102+
- Generates tests for each changed class.
103+
- Commits and pushes generated tests to the same branch.
104+
- Optionally fails the job when generation errors occur.
101105

102-
If validation fails, JAIPilot automatically performs iterative fixing passes using build failure logs. When required context classes are missing from local sources, JAIPilot can trigger dependency source download and retry.
106+
## Action Publishing
103107

104-
For Maven wrapper usage, JAIPilot only uses wrapper scripts when `.mvn/wrapper/maven-wrapper.properties` exists; otherwise it falls back to system `mvn`/`mvn.cmd`.
108+
See [docs/github-action-publishing.md](docs/github-action-publishing.md) for release tagging and publishing flow.
105109

106110
## License
107111

action.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ description: "Achieve 80%+ unit test coverage on every PR by automatically gener
33
author: "JAIPilot"
44

55
branding:
6-
icon: "cpu"
6+
icon: "shield"
77
color: "blue"
88

99
inputs:

docs/assets/jaipilot-logo.svg

Lines changed: 13 additions & 0 deletions
Loading

src/main/java/com/jaipilot/cli/JaipilotEndpointConfig.java

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,11 @@
22

33
public final class JaipilotEndpointConfig {
44

5-
public static final String DEFAULT_WEBSITE_BASE = "https://www.jaipilot.com";
65
public static final String DEFAULT_BACKEND_URL = "https://otxfylhjrlaesjagfhfi.supabase.co";
76

87
private JaipilotEndpointConfig() {
98
}
109

11-
public static String resolveWebsiteBase() {
12-
return trimTrailingSlash(firstNonBlank(
13-
System.getenv("JAIPILOT_WEBSITE_BASE"),
14-
System.getProperty("jaipilot.website.base"),
15-
DEFAULT_WEBSITE_BASE
16-
));
17-
}
18-
1910
public static String resolveBackendUrl() {
2011
return trimTrailingSlash(firstNonBlank(
2112
System.getenv("JAIPILOT_BACKEND_URL"),

src/main/java/com/jaipilot/cli/backend/HttpJunitLlmBackendClient.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ private String buildInvokeRequestBody(InvokeJunitLlmRequest request) throws IOEx
232232
putString(root, "type", request.type());
233233
putString(root, "cutName", request.cutName());
234234
putString(root, "testClassName", request.testClassName());
235-
putString(root, "mockitoVersion", request.mockitoVersion());
235+
putOptionalString(root, "mockitoVersion", request.mockitoVersion());
236236
putString(root, "cutCode", request.cutCode());
237237
putStringArray(root, "cachedContextClasses", request.cachedContextClasses());
238238
putString(root, "initialTestClassCode", request.initialTestClassCode());

src/main/java/com/jaipilot/cli/classpath/BuildToolClassResolutionService.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,6 @@ public ClassResolutionResult locateOrThrow(
9595

9696
public Optional<ResolvedSource> resolveSource(
9797
ClassResolutionResult classResult,
98-
Path projectRoot,
9998
Path moduleRoot,
10099
ResolutionOptions options
101100
) {
@@ -104,11 +103,10 @@ public Optional<ResolvedSource> resolveSource(
104103

105104
public ResolvedSource resolveSourceOrThrow(
106105
ClassResolutionResult classResult,
107-
Path projectRoot,
108106
Path moduleRoot,
109107
ResolutionOptions options
110108
) {
111-
return resolveSource(classResult, projectRoot, moduleRoot, options)
109+
return resolveSource(classResult, moduleRoot, options)
112110
.orElseThrow(() -> new ClasspathResolutionException(new ResolutionFailure(
113111
ResolutionFailureCategory.SOURCE_NOT_AVAILABLE,
114112
null,
@@ -156,7 +154,7 @@ public Optional<ResolvedSource> resolveSourceByFqcn(
156154
if (classResult.kind() == LocationKind.NOT_FOUND) {
157155
continue;
158156
}
159-
Optional<ResolvedSource> resolved = resolveSource(classResult, projectRoot, moduleRoot, normalizedOptions);
157+
Optional<ResolvedSource> resolved = resolveSource(classResult, moduleRoot, normalizedOptions);
160158
if (resolved.isPresent()) {
161159
return resolved;
162160
}

src/main/java/com/jaipilot/cli/service/JunitLlmSessionRunner.java

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ public final class JunitLlmSessionRunner {
2929
private static final int MAX_INTERACTIONS = 10;
3030
private static final int MAX_FETCH_ATTEMPTS = 120;
3131
private static final long FETCH_DELAY_MILLIS = 1_000L;
32-
private static final String DEFAULT_MOCKITO_VERSION = "5.11.0";
3332
private static final String MISSING_CONTEXT_CLASS_PLACEHOLDER = "Class not found";
3433
private static final String MAVEN_CLASSPATH_OUTPUT_FILE = ".classpath.txt";
3534
private static final String GRADLE_CLASSPATH_OUTPUT_FILE = ".gradle-classpath.txt";
@@ -78,6 +77,7 @@ public final class JunitLlmSessionRunner {
7877
private final MavenCommandBuilder mavenCommandBuilder;
7978
private final GradleCommandBuilder gradleCommandBuilder;
8079
private final ProcessExecutor processExecutor;
80+
private final MockitoVersionResolver mockitoVersionResolver;
8181

8282
public JunitLlmSessionRunner(
8383
JunitLlmBackendClient backendClient,
@@ -92,7 +92,8 @@ public JunitLlmSessionRunner(
9292
consoleLogger,
9393
new MavenCommandBuilder(),
9494
new GradleCommandBuilder(),
95-
new ProcessExecutor()
95+
new ProcessExecutor(),
96+
new MockitoVersionResolver(fileService)
9697
);
9798
}
9899

@@ -103,7 +104,8 @@ public JunitLlmSessionRunner(
103104
JunitLlmConsoleLogger consoleLogger,
104105
MavenCommandBuilder mavenCommandBuilder,
105106
GradleCommandBuilder gradleCommandBuilder,
106-
ProcessExecutor processExecutor
107+
ProcessExecutor processExecutor,
108+
MockitoVersionResolver mockitoVersionResolver
107109
) {
108110
this.backendClient = backendClient;
109111
this.fileService = fileService;
@@ -112,6 +114,9 @@ public JunitLlmSessionRunner(
112114
this.mavenCommandBuilder = mavenCommandBuilder == null ? new MavenCommandBuilder() : mavenCommandBuilder;
113115
this.gradleCommandBuilder = gradleCommandBuilder == null ? new GradleCommandBuilder() : gradleCommandBuilder;
114116
this.processExecutor = processExecutor == null ? new ProcessExecutor() : processExecutor;
117+
this.mockitoVersionResolver = mockitoVersionResolver == null
118+
? new MockitoVersionResolver(fileService)
119+
: mockitoVersionResolver;
115120
}
116121

117122
public JunitLlmSessionResult run(JunitLlmSessionRequest sessionRequest) throws Exception {
@@ -125,6 +130,10 @@ public JunitLlmSessionResult run(JunitLlmSessionRequest sessionRequest) throws E
125130
);
126131
List<String> cachedContextPaths = usedContextClassPathCache.read(cacheKeyPath);
127132
consoleLogger.announceCacheRead(cacheKeyPath, cachedContextPaths);
133+
String mockitoVersion = mockitoVersionResolver.resolve(
134+
sessionRequest.projectRoot(),
135+
sessionRequest.cutPath()
136+
);
128137

129138
String currentSessionId = blankToNull(sessionRequest.sessionId());
130139
String currentTestCode = normalizeNullableText(sessionRequest.newTestClassCode());
@@ -137,7 +146,7 @@ public JunitLlmSessionResult run(JunitLlmSessionRequest sessionRequest) throws E
137146
sessionRequest.operation().apiValue(),
138147
cutName,
139148
testClassName,
140-
DEFAULT_MOCKITO_VERSION,
149+
mockitoVersion,
141150
cutCode,
142151
buildCachedContextClasses(
143152
sessionRequest.projectRoot(),

0 commit comments

Comments
 (0)