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.