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
9 changes: 7 additions & 2 deletions .claude/docs/technology-stack.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <ocp|flipcash2> <version>`. 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

Expand Down
118 changes: 118 additions & 0 deletions Scripts/bump-contract.sh
Original file line number Diff line number Diff line change
@@ -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 <ocp|flipcash2> <version>
#
# 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 <ocp|flipcash2> <version>" >&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 .<pkg>: return "X.Y.Z"` line in `version` changes; directoryName's
# `return "<identity>"` 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:-<missing>} for $IDENTITY, expected $VERSION." >&2
restore
exit 1
fi

git add "$PACKAGE_SWIFT" "$PACKAGE_RESOLVED"
echo "✓ $IDENTITY pinned to $VERSION in both files, staged"
53 changes: 53 additions & 0 deletions Scripts/git-hooks/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -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 <<MSG
error: $identity is pinned to ${pinned} in $PACKAGE_SWIFT but ${resolved:-<missing>} 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
Loading