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
19 changes: 19 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,25 @@ concurrency:
cancel-in-progress: true

jobs:
contract:
name: Contract pin
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

# `scripts/sync-protos.sh --local` writes `commit: LOCAL` so the contract can be
# iterated on without pushing it. proto/ then reflects somebody's working tree, which
# no other checkout can reproduce, so it must not reach main.
- name: Reject a local contract sync
run: |
set -euo pipefail
if grep -q '^commit: LOCAL' ocp.lock; then
echo "::error::ocp.lock records a local sync. Push the contract change to ocp-protobuf-api, then re-run scripts/sync-protos.sh <sha>."
cat ocp.lock
exit 1
fi
echo "Pinned at $(awk '/^commit:/ {print $2}' ocp.lock)."

kotlin:
name: Kotlin codegen
runs-on: ubuntu-latest
Expand Down
11 changes: 11 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,17 @@ jobs:
exit 1
fi

# A local sync is for iterating, not releasing: proto/ came from a working tree and
# the published artifact would have no upstream commit behind it.
- name: Reject a local contract sync
run: |
set -euo pipefail
if grep -q '^commit: LOCAL' ocp.lock; then
echo "::error::ocp.lock records a local sync — refusing to publish it."
cat ocp.lock
exit 1
fi

- name: Validate Gradle wrapper
uses: gradle/actions/wrapper-validation@v4

Expand Down
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,26 @@ scripts/generate-swift.sh # refresh committed Swift
Commit the resulting `proto/`, `ocp.lock`, and `Sources/` together. CI re-runs both generators
and fails if `Sources/` does not match the protos.

## Local development

Trying a contract change does not need a release. `sync-protos.sh --local` reads a checkout of
[`ocp-protobuf-api`](https://github.com/code-payments/ocp-protobuf-api) directly, uncommitted
edits included:

```bash
scripts/sync-protos.sh --local ../ocp-protobuf-api # or set OCP_UPSTREAM_PATH
scripts/generate-swift.sh # only if you need the Swift side
```

That writes `commit: LOCAL` into `ocp.lock`. CI fails on it and the publish workflow refuses to
release it, so a local sync cannot reach `main` or Maven Central. Push the contract change and
re-run `scripts/sync-protos.sh <sha>` to get back to a reproducible pin.

Both apps can consume a checkout of this repo without a publish as well: `protoLocalRoot` in
Android's `local.properties`, `FLIPCASH_PROTO_LOCAL` for iOS. The whole loop, including what
stops a local state from shipping, is in `docs/proto-local-development.md` in the cross-platform
orchestrator directory.

## Releasing

`.github/workflows/publish.yml`, run from the Actions tab with a version like `0.1.0`. One
Expand Down
100 changes: 83 additions & 17 deletions scripts/sync-protos.sh
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
#!/usr/bin/env bash
#
# Sync the OCP contract protos from ocp-protobuf-api at a pinned commit.
# Sync the OCP contract protos from ocp-protobuf-api.
#
# scripts/sync-protos.sh # sync at the SHA in ocp.lock
# scripts/sync-protos.sh <sha|ref> # re-pin to <sha|ref>, then sync
# scripts/sync-protos.sh --local [path] # sync from a local checkout, uncommitted edits included
#
# Set OCP_UPSTREAM_URL to a local path to sync from a mirror instead of the network.
# --local is the contract-authoring loop: edit a .proto in an ocp-protobuf-api checkout,
# sync, regenerate, and build the apps against the result without pushing anything. The
# path defaults to $OCP_UPSTREAM_PATH, then to ../ocp-protobuf-api.
#
# A local sync writes `commit: LOCAL` to ocp.lock. CI and the publish workflow both reject
# that, so proto/ can never reach main or Maven Central without a real upstream commit
# behind it. Re-run with a sha once the contract change is pushed.
#
# Set OCP_UPSTREAM_URL to a local path to sync from a mirror instead of the network. That
# still clones, so it sees committed state only; --local reads the working tree.
#
set -euo pipefail

Expand All @@ -21,30 +31,70 @@ DEST="$ROOT/proto"
UPSTREAM_PKG='com.codeinc.gen.'
SDK_PKG='com.codeinc.opencode.gen.'

requested="${1:-}"
if [ -z "$requested" ]; then
[ -f "$LOCK" ] || { echo "no ocp.lock and no ref given; pass a sha to pin" >&2; exit 1; }
requested="$(awk '/^commit:/ {print $2}' "$LOCK")"
fi
mode="pinned"
requested=""
local_path=""

case "${1:-}" in
--local)
mode="local"
local_path="${2:-${OCP_UPSTREAM_PATH:-$ROOT/../ocp-protobuf-api}}"
;;
-h|--help)
sed -n '2,19p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
exit 0
;;
*)
requested="${1:-}"
;;
esac

tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT

echo "==> cloning $UPSTREAM_URL"
git clone --quiet "$UPSTREAM_URL" "$tmp/upstream"
git -C "$tmp/upstream" checkout --quiet "$requested"
sha="$(git -C "$tmp/upstream" rev-parse HEAD)"
subject="$(git -C "$tmp/upstream" log -1 --format='%s')"
echo "==> pinned at $sha ($subject)"
if [ "$mode" = "local" ]; then
SRC="$(cd "$local_path" 2>/dev/null && pwd)" || {
echo "no such directory: $local_path" >&2
echo "pass the path to an ocp-protobuf-api checkout, or set OCP_UPSTREAM_PATH." >&2
exit 1
}
echo "==> reading the working tree at $SRC"
if head="$(git -C "$SRC" rev-parse HEAD 2>/dev/null)"; then
worktree_state="clean"
git -C "$SRC" diff --quiet HEAD 2>/dev/null || worktree_state="uncommitted changes"
else
head="not a git checkout"
worktree_state="unversioned"
fi
echo "==> local HEAD $head ($worktree_state)"
else
if [ -z "$requested" ]; then
[ -f "$LOCK" ] || { echo "no ocp.lock and no ref given; pass a sha to pin" >&2; exit 1; }
requested="$(awk '/^commit:/ {print $2}' "$LOCK")"
if [ "$requested" = "LOCAL" ]; then
echo "ocp.lock records a local sync, so there is no upstream commit to re-sync from." >&2
echo "Pass a sha to re-pin, or re-run: scripts/sync-protos.sh --local [path]" >&2
exit 1
fi
fi

SRC="$tmp/upstream"
echo "==> cloning $UPSTREAM_URL"
git clone --quiet "$UPSTREAM_URL" "$SRC"
git -C "$SRC" checkout --quiet "$requested"
sha="$(git -C "$SRC" rev-parse HEAD)"
subject="$(git -C "$SRC" log -1 --format='%s')"
echo "==> pinned at $sha ($subject)"
fi

[ -d "$tmp/upstream/proto" ] || { echo "upstream has no proto/ directory" >&2; exit 1; }
[ -d "$SRC/proto" ] || { echo "upstream has no proto/ directory" >&2; exit 1; }

# Contract protos only. buf.yaml / buf.lock / buf.gen.yaml describe how the *contract*
# repo builds Go; they are not part of what this SDK ships.
rm -rf "$DEST"
mkdir -p "$DEST"
( cd "$tmp/upstream/proto" && find . -name '*.proto' -type f -print0 ) \
| ( cd "$tmp/upstream/proto" && xargs -0 -I{} sh -c 'mkdir -p "$1/$(dirname "{}")" && cp "{}" "$1/{}"' _ "$DEST" )
( cd "$SRC/proto" && find . -name '*.proto' -type f -print0 ) \
| ( cd "$SRC/proto" && xargs -0 -I{} sh -c 'mkdir -p "$1/$(dirname "{}")" && cp "{}" "$1/{}"' _ "$DEST" )

# Re-namespace for the Kotlin artifact. Swift is unaffected: java_package does not
# influence swift-protobuf naming.
Expand All @@ -63,12 +113,28 @@ if grep -rq "\"${UPSTREAM_PKG}" "$DEST"; then
exit 1
fi

cat > "$LOCK" <<LOCK_EOF
if [ "$mode" = "local" ]; then
cat > "$LOCK" <<LOCK_EOF
# LOCAL SYNC -- proto/ came from a working tree, not a pinned upstream commit.
# Not reproducible: CI and the publish workflow both fail while this says LOCAL.
# Push the contract change, then re-run scripts/sync-protos.sh <sha>.
upstream: $SRC (local working tree)
commit: LOCAL
head: $head ($worktree_state)
LOCK_EOF
else
cat > "$LOCK" <<LOCK_EOF
# Pinned upstream contract. Regenerate with scripts/sync-protos.sh <sha>.
upstream: code-payments/ocp-protobuf-api
commit: $sha
subject: $subject
LOCK_EOF
fi

echo "==> synced $(find "$DEST" -name '*.proto' | wc -l | tr -d ' ') proto file(s) into proto/"
echo "==> wrote $LOCK"

if [ "$mode" = "local" ]; then
echo
echo " ocp.lock now says LOCAL. This tree is for iterating, not for merging."
fi
Loading