Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 42 additions & 103 deletions README.md
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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

Expand All @@ -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 <fingerprint>
```

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 |
90 changes: 90 additions & 0 deletions docs/generation.md
Original file line number Diff line number Diff line change
@@ -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.
33 changes: 33 additions & 0 deletions docs/migration.md
Original file line number Diff line number Diff line change
@@ -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).
58 changes: 58 additions & 0 deletions docs/releasing.md
Original file line number Diff line number Diff line change
@@ -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 <fingerprint>
```

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.
Loading