Skip to content

Commit d844e39

Browse files
feat(code-governance): add closePR method for PR cleanup (#44)
* feat(code-governance): add closePR method for PR cleanup - Add closePR() method to close PRs without merging - Optional deleteBranch parameter to also remove source branch - Add closedAt field to PRRecord type - Update tests for new constructor parameter Enterprise feature requiring portal authentication. * chore: update version to 2.2.0 * fix(planning): correct getPlanStatus endpoint path Fixed endpoint path from /api/v1/orchestrator/plan/{id} to /api/v1/plan/{id} to match agent proxy routes. * feat(auth): standardize OAuth2 Basic authentication across SDK - Update addAuthHeaders() to use OAuth2 Basic auth format - Uses Authorization: Basic base64(clientId:clientSecret) header - Remove non-standard X-Client-ID and X-Client-Secret headers - X-License-Key retained as fallback for backward compatibility - Add comprehensive tests for OAuth2 Basic auth - Update CHANGELOG with authentication changes * BREAKING: Remove licenseKey, use only OAuth2 Basic auth (v3.0.0) - Remove licenseKey field from AxonFlowConfig - Remove X-License-Key header support - Simplify addAuthHeaders() to only use OAuth2 Basic auth - Update all tests to check for Authorization: Basic header - Update javadoc examples to use clientId/clientSecret - Update README and CHANGELOG with breaking changes Breaking Changes: - licenseKey config option removed - X-License-Key header no longer sent - clientId and clientSecret are now required for enterprise features * docs: Update README with v3.0.0 version and OAuth2 env vars - Update version to 3.0.0 in Maven and Gradle examples - Replace AXONFLOW_LICENSE_KEY with CLIENT_ID/CLIENT_SECRET env vars - Update error handling comment to reference credentials * fix(auth): make clientSecret optional for community mode clientId is now the only required credential for features requiring auth. clientSecret remains optional for community/self-hosted deployments but is still validated server-side for enterprise mode. Part of OAuth2 migration - PR #948 * feat(gateway): default empty userToken to 'anonymous' In community mode without OAuth2, userToken is used for audit/tracking purposes only. If null or empty, SDK now defaults to 'anonymous' so callers don't need to handle the fallback themselves. - Added default handling in ClientRequest constructor * docs: refine changelog to v2.2.0 (non-breaking release) - OAuth2-style auth is additive, not breaking (clientId/clientSecret optional) - Removes breaking change framing for minor version bump * fix: correct README version to 2.2.0
1 parent 8003c9e commit d844e39

14 files changed

Lines changed: 200 additions & 123 deletions

File tree

CHANGELOG.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,40 @@ All notable changes to the AxonFlow Java SDK will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [2.2.0] - 2026-01-08
9+
10+
### Added
11+
12+
- **OAuth2-style client credentials**: New `clientId()` and `clientSecret()` builder methods following OAuth2 client credentials pattern.
13+
- `clientId` is used for request identification (required for most API calls)
14+
- `clientSecret` is optional - community/self-hosted deployments work without it
15+
16+
- **Enterprise: Close PR** (`closePR`): Close a PR without merging and optionally delete the branch
17+
- Useful for cleaning up test/demo PRs created by code governance examples
18+
- Supports all Git providers: GitHub, GitLab, Bitbucket
19+
- Requires enterprise portal authentication
20+
21+
### Changed
22+
23+
- **Simplified authentication**: For community mode, simply provide `clientId` for request identification. No `clientSecret` needed.
24+
25+
```java
26+
// Community mode - no secret needed
27+
AxonFlowClient client = AxonFlowClient.builder()
28+
.endpoint("http://localhost:8080")
29+
.clientId("my-app") // Used for request identification
30+
.build();
31+
```
32+
33+
### Fixed
34+
35+
- **getPlanStatus endpoint**: Fixed endpoint path from `/api/v1/orchestrator/plan/{id}` to `/api/v1/plan/{id}` to match agent proxy routes
36+
37+
### Enterprise
38+
39+
- OAuth2 Basic auth: `Authorization: Basic base64(clientId:clientSecret)` replaces `X-License-Key` header
40+
- Removed `licenseKey()` builder method (use `clientId()`/`clientSecret()`)
41+
842
## [2.1.2] - 2026-01-07
943

1044
### Fixed

README.md

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,14 @@ Official Java SDK for [AxonFlow](https://getaxonflow.com) - AI Governance Platfo
1919
<dependency>
2020
<groupId>com.getaxonflow</groupId>
2121
<artifactId>axonflow-sdk</artifactId>
22-
<version>1.0.0</version>
22+
<version>2.2.0</version>
2323
</dependency>
2424
```
2525

2626
### Gradle
2727

2828
```groovy
29-
implementation 'com.getaxonflow:axonflow-sdk:1.0.0'
29+
implementation 'com.getaxonflow:axonflow-sdk:3.0.0'
3030
```
3131

3232
## Quick Start
@@ -45,7 +45,8 @@ public class GatewayExample {
4545
// Initialize client
4646
AxonFlow client = AxonFlow.create(AxonFlowConfig.builder()
4747
.endpoint("https://agent.getaxonflow.com")
48-
.licenseKey("your-license-key")
48+
.clientId("your-client-id")
49+
.clientSecret("your-client-secret")
4950
.build());
5051

5152
// Step 1: Pre-check the request
@@ -99,7 +100,8 @@ public class ProxyExample {
99100
public static void main(String[] args) {
100101
AxonFlow client = AxonFlow.create(AxonFlowConfig.builder()
101102
.endpoint("https://agent.getaxonflow.com")
102-
.licenseKey("your-license-key")
103+
.clientId("your-client-id")
104+
.clientSecret("your-client-secret")
103105
.build());
104106

105107
ClientResponse response = client.executeQuery(
@@ -126,7 +128,8 @@ public class ProxyExample {
126128
```java
127129
AxonFlowConfig config = AxonFlowConfig.builder()
128130
.endpoint("https://agent.getaxonflow.com") // Required
129-
.licenseKey("your-license-key") // Required for cloud
131+
.clientId("your-client-id")
132+
.clientSecret("your-client-secret") // Required for cloud
130133
.timeout(Duration.ofSeconds(30)) // Default: 60s
131134
.debug(true) // Enable request logging
132135
.insecureSkipVerify(false) // SSL verification (default: false)
@@ -142,7 +145,8 @@ The SDK supports configuration via environment variables:
142145
| Variable | Description |
143146
|----------|-------------|
144147
| `AXONFLOW_AGENT_URL` | AxonFlow agent URL |
145-
| `AXONFLOW_LICENSE_KEY` | License key for authentication |
148+
| `AXONFLOW_CLIENT_ID` | OAuth2 client ID for authentication |
149+
| `AXONFLOW_CLIENT_SECRET` | OAuth2 client secret for authentication |
146150
| `AXONFLOW_DEBUG` | Enable debug logging (`true`/`false`) |
147151

148152
## API Reference
@@ -216,7 +220,7 @@ The SDK provides typed exceptions for different error scenarios:
216220
try {
217221
PolicyApproval approval = client.getPolicyApprovedContext(request);
218222
} catch (AxonFlowAuthenticationException e) {
219-
// Invalid or missing license key
223+
// Invalid or missing credentials
220224
System.err.println("Authentication failed: " + e.getMessage());
221225
} catch (AxonFlowRateLimitException e) {
222226
// Rate limit exceeded
@@ -240,7 +244,8 @@ The SDK includes automatic retry with exponential backoff:
240244
```java
241245
AxonFlowConfig config = AxonFlowConfig.builder()
242246
.endpoint("https://agent.getaxonflow.com")
243-
.licenseKey("your-license-key")
247+
.clientId("your-client-id")
248+
.clientSecret("your-client-secret")
244249
.retryConfig(RetryConfig.builder()
245250
.maxAttempts(3)
246251
.initialDelayMs(100)
@@ -258,7 +263,8 @@ Enable caching for repeated policy checks:
258263
```java
259264
AxonFlowConfig config = AxonFlowConfig.builder()
260265
.endpoint("https://agent.getaxonflow.com")
261-
.licenseKey("your-license-key")
266+
.clientId("your-client-id")
267+
.clientSecret("your-client-secret")
262268
.cacheEnabled(true)
263269
.cacheTtl(Duration.ofMinutes(5))
264270
.cacheMaxSize(1000)
@@ -279,7 +285,8 @@ import com.getaxonflow.sdk.interceptors.*;
279285
// Initialize AxonFlow client
280286
AxonFlow axonflow = AxonFlow.create(AxonFlowConfig.builder()
281287
.endpoint("https://agent.getaxonflow.com")
282-
.licenseKey("your-license-key")
288+
.clientId("your-client-id")
289+
.clientSecret("your-client-secret")
283290
.build());
284291

285292
// Create interceptor

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
<groupId>com.getaxonflow</groupId>
88
<artifactId>axonflow-sdk</artifactId>
9-
<version>2.1.2</version>
9+
<version>2.2.0</version>
1010
<packaging>jar</packaging>
1111

1212
<name>AxonFlow Java SDK</name>

src/main/java/com/getaxonflow/sdk/AxonFlow.java

Lines changed: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@
3636
import java.io.Closeable;
3737
import java.io.IOException;
3838
import java.net.URI;
39+
import java.nio.charset.StandardCharsets;
3940
import java.util.ArrayList;
41+
import java.util.Base64;
4042
import java.util.Collections;
4143
import java.util.HashMap;
4244
import java.util.List;
@@ -723,7 +725,7 @@ public PlanResponse getPlanStatus(String planId) {
723725

724726
return retryExecutor.execute(() -> {
725727
Request httpRequest = buildRequest("GET",
726-
"/api/v1/orchestrator/plan/" + planId, null);
728+
"/api/v1/plan/" + planId, null);
727729
try (Response response = httpClient.newCall(httpRequest).execute()) {
728730
return parseResponse(response, PlanResponse.class);
729731
}
@@ -886,11 +888,7 @@ public ConnectorResponse queryConnector(ConnectorQuery query) {
886888
context.put("params", query.getParameters());
887889
}
888890

889-
// Determine clientId: prefer config.getClientId(), fallback to license key
890891
String clientId = config.getClientId();
891-
if (clientId == null || clientId.isEmpty()) {
892-
clientId = config.getLicenseKey();
893-
}
894892

895893
ClientRequest clientRequest = ClientRequest.builder()
896894
.query(query.getOperation())
@@ -1665,29 +1663,24 @@ private void addAuthHeaders(Request.Builder builder) {
16651663
return;
16661664
}
16671665

1668-
// Prefer license key
1669-
if (config.getLicenseKey() != null && !config.getLicenseKey().isEmpty()) {
1670-
builder.header("X-License-Key", config.getLicenseKey());
1671-
return;
1672-
}
1673-
1674-
// Fall back to client credentials
1675-
if (config.getClientId() != null && config.getClientSecret() != null) {
1676-
builder.header("X-Client-ID", config.getClientId());
1677-
builder.header("X-Client-Secret", config.getClientSecret());
1678-
}
1666+
// OAuth2-style: Authorization: Basic base64(clientId:clientSecret)
1667+
String credentials = config.getClientId() + ":" + config.getClientSecret();
1668+
String encoded = Base64.getEncoder().encodeToString(
1669+
credentials.getBytes(StandardCharsets.UTF_8)
1670+
);
1671+
builder.header("Authorization", "Basic " + encoded);
16791672
}
16801673

16811674
/**
16821675
* Requires credentials for enterprise features.
16831676
*
16841677
* @param feature the feature name for error message
1685-
* @throws AuthenticationException if no credentials are configured
1678+
* @throws AuthenticationException if clientId is not configured
16861679
*/
16871680
private void requireCredentials(String feature) {
16881681
if (!config.hasCredentials()) {
16891682
throw new AuthenticationException(
1690-
feature + " requires credentials. Set licenseKey or clientId/clientSecret in config."
1683+
feature + " requires clientId. Set clientId in config (clientSecret is optional for community mode)."
16911684
);
16921685
}
16931686
}
@@ -2128,6 +2121,36 @@ public PRRecord syncPRStatus(String prId) throws IOException {
21282121
}
21292122
}
21302123

2124+
/**
2125+
* Closes a PR without merging and optionally deletes the branch.
2126+
* This is an enterprise feature for cleaning up test/demo PRs.
2127+
* Supports all Git providers: GitHub, GitLab, Bitbucket.
2128+
*
2129+
* @param prId the PR record ID to close
2130+
* @param deleteBranch whether to also delete the source branch
2131+
* @return the closed PR record
2132+
* @throws IOException if the request fails
2133+
*/
2134+
public PRRecord closePR(String prId, boolean deleteBranch) throws IOException {
2135+
requirePortalLogin();
2136+
logger.debug("Closing PR: {} (deleteBranch={})", prId, deleteBranch);
2137+
2138+
String url = getPortalUrl() + "/api/v1/code-governance/prs/" + prId;
2139+
if (deleteBranch) {
2140+
url += "?delete_branch=true";
2141+
}
2142+
2143+
Request.Builder builder = new Request.Builder()
2144+
.url(url)
2145+
.delete();
2146+
2147+
addPortalSessionCookie(builder);
2148+
2149+
try (Response response = httpClient.newCall(builder.build()).execute()) {
2150+
return parseResponse(response, PRRecord.class);
2151+
}
2152+
}
2153+
21312154
/**
21322155
* Gets aggregated code governance metrics for the tenant.
21332156
*

src/main/java/com/getaxonflow/sdk/AxonFlowConfig.java

Lines changed: 5 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ public final class AxonFlowConfig {
5151
private final String endpoint;
5252
private final String clientId;
5353
private final String clientSecret;
54-
private final String licenseKey;
5554
private final Mode mode;
5655
private final Duration timeout;
5756
private final boolean debug;
@@ -64,7 +63,6 @@ private AxonFlowConfig(Builder builder) {
6463
this.endpoint = normalizeUrl(builder.endpoint != null ? builder.endpoint : DEFAULT_ENDPOINT);
6564
this.clientId = builder.clientId;
6665
this.clientSecret = builder.clientSecret;
67-
this.licenseKey = builder.licenseKey;
6866
this.mode = builder.mode != null ? builder.mode : Mode.PRODUCTION;
6967
this.timeout = builder.timeout != null ? builder.timeout : DEFAULT_TIMEOUT;
7068
this.debug = builder.debug;
@@ -87,13 +85,13 @@ private void validate() {
8785
/**
8886
* Checks if credentials are configured.
8987
*
90-
* <p>Returns true if either a license key or client credentials are set.
88+
* <p>Returns true if clientId is set.
89+
* clientSecret is optional for community mode but required for enterprise.
9190
*
92-
* @return true if credentials are available
91+
* @return true if clientId is available
9392
*/
9493
public boolean hasCredentials() {
95-
return (licenseKey != null && !licenseKey.isEmpty()) ||
96-
(clientId != null && clientSecret != null && !clientSecret.isEmpty());
94+
return clientId != null && !clientId.isEmpty();
9795
}
9896

9997
private String normalizeUrl(String url) {
@@ -122,7 +120,6 @@ public boolean isLocalhost() {
122120
* <li>AXONFLOW_AGENT_URL - The endpoint URL (kept for backwards compatibility)</li>
123121
* <li>AXONFLOW_CLIENT_ID - The client ID</li>
124122
* <li>AXONFLOW_CLIENT_SECRET - The client secret</li>
125-
* <li>AXONFLOW_LICENSE_KEY - The license key</li>
126123
* <li>AXONFLOW_MODE - Operating mode (production/sandbox)</li>
127124
* <li>AXONFLOW_TIMEOUT_SECONDS - Request timeout in seconds</li>
128125
* <li>AXONFLOW_DEBUG - Enable debug mode (true/false)</li>
@@ -149,11 +146,6 @@ public static AxonFlowConfig fromEnvironment() {
149146
builder.clientSecret(clientSecret);
150147
}
151148

152-
String licenseKey = System.getenv("AXONFLOW_LICENSE_KEY");
153-
if (licenseKey != null && !licenseKey.isEmpty()) {
154-
builder.licenseKey(licenseKey);
155-
}
156-
157149
String modeStr = System.getenv("AXONFLOW_MODE");
158150
if (modeStr != null && !modeStr.isEmpty()) {
159151
builder.mode(Mode.fromValue(modeStr));
@@ -188,10 +180,6 @@ public String getClientSecret() {
188180
return clientSecret;
189181
}
190182

191-
public String getLicenseKey() {
192-
return licenseKey;
193-
}
194-
195183
public Mode getMode() {
196184
return mode;
197185
}
@@ -233,13 +221,12 @@ public boolean equals(Object o) {
233221
insecureSkipVerify == that.insecureSkipVerify &&
234222
Objects.equals(endpoint, that.endpoint) &&
235223
Objects.equals(clientId, that.clientId) &&
236-
Objects.equals(licenseKey, that.licenseKey) &&
237224
mode == that.mode;
238225
}
239226

240227
@Override
241228
public int hashCode() {
242-
return Objects.hash(endpoint, clientId, licenseKey, mode, debug, insecureSkipVerify);
229+
return Objects.hash(endpoint, clientId, mode, debug, insecureSkipVerify);
243230
}
244231

245232
@Override
@@ -260,7 +247,6 @@ public static final class Builder {
260247
private String endpoint;
261248
private String clientId;
262249
private String clientSecret;
263-
private String licenseKey;
264250
private Mode mode;
265251
private Duration timeout;
266252
private boolean debug;
@@ -321,17 +307,6 @@ public Builder clientSecret(String clientSecret) {
321307
return this;
322308
}
323309

324-
/**
325-
* Sets the license key for SaaS authentication.
326-
*
327-
* @param licenseKey the license key
328-
* @return this builder
329-
*/
330-
public Builder licenseKey(String licenseKey) {
331-
this.licenseKey = licenseKey;
332-
return this;
333-
}
334-
335310
/**
336311
* Sets the operating mode.
337312
*

src/main/java/com/getaxonflow/sdk/interceptors/AnthropicInterceptor.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
* // Create AxonFlow client
3030
* AxonFlow axonflow = AxonFlow.builder()
3131
* .agentUrl("http://localhost:8080")
32-
* .licenseKey("your-license-key")
32+
* .clientId("your-client-id").clientSecret("your-client-secret")
3333
* .build();
3434
*
3535
* // Create interceptor

src/main/java/com/getaxonflow/sdk/interceptors/OpenAIInterceptor.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,9 @@
3030
* <pre>{@code
3131
* // Create AxonFlow client
3232
* AxonFlow axonflow = AxonFlow.builder()
33-
* .agentUrl("http://localhost:8080")
34-
* .licenseKey("your-license-key")
33+
* .endpoint("http://localhost:8080")
34+
* .clientId("your-client-id")
35+
* .clientSecret("your-client-secret")
3536
* .build();
3637
*
3738
* // Create interceptor

src/main/java/com/getaxonflow/sdk/interceptors/package-info.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
* // Create AxonFlow client
2424
* AxonFlow axonflow = AxonFlow.builder()
2525
* .agentUrl("http://localhost:8080")
26-
* .licenseKey("your-license-key")
26+
* .clientId("your-client-id").clientSecret("your-client-secret")
2727
* .build();
2828
*
2929
* // Wrap OpenAI calls

0 commit comments

Comments
 (0)