From 88335791219466f229376beefd86a9e5bed11491 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Tue, 25 Aug 2026 13:27:49 -0400 Subject: [PATCH] docs: restructure the README around the published package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The README had grown into working notes from the migration: the parity evidence, the toolchain archaeology, and the Central setup all sat above the things a consumer needs first. It read as a record of how the package came to exist rather than as documentation of the package. README.md now covers what the package is, how to depend on it, what is in it, and how to update the contract. The depth moves to docs/, kept because it is the only place the reasoning is written down: - docs/generation.md — the sync, both generator toolchains and their pins, the transitive grpc-swift-2 pin that is the reason toolchain.env exists, and what CI checks. - docs/releasing.md — the publish workflow, the signing secrets, and the one-time keyserver and namespace setup. - docs/migration.md — what the vendored copies were, the parity numbers from the cutover, and why that gate is gone. No content is dropped. --- README.md | 145 +++++++++++++-------------------------------- docs/generation.md | 90 ++++++++++++++++++++++++++++ docs/migration.md | 33 +++++++++++ docs/releasing.md | 58 ++++++++++++++++++ 4 files changed, 223 insertions(+), 103 deletions(-) create mode 100644 docs/generation.md create mode 100644 docs/migration.md create mode 100644 docs/releasing.md diff --git a/README.md b/README.md index bf48764..342ead1 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,36 @@ # ocp-client-protocol -Kotlin and Swift client SDKs for the Open Code Protocol, generated from the contract in -[`ocp-protobuf-api`](https://github.com/code-payments/ocp-protobuf-api) and published so the -apps consume a versioned dependency instead of vendoring `.proto` files and running protoc -themselves. - -`code-android-app` and `code-ios-app` used to each vendor their own copy of these protos and -generate independently — two copies of the contract, two generator toolchains, and no mechanism -that made them agree. This repo is the single generation point that replaced them. - -It is the sibling of [`flipcash2-client-protocol`](https://github.com/code-payments/flipcash2-client-protocol). -The two are separate packages because the contracts are independent — flipcash2 does not import -ocp — and because they belong to different orgs once the split lands. - -## Status - -Released, and both apps are on it. `0.1.0` is on Maven Central and tagged for SPM. Android -migrated in [code-android-app#1325](https://github.com/code-payments/code-android-app/pull/1325) -and iOS in [code-ios-app#645](https://github.com/code-payments/code-ios-app/pull/645), which -together deleted both vendored copies. - -Before the apps migrated, `scripts/verify-parity.sh` proved this repo is a drop-in replacement -for what they generated: 263 Kotlin/Java files against -`:definitions:opencode:models:generateDebugProto` and 9 Swift files against -`FlipcashAPI/.../Payments/Generated`, both identical, with the Kotlin split matching per -generator (4 grpc, 4 grpckt, 5 java, 119 kotlin, 131 validate-kt). That gate is retired with the -copies it compared against — an app that no longer generates has no second output to disagree -with. What guards the output now is `scripts/toolchain.env`, since a floating generator moves -the Swift without any contract change. +Kotlin and Swift client SDKs for the Open Code Protocol gRPC contract. The protos are synced +from [`ocp-protobuf-api`](https://github.com/code-payments/ocp-protobuf-api) at a pinned commit, +generated here, and published as one versioned package that both apps consume. + +| Language | Package | Generated namespace | +|---|---|---| +| Kotlin | `com.flipcash:ocp-client-protocol` on Maven Central | `com.codeinc.opencode.gen.*` | +| Swift | `OCPClientProtocol`, resolved by SPM from this repo's tags | `Ocp_*_V1_*` | + +Its sibling is [`flipcash2-client-protocol`](https://github.com/code-payments/flipcash2-client-protocol). +They are separate packages because the contracts are: flipcash2 does not import ocp. + +## Install + +```kotlin +implementation("com.flipcash:ocp-client-protocol:0.1.0") +``` + +```swift +.package(url: "https://github.com/code-payments/ocp-client-protocol", from: "0.1.0") +``` + +`code-android-app` pins the version in `gradle/libs.versions.toml`. `code-ios-app` pins it in +`FlipcashAPI/Package.swift` and re-exports the module, so app code still reaches these types +through `import FlipcashAPI`. + +## What it contains + +Four services — `Account`, `Currency`, `Messaging`, `Transaction` — plus the shared +`common/v1/model.proto`. The contract is owned upstream; `ocp.lock` records which commit of it +this package was generated from. ## Layout @@ -43,8 +46,7 @@ scripts/ generate-swift.sh regenerate Sources/ ``` -Generated Kotlin is not committed. It is a build input to a published JAR, so the -reviewable-diff argument that applies to Swift does not apply here. +Generated Kotlin is not committed — it is a build input to the published JAR. ## Updating the contract @@ -55,83 +57,20 @@ scripts/generate-swift.sh # refresh committed Swift ./gradlew build # Kotlin regenerates as part of the build ``` -## Things worth knowing - -- **This repo owns the `java_package` namespace.** Upstream ships `com.codeinc.gen.*`; the - Android app has always consumed `com.codeinc.opencode.gen.*` via a `sed`/`awk` pass at the - end of its `fetch-protos.sh`. `sync-protos.sh` does that rewrite instead, so the published - artifact needs no consumer-side post-processing. Swift is unaffected — `java_package` does - not influence swift-protobuf naming, which the byte-identical Swift output confirms. -- **`validate/validate.proto` is an include-path dependency only.** It is never generated. The - iOS build currently generates it and then deletes the resulting - `validate_validate.pb.swift`; keeping it in `proto_deps/` and off the generation list removes - the need for that step. -- **`grpc-kotlin` codegen is pinned to 1.4.1**, matching the app. 1.5.0 was tested: it differs - in line wrapping only (344 lines, no API change), so the bump is safe but belongs in its own - commit. -- **The JVM and Android variants of the protobuf Gradle plugin differ.** The JVM variant - registers the `java` builtin by default; the Android variant does not, which is why the app - declares `java` as a plugin and this repo configures the builtin instead. -- **The Swift generators are pinned, and one of the pins is transitive.** `brew install - protoc-gen-grpc-swift` was enough to reproduce the committed output in August 2026 and is - not enough now. The gRPC stub text is rendered by grpc-swift-2's `GRPCCodeGen`, which - grpc-swift-protobuf pulls in with a floating `from:` requirement — so the output moves - when *that* releases, with the plugin's own version unchanged. 2.2.1 added `Sendable` to - the metadata enums and 2.3.0 added `type:` to every `MethodDescriptor`: ~840 changed - lines across 9 files, no contract change. `scripts/toolchain.env` pins all four - versions and `install-swift-toolchain.sh` builds the plugins against them. -- **Coroutines are an explicit dependency.** The generated grpckt stubs reference - `kotlinx.coroutines.flow.Flow`; the app gets that from elsewhere in its graph, a standalone - artifact cannot. +Commit the resulting `proto/`, `ocp.lock`, and `Sources/` together. CI re-runs both generators +and fails if `Sources/` does not match the protos. ## Releasing `.github/workflows/publish.yml`, run from the Actions tab with a version like `0.1.0`. One -version covers both languages: the Kotlin artifact goes to Maven Central, and the git tag the -workflow pushes *is* the Swift Package release, because SPM resolves source straight from this -repo. - -The workflow refuses to publish a version that is already tagged, builds and signs everything -before it uploads anything, and tags last — so a broken POM or a stale `Sources/` fails while -the version number is still spendable. `dry_run` does everything except upload and tag. - -Consuming a release: +version covers both languages: the Kotlin artifact goes to Maven Central and the git tag the +workflow pushes *is* the Swift Package release. See [docs/releasing.md](docs/releasing.md) for +the required secrets and the one-time Central setup. -```kotlin -implementation("com.flipcash:ocp-client-protocol:0.1.0") -``` - -```swift -.package(url: "https://github.com/code-payments/ocp-client-protocol", from: "0.1.0") -``` - -### Secrets this needs +## Docs -Set in both repos. - -| Secret | What it is | +| Document | Covers | |---|---| -| `MAVEN_CENTRAL_USERNAME` | Central Portal user token, not the account login | -| `MAVEN_CENTRAL_PASSWORD` | the matching token password | -| `MAVEN_SIGNING_KEY` | ASCII-armored private key, `gpg --armor --export-secret-keys` | -| `MAVEN_SIGNING_KEY_ID` | last 8 characters of the key id | -| `MAVEN_SIGNING_KEY_PASSWORD` | the key's passphrase | - -**The signing key's public half must be on a keyserver Central queries.** Producing valid -`.asc` files is not enough: Central fetches the public key by fingerprint to check them, and -one it cannot find fails the whole deployment with `Could not find a public key by the key -fingerprint` against every signed file. Upload once, per key: - -```bash -gpg --keyserver keyserver.ubuntu.com --send-keys -``` - -The `com.flipcash` namespace is verified in the Central Portal. That was the other one-time -human step, and the one that needs a DNS TXT record. - -## Coordinates vs namespace - -Note that the artifact coordinates and the generated namespace are deliberately different. -The artifact publishes under `com.flipcash`; the code inside it stays in -`com.codeinc.opencode.gen.*`, because that is what the app imports and it is set by -`java_package` in the protos. +| [Code generation](docs/generation.md) | the pinned toolchain, what each generator emits, why the Swift is committed and the Kotlin is not | +| [Releasing](docs/releasing.md) | the publish workflow, the signing secrets, coordinates vs namespace | +| [Migration from vendored protos](docs/migration.md) | what this package replaced, and the parity evidence from the cutover | diff --git a/docs/generation.md b/docs/generation.md new file mode 100644 index 0000000..88f5f24 --- /dev/null +++ b/docs/generation.md @@ -0,0 +1,90 @@ +# Code generation + +One contract, two generator toolchains. Both read `proto/`, which `scripts/sync-protos.sh` +copies from upstream at the SHA recorded in `ocp.lock`. + +## Sync + +`sync-protos.sh` clones `ocp-protobuf-api`, checks out the requested ref, copies only the +contract `.proto` files (upstream's `buf.yaml`, `buf.lock`, and `buf.gen.yaml` describe how the +*contract* repo builds Go, and are not part of what this SDK ships), and rewrites `ocp.lock`. + +**This repo owns the `java_package` namespace.** Upstream ships `com.codeinc.gen.*`; the Android +app has always consumed `com.codeinc.opencode.gen.*`, via a `sed`/`awk` pass at the end of its +old `fetch-protos.sh`. `sync-protos.sh` does that rewrite instead, so the published artifact +needs no consumer-side post-processing, and it fails the sync if any `com.codeinc.gen.` survives +the pass. Swift is unaffected: `java_package` does not influence swift-protobuf naming. + +`proto_deps/validate/validate.proto` is an include-path dependency and is never generated. The +Gradle side gets it from the protovalidate plugin's own JAR; the Swift script passes +`proto_deps/` on the include path but keeps the file off the generation list. The iOS build used +to generate it and then delete the resulting `validate_validate.pb.swift` — that step is gone. + +## Kotlin + +Generated by the Gradle build, from versions pinned in `build.gradle.kts` rather than inherited +from a consumer, so the artifact is reproducible from that file alone: + +| Generator | Version | Output | +|---|---|---| +| protoc | 4.35.1 | `java` and `kotlin` builtins, both `lite` | +| protoc-gen-grpc-java | 1.83.1 | `grpc` stubs, `lite` | +| protoc-gen-grpc-kotlin | 1.4.1 | `grpckt` stubs, `lite` | +| protoc-gen-validate-kt | 0.1.1 | `validate-kt`, via the `dev.bmcreations.protovalidate` plugin in PGV mode | + +Two things that are easy to trip over: + +- **The JVM and Android variants of the protobuf Gradle plugin differ.** The JVM variant + registers the `java` builtin by default; the Android variant does not, which is why the app + declared `java` as a plugin and this repo configures the builtin instead. +- **Coroutines are an explicit dependency.** The generated `grpckt` stubs reference + `kotlinx.coroutines.flow.Flow`. The app got that from elsewhere in its graph; a standalone + artifact has to declare it, which is why `kotlinx-coroutines-core` is `api` here. So is + `protovalidate-runtime`, for the same reason on the `validate-kt` side. + +`grpc-kotlin` sits at 1.4.1 because that is what the Android app generated with at the cutover. +1.5.0 was tested and differs only in line wrapping — 344 lines, no API change — so the bump is +safe, but it belongs in its own commit. + +Generated Kotlin is not committed. It is a build input to a published JAR, so the +reviewable-diff argument that applies to the Swift below does not apply to it. + +## Swift + +`Sources/OCPClientProtocol/` is committed, because SPM ships plain source: for the Swift half, +the generated code *is* the artifact. That makes it possible to change a `.proto` and forget to +regenerate, so CI re-runs `scripts/generate-swift.sh` and fails if the tree moves. + +The generators are pinned in `scripts/toolchain.env`, and `install-swift-toolchain.sh` builds +the plugins against those pins into `.tools/`: + +``` +PROTOC_VERSION=33.1 +SWIFT_PROTOBUF_VERSION=1.33.3 +GRPC_SWIFT_PROTOBUF_VERSION=2.1.1 +GRPC_SWIFT_VERSION=2.2.0 +``` + +**One of those pins is transitive, and that is the whole reason this file exists.** The gRPC stub +text is rendered by grpc-swift-2's `GRPCCodeGen`, which grpc-swift-protobuf pulls in through a +floating `from:` requirement — so the output moves when *grpc-swift-2* releases, with the plugin's +own version unchanged. 2.2.1 added `Sendable` to the metadata enums; 2.3.0 added `type:` to every +`MethodDescriptor`. That was ~840 changed lines across 9 files for no contract change. +`brew install protoc-gen-grpc-swift` reproduced the committed output in August 2026 and does not +now. + +Bumping any of the four is a real change with a real diff. Do it in its own commit and expect +`Sources/` to move. + +## What CI checks + +`.github/workflows/ci.yml` runs on every push and PR — a release should never be the first time +codegen runs against a new contract or toolchain version. + +- **Kotlin**: `./gradlew build` (protoc plus compiling the result — if the generated code does + not compile, the published JAR is broken) and `./gradlew publishToMavenLocal`, which catches a + missing POM field or an unbuildable sources jar here rather than on the Central upload, where + the failure costs a burnt version number. It also writes per-generator file counts to the job + summary. +- **Swift**: `swift build`, then regenerate and `git diff --quiet -- Sources`. The toolchain cache + key is `scripts/toolchain.env`, so a bump rebuilds the plugins and nothing else does. diff --git a/docs/migration.md b/docs/migration.md new file mode 100644 index 0000000..8eac9b0 --- /dev/null +++ b/docs/migration.md @@ -0,0 +1,33 @@ +# Migration from vendored protos + +`code-android-app` and `code-ios-app` each used to vendor their own copy of the OCP protos and +generate independently — two copies of the contract, two generator toolchains, and nothing that +made them agree. This package replaced both. + +`0.1.0` shipped in August 2026. Android migrated in +[code-android-app#1325](https://github.com/code-payments/code-android-app/pull/1325) and iOS in +[code-ios-app#645](https://github.com/code-payments/code-ios-app/pull/645), which together +deleted the vendored copies; Android's remaining `:definitions:*` modules and its +`scripts/fetch-protos.sh` went in +[code-android-app#1326](https://github.com/code-payments/code-android-app/pull/1326). + +## The parity gate, and why it is gone + +Before the apps migrated, `scripts/verify-parity.sh` proved this repo was a drop-in replacement +for what they generated: 263 Kotlin/Java files against +`:definitions:opencode:models:generateDebugProto` and 9 Swift files against +`FlipcashAPI/.../Payments/Generated`, both identical, with the Kotlin split matching per +generator (4 grpc, 4 grpckt, 5 java, 119 kotlin, 131 validate-kt). + +That gate retired with the copies it compared against — an app that no longer generates has no +second output to disagree with. + +## What replaced it + +Contract drift between the platforms was possible because each vendored its own copy. With one +generation point it is possible only if the two apps sit on different package versions, which is +a visible version number rather than a silent content diff. + +What still needs watching is generator drift, which parity never guarded well anyway: the Swift +output moves on a transitive grpc-swift-2 release with no contract change at all. +`scripts/toolchain.env` is the guarantee now — see [Code generation](generation.md). diff --git a/docs/releasing.md b/docs/releasing.md new file mode 100644 index 0000000..5eaac4e --- /dev/null +++ b/docs/releasing.md @@ -0,0 +1,58 @@ +# Releasing + +`.github/workflows/publish.yml`, run from the Actions tab with a version like `0.1.0`. One +version covers both languages: the Kotlin artifact goes to Maven Central, and the git tag the +workflow pushes *is* the Swift Package release, since SPM resolves source straight from this +repo. + +The workflow refuses a version that is already tagged, builds and signs everything before it +uploads anything, and tags last — so a broken POM or a stale `Sources/` fails while the version +number is still spendable. Maven Central will not accept a re-published version either, which is +what makes the pre-flight check worth having. `dry_run` does everything except upload and tag. + +Consuming the result: + +```kotlin +implementation("com.flipcash:ocp-client-protocol:0.1.0") +``` + +```swift +.package(url: "https://github.com/code-payments/ocp-client-protocol", from: "0.1.0") +``` + +## Coordinates vs namespace + +The artifact coordinates and the generated namespace are deliberately different. The artifact +publishes under `com.flipcash`, the verified Central namespace; the code inside stays in +`com.codeinc.opencode.gen.*`, because that is what the app imports and it is set by +`java_package` in the protos. + +## Secrets + +Set in both this repo and `flipcash2-client-protocol`. + +| Secret | What it is | +|---|---| +| `MAVEN_CENTRAL_USERNAME` | Central Portal user token, not the account login | +| `MAVEN_CENTRAL_PASSWORD` | the matching token password | +| `MAVEN_SIGNING_KEY` | ASCII-armored private key, `gpg --armor --export-secret-keys` | +| `MAVEN_SIGNING_KEY_ID` | last 8 characters of the key id | +| `MAVEN_SIGNING_KEY_PASSWORD` | the key's passphrase | + +CI passes the key to Gradle through the `ORG_GRADLE_PROJECT_signingInMemoryKey*` properties. +Locally that is a no-op unless the same properties are set, so `publishToMavenLocal` still works +unsigned. + +## One-time setup, per key and per namespace + +**The signing key's public half must be on a keyserver Central queries.** Producing valid `.asc` +files is not enough: Central fetches the public key by fingerprint to check them, and one it +cannot find fails the whole deployment with `Could not find a public key by the key fingerprint` +against every signed file. + +```bash +gpg --keyserver keyserver.ubuntu.com --send-keys +``` + +The `com.flipcash` namespace is verified in the Central Portal. That was the other one-time human +step, and the one that needs a DNS TXT record.