From 64b8d5a603deb1e13efc4035fd0450cd4607c526 Mon Sep 17 00:00:00 2001 From: htjulia Date: Mon, 21 Sep 2026 15:14:23 +0900 Subject: [PATCH] chore(build): point the development version at the next release The committed default was `0.0.1-SNAPSHOT` and stayed there permanently: the release coordinate is injected with -Pversion, so the placeholder never needed to move. That held while the version was only a publishing coordinate. It stopped holding when the SSE `ready` frame started reporting it to clients as `platformVersion`. `0.0.1-SNAPSHOT` sorts BEFORE `0.0.1`. So a build of main -- carrying a frame that 0.0.1 does not have -- announced itself as older than the release that lacks it. Not vague, inverted. A console asking "does this server send ready?" got a number saying "pre-0.0.1" from a server that was sending it. The default now names the version being worked towards and is bumped right after each release. Decision "b" is unchanged in substance: release coordinates are still never committed and still arrive via -Pversion. What changed is which version the development placeholder holds -- the next one instead of the last. Six places carried the literal and had to move together, since one left behind would disagree with the rest: the version itself, a POM comment, the release workflow's two explanatory comments, the consume-verification harness's default coordinate, and the operations guide (en + ko), which now also states the bump-after-release rule that makes the new default stay true. CI gained the gate this needs. `platformVersion` is read from the jar manifest, and nothing in the test suite can observe it: tests run from exploded classes, where there is no manifest and the field is null by design. Dropping the manifest attribute would therefore ship platformVersion=null to every consumer with a fully green build. The build job now asserts, on the artifact, that the manifest version equals the version the build declared. Consumers of development snapshots are affected and the changelog says so: the GitHub Packages coordinate moves to 0.1.0-SNAPSHOT, so a build pinned to 0.0.1-SNAPSHOT stops receiving updates. Released coordinates are untouched. Also narrowed the `platformVersion` docs. They said null happens on "tests, IDE runs", which reads as common; every path a console actually connects to resolves protean as a jar, including the quickstart demo, so null is rare in practice. Verified: build.gradle and the manifest both report 0.1.0-SNAPSHOT, the CI check passes against the built artifact, and the PoC gate passes 6 combinations x 6 criteria with no SKIPPED and no unreported cell. Co-Authored-By: htjulia --- .github/workflows/ci.yml | 13 +++++++++++++ .github/workflows/release.yml | 11 +++++++---- CHANGELOG.ko.md | 8 ++++++++ CHANGELOG.md | 9 +++++++++ build.gradle | 12 +++++++++--- docs/guide/04-rest-api.ko.md | 2 +- docs/guide/04-rest-api.md | 2 +- docs/guide/11-operations.ko.md | 2 +- docs/guide/11-operations.md | 2 +- verification/consumer/build.gradle | 2 +- 10 files changed, 51 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 025d5e7..a90fc86 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -68,6 +68,19 @@ jobs: uses: gradle/actions/setup-gradle@v6 - name: bootJar run: ./gradlew bootJar --console=plain + # The SSE `ready` frame reports Implementation-Version to clients as `platformVersion`, and it is read from + # the jar manifest at runtime. Nothing in the test suite can see it: the tests run from exploded classes, where + # there is no manifest and the field is null by design. So dropping the manifest attribute would ship + # platformVersion=null to every consumer with a fully green build. Assert it on the artifact instead. + - name: jar manifest version matches the build + run: | + ver=$(./gradlew -q properties --no-configuration-cache | awk -F': ' '/^version: /{print $2}') + jar="build/libs/protean-${ver}.jar" + test -f "$jar" || { echo "::error::expected $jar, found: $(ls build/libs)"; exit 1; } + got=$(unzip -p "$jar" META-INF/MANIFEST.MF | tr -d '\r' | awk -F': ' '/^Implementation-Version: /{print $2}') + test "$got" = "$ver" \ + || { echo "::error::manifest says '${got:-}', build says '$ver' — platformVersion would lie to consumers"; exit 1; } + echo "platformVersion will report: $got" # Worker/container isolation tests self-skip when no bootJar is present (they # need the fat -boot.jar), so the `test` job never exercises them. This job diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 891128d..7329e83 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,9 +8,12 @@ # dispatch uploads first and creates NO tag, so a Drop lets the SAME version be retried. The tag + release/ # marker are created by a human AFTER the VALIDATED deployment is confirmed (see the release runbook). # -# Versioning (decision "b"): the source stays at 0.0.1-SNAPSHOT permanently; the release coordinate is never -# committed. It comes from the `version` input and is injected via -Pversion, which build.gradle reads through -# providers.gradleProperty('version'). So dispatching version=0.0.1 publishes org.htcom:protean:0.0.1. +# Versioning (decision "b"): the release coordinate is never committed. It comes from the `version` input and is +# injected via -Pversion, which build.gradle reads through providers.gradleProperty('version'). So dispatching +# version=0.1.0 publishes org.htcom:protean:0.1.0. The committed default is the NEXT version as a SNAPSHOT +# (0.1.0-SNAPSHOT while 0.1.0 is being worked towards), bumped right after each release — it used to hold the +# released version permanently, which made a development build announce itself as older than the release it +# already surpassed once the SSE `ready` frame started reporting it as `platformVersion`. # # Signing: in-memory (vanniktech signAllPublications → Bouncy Castle). No gpg binary or keyring is installed on the # runner; the armored private key is supplied as a string. The key, its passphrase, and the Central Portal user @@ -79,7 +82,7 @@ jobs: ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.ORG_GRADLE_PROJECT_mavenCentralPassword }} # The container isolation track tells consumers to run ghcr.io//protean-worker:, so a release # that only ships the library leaves that track with no image to pull: ci.yml pushes the image on main, but at - # the development version (0.0.1-SNAPSHOT), never at the release coordinate. Same -Pversion as the upload, and + # the development SNAPSHOT, never at the release coordinate. Same -Pversion as the upload, and # build.gradle puts that version in jib's `to.image`, so this pushes exactly one tag — there is no floating # `latest` for a release to fight a later snapshot over. Runs AFTER the Central upload deliberately — the upload is # the gated, expensive half, and a GHCR tag pushed for a deployment that is later Dropped is deletable, while diff --git a/CHANGELOG.ko.md b/CHANGELOG.ko.md index 631f010..a812c49 100644 --- a/CHANGELOG.ko.md +++ b/CHANGELOG.ko.md @@ -55,6 +55,14 @@ ### 변경 +- 커밋되는 개발 버전이 `0.1.0-SNAPSHOT` 이 됐다(종전에는 릴리스된 좌표 그대로인 + `0.0.1-SNAPSHOT`). **GitHub Packages 의 개발 스냅샷도 이 좌표로 옮겨 간다** — + `org.htcom:protean:0.0.1-SNAPSHOT` 에 고정된 빌드는 더 이상 갱신을 받지 못하므로 + `0.1.0-SNAPSHOT` 으로 옮겨야 한다. 릴리스 좌표는 영향이 없고 여전히 `-Pversion` 으로 + 주입하며 커밋하지 않는다. 바꾼 이유는 새 `ready` 프레임이다 — 이 값을 클라이언트에 + `platformVersion` 으로 내려보내는데, `0.0.1-SNAPSHOT` 은 `0.0.1` 보다 **앞서므로** + 모든 개발 빌드가 이미 넘어선 릴리스보다 자기가 낮다고 말하고 있었다. + - `debug.evaluate`·`debug.redefine` 이 이제 **`destructiveHint: true`** 로 광고한다(이전 `false`). 스펙상 이 힌트의 기본값이 `true` 라서 `false` 는 침묵이 아니라 **"아무 말도 않는 툴보다 안전하다"는 주장**이었다. 사실이 아니다 — `evaluate` 는 임의의 메서드·생성자 호출을 해석하고 diff --git a/CHANGELOG.md b/CHANGELOG.md index b03f597..dc67d74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,6 +64,15 @@ delegate to them, read the migration note at the end of this section. ### Changed +- The committed development version is now `0.1.0-SNAPSHOT` (it was `0.0.1-SNAPSHOT`, + left at the released coordinate). **Development snapshots on GitHub Packages move + with it** — a build pinned to `org.htcom:protean:0.0.1-SNAPSHOT` stops receiving + updates and must move to `0.1.0-SNAPSHOT`. Released coordinates are unaffected and + are still injected with `-Pversion`, never committed. The reason for the change is + the new `ready` frame: it reports this value to clients as `platformVersion`, and + `0.0.1-SNAPSHOT` sorts *before* `0.0.1`, so every development build was announcing + itself as older than a release it had already surpassed. + - `debug.evaluate` and `debug.redefine` now advertise **`destructiveHint: true`** (was `false`). The spec's default for that hint is `true`, so `false` was not silence — it claimed these were safer than a tool that says nothing. They are diff --git a/build.gradle b/build.gradle index d4a61e9..7003c70 100644 --- a/build.gradle +++ b/build.gradle @@ -29,8 +29,14 @@ plugins { group = 'org.htcom' // Development default is a SNAPSHOT; override with -Pversion= for a Central Portal upload. The Portal's // validate→Publish (VALIDATED) flow requires a non-SNAPSHOT version — a SNAPSHOT is instead PUT to the maven-snapshots -// repo (a different endpoint/permission), so Level 2 and the eventual go-live pass -Pversion=0.0.1. -version = providers.gradleProperty('version').getOrElse('0.0.1-SNAPSHOT') +// repo (a different endpoint/permission), so a go-live passes -Pversion=. +// +// The default names the version being WORKED TOWARDS, not the one last released, and it is bumped right after each +// release. This used to hold the released version permanently, which was harmless while the version was only a +// publishing coordinate — but the SSE `ready` frame reports it to clients as `platformVersion`, and `0.0.1-SNAPSHOT` +// sorts BEFORE `0.0.1`, so a build carrying post-0.0.1 features announced itself as older than the release that +// lacks them. A version nobody reads can be a placeholder; one on the wire cannot. +version = providers.gradleProperty('version').getOrElse('0.1.0-SNAPSHOT') java { toolchain { @@ -207,7 +213,7 @@ dependencies { publishing { publications { library(MavenPublication) { - // group=org.htcom, artifactId=protean (settings.gradle rootProject.name), version=0.0.1-SNAPSHOT. + // group=org.htcom, artifactId=protean (settings.gradle rootProject.name), version=the value set above. // components.java = plain jar (for consumption) + sources + javadoc + generated POM. // implementation dependencies are written to the POM as runtime scope, api as compile, so consumers resolve them transitively. // Sidecar worker artifact (C28, process track): the Shadow plugin already contributes the flat shaded diff --git a/docs/guide/04-rest-api.ko.md b/docs/guide/04-rest-api.ko.md index 7048659..2b81361 100644 --- a/docs/guide/04-rest-api.ko.md +++ b/docs/guide/04-rest-api.ko.md @@ -415,7 +415,7 @@ data: {"platform":"protean","platformVersion":"0.1.0","tracesEnabled":true, 여기에는 **연결 시점에 한 번 알아야 하고 이후 스트림으로는 알 수 없는** 값만 싣는다 — 주기적으로 바뀌는 값은 `metrics`·`modules`·`summary` 프레임의 몫이다: - `platform` — 구현체 식별자. 언제나 `"protean"`. 이 계약을 여러 구현이 공유하므로, 버전 숫자만 받은 콘솔이 그것이 누구의 버전인지 추측하지 않도록 함께 싣는다. **표시·진단용이며 동작을 분기하는 데 쓰지 않는다.** -- `platformVersion` — 이 라이브러리의 버전. jar manifest 의 `Implementation-Version` 에서 읽는다. manifest 가 없는 레이아웃(exploded classes — 테스트·IDE 실행)에서는 **`null`** 이다. 그 상황에서는 버전을 정말로 모르며, 임의의 대체 문자열은 "지금 어느 버전과 이야기하고 있나" 에 대한 지어낸 답이 된다. 필드 자체는 항상 있으므로 클라이언트가 "없음" 과 "모름" 을 구별할 일이 없다. +- `platformVersion` — 이 라이브러리의 버전. jar manifest 의 `Implementation-Version` 에서 읽는다. manifest 가 없는 레이아웃(exploded classes — 이 라이브러리 자신의 테스트, 그리고 클래스 디렉터리로 잡힌 IDE 실행)에서는 **`null`** 이다. 그 상황에서는 버전을 정말로 모르며, 임의의 대체 문자열은 "지금 어느 버전과 이야기하고 있나" 에 대한 지어낸 답이 된다. 실제로는 보기 드물다 — 소비자 앱은 protean 을 jar 로 해석하고 `examples/quickstart` 데모도 그러므로, **콘솔이 실제로 붙는 경로는 전부 진짜 버전을 내려보낸다.** 필드 자체는 항상 있으므로 클라이언트가 "없음" 과 "모름" 을 구별할 일이 없다. - `tracesEnabled` / `metricsEnabled` — `protean.trace.enabled` 와 `protean.trace.metrics.enabled`. **"기록이 꺼져 있다"** 와 **"켜져 있는데 아직 아무 일도 없었다"** 를 가른다. 빈 스트림만으로는 이 둘이 구별되지 않는다. - `buffered` — 이 프레임 **다음에 오는 `trace` 프레임이 싣는 행 수**이며, 링이 보유한 행 수가 아니다. `protean.trace.capacity` 가 재생 상한 200 을 넘으면 둘이 달라진다. 숫자만 알리고 행은 주지 않는 것은 둘 다 안 하느니만 못하다. - `tickMs` — 푸시 주기(`1000`). 클라이언트의 침묵 감지 워치독이 가정 대신 계약을 기준으로 자기 값을 정할 수 있게 한다. diff --git a/docs/guide/04-rest-api.md b/docs/guide/04-rest-api.md index ae9ccb2..24e80d7 100644 --- a/docs/guide/04-rest-api.md +++ b/docs/guide/04-rest-api.md @@ -415,7 +415,7 @@ data: {"platform":"protean","platformVersion":"0.1.0","tracesEnabled":true, It carries only what a client must know **once, at connect time** and cannot derive from the stream afterwards — anything that changes on a schedule belongs to the `metrics`/`modules`/`summary` frames instead: - `platform` — implementation identifier, always `"protean"`. Present so a console watching several implementations of this contract is not left reading a bare version number and guessing whose it is. **Display and diagnostics only — do not branch behaviour on it.** -- `platformVersion` — this library's version, read from the jar manifest (`Implementation-Version`). **`null`** when running from a layout that has no manifest (exploded classes: tests, IDE runs) — the version is genuinely unknown there, and a placeholder would be a fabricated answer. The field is always present, so a client never has to distinguish "absent" from "unknown". +- `platformVersion` — this library's version, read from the jar manifest (`Implementation-Version`). **`null`** when running from a layout that has no manifest (exploded classes: the library's own test suite, and IDE runs configured against class directories) — the version is genuinely unknown there, and a placeholder would be a fabricated answer. In practice it is rarely seen: a consumer application resolves protean as a jar, and so does the `examples/quickstart` demo, so every path a console actually connects to reports a real version. The field is always present, so a client never has to distinguish "absent" from "unknown". - `tracesEnabled` / `metricsEnabled` — `protean.trace.enabled` and `protean.trace.metrics.enabled`. They separate **"recording is off"** from **"recording is on but nothing has happened yet"**, which an empty stream cannot. - `buffered` — how many rows the `trace` frame that follows this one carries, **not** how many the ring holds. The two differ whenever `protean.trace.capacity` exceeds the 200-row replay cap; announcing a count while withholding the rows would be worse than announcing neither. - `tickMs` — the push period (`1000`), so a client's silence watchdog has a contract to size itself against instead of assuming one. diff --git a/docs/guide/11-operations.ko.md b/docs/guide/11-operations.ko.md index 59fbe73..bb83fb2 100644 --- a/docs/guide/11-operations.ko.md +++ b/docs/guide/11-operations.ko.md @@ -201,7 +201,7 @@ CI 는 일부러 다르게 하며 그 차이는 고칠 버그가 아니다: CI - 발행 publication 은 `components.java`(plain jar + Shadow 플러그인이 기여하는 **worker** shaded jar — `shadowed` bundling 속성으로 구분되어 일반 소비자는 여전히 plain jar 를 받음) + sources jar + javadoc jar + 생성 POM. - **worker 컨테이너 이미지**는 Jib 로 별도 발행한다: `GITHUB_OWNER`/`GITHUB_ACTOR`/`GITHUB_TOKEN` 이 설정되면 `./gradlew jib` 가 `ghcr.io//protean-worker:` 로 push 하고(없으면 `jibDockerBuild`/`jibBuildTar` 로 로컬 빌드), CI 는 라이브러리 발행과 동일 gate 로 `main` push 시에만 실행한다. - JDBC 드라이버(`mysql-connector-j`, `postgresql`)는 protean 자기 bootJar·테스트엔 필요하지만 소비자에게 transitive 로 강제하지 않으려고 발행 POM 에서 `optional=true` 로 표시된다. 소비자가 worker DB 프로비저닝을 쓰면 자기 드라이버를 명시 추가해야 한다. -- **릴리스는 Maven Central**(Sonatype Central Portal)로 간다 — `com.vanniktech.maven.publish.base` 플러그인. 릴리스 좌표는 커밋하지 않는다: 소스 버전은 `0.0.1-SNAPSHOT` 으로 두고 릴리스는 `-Pversion=` 로 주입한다. `publishToMavenCentral` 은 먼저 `verifyReleaseArtifact` 게이트(아티팩트 집합 + 실제 소비 테스트)를 돌린 뒤 업로드하며, 배포는 **VALIDATED** 에서 멈추고 실공개는 Portal 에서 사람이 따로 Publish 한다. 서명은 in-memory 이고 property-gated(`signingInMemoryKey*`)라 키가 없는 빌드는 다른 곳에 unsigned 로 발행된다. 업로드는 수동 `Release` 워크플로(`workflow_dispatch`)가 돌리며 tag push 로는 발동하지 않는다. +- **릴리스는 Maven Central**(Sonatype Central Portal)로 간다 — `com.vanniktech.maven.publish.base` 플러그인. 릴리스 좌표는 커밋하지 않는다: 릴리스는 `-Pversion=` 로 주입한다. 대신 커밋되는 것은 **다음** 버전의 SNAPSHOT(0.1.0 을 향해 작업 중이면 `0.1.0-SNAPSHOT`)이고, **릴리스 직후 bump 한다**. SSE `ready` 프레임이 이 값을 `platformVersion` 으로 클라이언트에 내려보내기 때문이다 — 방금 릴리스한 버전을 기본값으로 두면 모든 개발 빌드가 이미 넘어선 릴리스보다 자기가 **낮다고** 말하게 된다(`0.0.1-SNAPSHOT` 은 `0.0.1` 보다 앞선다). `publishToMavenCentral` 은 먼저 `verifyReleaseArtifact` 게이트(아티팩트 집합 + 실제 소비 테스트)를 돌린 뒤 업로드하며, 배포는 **VALIDATED** 에서 멈추고 실공개는 Portal 에서 사람이 따로 Publish 한다. 서명은 in-memory 이고 property-gated(`signingInMemoryKey*`)라 키가 없는 빌드는 다른 곳에 unsigned 로 발행된다. 업로드는 수동 `Release` 워크플로(`workflow_dispatch`)가 돌리며 tag push 로는 발동하지 않는다. - **GitHub Packages** 는 개발 스냅샷을 담는다(CI 가 `main` push 마다 발행). URL·인증을 `build.gradle` 에 하드코딩하지 않고 `gradle.properties`/환경변수로 외부화한다. `githubOwner`/`githubRepo`/`githubActor`/`githubToken`(또는 `GITHUB_OWNER`/`GITHUB_REPO`/`GITHUB_ACTOR`/`GITHUB_TOKEN` env)이 **모두 주어졌을 때만** 원격 repository 가 등록되고(없으면 mavenLocal 만), 그때 `publishLibraryPublicationToGitHubPackagesRepository` 태스크로 발행한다. 자격 템플릿은 `gradle.properties.example` 참고. ## 관련 문서 diff --git a/docs/guide/11-operations.md b/docs/guide/11-operations.md index 4cdd26d..3fc79f4 100644 --- a/docs/guide/11-operations.md +++ b/docs/guide/11-operations.md @@ -201,7 +201,7 @@ CI does this differently on purpose, and that difference is not a bug to fix: it - The publication comprises `components.java` (plain jar + the **worker** shaded jar the Shadow plugin contributes, distinguished by the `shadowed` bundling attribute so default consumers still resolve the plain jar) + sources jar + javadoc jar + the generated POM. - The **worker container image** publishes separately via Jib: `./gradlew jib` pushes `ghcr.io//protean-worker:` when `GITHUB_OWNER`/`GITHUB_ACTOR`/`GITHUB_TOKEN` are set (otherwise `jibDockerBuild`/`jibBuildTar` build it locally). CI runs this on pushes to `main` only, gated exactly like the library publish. - The JDBC drivers (`mysql-connector-j`, `postgresql`) are needed for protean's own bootJar/tests but are marked `optional=true` in the published POM so as not to force them transitively on consumers. A consumer using worker DB provisioning must add its own driver explicitly. -- **Releases go to Maven Central** (Sonatype Central Portal) via the `com.vanniktech.maven.publish.base` plugin. The release coordinate is never committed — the source version stays `0.0.1-SNAPSHOT` and the release is injected with `-Pversion=`. `publishToMavenCentral` first runs the `verifyReleaseArtifact` gate (artifact set + a real consume test), then uploads; the deployment stops at **VALIDATED** and going live is a separate manual Publish on the Portal. Signing is in-memory and property-gated (`signingInMemoryKey*`), so a build without the key still publishes unsigned elsewhere. The upload is driven by the manual `Release` workflow (`workflow_dispatch`), never by a tag push. +- **Releases go to Maven Central** (Sonatype Central Portal) via the `com.vanniktech.maven.publish.base` plugin. The release coordinate is never committed — the release is injected with `-Pversion=`. What *is* committed is the next version as a SNAPSHOT (`0.1.0-SNAPSHOT` while 0.1.0 is being worked towards), **bumped right after each release**: the SSE `ready` frame reports this value to clients as `platformVersion`, and a default left at the version just released would make every development build announce itself as older than a release it already surpassed (`0.0.1-SNAPSHOT` sorts before `0.0.1`). `publishToMavenCentral` first runs the `verifyReleaseArtifact` gate (artifact set + a real consume test), then uploads; the deployment stops at **VALIDATED** and going live is a separate manual Publish on the Portal. Signing is in-memory and property-gated (`signingInMemoryKey*`), so a build without the key still publishes unsigned elsewhere. The upload is driven by the manual `Release` workflow (`workflow_dispatch`), never by a tag push. - **GitHub Packages** carries the development snapshots (CI publishes on pushes to `main`). The URL/credentials are not hardcoded in `build.gradle` but externalized via `gradle.properties`/environment variables. The remote repository is registered **only when all of** `githubOwner`/`githubRepo`/`githubActor`/`githubToken` (or the `GITHUB_OWNER`/`GITHUB_REPO`/`GITHUB_ACTOR`/`GITHUB_TOKEN` env vars) are provided (otherwise mavenLocal only), and it is then published via the `publishLibraryPublicationToGitHubPackagesRepository` task. For the credential template see `gradle.properties.example`. ## Related documents diff --git a/verification/consumer/build.gradle b/verification/consumer/build.gradle index d480a8c..af6c1bd 100644 --- a/verification/consumer/build.gradle +++ b/verification/consumer/build.gradle @@ -30,7 +30,7 @@ repositories { // The version under test is injected by the root release gate (-PproteanVersion=); defaults to the dev // SNAPSHOT so this build is runnable on its own after a plain publishToMavenLocal. -def proteanVersion = (findProperty('proteanVersion') ?: '0.0.1-SNAPSHOT').toString() +def proteanVersion = (findProperty('proteanVersion') ?: '0.1.0-SNAPSHOT').toString() dependencies { implementation "org.htcom:protean:${proteanVersion}"