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
13 changes: 11 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ jobs:
- name: Verify the publishable artifacts assemble
run: ./gradlew publishToMavenLocal

- name: Check the contract metadata matches the lock file
run: |
set -euo pipefail
./gradlew generateContractInfo
scripts/check-contract-info.sh

- name: Report generated file counts
run: |
set -euo pipefail
Expand Down Expand Up @@ -99,7 +105,10 @@ jobs:
run: |
set -euo pipefail
scripts/generate-swift.sh
if git diff --quiet -- Sources; then
# ContractInfo.swift is excluded below: its version line is stamped by publish.yml
# just before tagging, so on a released tag it will not match what codegen produces
# here. Its commit field is what matters and check-contract-info.sh guards that.
if git diff --quiet -- Sources ':(exclude)Sources/OCPClientProtocol/ContractInfo.swift'; then
echo "Sources/ matches the protos."
exit 0
fi
Expand All @@ -109,5 +118,5 @@ jobs:
echo "::error::Sources/ does not match what codegen produces."
echo "Run scripts/generate-swift.sh and commit the result. Pinned toolchain:"
cat scripts/toolchain.env | grep -v '^#'
git --no-pager diff --stat -- Sources
git --no-pager diff --stat -- Sources ':(exclude)Sources/OCPClientProtocol/ContractInfo.swift'
exit 1
24 changes: 22 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,10 @@ jobs:
set -euo pipefail
scripts/install-swift-toolchain.sh
scripts/generate-swift.sh
if ! git diff --quiet -- Sources; then
if ! git diff --quiet -- Sources ':(exclude)Sources/OCPClientProtocol/ContractInfo.swift'; then
echo "::error::Sources/ does not match what codegen produces — refusing to tag it."
echo "The tag is the Swift release, so it has to point at generated code that is current."
git --no-pager diff --stat -- Sources
git --no-pager diff --stat -- Sources ':(exclude)Sources/OCPClientProtocol/ContractInfo.swift'
exit 1
fi

Expand All @@ -121,6 +121,26 @@ jobs:
ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.MAVEN_SIGNING_KEY_PASSWORD }}
run: ./gradlew publishAndReleaseToMavenCentral --no-configuration-cache

# ContractInfo.swift ships the version, and on main it reads "<next>-dev" because the
# number is not real until this workflow runs. Regenerate it with the real one and
# commit, so the tag -- which IS the Swift release -- carries a released version.
#
# The commit is never pushed to main. Only the tag is pushed, so main keeps -dev and
# this workflow never needs write access to a protected branch. The tagged commit is
# reachable from the tag alone, which is exactly what SPM resolves.
- name: Stamp the released version into ContractInfo.swift
if: ${{ !inputs.dry_run }}
env:
RELEASE_VERSION: ${{ inputs.version }}
run: |
set -euo pipefail
scripts/generate-swift.sh
scripts/check-contract-info.sh
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add Sources/OCPClientProtocol/ContractInfo.swift
git commit -m "chore(release): stamp ${{ inputs.version }} into ContractInfo.swift"

# Last, because the tag is the Swift release: once it exists, SPM consumers resolve
# it immediately. Tagging before the JAR is up would ship half a version.
- name: Tag the release
Expand Down
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,19 @@ called out explicitly even when nothing else did.
release notes, so a version with no entry here does not release. Write the entry in the same PR that
syncs the contract, while the diff is still in front of you.

## 0.5.0

No contract change. Still synced to
[`ocp-protobuf-api@82202912`](https://github.com/code-payments/ocp-protobuf-api/commit/82202912574e122bba90025fe8b292d5a3f04c05).

### Added

- `OcpContractInfo` (Kotlin) and `OCPContractInfo` (Swift), carrying `VERSION` /
`version` and `PROTO_COMMIT` / `protoCommit` for the upstream commit this package was
generated from, plus `shortProtoCommit` and `isLocal`. A package built from
`sync-protos.sh --local` reports `LOCAL` as its commit, so a consumer can tell a local
contract build from a pinned one at runtime.

## 0.4.0

Synced to [`ocp-protobuf-api@82202912`](https://github.com/code-payments/ocp-protobuf-api/commit/82202912574e122bba90025fe8b292d5a3f04c05).
Expand Down
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,28 @@ Four services — `Account`, `Currency`, `Messaging`, `Transaction` — plus the
`common/v1/model.proto`. The contract is owned upstream; `ocp.lock` records which commit of it
this package was generated from.

## Knowing what you built against

Both generated clients carry the package version and the upstream commit they were
generated from:

```kotlin
OcpContractInfo.VERSION // "0.4.0"
OcpContractInfo.shortProtoCommit // "82202912", or "LOCAL"
```

```swift
OCPContractInfo.version // "0.4.0"
OCPContractInfo.shortProtoCommit // "82202912", or "LOCAL"
```

A build on a local proto sync reports `LOCAL` for the commit, because that is what
`sync-protos.sh --local` writes to `ocp.lock` and both generators read it from there.
`isLocal` is the flag to branch on.

The Swift file's version reads `<next>-dev` on `main`. `publish.yml` stamps the real
number into the commit it tags, so a resolved SPM tag always carries a released version.

## Layout

```
Expand Down
15 changes: 15 additions & 0 deletions Sources/OCPClientProtocol/ContractInfo.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Generated by scripts/generate-swift.sh. Do not edit.

public enum OCPContractInfo {
public static let version = "0.5.0-dev"
public static let protoCommit = "82202912574e122bba90025fe8b292d5a3f04c05"

public static var isLocal: Bool { protoCommit == localSentinel }

public static var shortProtoCommit: String {
isLocal ? protoCommit : String(protoCommit.prefix(shortLength))
}

private static let localSentinel = "LOCAL"
private static let shortLength = 8
}
63 changes: 63 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,66 @@ mavenPublishing {
}
}
}

// The published artifact is entirely generated protobuf, so there is nowhere for a
// consumer to read which contract it holds. This writes that in: the version Gradle is
// building under, and the upstream commit ocp.lock is pinned at. `sync-protos.sh --local`
// writes `commit: LOCAL`, so a composite-build consumer sees LOCAL with no extra handling.
abstract class GenerateContractInfo : DefaultTask() {
@get:InputFile
abstract val lockFile: RegularFileProperty

@get:Input
abstract val packageVersion: Property<String>

@get:OutputDirectory
abstract val outputDir: DirectoryProperty

@TaskAction
fun generate() {
val lock = lockFile.get().asFile
val commit = lock.readLines()
.firstOrNull { it.startsWith("commit:") }
?.removePrefix("commit:")
?.trim()
.orEmpty()
require(commit.isNotEmpty()) { "${lock.name}: no 'commit:' line" }

val dir = outputDir.get().asFile.resolve("com/codeinc/opencode/gen")
dir.mkdirs()
dir.resolve("OcpContractInfo.kt").writeText(
"""
// Generated by the generateContractInfo Gradle task. Do not edit.
package com.codeinc.opencode.gen

public object OcpContractInfo {
public const val VERSION: String = "${packageVersion.get()}"
public const val PROTO_COMMIT: String = "$commit"

public val isLocal: Boolean get() = PROTO_COMMIT == LOCAL_SENTINEL

public val shortProtoCommit: String
get() = if (isLocal) PROTO_COMMIT else PROTO_COMMIT.take(SHORT_LENGTH)

private const val LOCAL_SENTINEL: String = "LOCAL"
private const val SHORT_LENGTH: Int = 8
}

""".trimIndent()
)
}
}

// Captured eagerly rather than through a provider: `version` is resolved at configuration
// time from RELEASE_VERSION, and reading `project` from a task action is not permitted.
val contractInfoVersion = version.toString()

val generateContractInfo = tasks.register<GenerateContractInfo>("generateContractInfo") {
lockFile.set(layout.projectDirectory.file("ocp.lock"))
packageVersion.set(contractInfoVersion)
outputDir.set(layout.buildDirectory.dir("generated/sources/contractinfo/main/kotlin"))
}

kotlin.sourceSets.named("main") {
kotlin.srcDir(generateContractInfo)
}
32 changes: 32 additions & 0 deletions scripts/check-contract-info.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
#
# Asserts that the generated contract metadata agrees with ocp.lock.
#
# The Swift file is committed and excluded from the `git diff -- Sources` codegen check
# (its version line moves at release time, which that check would read as drift), so this
# is what guards it instead. The Kotlin file is generated into build/ on every build and
# is checked here for the same reason: one script, both languages, run by CI and by hand.
#
set -euo pipefail

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
LOCK="$ROOT/ocp.lock"
SWIFT="$ROOT/Sources/OCPClientProtocol/ContractInfo.swift"
KOTLIN="$ROOT/build/generated/sources/contractinfo/main/kotlin/com/codeinc/opencode/gen/OcpContractInfo.kt"

fail() { echo "ERROR: $*" >&2; exit 1; }

expected="$(awk '/^commit:/ {print $2}' "$LOCK")"
[ -n "$expected" ] || fail "$LOCK has no 'commit:' line"

[ -f "$SWIFT" ] || fail "missing $SWIFT -- run scripts/generate-swift.sh"
swift_commit="$(sed -n 's/.*protoCommit = "\([^"]*\)".*/\1/p' "$SWIFT")"
[ "$swift_commit" = "$expected" ] || \
fail "ContractInfo.swift protoCommit is '$swift_commit', ocp.lock says '$expected'"

[ -f "$KOTLIN" ] || fail "missing $KOTLIN -- run ./gradlew generateContractInfo"
kotlin_commit="$(sed -n 's/.*PROTO_COMMIT: String = "\([^"]*\)".*/\1/p' "$KOTLIN")"
[ "$kotlin_commit" = "$expected" ] || \
fail "OcpContractInfo.kt PROTO_COMMIT is '$kotlin_commit', ocp.lock says '$expected'"

echo "contract metadata agrees with ocp.lock: $expected"
33 changes: 33 additions & 0 deletions scripts/generate-swift.sh
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,37 @@ while IFS= read -r f; do
fi
done < <(cd "$PROTO_DIR" && find . -name '*.proto' -type f | sed "s#^\./#$PROTO_DIR/#" | sort)

# ContractInfo is the one hand-written file in this directory, and it has to be written
# here rather than by sync-protos.sh: OUT is wiped above, so anything sync writes would
# not survive a regenerate. On main the version is the next CHANGELOG entry plus -dev;
# publish.yml re-runs this with RELEASE_VERSION set and commits the result onto the tag.
commit="$(awk '/^commit:/ {print $2}' "$ROOT/ocp.lock")"
[ -n "$commit" ] || { echo "ocp.lock: no commit: line" >&2; exit 1; }

version="${RELEASE_VERSION:-}"
if [ -z "$version" ]; then
changelog_version="$(awk '/^## / {print $2; exit}' "$ROOT/CHANGELOG.md")"
[ -n "$changelog_version" ] || { echo "CHANGELOG.md: no '## <version>' heading" >&2; exit 1; }
version="${changelog_version}-dev"
fi

cat > "$OUT/ContractInfo.swift" <<SWIFT_EOF
// Generated by scripts/generate-swift.sh. Do not edit.

public enum OCPContractInfo {
public static let version = "$version"
public static let protoCommit = "$commit"

public static var isLocal: Bool { protoCommit == localSentinel }

public static var shortProtoCommit: String {
isLocal ? protoCommit : String(protoCommit.prefix(shortLength))
}

private static let localSentinel = "LOCAL"
private static let shortLength = 8
}
SWIFT_EOF

echo "==> generated $(find "$OUT" -name '*.swift' | wc -l | tr -d ' ') Swift file(s) into Sources/OCPClientProtocol/"
echo "==> ContractInfo.swift: $version / $commit"
Loading