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
14 changes: 0 additions & 14 deletions .github/actions/managed-java/action.yml

This file was deleted.

19 changes: 0 additions & 19 deletions .github/actions/managed-maven/action.yml

This file was deleted.

17 changes: 17 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# SPDX-FileCopyrightText: The ilo Authors
# SPDX-License-Identifier: 0BSD

version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: daily
assignees:
- sebhoss
- package-ecosystem: maven
directory: /
schedule:
interval: daily
assignees:
- sebhoss
31 changes: 0 additions & 31 deletions .github/workflows/codeql-analysis.yml

This file was deleted.

34 changes: 10 additions & 24 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
steps:
- id: checkout
name: Clone Git Repository
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
- id: last_release
Expand All @@ -38,8 +38,16 @@ jobs:
if: steps.commits.outputs.count > 0
run: echo "iso8601=$(date --utc --iso-8601=seconds)" >> $GITHUB_OUTPUT
- id: setup-java
uses: ./.github/actions/managed-maven
name: Set up Java
if: steps.commits.outputs.count > 0
uses: actions/setup-java@v4
with:
java-version: 25
distribution: temurin
cache: maven
server-id: ossrh
server-username: MAVEN_CENTRAL_USERNAME
server-password: MAVEN_CENTRAL_TOKEN
- id: gpg
name: GPG Key
if: steps.commits.outputs.count > 0
Expand Down Expand Up @@ -79,25 +87,3 @@ jobs:
draft: false
prerelease: false
generate_release_notes: true
- id: mail
name: Send Mail
if: steps.commits.outputs.count > 0
uses: dawidd6/action-send-mail@v3
with:
server_address: ${{ secrets.MAIL_SERVER }}
server_port: ${{ secrets.MAIL_PORT }}
username: ${{ secrets.MAIL_USERNAME }}
password: ${{ secrets.MAIL_PASSWORD }}
subject: ${{ github.event.repository.name }} version ${{ steps.release.outputs.version }} published
body: See ${{ steps.create_release.outputs.url }} for details.
to: ${{ secrets.MAIL_RECIPIENT }}
from: ${{ secrets.MAIL_SENDER }}
- id: matrix
name: Send Matrix Message
if: steps.commits.outputs.count > 0
uses: s3krit/matrix-message-action@v0.0.3
with:
room_id: ${{ secrets.MATRIX_ROOM_ID }}
access_token: ${{ secrets.MATRIX_ACCESS_TOKEN }}
message: ${{ github.event.repository.name }} version [${{ steps.release.outputs.version }}](${{ steps.create_release.outputs.url }}) published
server: ${{ secrets.MATRIX_SERVER }}
26 changes: 21 additions & 5 deletions .github/workflows/update-parent.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,30 @@ jobs:
parent:
runs-on: ubuntu-latest
steps:
- name: Clone Git Repository
uses: actions/checkout@v4
- uses: ./.github/actions/managed-java
- name: Update Parent
- id: checkout
name: Clone Git Repository
uses: actions/checkout@v6
- id: graal
name: Set up GraalVM
uses: graalvm/setup-graalvm@v1
with:
version: latest
java-version: 25
github-token: ${{ secrets.GITHUB_TOKEN }}
- id: cache
name: Cache Maven Repository
uses: actions/cache@v5
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-maven-
- id: parent
name: Update Parent
run: mvn --batch-mode --define generateBackupPoms=false versions:update-parent
- id: cpr
name: Create Pull Request
uses: peter-evans/create-pull-request@v5
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.PAT }}
commit-message: Update parent to latest version
Expand Down
26 changes: 22 additions & 4 deletions .github/workflows/verify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,26 @@ jobs:
verify:
runs-on: ubuntu-latest
steps:
- name: Clone Git Repository
uses: actions/checkout@v4
- uses: ./.github/actions/managed-java
- name: Build with Maven
- id: checkout
name: Clone Git Repository
uses: actions/checkout@v6
- id: graal
name: Set up GraalVM
uses: graalvm/setup-graalvm@v1
with:
version: latest
java-version: 25
github-token: ${{ secrets.GITHUB_TOKEN }}
- id: cache
name: Cache Maven Repository
uses: actions/cache@v5
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-maven-
# The native profile runs PMD + SpotBugs (bound to verify) and builds and runs the
# native-image smoke check, so a green run proves both code quality and native compatibility.
- id: verify
name: Verify Project
run: mvn --batch-mode --activate-profiles=native verify
57 changes: 57 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<!--
SPDX-FileCopyrightText: The devcontainer.java Authors
SPDX-License-Identifier: 0BSD
-->

# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## What this is

A single-purpose Java library that parses and builds [devcontainer.json](https://containers.dev/implementors/json_reference/) files. Published to Maven Central as `wtf.metio.devcontainer:devcontainer.java`. No CLI, no runtime — it is a model + Jackson glue.

## Commands

JDK 25 (`global.jdkVersion` in `pom.xml`), Maven. Inherits most config from the `wtf.metio.maven:maven-parent` POM. That parent does **not** manage third-party dependency versions, so this POM pins them itself: Jackson and JUnit via imported BOMs in `<dependencyManagement>`, record-builder via the `version.record-builder` property (shared between the dependency and the annotation-processor path — they must move together).

- Build & test: `mvn --batch-mode verify`
- Native-compatibility check (what CI's PR check runs — the GraalVM `native` profile): `mvn --batch-mode --activate-profiles=native verify`
- Single test class: `mvn test -Dtest=DevcontainerParsingTest`

`verify` also runs the quality gates (see below). `.github/workflows/verify.yml` runs the native command above on GraalVM 25, so PMD, SpotBugs, and the native smoke check all gate every PR.

The local machine has no JDK/Maven installed; run builds through `ilo` (see global instructions) or in the GraalVM container CI uses.

## Code quality gates (bound to `verify`)

- **PMD** (`check` + `cpd-check`, from the parent) with a focused ruleset in `config/pmd/ruleset.xml`. The generated-sources root is excluded via `excludeRoots`.
- **SpotBugs** (`check`) with `config/spotbugs/exclude.xml`. The exclusions are deliberate: representation-exposure findings are expected for records holding `List`/`Map`, and the generated `*Builder` classes are not ours to restyle.

Both fail the build on findings. The PMD engine is pinned to 7.x so it can parse JDK 25 sources. When a quality gate flags generated code, exclude the generated artifact (root or class pattern) rather than the rule globally.

## Architecture

The schema is modeled as a graph of Java **records**, rooted at `Devcontainer`. Supporting records (`Build`, `Command`, `HostRequirements`, `PortAttribute`) and enums (`OnAutoForward`, `Protocol`, `ShutdownAction`, `UserEnvProbe`, `WaitFor`) cover the nested and constrained fields. Records carry the spec docs as Javadoc on each component.

- **record-builder annotation processor** (`io.soabase.record-builder`, `provided` scope) generates a `<Record>Builder` for every `@RecordBuilder` record at compile time. Records implement `<Record>Builder.With` to get wither methods. The build method is renamed to `create()` via `@RecordBuilder.Options`. These `*Builder` classes do not exist until you compile — don't go looking for them in source.
- **Jackson 3** (`tools.jackson.*` packages, `tools.jackson` BOM coordinates — not the Jackson 2 `com.fasterxml.jackson.*`). The mapper is immutable and built via `JsonMapper.builder()...build()`; there are no `enable()`/`disable()` mutators on an instance. Jackson 3 throws unchecked `JacksonException`, so the `parse(...)` methods declare no checked `IOException`.
- **Parsing** lives entirely in `Devcontainer.parse(...)` overloads (Path/File/String), backed by `defaultObjectMapper()`: a `JsonMapper` that disables `FAIL_ON_UNKNOWN_PROPERTIES` (forward-compat with spec additions) and enables `ACCEPT_SINGLE_VALUE_AS_ARRAY` (the spec lets array fields appear as a single scalar).
- **`Command` is a polymorphic field.** In the spec a lifecycle command may be a string, a string array, or an object of named commands. `Command` holds all three (`string`, `array`, `object`) with at most one non-null, populated by `CommandDeserializer` (a `StdDeserializer` wired via `@JsonDeserialize`). The `object` variant recurses into more `Command`s.
- **JPMS:** `module-info.java` opens the package to `tools.jackson.databind` (needed for reflective deserialization) and requires record-builder + java.compiler as `static` (compile-only).

## GraalVM native image — keep reflect-config.json in sync

The library's contract is that downstream consumers (e.g. ilo) can native-compile an app that depends on it. `src/main/resources/META-INF/native-image/.../reflect-config.json` registers every record and enum for reflection so Jackson works in a native image. **When you add a new record or enum type to the model, add a corresponding entry to this file**, or native-image deserialization will fail at runtime (not compile time) in consumers.

The `native` profile verifies this contract by mimicking a consumer: it adds `src/native/java` as a source root (so the stand-in never ships in the published jar), builds `NativeImageSmokeTest` into a native image via the plugin's `compile-no-fork` goal, then runs it (`exec`). Building exercises `reflect-config.json`; running surfaces missing runtime reflection metadata. When you add a model field worth covering, exercise it in that smoke main. It deliberately does **not** native-compile the JUnit test suite — that would only test JUnit's own native compatibility, not the library's.

## Tests are data-driven

`DevcontainerParsingTest` is a `@TestFactory` that pairs JSON fixtures in `src/test/resources/*.json` with assertion lambdas. To test a new field or parsing case: add a fixture file and a `Map.entry("fixture.json", devcontainer -> ...)` with the expected values. `DevcontainerBuilderTest` covers the generated builders/withers.

## Conventions

- Every file carries an SPDX header; the project license is **0BSD**. Match the existing header when adding files.
- DCO sign-off is required: commit with `git commit --signoff`.
- Releases are **calendar-versioned and automated** (`.github/workflows/release.yml`, Friday cron, version = `date +'%Y.%-m.%-d'`). A release only happens when commits since the last tag touched `src/main/java` or `pom.xml`. The POM version stays `0.0.0-SNAPSHOT` in the tree; the real version is injected at release time.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ SPDX-FileCopyrightText: The devcontainer.java Authors
SPDX-License-Identifier: 0BSD
-->

# devcontainer.java [![Chat](https://img.shields.io/badge/matrix-%23talk.metio:matrix.org-brightgreen.svg?style=social&label=Matrix)](https://matrix.to/#/#talk.metio:matrix.org)
# devcontainer.java

Java implementation for the [devcontainer](https://containers.dev/implementors/json_reference/) file specification.

Expand Down
22 changes: 22 additions & 0 deletions config/pmd/ruleset.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
SPDX-FileCopyrightText: The devcontainer.java Authors
SPDX-License-Identifier: 0BSD
-->
<ruleset name="devcontainer.java"
xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 https://pmd.sourceforge.io/ruleset_2_0_0.xsd">

<description>
Focused, low-noise rules: dead code, unnecessary imports, and cognitive complexity. Kept
deliberately small so violations stay actionable.
</description>

<rule ref="category/java/codestyle.xml/UnnecessaryImport"/>
<rule ref="category/java/bestpractices.xml/UnusedLocalVariable"/>
<rule ref="category/java/bestpractices.xml/UnusedPrivateField"/>
<rule ref="category/java/bestpractices.xml/UnusedPrivateMethod"/>
<rule ref="category/java/design.xml/CognitiveComplexity"/>

</ruleset>
29 changes: 29 additions & 0 deletions config/spotbugs/exclude.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
SPDX-FileCopyrightText: The devcontainer.java Authors
SPDX-License-Identifier: 0BSD
-->
<FindBugsFilter xmlns="https://github.com/spotbugs/filter/3.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://github.com/spotbugs/filter/3.0.0 https://raw.githubusercontent.com/spotbugs/spotbugs/master/spotbugs/etc/findbugsfilter.xsd">

<!--
The records model devcontainer.json and carry List/Map components. A record's canonical
constructor and accessor necessarily store and return those references, which SpotBugs reports
as representation exposure. The exposure is intended: the records are plain, immutable-by-convention
data carriers, not defensive value objects.
-->
<Match>
<Bug pattern="EI_EXPOSE_REP,EI_EXPOSE_REP2"/>
</Match>

<!--
record-builder generates the *Builder classes (and their nested With / _FromWith types). Their
factory methods mirror the record name (e.g. Devcontainer(...)), which trips the method-naming
convention check. Generated code is not ours to restyle, so it is excluded from analysis.
-->
<Match>
<Class name="~.*Builder(\$.*)?"/>
</Match>

</FindBugsFilter>
18 changes: 18 additions & 0 deletions dev/Containerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# SPDX-FileCopyrightText: The devcontainer.java Authors
# SPDX-License-Identifier: 0BSD

FROM ghcr.io/graalvm/native-image-community:25

# Install Maven
ENV MAVEN_HOME /usr/share/maven

COPY --from=docker.io/library/maven:3-eclipse-temurin-21 ${MAVEN_HOME} ${MAVEN_HOME}
COPY --from=docker.io/library/maven:3-eclipse-temurin-21 /usr/local/bin/mvn-entrypoint.sh /usr/local/bin/mvn-entrypoint.sh
COPY --from=docker.io/library/maven:3-eclipse-temurin-21 /usr/share/maven/ref/settings-docker.xml /usr/share/maven/ref/settings-docker.xml

RUN ln -s ${MAVEN_HOME}/bin/mvn /usr/bin/mvn

ENV MAVEN_CONFIG "/root/.m2"

ENTRYPOINT ["/usr/local/bin/mvn-entrypoint.sh"]
CMD ["mvn"]
4 changes: 0 additions & 4 deletions java.properties

This file was deleted.

Loading
Loading