diff --git a/.github/scripts/validate-published-poms.sh b/.github/scripts/validate-published-poms.sh
new file mode 100755
index 00000000..dd730985
--- /dev/null
+++ b/.github/scripts/validate-published-poms.sh
@@ -0,0 +1,73 @@
+#!/usr/bin/env bash
+# Fails the build if any published SKaiNET POM contains invalid coordinates.
+#
+# Catches the class of bug shipped in 0.19.0, where `skainet-backend-cpu`'s POM
+# declared `sk.ainet:skainet-backend-api-jvm:unspecified` because
+# `skainet-backend-api` was not configured to publish and the root
+# `allprojects { group = "sk.ainet" }` disagreed with `GROUP=sk.ainet.core`.
+#
+# Two checks per generated POM under ~/.m2/repository/sk/ainet/core/**:
+# 1. No `unspecified` anywhere in the POM.
+# 2. Every `` whose `` starts with `skainet-` uses
+# `sk.ainet.core` — `project(...)` deps on sibling
+# modules must resolve to the same publish group.
+
+set -euo pipefail
+
+REPO_ROOT="${HOME}/.m2/repository/sk/ainet/core"
+
+if [[ ! -d "${REPO_ROOT}" ]]; then
+ echo "ERROR: no published artifacts found under ${REPO_ROOT}" >&2
+ echo "Did ./gradlew publishToMavenLocal run successfully?" >&2
+ exit 1
+fi
+
+mapfile -t POMS < <(find "${REPO_ROOT}" -type f -name '*.pom' | sort)
+
+if [[ ${#POMS[@]} -eq 0 ]]; then
+ echo "ERROR: no .pom files under ${REPO_ROOT}" >&2
+ exit 1
+fi
+
+echo "Scanning ${#POMS[@]} published POMs..."
+
+report_file="$(mktemp)"
+trap 'rm -f "${report_file}"' EXIT
+
+for pom in "${POMS[@]}"; do
+ rel="${pom#${REPO_ROOT}/}"
+
+ if grep -Fq 'unspecified' "${pom}"; then
+ {
+ echo "FAIL ${rel}: contains unspecified"
+ grep -n 'unspecified' "${pom}" | sed 's/^/ /'
+ } >> "${report_file}"
+ fi
+
+ bad_deps="$(awk '
+ // { inDep=1; block=""; next }
+ inDep { block = block "\n" $0 }
+ /<\/dependency>/ {
+ inDep=0
+ if (block ~ /skainet-/ && block !~ /sk\.ainet\.core<\/groupId>/) {
+ print block
+ }
+ }
+ ' "${pom}")"
+
+ if [[ -n "${bad_deps}" ]]; then
+ {
+ echo "FAIL ${rel}: skainet-* dependency with non-sk.ainet.core group"
+ printf '%s\n' "${bad_deps}" | sed 's/^/ /'
+ } >> "${report_file}"
+ fi
+done
+
+if [[ -s "${report_file}" ]]; then
+ cat "${report_file}" >&2
+ echo "" >&2
+ echo "POM validation failed. See the 0.19.1 CHANGELOG entry for the regression this check prevents." >&2
+ exit 1
+fi
+
+echo "All ${#POMS[@]} POMs look good."
diff --git a/.github/workflows/verify-poms.yml b/.github/workflows/verify-poms.yml
new file mode 100644
index 00000000..d299192b
--- /dev/null
+++ b/.github/workflows/verify-poms.yml
@@ -0,0 +1,38 @@
+name: Verify published POMs
+
+on: [push, pull_request]
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ verify-poms:
+ name: Publish to Maven local and validate POM coordinates
+ runs-on: ubuntu-latest
+ timeout-minutes: 45
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v6
+
+ - name: Copy CI gradle.properties
+ run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
+
+ - name: Set up JDK 25
+ uses: actions/setup-java@v5
+ with:
+ distribution: 'zulu'
+ java-version: 25
+
+ - name: Publish to Maven local
+ env:
+ GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx4g -Dfile.encoding=UTF-8"
+ run: |
+ ./gradlew --no-daemon --stacktrace --no-configuration-cache \
+ -PRELEASE_SIGNING_ENABLED=false \
+ -PsignAllPublications=false \
+ publishToMavenLocal
+
+ - name: Validate POM coordinates
+ run: ./.github/scripts/validate-published-poms.sh
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3f607a29..b80d1b67 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,15 @@
## [Unreleased]
+## [0.19.1] - 2026-04-21
+
+### Fixed
+
+- **Broken POM for `skainet-backend-cpu`**: The 0.19.0 POM for `sk.ainet.core:skainet-backend-cpu-*` declared a runtime dependency on `sk.ainet:skainet-backend-api-jvm:unspecified` — wrong group coordinate and no valid version, because `skainet-backend-api` was not configured to publish and the root `allprojects { group = "sk.ainet" }` disagreed with the `GROUP=sk.ainet.core` used by vanniktech's maven publish plugin. Consumers pulling 0.19.0 hit unresolved-dependency errors. Fixed by:
+ - Applying `vanniktech.mavenPublish` and setting `POM_ARTIFACT_ID=skainet-backend-api` on `skainet-backend-api` so it is actually published alongside the BOM entry that already referenced it.
+ - Aligning `allprojects { group = "sk.ainet.core" }` with the `GROUP` property and pinning `version` from `VERSION_NAME` so `project(...)` coordinates in generated POMs are consistent.
+- **CI guard**: New `verify-published-poms` job publishes to the local Maven repository and fails the build if any generated `.pom` contains `unspecified` or references a project-local group outside `sk.ainet.core`, preventing a regression of this class of coordinate bug.
+
## [0.19.0] - 2026-04-20
### Added
diff --git a/build.gradle.kts b/build.gradle.kts
index 0df84bb7..aed173fb 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -14,7 +14,8 @@ plugins {
}
allprojects {
- group = "sk.ainet"
+ group = "sk.ainet.core"
+ version = providers.gradleProperty("VERSION_NAME").getOrElse("unspecified")
}
// Require JDK 21+ but allow any newer version (produces Java 21 bytecode via --release / jvmTarget)
diff --git a/skainet-backends/skainet-backend-api/build.gradle.kts b/skainet-backends/skainet-backend-api/build.gradle.kts
index a573fcca..601c87ed 100644
--- a/skainet-backends/skainet-backend-api/build.gradle.kts
+++ b/skainet-backends/skainet-backend-api/build.gradle.kts
@@ -4,6 +4,7 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidMultiplatformLibrary)
+ alias(libs.plugins.vanniktech.mavenPublish)
id("sk.ainet.dokka")
}
diff --git a/skainet-backends/skainet-backend-api/gradle.properties b/skainet-backends/skainet-backend-api/gradle.properties
new file mode 100644
index 00000000..a0f9c064
--- /dev/null
+++ b/skainet-backends/skainet-backend-api/gradle.properties
@@ -0,0 +1,2 @@
+POM_ARTIFACT_ID=skainet-backend-api
+POM_NAME=skainet backend-neutral API