W8b release rehearsal (2026-10-11)
Dry-run proof for the family release engineering. NOTHING was published to
any remote registry, no tags were created, no publish workflow runs were
triggered: every publish here went to mavenLocal and every registry touch
was a read-only GET. The first real upload happens only on operator go,
through docs/release.md.
Branch heads rehearsed: guard-core-jvm 93f878f, spring-boot-guard
8a87184, ktor-guard 64dd550, guard-agent-jvm 9bd686e (feat/w8b-release).
1. Verified live
1.1 Workflow lint
actionlint clean on all four release.yml (plus the existing per-repo
workflows unaffected by this wave):
guard-core-jvm release.yml: CLEAN
spring-boot-guard release.yml: CLEAN
ktor-guard release.yml: CLEAN
guard-agent-jvm release.yml: CLEAN
1.2 Clean publishToMavenLocal, all four artifacts
From clean clones, ~/.m2/repository/com/guard-core wiped first, JDK 17
(temurin 17.0.6), Gradle wrapper 9.8.1:
./gradlew clean :guard-core-engine:publishToMavenLocal :guard-core:publishToMavenLocal
BUILD SUCCESSFUL in 24s (18 tasks)
guard-core-engine/4.3.2/: jar, sources-jar, javadoc-jar, module, pom
guard-core/4.3.2/: jar, sources-jar, javadoc-jar, module, pom
Consumers, each with GUARD_CORE_USE_COMPOSITE=false ./gradlew clean
publishToMavenLocal (the release-workflow mode):
GUARD_CORE_USE_COMPOSITE is disabled: resolving com.guard-core:guard-core from
the repositories (mavenLocal, then Maven Central).
BUILD SUCCESSFUL (spring-boot-guard 1.0.0-SNAPSHOT, ktor-guard 1.0.0-SNAPSHOT,
guard-agent-jvm 3.2.2-SNAPSHOT)
The builds compile in that mode, which is only possible because
com.guard-core:guard-core:4.3.2 actually resolved from mavenLocal (Central
does not serve it: HTTP 404, section 1.6) - the rehearsal stands in for the
registry exactly as release day will.
1.3 POM coordinate proof (the Portal validator surface)
Parsed from mavenLocal (python xml.etree), all five publications:
guard-core-engine 4.3.2: groupId com.guard-core; licenses MIT + Apache-2.0;
developers rennf93; scm github.com/Guard-Core/guard-core-jvm;
deps: kotlin-stdlib 2.4.21 (runtime)
guard-core 4.3.2: groupId com.guard-core; licenses MIT + Apache-2.0;
deps: com.guard-core:guard-core-engine:4.3.2 (compile), lettuce-core
6.7.1.RELEASE, maxmind-db 4.2.0, slf4j-api 2.0.17 (compile),
kotlinx-coroutines/serialization (runtime)
spring-boot-guard 1.0.0-SNAPSHOT: MIT; deps: com.guard-core:guard-core:4.3.2
(compile), kotlin-reflect (compile)
ktor-guard 1.0.0-SNAPSHOT: MIT; deps: com.guard-core:guard-core:4.3.2
(compile), ktor-server-core-jvm 3.6.0 (runtime)
guard-agent-jvm 3.2.2-SNAPSHOT: MIT; deps: kotlin-stdlib, coroutines,
lettuce (runtime) - NO guard-core dependency (engine-free POM, by design:
compile-only facade contract)
Every Portal-required POM element is present in all five: name, description, url, licenses, developers, scm. Sources jars are non-trivial (engine facade sources jar: 61 entries); javadoc jars are non-empty (README carrier until Dokka lands - the Portal validator rejects empty javadoc jars).
1.4 Composite toggle semantics (the release-mode proof)
./gradlew dependencyInsight --dependency guard-core --configuration
compileClasspath in spring-boot-guard:
composite OFF (release mode):
com.guard-core:guard-core:4.3.2 <- external module (mavenLocal ->
Maven Central on release day)
composite ON (dev/CI default):
com.guard-core:guard-core:4.3.2
\--- project ':guard-core-jvm:guard-core' <- included build substitution
Same selector, two resolution worlds, switched only by the env var. The settings log line prints the active mode in every invocation.
1.5 Staging bundle + signing (what the Portal actually receives)
./gradlew :guard-core-engine:publishMavenPublicationToCentralStagingRepository
then zipped from the workspace root - the exact bundle the upload step posts:
com/guard-core/guard-core-engine/4.3.2/
guard-core-engine-4.3.2.jar / .pom / -sources.jar / -javadoc.jar / .module
+ .md5 / .sha1 for every file (Portal-mandatory) + .sha256 / .sha512
+ maven-metadata.xml with checksums
34 files, 627,914 bytes uncompressed
GPG path proven with a throwaway key (temp GNUPGHOME, deleted after): with
GPG_PRIVATE_KEY + GPG_PASSPHRASE set, the signing block activates and the
staging dir gains .asc for all five files (jar, pom, sources, javadoc,
module) - the Portal's every-file signature requirement. The rehearsal
bundles were unsigned (no secret material involved); release CI signs.
INCIDENT CAUGHT AND FIXED: the first bundle simulation exposed a real
workflow bug - the relative zip path dropped central-bundle.zip inside the
module directory while the upload step reads the workspace root. Fixed in all
four workflows (zip -qr "$GITHUB_WORKSPACE/central-bundle.zip" ., commits
93f878f / 8a87184 / 64dd550 / 9bd686e). This is the rehearsal doing its job.
1.6 Registry state + existence-check behavior
Live probes of the exact URLs the workflows query:
repo1.maven.org/maven2/com/guard-core/ -> 404 (no prior art)
repo1.maven.org/.../guard-core-engine/4.3.2/...pom -> 404
repo1.maven.org/.../guard-core/4.3.2/...pom -> 404
repo1.maven.org/.../spring-boot-guard/4.3.2/...pom -> 404 (probe used 4.3.2;
the real consumer check queries the artifact's own tag version)
repo1.maven.org/.../ktor-guard/4.3.2/...pom -> 404
Central search index g:com.guard-core -> 0 artifacts
404 is the expected pre-release state and maps to the publish path in the workflows; 200 maps to skip (the changed-only rerun safety); any other code aborts. The full train is well under the 2026-10-01 publishing limits (34 files for the largest deployment; the 90th-percentile monthly threshold is 1,167 files / 78 MB / 7 releases).
1.7 Tag-consistency audit simulation
The exact job logic executed against the real workspaces:
[v4.3.2] guard-core-jvm PASS: tag v4.3.2 == version 4.3.2
[v4.3.1] guard-core-jvm FAIL: tag v4.3.1 != version 4.3.2
[v4.3.2-rc1] guard-core-jvm FAIL (suffix rejected at the equality check)
[v1.0.0] spring-boot-guard FAIL: version 1.0.0-SNAPSHOT is a SNAPSHOT
(flip missing) - the intended message
[v3.2.2] guard-agent-jvm FAIL: version 3.2.2-SNAPSHOT (same)
post-flip sim (1.0.0 -> tag v1.0.0) PASS
2. Auth-path decision and what is pending (operator)
Verified (2026-10-11, central.sonatype.org Publisher API docs + release notes):
- The Central Portal Publisher API authenticates with a Portal user token
(
Authorization: Bearer base64(username:password)). The workflows are built for this path via four secrets:CENTRAL_PORTAL_USERNAME,CENTRAL_PORTAL_PASSWORD,GPG_PRIVATE_KEY,GPG_PASSPHRASE. - OIDC trusted publishing does not exist for Maven Central (npm/PyPI/
Packagist have it; Maven does not). Therefore the publish jobs carry
contents: readonly and noid-tokenscope. If Sonatype ships trusted publishing, that is a follow-up, not a blocker. - GPG signing is mandatory (every file needs
.asc; proven in 1.5). - New publishing limits took effect 2026-10-01 (announced 2026-06-16, enforcement pushed from Aug 11; 30-day grace): three monthly per-org metrics (file count, release size, release count), Usage Center is the source of truth. This family is orders of magnitude below the thresholds.
Pending on the operator (each blocks the corresponding step of docs/release.md):
- Portal user token generated and the two
CENTRAL_PORTAL_*secrets set on all four repos. Blocks: the publish jobs' credential gate. - GPG key pair created, the public key uploaded to a keyserver (the Portal
validates against the keyserver copy), and the armored secret key +
passphrase set as
GPG_PRIVATE_KEY/GPG_PASSPHRASEon all four repos (4 secrets per repo in total, counting the two Portal ones). Blocks: the publish jobs' GPG gate (and Portal validation). - GitHub releases created per tag at train time (the publish jobs fire on
release: created, not on the tag push alone). Blocks: publishing only.
RESOLVED (was pending item 1): the Portal namespace com.guard-core flipped
to VERIFIED 2026-10-11 16:15 (Portal badge, Org Guard-core). History:
com.guardcore was abandoned 2026-10-11 (guardcore.com is a parked domain
the operator does not own, making that claim unverifiable); com.guard-core
reverses to the owned guard-core.com; the operator re-registered, swapped
the apex TXT to the new verification key, and the Portal validated it.
3. Remaining operator steps (release day, condensed)
The full choreography is docs/release.md; the condensed checklist:
- Confirm the Portal shows
com.guard-coreVerified; secrets set on all four repos (section 2 items 1-2); Usage Center clean. - Engine: tag
v4.3.2on a green master -> gates green -> create the GitHub release -> publish job uploads + VALIDATES both deployments (engine first, facade second) -> click Release on both in the Portal UI -> repo1 answers 200 for both artifacts. - Adapters + agent, in any order (each guarded by its own pre-flight):
version-flip PR merged (
1.0.0-SNAPSHOT->1.0.0,3.2.2-SNAPSHOT->3.2.2; CHANGELOG## [SEMVER] - YYYY-MM-DD) -> tag -> release created -> pre-flight proves the engine live -> publish -> Portal release -> repo1 200. - Post-release audits (per tag): GitHub release page exists; repo1 200 per artifact; no deployment left VALIDATED/PENDING in the Portal; Usage Center clean. Then move the consumers' pinned engine refs to the released engine SHA in the next regular development PR.
Rollback: before the Portal click, Drop the deployment (nothing published); after the click there is no undo - only a fix version (artifacts are immutable; tags of published versions never get deleted).