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
9 changes: 8 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,14 @@ jobs:
# distributionManagement points at GitHub Packages, so we override
# the deploy target to the local staging directory configured in the
# JReleaser <stagingRepositories> block (target/staging-deploy).
run: ./mvnw -B -Pfull-build clean deploy -DaltDeploymentRepository=local::file:./target/staging-deploy
#
# The path is ABSOLUTE (github.workspace) on purpose: in the multi-module
# reactor a relative ./target/staging-deploy would resolve per-module and
# scatter each module's artifacts under its own target/, so JReleaser
# (running once at the root) would not find them. An absolute path stages
# every module — core, the feature modules, spring-services-all and the BOM —
# into the single root staging directory JReleaser reads.
run: ./mvnw -B -Pfull-build clean deploy -DaltDeploymentRepository=local::file:${{ github.workspace }}/target/staging-deploy

- name: Publish with JReleaser
run: ./mvnw -B -Pdeploy-release jreleaser:full-release -N
Expand Down
78 changes: 63 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,52 @@ into the full stack with a single import or pick the parts they need.
Releases are published to Maven Central; SNAPSHOTs are published to the Sonatype Central Portal
snapshot repository on every push to `main`.

`spring-services` is a **Maven multi-module reactor**. The bare `com.open-elements:spring-services`
coordinate is the reactor parent (a `pom`, no classes) — depend on one of the modules instead.

**Everything (drop-in):** `spring-services-all` bundles the core plus every optional feature module.

```xml
<dependency>
<groupId>com.open-elements</groupId>
<artifactId>spring-services</artifactId>
<artifactId>spring-services-all</artifactId>
<version><!-- latest released version --></version>
</dependency>
```

**À la carte:** import the BOM once, then declare `spring-services-core` plus only the feature
modules you need (`spring-services-slack`, `spring-services-mcp`, `spring-services-email`,
`spring-services-search`, `spring-services-dbbackup`) without versions:

```xml
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.open-elements</groupId>
<artifactId>spring-services-bom</artifactId>
<version><!-- latest released version --></version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<dependency>
<groupId>com.open-elements</groupId>
<artifactId>spring-services-core</artifactId>
</dependency>
<dependency>
<groupId>com.open-elements</groupId>
<artifactId>spring-services-slack</artifactId>
</dependency>
</dependencies>
```

Each feature module self-activates when present on the classpath, so consumers pull only the
dependencies they use. Migrating from the pre-split `spring-services` artifact? See
[`docs/releases/upgrade-to-1.4.md`](docs/releases/upgrade-to-1.4.md).

To consume a `-SNAPSHOT` build, also declare the Central Portal snapshot repository (releases still
come from Maven Central):

Expand Down Expand Up @@ -272,23 +310,33 @@ later release). Consumers must run one Flyway migration when upgrading.

## Project Layout

A Maven reactor: `spring-services` (parent, `pom`) aggregates the modules below. All classes stay
under `com.openelements.spring.base.*` regardless of the module that ships them.

```
src/main/java/com/openelements/spring/base/
├── FullSpringServiceConfig.java — Aggregate @Import of every feature config
├── data/ — Generic CRUD abstractions
├── events/ — Lifecycle events
├── security/ — Filter chain, JWT converter, AuthService
│ ├── apikey/ — X-API-Key authentication filter
│ └── user/ — Local user mirror
├── services/
│ ├── apikey/ — API key data layer
│ ├── search/ — Meilisearch client, startup reindex, highlighter
│ ├── settings/ — Key/value settings
│ ├── tag/ — Tag CRUD + PreTagDeleteEvent
│ └── webhook/ — Outbound webhook delivery
└── tenant/ — Multi-tenancy primitives
spring-services/ — reactor parent (packaging=pom)
├── spring-services-core — all 7 @Entity + single persistence unit, the core
│ auto-configuration (SpringServicesCoreAutoConfiguration) and
│ the always-present features:
│ data/ — generic CRUD abstractions
│ events/ — lifecycle events
│ security/ — filter chains, JWT converter, AuthService
│ apikey/, user/
│ services/ — apikey, settings, tag, comment, audit,
│ webhook, translation, system
│ tenant/, apitoken/
├── spring-services-slack — Slack messaging (→ slack-api-client)
├── spring-services-mcp — MCP server (→ MCP SDK)
├── spring-services-email — SMTP email (→ spring-boot-starter-mail)
├── spring-services-search — Meilisearch full-text search (RestClient, no extra dep)
├── spring-services-dbbackup — db-backup sidecar client (RestClient, no extra dep)
├── spring-services-all — everything bundle (depends on all modules; no config of its own)
└── spring-services-bom — bill of materials for lockstep versioning
```

Each optional feature module ships its own `@AutoConfiguration` guarded by `@ConditionalOnClass`, so
it self-activates when present and never pulls its heavy dependency into a consumer that skips it.

## Release Process

### SNAPSHOT Publishing (automatic)
Expand Down
138 changes: 138 additions & 0 deletions docs/releases/upgrade-to-1.4.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# Upgrade prompt: `com.open-elements:spring-services` 1.3.0 → 1.4.0

`spring-services` 1.4.0 restructures the library into a **Maven multi-module reactor**. This is a
**breaking coordinate change** — but **not** a Java-API or database change:

- The `com.open-elements:spring-services` coordinate is now the **reactor parent** (`packaging=pom`).
It no longer resolves to a jar with classes, so a consumer that declares it as a dependency will
fail to compile.
- The library is split into `spring-services-core` (all seven entities, single persistence unit) plus
optional feature modules — `spring-services-slack`, `spring-services-mcp`, `spring-services-email`,
`spring-services-search`, `spring-services-dbbackup` — an **everything** bundle
`spring-services-all`, and a BOM `spring-services-bom`.
- Consumers now depend only on the features they use; a consumer that used everything switches to
`spring-services-all` (drop-in). Heavy/rare libraries (`slack-api-client`, the MCP SDK,
`spring-boot-starter-mail`) are no longer pulled unless the matching module is present.

There are **no public Java API changes** (method signatures, DTOs) and **no schema change** — the
`oe_spring_services` schema introduced in 1.3.0 is unchanged. The one internal rename: the starter
auto-configuration `com.openelements.spring.base.SpringServicesAutoConfiguration` is now
`SpringServicesCoreAutoConfiguration` (only relevant if you referenced it in an
`spring.autoconfigure.exclude` list).

> If you are upgrading from a version **older than 1.3.0**, apply the 1.3.0 database migration first
> (see `upgrade-to-1.3.md`: move the tables into `oe_spring_services`), then this coordinate swap.

This file is a self-contained prompt for an agent (Claude Code, etc.) to run inside a consumer repo.
Paste it verbatim.

---

## Prompt

You are working inside a Spring Boot service that depends on `com.open-elements:spring-services`.
Goal: upgrade from `1.3.0` to `1.4.0`. The library became a Maven reactor; you must change the
dependency coordinate. No application code, schema, or configuration changes are required.

### What changed in 1.4.0

#### Dependency coordinate (required change)

The old single coordinate no longer ships classes. Choose one of two migration paths.

**Path A — everything (drop-in).** Replace `spring-services` with `spring-services-all`:

```xml
<!-- before -->
<dependency>
<groupId>com.open-elements</groupId>
<artifactId>spring-services</artifactId>
<version>1.3.0</version>
</dependency>

<!-- after -->
<dependency>
<groupId>com.open-elements</groupId>
<artifactId>spring-services-all</artifactId>
<version>1.4.0</version>
</dependency>
```

`spring-services-all` bundles core plus every feature module, reproducing the pre-split behaviour.

**Path B — à-la-carte (only what you use).** Import the BOM once, then declare `spring-services-core`
plus only the feature modules you need, without versions:

```xml
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.open-elements</groupId>
<artifactId>spring-services-bom</artifactId>
<version>1.4.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<dependency>
<groupId>com.open-elements</groupId>
<artifactId>spring-services-core</artifactId>
</dependency>
<!-- add only the features you use, e.g.: -->
<dependency>
<groupId>com.open-elements</groupId>
<artifactId>spring-services-slack</artifactId>
</dependency>
</dependencies>
```

Feature module artifact ids: `spring-services-slack`, `spring-services-mcp`,
`spring-services-email`, `spring-services-search`, `spring-services-dbbackup`.

#### Self-activation

Each feature module self-activates when it is on the classpath (via its own auto-configuration),
gated by the same enable properties as before (e.g. `openelements.mcp.enabled`,
`openelements.meilisearch.enabled`, `openelements.db-backup.enabled`). Slack and email activate when
their module is present. You do not need `@Import` to wire a feature — adding the module is enough.

#### Auto-configuration rename (only if you excluded it)

If your application excluded the starter auto-configuration by name, rename it:

```
com.openelements.spring.base.SpringServicesAutoConfiguration
->
com.openelements.spring.base.SpringServicesCoreAutoConfiguration
```

Most applications never reference this class and have nothing to do here.

#### No API, schema, or configuration changes

No public Java types or signatures changed. The database schema (`oe_spring_services`, from 1.3.0) is
unchanged — no migration. Property names are unchanged.

### Steps

1. Find the consumer's build file that declares `com.open-elements:spring-services`.
2. Replace it per **Path A** (switch to `spring-services-all`) or **Path B** (BOM + `-core` + chosen
feature modules). Bump the version to `1.4.0`.
3. If you kept a hand-written `spring.autoconfigure.exclude` for the starter, rename it to
`SpringServicesCoreAutoConfiguration`.
4. Resolve dependencies (`mvn -U dependency:resolve` or `./gradlew --refresh-dependencies`).
5. Compile and run the consumer's test suite to verify the build is green.
6. Commit the coordinate change.

### Guard rails / Don't do this

- Do **not** keep depending on the bare `com.open-elements:spring-services` coordinate — it is now a
parent pom and carries no classes.
- Do **not** re-add `slack-api-client`, the MCP SDK, or `spring-boot-starter-mail` by hand — pull the
corresponding feature module instead, which brings the dependency transitively.
- Do **not** change the database or any `@Table(schema = ...)` — there is no schema change in 1.4.0.
- Do **not** mix explicit module versions with the BOM import; let the BOM manage them.
- Do **not** bundle unrelated upgrades into this coordinate change.
2 changes: 1 addition & 1 deletion docs/specs/INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@
| 011 | 011-security-config-hygiene | Security config hygiene | backend, security, architecture | Tighten Spring Boot auto-configuration annotations, drop synchronized from UserService hot path, centralise JSON AuthenticationEntryPoint, use SecurityContextHolderStrategy, replace UserInformation sentinel with Optional, lock Roles utility class | — | done |
| 012 | 012-scim-foundation-user-model | SCIM foundation user model | backend, security, database | Relax UserEntity.sub, add externalId / userName / active, JIT-login correlation and backfill, AccessDeniedException gate for deactivated users — prepares the data model for the SCIM-Provider spec without introducing SCIM endpoints | — | done |
| 013 | 013-dedicated-schema-and-starter | Dedicated schema + starter | backend, database, architecture, build | Move all 7 library tables into a fixed `oe_spring_services` schema (single persistence unit) and ship a real Spring Boot starter that registers the library package additively via AutoConfigurationPackages — zero-config consumption. Migrations stay consumer-managed; the library ships ready-to-apply per-release SQL in the upgrade doc. Includes the app→lib cross-entity reference contract | #25 | done |
| 014 | 014-multi-module-restructuring | Multi-module restructuring | build, architecture, infrastructure | Restructure the single artifact into a Maven reactor: `spring-services-core` (all 7 entities, single persistence unit) + optional feature modules (`slack`, `mcp`, `email`, `search`, `dbbackup`) + `spring-services-all` aggregator jar + `spring-services-bom`. Lockstep versioning; per-module `@AutoConfiguration` guarded by `@ConditionalOnClass`; builds on spec 013 (released first). Breaking change: `spring-services` coordinate becomes the reactor parent, consumers migrate to `spring-services-all` | #27 | in progress |
| 014 | 014-multi-module-restructuring | Multi-module restructuring | build, architecture, infrastructure | Restructure the single artifact into a Maven reactor: `spring-services-core` (all 7 entities, single persistence unit) + optional feature modules (`slack`, `mcp`, `email`, `search`, `dbbackup`) + `spring-services-all` aggregator jar + `spring-services-bom`. Lockstep versioning; per-module `@AutoConfiguration` guarded by `@ConditionalOnClass`; builds on spec 013 (released first). Breaking change: `spring-services` coordinate becomes the reactor parent, consumers migrate to `spring-services-all` | #27 | done |
| 015 | 015-scim-users-provider | SCIM Users provider | backend, security, api, database | SCIM 2.0 service-provider first slice: an isolated `/scim/v2/**` bearer-token filter chain (static `openelements.scim.token`), discovery endpoints (ServiceProviderConfig/ResourceTypes/Schemas), and the Users resource (list/filter, POST, GET, PUT full-replace, soft DELETE) mapped onto spec 012's `UserEntity`. `POST` collision → `409 uniqueness`; DELETE is soft (`deleted`/`deleted_at`); writes audited under a reserved SCIM service principal. Groups+membership and Group→Role mapping deferred to follow-up issues; grounded in real Authentik 2026.5.4 traffic (#21) with a pre-merge verification test plan. | #21 | open |

7 changes: 7 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@

<modules>
<module>spring-services-core</module>
<module>spring-services-slack</module>
<module>spring-services-search</module>
<module>spring-services-dbbackup</module>
<module>spring-services-email</module>
<module>spring-services-mcp</module>
<module>spring-services-all</module>
<module>spring-services-bom</module>
</modules>

<!-- Shared third-party versions for all modules (module-internal concern; not pushed to the org parent). -->
Expand Down
6 changes: 4 additions & 2 deletions release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ generate_release_doc() {
}

echo "Releasing version $NEW_VERSION"
./mvnw versions:set -DnewVersion=$NEW_VERSION
# -DprocessAllModules keeps the reactor lockstep: it bumps the parent and every
# module (each references the parent version explicitly, not ${revision}).
./mvnw versions:set -DnewVersion=$NEW_VERSION -DprocessAllModules

# Build and test locally so we never push a tag that fails CI.
#
Expand Down Expand Up @@ -96,6 +98,6 @@ git tag "v$NEW_VERSION"
git push origin "v$NEW_VERSION"

echo "Setting version to $NEXT_VERSION"
./mvnw versions:set -DnewVersion=$NEXT_VERSION
./mvnw versions:set -DnewVersion=$NEXT_VERSION -DprocessAllModules
git commit -am "Version $NEXT_VERSION"
git push
Loading