Skip to content

Commit 8332d01

Browse files
committed
Run NATS CI without Docker; mirror Redis CI pattern
The nats_integration_test job previously relied on Testcontainers (Docker) being preinstalled on the runner. That works for ubuntu-latest today but isn't a guarantee for self-hosted runners or alternate images. Mirroring how the existing redis / redis_cluster jobs install their server binary directly: - Download nats-server v2.10.22 from GitHub releases, drop into /usr/local/bin, start with -js + JetStream dir as a background process and wait for the listener. - Set NATS_RUNNING=true and NATS_URL=nats://127.0.0.1:4222 for the Gradle invocation. Mirrors REDIS_RUNNING used by RedisRunning. AbstractNatsBootIT now reads NATS_RUNNING / NATS_URL: when present, it points the dynamic property at the external server and skips @container instantiation entirely. Local dev without those env vars falls back to Testcontainers, which itself skips gracefully when Docker isn't available. NatsBackendEndToEndIT now extends AbstractNatsBootIT instead of duplicating the connection-URL plumbing. Uploads /tmp/nats.log as an artifact on every CI run so failures are debuggable without GitHub admin auth on the workflow logs. Assisted-By: Claude Code
1 parent efd3b5c commit 8332d01

3 files changed

Lines changed: 70 additions & 50 deletions

File tree

.github/workflows/java-ci.yaml

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -360,12 +360,42 @@ jobs:
360360
- name: Set up Gradle
361361
uses: gradle/actions/setup-gradle@v4
362362

363-
# Docker is preinstalled on ubuntu-latest runners; Testcontainers picks it up
364-
# automatically. No Redis is needed for this job — every test under @Tag("nats")
365-
# talks to a JetStream container via Testcontainers (or runs as a pure unit test).
363+
# Install + start nats-server directly (not via Docker — mirrors how the other CI
364+
# jobs install redis-server). Tests detect NATS_RUNNING and connect to localhost
365+
# instead of pulling a Testcontainers image.
366+
- name: Install nats-server
367+
run: |
368+
NATS_VERSION=v2.10.22
369+
curl -sSL "https://github.com/nats-io/nats-server/releases/download/${NATS_VERSION}/nats-server-${NATS_VERSION}-linux-amd64.tar.gz" \
370+
| tar -xz -C /tmp
371+
sudo mv "/tmp/nats-server-${NATS_VERSION}-linux-amd64/nats-server" /usr/local/bin/nats-server
372+
nats-server --version
373+
374+
- name: Start nats-server
375+
run: |
376+
mkdir -p /tmp/jetstream
377+
nohup nats-server -js -sd /tmp/jetstream -p 4222 > /tmp/nats.log 2>&1 &
378+
for i in $(seq 1 20); do
379+
if (echo > /dev/tcp/127.0.0.1/4222) 2>/dev/null; then
380+
echo "nats-server ready after ${i}s"; break
381+
fi
382+
sleep 1
383+
done
384+
366385
- name: Run NATS tests
386+
env:
387+
NATS_RUNNING: "true"
388+
NATS_URL: nats://127.0.0.1:4222
367389
run: ./gradlew :rqueue-nats:test :rqueue-spring-boot-starter:test :rqueue-spring:test -DincludeTags=nats
368390

391+
- name: Upload nats-server log
392+
if: always()
393+
uses: actions/upload-artifact@v4
394+
with:
395+
name: nats-server-log
396+
path: /tmp/nats.log
397+
if-no-files-found: ignore
398+
369399
- name: Upload JaCoCo exec data
370400
if: always()
371401
uses: actions/upload-artifact@v4

rqueue-spring-boot-starter/src/test/java/com/github/sonus21/rqueue/spring/boot/integration/AbstractNatsBootIT.java

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,25 +26,41 @@
2626
/**
2727
* Common Testcontainers + dynamic-property boilerplate for NATS-backed end-to-end tests.
2828
*
29+
* <p>Mirrors the existing Redis test pattern (see {@code RedisRunning} / {@code REDIS_RUNNING}):
30+
* when the {@code NATS_RUNNING} environment variable is set the tests assume an externally
31+
* managed nats-server is reachable at {@code NATS_URL} (default {@code nats://127.0.0.1:4222})
32+
* and skip Testcontainers entirely. CI sets {@code NATS_RUNNING=true} after starting nats-server
33+
* via apt; local dev leaves it unset and falls back to Testcontainers, which itself skips
34+
* gracefully when Docker isn't available.
35+
*
2936
* <p>Subclasses declare their own {@code @SpringBootApplication} test config (typically excluding
3037
* Redis auto-config, see {@link NatsBackendEndToEndIT} for the reference pattern) and any
31-
* {@code @RqueueListener} beans they need. The container is lifecycle-managed by the
32-
* {@link Testcontainers} extension and shared across all tests in a single subclass.
38+
* {@code @RqueueListener} beans they need.
3339
*/
3440
@Testcontainers(disabledWithoutDocker = true)
3541
abstract class AbstractNatsBootIT {
3642

43+
static final boolean USE_EXTERNAL_NATS = System.getenv("NATS_RUNNING") != null;
44+
45+
static final String EXTERNAL_NATS_URL =
46+
System.getenv().getOrDefault("NATS_URL", "nats://127.0.0.1:4222");
47+
3748
@Container
38-
static final GenericContainer<?> NATS = new GenericContainer<>(
39-
DockerImageName.parse("nats:2.10-alpine"))
40-
.withCommand("-js")
41-
.withExposedPorts(4222)
42-
.waitingFor(Wait.forLogMessage(".*Server is ready.*\\n", 1));
49+
static final GenericContainer<?> NATS = USE_EXTERNAL_NATS
50+
? null
51+
: new GenericContainer<>(DockerImageName.parse("nats:2.10-alpine"))
52+
.withCommand("-js")
53+
.withExposedPorts(4222)
54+
.waitingFor(Wait.forLogMessage(".*Server is ready.*\\n", 1));
4355

4456
@DynamicPropertySource
4557
static void natsProps(DynamicPropertyRegistry r) {
46-
r.add(
47-
"rqueue.nats.connection.url",
48-
() -> "nats://" + NATS.getHost() + ":" + NATS.getMappedPort(4222));
58+
if (USE_EXTERNAL_NATS) {
59+
r.add("rqueue.nats.connection.url", () -> EXTERNAL_NATS_URL);
60+
} else {
61+
r.add(
62+
"rqueue.nats.connection.url",
63+
() -> "nats://" + NATS.getHost() + ":" + NATS.getMappedPort(4222));
64+
}
4965
}
5066
}

rqueue-spring-boot-starter/src/test/java/com/github/sonus21/rqueue/spring/boot/integration/NatsBackendEndToEndIT.java

Lines changed: 11 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -33,60 +33,34 @@
3333
import org.springframework.boot.test.context.SpringBootTest;
3434
import org.springframework.context.annotation.Import;
3535
import org.springframework.stereotype.Component;
36-
import org.springframework.test.context.DynamicPropertyRegistry;
37-
import org.springframework.test.context.DynamicPropertySource;
38-
import org.testcontainers.containers.GenericContainer;
39-
import org.testcontainers.containers.wait.strategy.Wait;
40-
import org.testcontainers.junit.jupiter.Container;
41-
import org.testcontainers.junit.jupiter.Testcontainers;
42-
import org.testcontainers.utility.DockerImageName;
4336

4437
/**
45-
* End-to-end integration test wiring a Spring Boot application against a Testcontainers-managed
46-
* NATS JetStream instance via {@code rqueue.backend=nats}, an {@link RqueueListener}, and the
47-
* default {@link RqueueMessageEnqueuer}. It exercises the full intended path:
38+
* End-to-end integration test wiring a Spring Boot application against a NATS JetStream
39+
* instance via {@code rqueue.backend=nats}, an {@link RqueueListener}, and the default
40+
* {@link RqueueMessageEnqueuer}. It exercises the full intended path:
4841
*
4942
* <pre>
5043
* Enqueue -> JetStreamMessageBroker.enqueue -> JetStream stream
5144
* -> BrokerMessagePoller.pop -> @RqueueListener invocation -> broker.ack
5245
* </pre>
5346
*
47+
* <p>The NATS instance is supplied by {@link AbstractNatsBootIT}: when {@code NATS_RUNNING=true}
48+
* (CI), the test connects to a locally running nats-server; otherwise it falls back to a
49+
* Testcontainers-managed container, which itself skips gracefully without Docker.
50+
*
5451
* <p>Boots without any Redis at all: every Redis-shaped bean (config DAOs, dashboard controllers,
5552
* pub/sub channel, schedulers) is gated by {@code @Conditional(RedisBackendCondition.class)} and
5653
* stays out of the context when {@code rqueue.backend=nats}. {@code DataRedisAutoConfiguration}
57-
* is excluded so Spring Boot doesn't try to wire a Lettuce client either. The whole produce-and-
58-
* consume loop runs through JetStream.
59-
*
60-
* <p>The {@link TestListener} is explicitly imported (rather than relying on package scan) so the
61-
* listener is reachable regardless of where the test harness places the {@code @SpringBootTest}'s
62-
* scan root.
54+
* is excluded so Spring Boot doesn't try to wire a Lettuce client either.
6355
*/
6456
@SpringBootTest(
6557
classes = NatsBackendEndToEndIT.TestApp.class,
6658
properties = {"rqueue.backend=nats"})
67-
@Testcontainers(disabledWithoutDocker = true)
6859
@Tag("nats")
69-
class NatsBackendEndToEndIT {
70-
71-
@Container
72-
static final GenericContainer<?> NATS = new GenericContainer<>(
73-
DockerImageName.parse("nats:2.10-alpine"))
74-
.withCommand("-js")
75-
.withExposedPorts(4222)
76-
.waitingFor(Wait.forLogMessage(".*Server is ready.*\\n", 1));
77-
78-
@DynamicPropertySource
79-
static void registerProps(DynamicPropertyRegistry r) {
80-
r.add(
81-
"rqueue.nats.connection.url",
82-
() -> "nats://" + NATS.getHost() + ":" + NATS.getMappedPort(4222));
83-
}
84-
85-
@Autowired
86-
RqueueMessageEnqueuer enqueuer;
60+
class NatsBackendEndToEndIT extends AbstractNatsBootIT {
8761

88-
@Autowired
89-
TestListener listener;
62+
@Autowired RqueueMessageEnqueuer enqueuer;
63+
@Autowired TestListener listener;
9064

9165
@Test
9266
void enqueueIsReceivedByListener() throws Exception {

0 commit comments

Comments
 (0)