diff --git a/.claude/docs/technology-stack.md b/.claude/docs/technology-stack.md index b5e4b9e94..53494a138 100644 --- a/.claude/docs/technology-stack.md +++ b/.claude/docs/technology-stack.md @@ -19,8 +19,13 @@ Android consumes the Kotlin half of the same two packages, so both apps now gene place instead of each vendoring the contract. **To pick up a contract change:** sync and release it in the client-protocol repo (its README has -the steps), then bump the `exact:` version in `FlipcashAPI/Package.swift`. Nothing in this repo -needs protoc, swift-protobuf, or the grpc-swift plugin installed. +the steps), then bump the pin with `./Scripts/bump-contract.sh `. Run it +only after the tag is published: it checks with `git ls-remote`, edits the pin in +`FlipcashAPI/Package.swift`, re-resolves with the local-override env vars unset, and stages both that +file and the workspace `Package.resolved`. Xcode Cloud resolves only from `Package.resolved` and will +not update it, so the pre-commit hook rejects a commit where the staged pin and the staged +`Package.resolved` version disagree. Nothing in this repo needs protoc, swift-protobuf, or the +grpc-swift plugin installed. ## Required Technologies diff --git a/Scripts/bump-contract.sh b/Scripts/bump-contract.sh new file mode 100755 index 000000000..850b78405 --- /dev/null +++ b/Scripts/bump-contract.sh @@ -0,0 +1,118 @@ +#!/usr/bin/env bash +# +# Bump a contract package pin in FlipcashAPI/Package.swift and update the +# workspace Package.resolved to match, so the two land in one commit. +# Xcode Cloud resolves only from Package.resolved and refuses to update it; the +# pre-commit hook rejects a pin that disagrees with it. +# +# Usage: +# ./Scripts/bump-contract.sh +# +# Run it only after the version's tag is published on the client-protocol repo. +# The script checks with `git ls-remote` and refuses otherwise. +# +# FLIPCASH_PROTO_LOCAL / FLIPCASH_PROTO_LOCAL_PACKAGES are unset for the run: local +# mode drops the contract entries from Package.resolved, which would make the +# result wrong. Your shell keeps its own values. +# +# On success both files are staged. If resolution changes anything in +# Package.resolved besides that package's entry (and originHash), both files are +# restored and the script exits 1. + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +cd "$REPO_ROOT" + +PACKAGE_SWIFT="FlipcashAPI/Package.swift" +PACKAGE_RESOLVED="Code.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved" + +usage() { + echo "Usage: ./Scripts/bump-contract.sh " >&2 + exit 2 +} + +[[ $# -eq 2 ]] || usage +PKG="$1" +VERSION="$2" + +case "$PKG" in + ocp) IDENTITY="ocp-client-protocol" ;; + flipcash2) IDENTITY="flipcash2-client-protocol" ;; + *) usage ;; +esac + +if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "error: '$VERSION' is not a X.Y.Z version." >&2 + exit 2 +fi + +if ! git diff --quiet -- "$PACKAGE_SWIFT" "$PACKAGE_RESOLVED" || ! git diff --cached --quiet -- "$PACKAGE_SWIFT" "$PACKAGE_RESOLVED"; then + echo "error: $PACKAGE_SWIFT or Package.resolved already has uncommitted changes; commit or restore them first." >&2 + exit 1 +fi + +if [[ -z "$(git ls-remote --tags "https://github.com/code-payments/$IDENTITY" "$VERSION")" ]]; then + echo "error: tag $VERSION not found on code-payments/$IDENTITY. Publish the release first." >&2 + exit 1 +fi + +restore() { + git checkout -- "$PACKAGE_SWIFT" "$PACKAGE_RESOLVED" + git restore --staged -- "$PACKAGE_SWIFT" "$PACKAGE_RESOLVED" 2>/dev/null || true +} + +# Only the `case .: return "X.Y.Z"` line in `version` changes; directoryName's +# `return ""` lines don't match the digits pattern. +sed -i.bak -E "s/^([[:space:]]*case \.$PKG:[[:space:]]*return \")[0-9]+\.[0-9]+\.[0-9]+[^\"]*(\")/\1$VERSION\2/" "$PACKAGE_SWIFT" +rm -f "$PACKAGE_SWIFT.bak" + +PINNED="$(sed -nE "s/^[[:space:]]*case \.$PKG:[[:space:]]*return \"([0-9]+\.[0-9]+\.[0-9]+[^\"]*)\"[[:space:]]*$/\1/p" "$PACKAGE_SWIFT")" +if [[ "$PINNED" != "$VERSION" ]]; then + echo "error: could not set the $PKG pin in $PACKAGE_SWIFT." >&2 + restore + exit 1 +fi + +echo "Resolving $IDENTITY $VERSION..." +if ! env -u FLIPCASH_PROTO_LOCAL -u FLIPCASH_PROTO_LOCAL_PACKAGES \ + xcodebuild -resolvePackageDependencies -project Code.xcodeproj -scheme Flipcash >/dev/null; then + echo "error: xcodebuild -resolvePackageDependencies failed." >&2 + restore + exit 1 +fi + +# Flatten each pin to "identity location revision version" so the committed and the +# resolved file compare line by line. Everything except this identity must be +# identical; originHash isn't a pin, so it never enters the comparison. +flatten_pins() { + awk ' + /"identity" :/ { gsub(/.*: "|",?$/, ""); id = $0; loc = rev = ver = "-" } + /"location" :/ { gsub(/.*: "|",?$/, ""); loc = $0 } + /"revision" :/ { gsub(/.*: "|",?$/, ""); rev = $0 } + /"version" :/ { gsub(/.*: "|",?$/, ""); ver = $0; print id, loc, rev, ver } + ' +} +UNEXPECTED="$(diff \ + <(git show "HEAD:$PACKAGE_RESOLVED" | flatten_pins | grep -v "^$IDENTITY ") \ + <(flatten_pins < "$PACKAGE_RESOLVED" | grep -v "^$IDENTITY ") || true)" +if [[ -n "$UNEXPECTED" ]]; then + echo "error: resolving changed Package.resolved beyond $IDENTITY:" >&2 + echo "$UNEXPECTED" >&2 + restore + exit 1 +fi + +NEW="$(awk -v id="$IDENTITY" ' + $0 ~ "\"identity\" : \"" id "\"" { found = 1; next } + found && /"version" :/ { gsub(/.*: "|".*/, ""); print; exit } +' "$PACKAGE_RESOLVED")" +if [[ "$NEW" != "$VERSION" ]]; then + echo "error: Package.resolved records ${NEW:-} for $IDENTITY, expected $VERSION." >&2 + restore + exit 1 +fi + +git add "$PACKAGE_SWIFT" "$PACKAGE_RESOLVED" +echo "✓ $IDENTITY pinned to $VERSION in both files, staged" diff --git a/Scripts/git-hooks/pre-commit b/Scripts/git-hooks/pre-commit index 42d2c3653..b3974f3fc 100755 --- a/Scripts/git-hooks/pre-commit +++ b/Scripts/git-hooks/pre-commit @@ -49,3 +49,56 @@ while IFS= read -r -d '' staged_path; do esac done done < <(git diff --cached --name-only -z) + +# Contract pins: FlipcashAPI/Package.swift pins each contract package with an +# exact version, and the workspace Package.resolved has to record the same one. +# Xcode Cloud resolves only from Package.resolved and refuses to update it, so a +# pin that moved without it fails every deploy. Pure text parsing of the staged +# blobs; no network and no xcodebuild. Skipped unless one of the two is staged. + +PACKAGE_SWIFT="FlipcashAPI/Package.swift" +PACKAGE_RESOLVED="Code.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved" + +staged_names="$(git diff --cached --name-only)" +if printf '%s\n' "$staged_names" | grep -qxF -e "$PACKAGE_SWIFT" -e "$PACKAGE_RESOLVED"; then + # `:path` is the index version, which is the staged blob when the file is in + # this commit and the committed one otherwise. + swift_src="$(git show ":$PACKAGE_SWIFT")" + resolved_src="$(git show ":$PACKAGE_RESOLVED")" + + # `case .ocp: return "ocp-client-protocol"` -> "ocp ocp-client-protocol" + identities="$(printf '%s\n' "$swift_src" | sed -nE 's/^[[:space:]]*case \.([A-Za-z0-9_]+):[[:space:]]*return "([A-Za-z][A-Za-z0-9-]*)"[[:space:]]*$/\1 \2/p')" + # `case .ocp: return "0.6.0"` -> "ocp 0.6.0" + pins="$(printf '%s\n' "$swift_src" | sed -nE 's/^[[:space:]]*case \.([A-Za-z0-9_]+):[[:space:]]*return "([0-9]+\.[0-9]+\.[0-9]+[^"]*)"[[:space:]]*$/\1 \2/p')" + + mismatch=0 + while read -r pkg pinned; do + [ -n "$pkg" ] || continue + identity="$(printf '%s\n' "$identities" | awk -v p="$pkg" '$1 == p { print $2; exit }')" + if [ -z "$identity" ]; then + echo "error: no Package.resolved identity found for '$pkg' in $PACKAGE_SWIFT (ContractPackage.directoryName)." >&2 + exit 1 + fi + # The `version` line that follows this identity's `"identity"` line. + resolved="$(printf '%s\n' "$resolved_src" | awk -v id="$identity" ' + $0 ~ "\"identity\" : \"" id "\"" { found = 1; next } + found && /"version" :/ { gsub(/.*: "|".*/, ""); print; exit } + found && /"identity" :/ { exit } + ')" + if [ "$resolved" != "$pinned" ]; then + mismatch=1 + cat >&2 <} in Package.resolved. + +Xcode Cloud resolves only from Package.resolved and will not update it. +Bump both together, once the ${pinned} tag is published: + ./Scripts/bump-contract.sh $pkg ${pinned} +or resolve by hand and stage the result: + xcodebuild -resolvePackageDependencies -project Code.xcodeproj -scheme Flipcash + git add $PACKAGE_RESOLVED + +MSG + fi + done <<< "$pins" + [ "$mismatch" -eq 0 ] || exit 1 +fi