Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 150 additions & 19 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
# This workflow will build a Java project with Maven
# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven

name: build

Expand All @@ -25,40 +23,173 @@ on:
branches: [ main ]

jobs:
build:
compile-matrix:
runs-on: ubuntu-latest
strategy:
matrix:
jdk: [8, 11, 17]
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Set up JDK
uses: actions/setup-java@v1
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: ${{ matrix.jdk }}
- name: Cache Maven packages
uses: actions/cache@v4
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
key: ${{ runner.os }}-maven-compile-${{ matrix.jdk }}-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-maven-compile-${{ matrix.jdk }}-
${{ runner.os }}-maven-
- name: JDK 8
if: matrix.jdk == '8'
- name: Compile
run: mvn -B clean compile -Dmaven.gitcommitid.skip=true

unit-integration-pr:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 8
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 8
- name: Cache Maven packages
uses: actions/cache@v4
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-unit-integration-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-maven-unit-integration-
${{ runner.os }}-maven-
- name: Run unit and integration tests
uses: nick-fields/retry@v3
with:
timeout_minutes: 3
timeout_minutes: 4
max_attempts: 3
retry_wait_seconds: 1
command: mvn -B clean package -P travis jacoco:report -Dmaven.gitcommitid.skip=true
- name: JDK 11
if: matrix.jdk == '11'
run: mvn -B clean compile -Dmaven.gitcommitid.skip=true
- name: JDK 17
if: matrix.jdk == '17'
run: mvn -B clean compile -Dmaven.gitcommitid.skip=true
command: mvn -B clean test -P travis jacoco:report -Dmaven.gitcommitid.skip=true
- name: Upload coverage to Codecov
if: matrix.jdk == '8'
uses: codecov/codecov-action@v1
uses: codecov/codecov-action@v4
with:
file: ${{ github.workspace }}/apollo-*/target/site/jacoco/jacoco.xml
files: ${{ github.workspace }}/apollo-*/target/site/jacoco/jacoco.xml
Comment on lines 73 to 76
Copy link
Contributor

@coderabbitai coderabbitai bot Feb 18, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

codecov/codecov-action v4 token requirement documentation

💡 Result:

For codecov/codecov-action@v4, tokenless uploads are not supported (i.e., you should provide CODECOV_TOKEN via with: token: or env: CODECOV_TOKEN). The one exception called out in the v4 docs is that PRs from forks into an upstream public repo can upload without the upstream repo’s token. [1]

Codecov’s token docs also explain that private repos always require a token, and that tokenless uploading for public repos depends on org/repo settings—but the docs note that disabling token authentication for public repos is only available on codecov-action > v5.0 (or codecov-cli > 0.9), reinforcing that v4 generally requires tokens. [2]

Primary references: Codecov Action README (v4 release notes) [1], Codecov “Codecov Tokens” documentation [2].

Sources: [1] (github.com) [2] (docs.codecov.com)

Citations:


codecov-action@v4 requires a token — upload will fail without it.

Starting with v4, codecov/codecov-action requires CODECOV_TOKEN to be explicitly provided. Without it, the coverage upload step will fail in most scenarios. The only exception is pull requests from forks into public upstream repositories.

Proposed fix
    - name: Upload coverage to Codecov
      uses: codecov/codecov-action@v4
      with:
        files: ${{ github.workspace }}/apollo-*/target/site/jacoco/jacoco.xml
+       token: ${{ secrets.CODECOV_TOKEN }}
📝 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: Upload coverage to Codecov
if: matrix.jdk == '8'
uses: codecov/codecov-action@v1
uses: codecov/codecov-action@v4
with:
files: ${{ github.workspace }}/apollo-*/target/site/jacoco/jacoco.xml
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
files: ${{ github.workspace }}/apollo-*/target/site/jacoco/jacoco.xml
token: ${{ secrets.CODECOV_TOKEN }}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/build.yml around lines 70 - 73, The Codecov upload step
"Upload coverage to Codecov" uses codecov/codecov-action@v4 which requires a
token; update that job to pass the token (e.g., add a with: token: ${{
secrets.CODECOV_TOKEN }} or set CODECOV_TOKEN via env/secrets) so the action
receives the required CODECOV_TOKEN for uploads when running in non-fork PR or
CI contexts; ensure the token reference uses the repository secret name you have
configured.

Copy link
Member Author

@nobodyiam nobodyiam Feb 18, 2026

Choose a reason for hiding this comment

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

Thanks for the note. I’m intentionally not forcing a token here: this repo runs as a public upstream + fork PR flow, and codecov-action@v4 is working in this setup (coverage upload passed on the latest run). Adding a required secret here may cause fork-PR visibility issues. If uploads become unstable later, I’ll follow up with a dedicated token/action upgrade change.

Copy link
Contributor

Choose a reason for hiding this comment

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

@nobodyiam Thank you for the clarification! Your approach makes perfect sense — since the repository operates as a public upstream with fork PRs, you're correctly leveraging the exception where codecov-action@v4 uploads without a token. The successful coverage uploads in this PR confirm that setup is working as expected, and avoiding secret visibility concerns for fork contributors is a good practice.

I appreciate you explaining the context, and your plan to revisit if needed is reasonable. 👍


🧠 Learnings used
Learnt from: CR
Repo: apolloconfig/apollo-java PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-08T04:55:31.357Z
Learning: Applies to CHANGES.md : Format CHANGES.md entries as Markdown links with link text as change description and link target as PR URL (not issue URL), e.g., [Fix ... detailed summary](https://github.com/apolloconfig/apollo-java/pull/123)

Learnt from: CR
Repo: apolloconfig/apollo-java PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-08T04:55:31.357Z
Learning: Applies to {pom.xml,*/pom.xml} : Multi-module Maven repository with root pom.xml containing core modules: apollo-core, apollo-client, apollo-client-config-data, apollo-mockserver, apollo-openapi, and apollo-plugin


compat-api:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 8
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 8
- name: Cache Maven packages
uses: actions/cache@v4
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-compat-api-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-maven-compat-api-
${{ runner.os }}-maven-
- name: Build local artifacts for api compatibility
run: |
mvn -B -pl apollo-core,apollo-client,apollo-mockserver -am \
-DskipTests install -Dmaven.gitcommitid.skip=true
- name: Run api compatibility tests
run: |
mvn -B -f apollo-compat-tests/pom.xml -pl apollo-api-compat-it \
test -Dmaven.gitcommitid.skip=true

compat-spring:
runs-on: ubuntu-latest
strategy:
matrix:
include:
- name: spring-3.1.1-jdk8
java: 8
spring_framework: 3.1.1.RELEASE
java_version_prop: 1.8
- name: spring-6.1-jdk17
java: 17
spring_framework: 6.1.18
java_version_prop: 17
name: compat-spring-${{ matrix.name }}
steps:
- uses: actions/checkout@v4
- name: Set up JDK
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: ${{ matrix.java }}
- name: Cache Maven packages
uses: actions/cache@v4
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-compat-spring-${{ matrix.name }}-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-maven-compat-spring-${{ matrix.name }}-
${{ runner.os }}-maven-
- name: Build local artifacts for compat tests
run: |
mvn -B -pl apollo-core,apollo-client,apollo-mockserver -am \
-DskipTests install -Dmaven.gitcommitid.skip=true
- name: Run spring compatibility tests
run: |
mvn -B -f apollo-compat-tests/pom.xml -pl apollo-spring-compat-it \
-Dspring.framework.version=${{ matrix.spring_framework }} \
-Djava.version=${{ matrix.java_version_prop }} \
test -Dmaven.gitcommitid.skip=true

compat-spring-boot:
runs-on: ubuntu-latest
strategy:
matrix:
include:
- name: spring-boot-2.7-jdk8
java: 8
spring_boot: 2.7.18
java_version_prop: 1.8
compat_slf4j: 1.7.36
compat_vintage: 5.7.0
- name: spring-boot-3.3-jdk17
java: 17
spring_boot: 3.3.10
java_version_prop: 17
compat_slf4j: 2.0.17
compat_vintage: 5.10.5
- name: spring-boot-4.0-jdk17
java: 17
spring_boot: 4.0.0
java_version_prop: 17
compat_slf4j: 2.0.17
compat_vintage: 6.0.1
name: compat-spring-boot-${{ matrix.name }}
steps:
- uses: actions/checkout@v4
- name: Set up JDK
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: ${{ matrix.java }}
- name: Cache Maven packages
uses: actions/cache@v4
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-compat-spring-boot-${{ matrix.name }}-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-maven-compat-spring-boot-${{ matrix.name }}-
${{ runner.os }}-maven-
- name: Build local artifacts for spring boot compatibility
run: |
mvn -B -pl apollo-core,apollo-client,apollo-mockserver,apollo-client-config-data -am \
-DskipTests \
install -Dmaven.gitcommitid.skip=true
- name: Run spring boot compatibility tests
run: |
mvn -B -f apollo-compat-tests/pom.xml -pl apollo-spring-boot-compat-it \
-Dspring-boot.version=${{ matrix.spring_boot }} \
-Djava.version=${{ matrix.java_version_prop }} \
-Dcompat.slf4j.version=${{ matrix.compat_slf4j }} \
-Dcompat.junit.vintage.version=${{ matrix.compat_vintage }} \
test -Dmaven.gitcommitid.skip=true
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Apollo Java 2.5.0
* [Feature Added a new feature to get instance count by namespace.](https://github.com/apolloconfig/apollo-java/pull/103)
* [Feature Support retry in open api client.](https://github.com/apolloconfig/apollo-java/pull/105)
* [Support Spring Boot 4.0 bootstrap context package relocation for apollo-client-config-data](https://github.com/apolloconfig/apollo-java/pull/115)
* [Test Overhaul automated compatibility coverage across API/Spring/Spring Boot scenarios](https://github.com/apolloconfig/apollo-java/pull/123)

------------------
All issues and pull requests are [here](https://github.com/apolloconfig/apollo-java/milestone/5?closed=1)
10 changes: 10 additions & 0 deletions apollo-client-config-data/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -73,5 +73,15 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.ctrip.framework.apollo</groupId>
<artifactId>apollo-mockserver</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor.netty</groupId>
<artifactId>reactor-netty-http</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
package com.ctrip.framework.apollo.config.data.extension.initialize;

import com.ctrip.framework.apollo.config.data.extension.properties.ApolloClientProperties;
import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties;
import org.springframework.boot.context.properties.bind.BindHandler;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder;
Expand All @@ -35,10 +34,4 @@ public ApolloClientProperties createApolloClientProperties(
return binder.bind(PROPERTIES_PREFIX,
Bindable.of(ApolloClientProperties.class), bindHandler).orElse(null);
}

public OAuth2ClientProperties createOauth2ClientProperties(Binder binder,
BindHandler bindHandler) {
return binder.bind("spring.security.oauth2.client", Bindable.of(OAuth2ClientProperties.class),
bindHandler).orElse(null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@
import com.ctrip.framework.apollo.util.http.HttpClient;
import com.ctrip.framework.foundation.internals.ServiceBootstrap;
import java.util.List;
import java.util.function.Consumer;
import org.apache.commons.logging.Log;
import org.springframework.boot.context.properties.bind.BindHandler;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.logging.DeferredLogFactory;
import org.springframework.boot.web.reactive.function.client.WebClientCustomizer;
import org.springframework.util.CollectionUtils;
import org.springframework.web.reactive.function.client.WebClient;

Expand Down Expand Up @@ -55,11 +55,11 @@ public void initialize(ApolloClientProperties apolloClientProperties, Binder bin
.loadAllOrdered(ApolloClientWebClientCustomizerFactory.class);
if (!CollectionUtils.isEmpty(factories)) {
for (ApolloClientWebClientCustomizerFactory factory : factories) {
WebClientCustomizer webClientCustomizer = factory
Consumer<WebClient.Builder> webClientCustomizer = factory
.createWebClientCustomizer(apolloClientProperties, binder, bindHandler, this.log,
this.bootstrapContext);
if (webClientCustomizer != null) {
webClientCustomizer.customize(webClientBuilder);
webClientCustomizer.accept(webClientBuilder);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,15 @@
import com.ctrip.framework.apollo.util.http.HttpRequest;
import com.ctrip.framework.apollo.util.http.HttpResponse;
import com.google.gson.Gson;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.net.URI;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.springframework.http.HttpStatus;
import org.springframework.util.CollectionUtils;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

Expand All @@ -35,6 +39,10 @@
*/
public class ApolloWebClientHttpClient implements HttpClient {

private static final Method CLIENT_RESPONSE_STATUS_CODE_METHOD = resolveClientResponseStatusCodeMethod();
private static final ConcurrentMap<Class<?>, Method> STATUS_CODE_VALUE_METHOD_CACHE =
new ConcurrentHashMap<Class<?>, Method>();

private final WebClient webClient;

private final Gson gson;
Expand Down Expand Up @@ -64,15 +72,16 @@ private <T> HttpResponse<T> doGetInternal(HttpRequest httpRequest, Type response
}
}
return requestHeadersSpec.exchangeToMono(clientResponse -> {
if (HttpStatus.OK.equals(clientResponse.statusCode())) {
int statusCode = this.resolveStatusCode(clientResponse);
if (HttpStatus.OK.value() == statusCode) {
return clientResponse.bodyToMono(String.class)
.map(body -> new HttpResponse<T>(HttpStatus.OK.value(),
gson.fromJson(body, responseType)));
}
if (HttpStatus.NOT_MODIFIED.equals(clientResponse.statusCode())) {
if (HttpStatus.NOT_MODIFIED.value() == statusCode) {
return Mono.just(new HttpResponse<T>(HttpStatus.NOT_MODIFIED.value(), null));
}
return Mono.error(new ApolloConfigStatusCodeException(clientResponse.rawStatusCode(),
return Mono.error(new ApolloConfigStatusCodeException(statusCode,
String.format("Get operation failed for %s", httpRequest.getUrl())));
}).block();
}
Expand All @@ -82,4 +91,43 @@ public <T> HttpResponse<T> doGet(HttpRequest httpRequest, Type responseType)
throws ApolloConfigException {
return this.doGetInternal(httpRequest, responseType);
}

/**
* Resolve HTTP status code across Spring WebFlux 5/6/7.
*
* <p>ClientResponse#statusCode has different return types across major versions
* (HttpStatus in Spring 5, HttpStatusCode in Spring 6/7). Calling it directly would bind
* to one method descriptor at compile time and could fail on another runtime version.
* Reflection keeps this bridge binary-compatible for Boot 2/3/4 compatibility tests.
*/
private int resolveStatusCode(Object clientResponse) {
try {
Object statusCode = CLIENT_RESPONSE_STATUS_CODE_METHOD.invoke(clientResponse);
if (statusCode == null) {
throw new ApolloConfigException("Failed to resolve response status code: statusCode is null");
}
Method valueMethod = STATUS_CODE_VALUE_METHOD_CACHE.computeIfAbsent(statusCode.getClass(),
ApolloWebClientHttpClient::resolveStatusCodeValueMethod);
Object value = valueMethod.invoke(statusCode);
return ((Number) value).intValue();
} catch (Exception ex) {
throw new ApolloConfigException("Failed to resolve response status code", ex);
}
}

private static Method resolveClientResponseStatusCodeMethod() {
try {
return ClientResponse.class.getMethod("statusCode");
} catch (NoSuchMethodException ex) {
throw new ExceptionInInitializerError(ex);
}
}

private static Method resolveStatusCodeValueMethod(Class<?> statusCodeType) {
try {
return statusCodeType.getMethod("value");
} catch (NoSuchMethodException ex) {
throw new IllegalStateException("Failed to resolve value() method from " + statusCodeType, ex);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,20 @@

import com.ctrip.framework.apollo.config.data.extension.properties.ApolloClientProperties;
import com.ctrip.framework.apollo.core.spi.Ordered;
import java.util.function.Consumer;
import org.apache.commons.logging.Log;
import org.springframework.boot.context.properties.bind.BindHandler;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.web.reactive.function.client.WebClientCustomizer;
import org.springframework.lang.Nullable;
import org.springframework.web.reactive.function.client.WebClient;

/**
* @author vdisk <vdisk@foxmail.com>
*/
public interface ApolloClientWebClientCustomizerFactory extends Ordered {
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

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

Changing this SPI method signature (from WebClientCustomizer to Consumer<WebClient.Builder>) is a breaking change for external implementers and will also break binary compatibility for existing service providers. Consider keeping backward compatibility by either (a) adding a new method with the Consumer<WebClient.Builder> return type while retaining the old method as a default/deprecated bridge, or (b) introducing a new SPI interface name and supporting both via ServiceBootstrap.loadAllOrdered(...).

Copilot uses AI. Check for mistakes.
Copy link
Member Author

Choose a reason for hiding this comment

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

Addressed with a compatibility bridge. ApolloClientWebClientCustomizerFactory now keeps the new Consumer-based API while providing a default reflective adapter path for legacy implementations compiled against the old WebClientCustomizer signature. Included in commit 264b7bb.

Copy link
Member Author

Choose a reason for hiding this comment

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

Follow-up update: I removed the interface-level compatibility bridge and restored a simple SPI method signature. We re-checked current usage and there is no in-repo legacy implementation depending on the old return type, so keeping the interface straightforward is preferred here.


/**
* create a WebClientCustomizer instance
* create a webclient builder customizer
*
* @param apolloClientProperties apollo client binded properties
* @param binder properties binder
Expand All @@ -41,10 +42,10 @@ public interface ApolloClientWebClientCustomizerFactory extends Ordered {
* Spring Boot 3.x or
* org.springframework.boot.bootstrap.ConfigurableBootstrapContext
* for Spring Boot 4.x)
* @return WebClientCustomizer instance or null
* @return customizer instance or null
*/
@Nullable
WebClientCustomizer createWebClientCustomizer(ApolloClientProperties apolloClientProperties,
Consumer<WebClient.Builder> createWebClientCustomizer(ApolloClientProperties apolloClientProperties,
Binder binder, BindHandler bindHandler, Log log,
Object bootstrapContext);
}
Loading