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}"