Skip to content
This repository was archived by the owner on Jun 18, 2026. It is now read-only.

Commit 4449fe2

Browse files
author
Repo Gardener
committed
fix(ci/docker): repair broken Docker build + harden workflow + document pitfalls
Docker has been red for 5+ consecutive runs because several *Test.java files were checked in under Gvisual/src/, where JUnit isn't on the production classpath. javac fails with 'package org.junit does not exist' before the test stage even runs. Changes: * Move 14 stray tests from Gvisual/src/test/ and Gvisual/src/gvisual/ into the canonical Gvisual/test/gvisual/ tree (keeping the newer GraphCompressorTest copy). * Fix the moved GraphInfluenceCampaignPlannerTest to use the real 3-arg Edge ctor (Edge('e', a, b)) and float weights, and GraphMatrixExporterTest to drop the non-existent EdgeType.blue constant. * Dockerfile: use -encoding UTF-8 (box-drawing characters in sources), --release 17 (codebase uses var + Stream.toList), exclude *Test.java from production javac, switch HEALTHCHECK to a headless jar tf check, add the dockerfile syntax pragma. * docker.yml: pin Trivy DB to a multi-source ghcr/ecr list (prevents flaky scan failures), enable buildx SBOM + max-mode provenance on pushes, add actions/attest-build-provenance for SLSA-style signing on tagged releases, and add a real smoke-test step (pull by digest, verify entrypoint, verify JAR loads, verify non-root uid=1001). * CONTRIBUTING.md: add a 'Common Pitfalls' section documenting all six of the footguns above so this never regresses. * .gitignore: stop tracking transient sources.txt / build/ outputs. Verification: full canonical test set compiles cleanly under --release 17 and the 15 CI-listed test classes pass locally (347/347). [gardener] tasks: docker_workflow, contributing_md
1 parent 378ae2b commit 4449fe2

20 files changed

Lines changed: 202 additions & 618 deletions

.github/workflows/docker.yml

Lines changed: 68 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,27 @@ permissions:
1212
contents: read
1313
packages: write
1414
security-events: write # for Trivy SARIF upload
15+
id-token: write # for SLSA build provenance attestations
16+
attestations: write # for SLSA build provenance attestations
1517

1618
concurrency:
1719
group: docker-${{ github.ref }}
1820
cancel-in-progress: true
1921

22+
env:
23+
REGISTRY: ghcr.io
24+
IMAGE_NAME: ${{ github.repository }}
25+
# Pin Trivy DB sources so a registry hiccup on ghcr.io's public
26+
# trivy-db doesn't fail the security scan job intermittently.
27+
TRIVY_DB_REPOSITORY: ghcr.io/aquasecurity/trivy-db,public.ecr.aws/aquasecurity/trivy-db
28+
TRIVY_JAVA_DB_REPOSITORY: ghcr.io/aquasecurity/trivy-java-db,public.ecr.aws/aquasecurity/trivy-java-db
29+
2030
jobs:
2131
docker:
2232
runs-on: ubuntu-latest
33+
outputs:
34+
digest: ${{ steps.build.outputs.digest }}
35+
image-ref: ${{ steps.scan-image.outputs.image }}
2336
steps:
2437
- name: Checkout
2538
uses: actions/checkout@v6
@@ -28,14 +41,18 @@ jobs:
2841
id: meta
2942
uses: docker/metadata-action@v6
3043
with:
31-
images: ghcr.io/${{ github.repository }}
44+
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
3245
tags: |
3346
type=semver,pattern={{version}}
3447
type=semver,pattern={{major}}.{{minor}}
3548
type=semver,pattern={{major}}
3649
type=edge,branch=master
3750
type=sha,prefix=sha-
3851
type=raw,value=latest,enable={{is_default_branch}}
52+
labels: |
53+
org.opencontainers.image.title=GraphVisual
54+
org.opencontainers.image.description=Interactive JUNG-based social network graph visualization and analysis tool
55+
org.opencontainers.image.licenses=MIT
3956
4057
- name: Set up QEMU
4158
uses: docker/setup-qemu-action@v4
@@ -49,7 +66,7 @@ jobs:
4966
if: github.event_name != 'pull_request'
5067
uses: docker/login-action@v4
5168
with:
52-
registry: ghcr.io
69+
registry: ${{ env.REGISTRY }}
5370
username: ${{ github.actor }}
5471
password: ${{ secrets.GITHUB_TOKEN }}
5572

@@ -64,6 +81,8 @@ jobs:
6481
labels: ${{ steps.meta.outputs.labels }}
6582
cache-from: type=gha
6683
cache-to: type=gha,mode=max
84+
provenance: ${{ github.event_name != 'pull_request' && 'mode=max' || 'false' }}
85+
sbom: ${{ github.event_name != 'pull_request' }}
6786
# Load image locally for scanning even on PRs (load only works with single platform)
6887
load: ${{ github.event_name == 'pull_request' }}
6988

@@ -74,11 +93,14 @@ jobs:
7493
# Use the first tag from metadata (locally loaded)
7594
echo "image=$(echo '${{ steps.meta.outputs.tags }}' | head -1)" >> "$GITHUB_OUTPUT"
7695
else
77-
echo "image=ghcr.io/${{ github.repository }}:sha-${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"
96+
echo "image=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}" >> "$GITHUB_OUTPUT"
7897
fi
7998
8099
- name: Trivy vulnerability scan
81100
uses: aquasecurity/trivy-action@v0.36.0
101+
env:
102+
TRIVY_DB_REPOSITORY: ${{ env.TRIVY_DB_REPOSITORY }}
103+
TRIVY_JAVA_DB_REPOSITORY: ${{ env.TRIVY_JAVA_DB_REPOSITORY }}
82104
with:
83105
image-ref: ${{ steps.scan-image.outputs.image }}
84106
format: table
@@ -89,6 +111,9 @@ jobs:
89111
- name: Trivy SARIF report
90112
if: github.event_name != 'pull_request'
91113
uses: aquasecurity/trivy-action@v0.36.0
114+
env:
115+
TRIVY_DB_REPOSITORY: ${{ env.TRIVY_DB_REPOSITORY }}
116+
TRIVY_JAVA_DB_REPOSITORY: ${{ env.TRIVY_JAVA_DB_REPOSITORY }}
92117
with:
93118
image-ref: ${{ steps.scan-image.outputs.image }}
94119
format: sarif
@@ -103,10 +128,45 @@ jobs:
103128
sarif_file: trivy-results.sarif
104129
continue-on-error: true # don't fail if Advanced Security is disabled
105130

106-
- name: Verify image (on push)
131+
- name: Attest build provenance
132+
if: github.event_name != 'pull_request'
133+
uses: actions/attest-build-provenance@v3
134+
with:
135+
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
136+
subject-digest: ${{ steps.build.outputs.digest }}
137+
push-to-registry: true
138+
continue-on-error: true # don't block release if attestations API hiccups
139+
140+
- name: Verify image (smoke test on push)
107141
if: github.event_name != 'pull_request'
108142
run: |
109-
docker pull ghcr.io/${{ github.repository }}:sha-${GITHUB_SHA::7}
110-
docker run --rm ghcr.io/${{ github.repository }}:sha-${GITHUB_SHA::7} \
111-
-version 2>&1 | head -5
112-
echo "✅ Image verified successfully"
143+
set -e
144+
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}"
145+
echo "::group::Pull image"
146+
docker pull "$IMAGE"
147+
echo "::endgroup::"
148+
149+
echo "::group::Inspect labels"
150+
docker inspect "$IMAGE" --format '{{ json .Config.Labels }}' | python3 -m json.tool
151+
echo "::endgroup::"
152+
153+
echo "::group::Verify entrypoint resolves"
154+
# Entrypoint is `java`; -version reports JRE version. Must exit 0.
155+
docker run --rm --entrypoint java "$IMAGE" -version 2>&1 | head -5
156+
echo "::endgroup::"
157+
158+
echo "::group::Verify JAR is loadable"
159+
# Default CMD runs the GUI which needs $DISPLAY; instead, sanity-
160+
# check the JAR via `jar tf` so we fail fast on a packaging bug
161+
# (missing manifest, empty fat-jar, etc.).
162+
docker run --rm --entrypoint sh "$IMAGE" -c \
163+
'jar tf Gvisual.jar | head -20 && jar tf Gvisual.jar | wc -l'
164+
echo "::endgroup::"
165+
166+
echo "::group::Verify non-root user"
167+
USER_LINE=$(docker run --rm --entrypoint id "$IMAGE")
168+
echo "$USER_LINE"
169+
echo "$USER_LINE" | grep -q 'uid=1001' || { echo "::error::container is not running as non-root uid 1001"; exit 1; }
170+
echo "::endgroup::"
171+
172+
echo "? Image verified successfully: $IMAGE"

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,8 @@ Gvisual/nbproject/private/
1313
# OS generated
1414
.DS_Store
1515
Thumbs.db
16+
17+
Gvisual/sources.txt
18+
Gvisual/test_sources.txt
19+
Gvisual/build/
20+

CONTRIBUTING.md

Lines changed: 81 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -317,16 +317,34 @@ The `gvisual` package contains 158 classes organized into functional areas. Here
317317
```bash
318318
cd Gvisual
319319
mkdir -p build/classes
320-
find src -name '*.java' > sources.txt
321-
javac -cp "$(find lib -name '*.jar' | tr '\n' ':')" -d build/classes @sources.txt
320+
# Production sources only — exclude any *Test.java that may have been
321+
# checked in under src/ by mistake (see "Common Pitfalls" below).
322+
find src -name '*.java' ! -name '*Test.java' > sources.txt
323+
javac -encoding UTF-8 --release 17 \
324+
-cp "$(find lib -name '*.jar' -not -path '*/test/*' | tr '\n' ':')" \
325+
-d build/classes @sources.txt
322326
```
323327

328+
Notes:
329+
330+
- **Use `-encoding UTF-8`**. Several source files contain box-drawing characters
331+
(╔ ╚ ║ etc.) used in ASCII diagrams; without the flag, `javac` falls back
332+
to the platform default and fails on Windows (`cp1252`).
333+
- **Target JDK 17** (`--release 17`). The codebase uses `var` (Java 10+) and
334+
`Stream.toList()` (Java 16+) in places, so it will not compile cleanly
335+
under `--release 8` or `11` even though older docs suggested it.
336+
- Keep `lib/test/*.jar` *off* the production classpath. JUnit symbols must
337+
not be resolvable from production code.
338+
324339
### Run Tests
325340

326341
```bash
327342
mkdir -p build/test/classes
328-
find test -name '*.java' > test_sources.txt
329-
javac -cp "build/classes:$(find lib -name '*.jar' | tr '\n' ':')" -d build/test/classes @test_sources.txt
343+
# Only canonical test files (test/**/*Test.java). See "Common Pitfalls".
344+
find test -name '*Test.java' > test_sources.txt
345+
javac -encoding UTF-8 --release 17 \
346+
-cp "build/classes:$(find lib -name '*.jar' | tr '\n' ':')" \
347+
-d build/test/classes @test_sources.txt
330348

331349
# Run specific test classes
332350
java -cp "build/classes:build/test/classes:$(find lib -name '*.jar' | tr '\n' ':')" \
@@ -335,16 +353,73 @@ java -cp "build/classes:build/test/classes:$(find lib -name '*.jar' | tr '\n' ':
335353
# Run all tests (use a test runner or list all test classes)
336354
```
337355

356+
### Common Pitfalls
357+
358+
These are real footguns that have broken CI and Docker builds in the past —
359+
please keep them in mind when contributing:
360+
361+
1. **Never check in `*Test.java` files under `Gvisual/src/`.** The canonical
362+
test root is `Gvisual/test/`. JUnit is not on the production classpath,
363+
so a stray test under `src/` causes `package org.junit does not exist`
364+
in both the Docker build and the CI "Compile source" step. If you wrote
365+
a test alongside a feature in your editor, move it to the matching
366+
package under `Gvisual/test/` before committing.
367+
2. **Check both `src/test/` and `test/` for duplicates** when moving files.
368+
We had a `GraphCompressorTest` checked in under both for a while; the
369+
one under `src/test/` was a stale older copy. When in doubt, keep the
370+
version under `Gvisual/test/gvisual/` and delete the other.
371+
3. **`Edge` has no two-arg constructor.** Use
372+
`new Edge("e", "a", "b")` (type code, vertex1, vertex2). The `"e"`
373+
type code is fine for tests that don't care about the relationship
374+
category.
375+
4. **`Edge#setWeight(float)` takes a `float`, not a `double`.** Use
376+
`setWeight(0.9f)`, not `setWeight(0.9)` — the latter triggers "possible
377+
lossy conversion from double to float".
378+
5. **`EdgeType` is a closed enum.** The valid constants are `FRIEND`,
379+
`CLASSMATE`, `FAMILIAR`, `STRANGER`, `STUDY_GROUP`. There is no
380+
`EdgeType.blue` — use the type *code* string (`"c"` for CLASSMATE)
381+
when constructing `Edge` instances, or `EdgeType.CLASSMATE.getColor()`
382+
when you need the colour.
383+
6. **`HEALTHCHECK` cannot call the GUI.** The default `CMD` launches the
384+
Swing UI which needs `$DISPLAY`. The container healthcheck must verify
385+
something headless (we use `jar tf Gvisual.jar`).
386+
387+
If you hit a compile error that isn't covered here, please add it to this
388+
list as part of your fix — future contributors will thank you.
389+
338390
The test suite has **133 test classes** (~4,640+ `@Test` methods) covering most analyzers and exporters. CI runs a subset against JDK 11 and 17 — see `.github/workflows/ci.yml` for the canonical list.
339391

340392
### Docker
341393

342394
```bash
395+
# Build the image (multi-stage; compiles source, runs a fast test subset,
396+
# then produces a fat JAR in the runtime image).
343397
docker build -t graphvisual .
344-
docker run graphvisual
398+
399+
# Run the GUI with X11 forwarding from a Linux host.
400+
docker run --rm \
401+
-e DISPLAY=$DISPLAY \
402+
-v /tmp/.X11-unix:/tmp/.X11-unix \
403+
graphvisual
404+
405+
# Sanity-check that the JAR is loadable (works without $DISPLAY).
406+
docker run --rm --entrypoint sh graphvisual -c 'jar tf Gvisual.jar | head'
345407
```
346408

347-
The Docker build compiles all source, runs the full test suite, and produces a fat JAR.
409+
The Docker build performs:
410+
411+
1. JDK 17 source compilation (production only, no `*Test.java` from `src/`).
412+
2. Test compilation against the canonical `test/` tree.
413+
3. A **fast deterministic test subset** (5 stable test classes) as a smoke
414+
barrier — the full suite runs in CI, not in `docker build`, so image
415+
builds stay fast and reproducible.
416+
4. Fat-JAR assembly with signed-JAR signatures stripped.
417+
5. A minimal runtime image based on `eclipse-temurin:17-jre` running as a
418+
non-root user (`uid=1001`).
419+
420+
The published image is also scanned by Trivy and (on tagged releases)
421+
signed with a SLSA build-provenance attestation via
422+
`actions/attest-build-provenance`. See `.github/workflows/docker.yml`.
348423

349424
### Code Coverage
350425

Dockerfile

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
# syntax=docker/dockerfile:1.7
12
# ============================================================
23
# GraphVisual — Multi-stage Dockerfile
3-
# Builds the Java/Ant JUNG graph visualization project and
4-
# packages it as a runnable JAR with all dependencies.
4+
# Builds the Java/JUNG graph visualization project and packages
5+
# it as a runnable fat JAR with all runtime dependencies.
56
# ============================================================
67

78
# ---- Stage 1: Build ----
@@ -12,30 +13,40 @@ WORKDIR /app
1213
# Copy project files
1314
COPY Gvisual/ ./Gvisual/
1415

15-
# Download JUnit for test compilation
16+
# Download JUnit for test compilation (cached as a separate layer)
1617
RUN mkdir -p Gvisual/lib/test \
17-
&& curl -sL -o Gvisual/lib/test/junit-4.13.2.jar \
18+
&& curl -fsSL -o Gvisual/lib/test/junit-4.13.2.jar \
1819
https://repo1.maven.org/maven2/junit/junit/4.13.2/junit-4.13.2.jar \
19-
&& curl -sL -o Gvisual/lib/test/hamcrest-core-1.3.jar \
20+
&& curl -fsSL -o Gvisual/lib/test/hamcrest-core-1.3.jar \
2021
https://repo1.maven.org/maven2/org/hamcrest/hamcrest-core/1.3/hamcrest-core-1.3.jar
2122

22-
# Compile source
23+
# Compile production sources.
24+
#
25+
# A handful of *Test.java files have historically been checked in under
26+
# Gvisual/src/ alongside production code; production javac must NOT see
27+
# those (no JUnit on the production classpath). The canonical test root
28+
# is Gvisual/test/.
29+
#
30+
# Target JDK 11 bytecode — the codebase uses `var` (Java 10+) and stream
31+
# `.toList()` (Java 16+) in a few places, but compiles cleanly under
32+
# --release 11 elsewhere. We use 17 to match CI's upper matrix entry.
2333
RUN mkdir -p Gvisual/build/classes \
24-
&& find Gvisual/src -name '*.java' > Gvisual/sources.txt \
25-
&& javac -source 8 -target 8 \
34+
&& find Gvisual/src -name '*.java' ! -name '*Test.java' > Gvisual/sources.txt \
35+
&& javac -encoding UTF-8 --release 17 \
2636
-cp "$(find Gvisual/lib -name '*.jar' -not -path '*/test/*' | tr '\n' ':')" \
2737
-d Gvisual/build/classes \
2838
@Gvisual/sources.txt
2939

3040
# Compile tests
3141
RUN mkdir -p Gvisual/build/test/classes \
32-
&& find Gvisual/test -name '*.java' > Gvisual/test-sources.txt \
33-
&& javac -source 8 -target 8 \
42+
&& find Gvisual/test -name '*Test.java' > Gvisual/test-sources.txt \
43+
&& javac -encoding UTF-8 --release 17 \
3444
-cp "Gvisual/build/classes:$(find Gvisual/lib -name '*.jar' | tr '\n' ':')" \
3545
-d Gvisual/build/test/classes \
3646
@Gvisual/test-sources.txt
3747

38-
# Run tests to verify build
48+
# Run a fast, deterministic subset of tests to verify the build.
49+
# Full test suite runs in CI; the Docker build only needs a smoke barrier.
3950
RUN java -cp "Gvisual/build/classes:Gvisual/build/test/classes:$(find Gvisual/lib -name '*.jar' | tr '\n' ':')" \
4051
org.junit.runner.JUnitCore \
4152
app.UtilMethodsTest \
@@ -44,7 +55,8 @@ RUN java -cp "Gvisual/build/classes:Gvisual/build/test/classes:$(find Gvisual/li
4455
gvisual.ShortestPathFinderTest \
4556
gvisual.CommunityDetectorTest
4657

47-
# Create fat JAR with all dependencies merged
58+
# Create fat JAR with all dependencies merged.
59+
# Excludes signed-JAR signatures (would invalidate after merge) and source/javadoc jars.
4860
RUN mkdir -p Gvisual/dist \
4961
&& cd Gvisual/build/classes \
5062
&& for jar in $(find /app/Gvisual/lib -name '*.jar' \
@@ -64,13 +76,13 @@ LABEL org.opencontainers.image.title="GraphVisual" \
6476
org.opencontainers.image.source="https://github.com/sauravbhattacharya001/GraphVisual" \
6577
org.opencontainers.image.licenses="MIT"
6678

67-
# Install X11 libraries for Swing GUI support (optional X11 forwarding)
79+
# X11 libraries for Swing GUI support (optional X11 forwarding from host)
6880
RUN apt-get update \
6981
&& apt-get install -y --no-install-recommends \
7082
libx11-6 libxext6 libxrender1 libxtst6 libxi6 libfreetype6 fontconfig \
7183
&& rm -rf /var/lib/apt/lists/*
7284

73-
# Create non-root user
85+
# Non-root user
7486
RUN groupadd --gid 1001 graphvisual \
7587
&& useradd --uid 1001 --gid graphvisual --no-log-init --create-home graphvisual
7688

@@ -83,12 +95,12 @@ COPY --chown=graphvisual:graphvisual Gvisual/images/ ./images/
8395

8496
USER graphvisual
8597

86-
# Health check: verify JAR is valid
98+
# Health check: verify the JAR is loadable. The GUI itself needs $DISPLAY,
99+
# so we just sanity-check that `jar tf` can read the archive.
87100
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
88-
CMD java -jar Gvisual.jar --version 2>/dev/null || java -cp Gvisual.jar gvisual.Main --help 2>/dev/null || exit 0
101+
CMD jar tf Gvisual.jar > /dev/null 2>&1 || exit 1
89102

90-
# Default: run the GUI application
91-
# For headless/X11 forwarding: docker run -e DISPLAY=$DISPLAY -v /tmp/.X11-unix:/tmp/.X11-unix graphvisual
92-
# For CI/testing only: override CMD with test runner
103+
# Default: run the GUI application.
104+
# For X11 forwarding from host: docker run -e DISPLAY=$DISPLAY -v /tmp/.X11-unix:/tmp/.X11-unix <image>
93105
ENTRYPOINT ["java"]
94106
CMD ["-jar", "Gvisual.jar"]

0 commit comments

Comments
 (0)