Skip to content

Commit 26dfa88

Browse files
tmmampagent
andauthored
chore(zero-swift): fail CI on undeclared public API breaks (#37)
swift/scripts/api-breaks.sh dumps the public API of the last release (the mirror's newest tag) and of this checkout, with the digester, in a scratch repository shaped like the mirror, removes package declarations from both dumps, and compares them. It fails on a break unless a pending changeset or the version being released bumps zero-swift by a minor (below 1.0) or a major. Linux and macOS CI run it; macOS covers the Apple-only API. Amp-Thread-ID: https://ampcode.com/threads/T-c513a037-a257-45e0-b9a9-15c15edfe859 Co-authored-by: Amp <amp@ampcode.com>
1 parent 8348b50 commit 26dfa88

4 files changed

Lines changed: 123 additions & 1 deletion

File tree

‎.changeset/README.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,9 @@ request open with the pending version and changelog. Merging it sets the
1919
version in `packages/zero-swift-cli/package.json` and `swift/VERSION`, and the
2020
workflow then publishes the npm package and tags the SwiftPM mirror after
2121
approval in the `swift-release` environment.
22+
23+
A change that breaks the Swift package's public API, so that an app building
24+
against the last release no longer compiles, needs a `minor` changeset while
25+
zero-swift is below 1.0, and a `major` one from 1.0. CI runs
26+
`swift/scripts/api-breaks.sh`, which lists the breaks since the last release
27+
and fails unless a changeset declares them.

‎.github/workflows/swift-macos.yml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ jobs:
4444
run: swift test
4545
- name: Docs
4646
run: scripts/docs.sh
47+
# The Apple-only API, which Linux doesn't build.
48+
- name: API breaks
49+
run: scripts/api-breaks.sh
4750
- name: Test (iOS Simulator)
4851
run: |
4952
device="$(xcrun simctl list devices available -j |

‎.github/workflows/swift.yml‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ jobs:
7373
name: generated-vectors
7474
path: packages/zero-conformance/generated/
7575
- name: Install system libraries
76-
run: apt-get update && apt-get install -y --no-install-recommends libsqlite3-dev
76+
run: apt-get update && apt-get install -y --no-install-recommends libsqlite3-dev jq
7777
- name: Format
7878
run: swift format lint --strict --recursive Sources Tests Package.swift
7979
- name: Build
@@ -84,6 +84,12 @@ jobs:
8484
# Apple-only; swift-macos.yml checks it.
8585
- name: Docs
8686
run: scripts/docs.sh Zero ZeroTesting
87+
# Fails on a public API break since the last release that no changeset
88+
# declares. The checkout belongs to another user in the container.
89+
- name: API breaks
90+
run: |
91+
git config --global --add safe.directory "$GITHUB_WORKSPACE"
92+
scripts/api-breaks.sh
8793
# zbugs' Swift port, checked against zbugs' TS queries and mutators on
8894
# the samples in drift-samples.ts (the vectors job checks the record is
8995
# current).

‎swift/scripts/api-breaks.sh‎

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
#!/bin/sh
2+
# Fails when the public API breaks compared with the last release, unless the
3+
# break is declared: a pending changeset, or the version being released, bumps
4+
# zero-swift enough for it (a minor below 1.0, a major from 1.0).
5+
#
6+
# scripts/api-breaks.sh [<baseline tag>] default: the mirror's newest tag
7+
#
8+
# The digester (`swift package diagnose-api-breaking-changes`) needs the
9+
# package at the root of a git repository, so this runs it in a scratch
10+
# repository shaped like the SwiftPM mirror (wevm/zero-swift): the released tag,
11+
# with this checkout's swift/ committed on top. Its API dumps also hold
12+
# `package` declarations, which apps can't use, so they're removed from both
13+
# sides before comparing. On Linux the Apple-only API (ZeroUI, most of
14+
# ZeroVirtual) isn't built, so swift-macos.yml runs this too.
15+
set -eu
16+
cd "$(dirname "$0")/.."
17+
swift=$(pwd)
18+
root=$(cd .. && pwd)
19+
mirror=https://github.com/wevm/zero-swift.git
20+
products="Zero ZeroUI ZeroVirtual ZeroTesting"
21+
22+
baseline=${1:-$(git ls-remote --tags --refs "$mirror" | sed 's|.*refs/tags/||' |
23+
sort -V | tail -1)}
24+
25+
work=$(mktemp -d)
26+
dumps=$(mktemp -d)
27+
trap 'rm -rf "$work" "$dumps"' EXIT
28+
29+
git -C "$work" init -q
30+
git -C "$work" fetch -q --depth 1 "$mirror" "refs/tags/$baseline:refs/tags/$baseline"
31+
git -C "$work" checkout -q "$baseline"
32+
git -C "$work" rm -rq .
33+
# This checkout's swift/, with uncommitted changes, as the release would have it.
34+
git ls-files -z --cached --others --exclude-standard |
35+
tar --null -cf - -T - | tar -xf - -C "$work"
36+
git -C "$work" add -A
37+
git -C "$work" -c user.name=api-breaks -c user.email=api-breaks@localhost \
38+
commit -qm current --allow-empty
39+
40+
digester=$(command -v swift-api-digester || xcrun --find swift-api-digester 2>/dev/null ||
41+
echo "$(dirname "$(readlink -f "$(command -v swift)")")/swift-api-digester")
42+
43+
cd "$work"
44+
# Dumps each side's API, keeps only its public declarations, and compares
45+
# them. Stripping `package` declarations from both sides matters: one that
46+
# was public and became `package` reads as removed.
47+
for side in old:$baseline new:HEAD; do
48+
name=${side%%:*} rev=${side#*:}
49+
swift package diagnose-api-breaking-changes "$rev" --products $products \
50+
--baseline-dir "$dumps/$name" >"$dumps/$name.log" 2>&1 || true
51+
if ! ls "$dumps/$name"/*/*.json >/dev/null 2>&1; then
52+
echo "Couldn't dump the API at $rev:" >&2
53+
cat "$dumps/$name.log" >&2
54+
exit 1
55+
fi
56+
for dump in "$dumps/$name"/*/*.json; do
57+
jq 'walk(if type == "object" and has("children")
58+
then .children |= map(select(.isInternal != true)) else . end)' \
59+
"$dump" >"$dumps/$name-$(basename "$dump")"
60+
done
61+
done
62+
breaks=
63+
for old in "$dumps"/old-*.json; do
64+
module=$(basename "$old" .json) module=${module#old-}
65+
new="$dumps/new-$module.json"
66+
if [ ! -e "$new" ]; then
67+
breaks="$breaks$module: module has been removed
68+
"
69+
continue
70+
fi
71+
"$digester" -diagnose-sdk -print-module -input-paths "$old" -input-paths "$new" \
72+
-o "$dumps/$module.txt"
73+
breaks="$breaks$(grep -v -e '^/\*' -e '^$' "$dumps/$module.txt" || true)
74+
"
75+
done
76+
breaks=$(printf '%s' "$breaks" | sed '/^$/d')
77+
if [ -z "$breaks" ]; then
78+
echo "No public API breaks since $baseline."
79+
exit 0
80+
fi
81+
82+
echo "Public API breaks since $baseline:"
83+
printf '%s\n' "$breaks" | sed 's/^/ /'
84+
case $baseline in
85+
0.*) needed=minor levels='minor|major' ;;
86+
*) needed=major levels=major ;;
87+
esac
88+
# A pending changeset that declares the break.
89+
if grep -lqE "^['\"]zero-swift['\"]: *($levels)\$" "$root"/.changeset/*.md 2>/dev/null; then
90+
echo "Allowed: a pending changeset bumps zero-swift by a $needed or more."
91+
exit 0
92+
fi
93+
# The version pull request, or main after it merged: the changesets are gone,
94+
# and the version being released declares the break.
95+
version=$(cat "$swift/VERSION")
96+
old_major=${baseline%%.*} old_minor=${baseline#*.} old_minor=${old_minor%%.*}
97+
new_major=${version%%.*} new_minor=${version#*.} new_minor=${new_minor%%.*}
98+
if [ "$new_major" -gt "$old_major" ] ||
99+
{ [ "$needed" = minor ] && [ "$new_major" -eq "$old_major" ] &&
100+
[ "$new_minor" -gt "$old_minor" ]; }; then
101+
echo "Allowed: $version is a $needed or more after $baseline."
102+
exit 0
103+
fi
104+
echo "These break apps that build against zero-swift $baseline. Keep the old" \
105+
"API (deprecate it), or add a changeset that bumps zero-swift by a $needed" \
106+
"and tells apps what to change." >&2
107+
exit 1

0 commit comments

Comments
 (0)