Skip to content

Commit eef347c

Browse files
authored
Measure package size on PRs (#309)
Assisted-By: devx/2d407d2d-edb2-4884-b788-a5b3151d00f1
1 parent c54b791 commit eef347c

2 files changed

Lines changed: 392 additions & 0 deletions

File tree

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
#!/usr/bin/env bash
2+
3+
set -euo pipefail
4+
5+
script_repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)
6+
repo_root=${PACKAGE_SIZE_REPO_ROOT:-"$script_repo_root"}
7+
8+
measure_web=${MEASURE_WEB:-true}
9+
measure_react_native=${MEASURE_REACT_NATIVE:-true}
10+
measure_android=${MEASURE_ANDROID:-true}
11+
12+
size_bytes() {
13+
local file=$1
14+
15+
stat -c "%s" "$file" 2>/dev/null || stat -f "%z" "$file"
16+
}
17+
18+
human_bytes() {
19+
local bytes=$1
20+
21+
awk -v bytes="$bytes" 'BEGIN {
22+
split("B KiB MiB GiB", units, " ");
23+
value = bytes;
24+
unit = 1;
25+
while (value >= 1024 && unit < 4) {
26+
value = value / 1024;
27+
unit++;
28+
}
29+
if (unit == 1) {
30+
printf "%d %s", value, units[unit];
31+
} else {
32+
printf "%.1f %s", value, units[unit];
33+
}
34+
}'
35+
}
36+
37+
record_artifact() {
38+
local platform=$1
39+
local artifact=$2
40+
local file=$3
41+
local bytes
42+
43+
bytes=$(size_bytes "$file")
44+
printf "%s\t%s\t%s\n" "$platform" "$artifact" "$bytes" >> "$output_file"
45+
}
46+
47+
measure_web_package() {
48+
local pack_dir
49+
local tarball
50+
51+
pack_dir=$(mktemp -d "${TMPDIR:-/tmp}/checkout-kit-web-pack.XXXXXX")
52+
(
53+
cd "$repo_root/platforms/web"
54+
pnpm build
55+
pnpm pack --pack-destination "$pack_dir"
56+
)
57+
58+
tarball=$(find "$pack_dir" -name "*.tgz" -type f -print -quit)
59+
record_artifact "Web" "npm tarball" "$tarball"
60+
}
61+
62+
measure_react_native_package() {
63+
local pack_dir
64+
local tarball
65+
66+
pack_dir=$(mktemp -d "${TMPDIR:-/tmp}/checkout-kit-react-native-pack.XXXXXX")
67+
(
68+
cd "$repo_root/platforms/react-native"
69+
pnpm module clean
70+
pnpm module build
71+
cd modules/@shopify/checkout-kit-react-native
72+
pnpm pack --pack-destination "$pack_dir"
73+
)
74+
75+
tarball=$(find "$pack_dir" -name "*.tgz" -type f -print -quit)
76+
record_artifact "React Native" "npm tarball" "$tarball"
77+
}
78+
79+
measure_android_package() {
80+
local aar="$repo_root/platforms/android/lib/build/outputs/aar/lib-release.aar"
81+
82+
(
83+
cd "$repo_root/platforms/android"
84+
./gradlew :lib:assembleRelease --console=plain
85+
)
86+
87+
record_artifact "Android" "release AAR" "$aar"
88+
}
89+
90+
collect_measurements() {
91+
output_file=${1:?usage: measure-package-size collect <output-file>}
92+
: > "$output_file"
93+
94+
if [[ "$measure_web" == "true" ]]; then
95+
measure_web_package
96+
fi
97+
98+
if [[ "$measure_react_native" == "true" ]]; then
99+
measure_react_native_package
100+
fi
101+
102+
if [[ "$measure_android" == "true" ]]; then
103+
measure_android_package
104+
fi
105+
}
106+
107+
render_comment() {
108+
local base_file=${1:?usage: measure-package-size render <base-file> <head-file> <output-file>}
109+
local head_file=${2:?usage: measure-package-size render <base-file> <head-file> <output-file>}
110+
local comment_file=${3:?usage: measure-package-size render <base-file> <head-file> <output-file>}
111+
112+
awk -F '\t' -v base_path="$base_file" '
113+
function human(bytes, units, value, unit) {
114+
if (bytes == "") {
115+
return "unavailable";
116+
}
117+
118+
split("B KiB MiB GiB", units, " ");
119+
value = bytes + 0;
120+
unit = 1;
121+
while (value >= 1024 && unit < 4) {
122+
value = value / 1024;
123+
unit++;
124+
}
125+
126+
if (unit == 1) {
127+
return sprintf("%d %s", value, units[unit]);
128+
}
129+
130+
return sprintf("%.1f %s", value, units[unit]);
131+
}
132+
133+
function delta(head, base, diff) {
134+
if (head == "" || base == "") {
135+
return "unavailable";
136+
}
137+
138+
diff = (head + 0) - (base + 0);
139+
if (diff > 0) {
140+
return "+" human(diff);
141+
}
142+
if (diff < 0) {
143+
return "-" human(-diff);
144+
}
145+
146+
return "0 B";
147+
}
148+
149+
function remember(platform, artifact, key) {
150+
key = platform SUBSEP artifact;
151+
if (!(key in seen)) {
152+
seen[key] = 1;
153+
order[++order_count] = key;
154+
platforms[key] = platform;
155+
artifacts[key] = artifact;
156+
}
157+
return key;
158+
}
159+
160+
FILENAME == base_path {
161+
if (NF >= 3) {
162+
key = remember($1, $2);
163+
base_bytes[key] = $3;
164+
}
165+
next;
166+
}
167+
168+
NF >= 3 {
169+
key = remember($1, $2);
170+
head_bytes[key] = $3;
171+
next;
172+
}
173+
174+
END {
175+
print "<!-- checkout-kit-package-size -->";
176+
print "## Package Size";
177+
print "";
178+
print "| Platform | Artifact | Base | Head | Delta |";
179+
print "| --- | --- | ---: | ---: | ---: |";
180+
181+
if (order_count == 0) {
182+
print "| - | - | - | - | - |";
183+
} else {
184+
for (i = 1; i <= order_count; i++) {
185+
key = order[i];
186+
printf "| %s | %s | %s | %s | %s |\n",
187+
platforms[key],
188+
artifacts[key],
189+
human(base_bytes[key]),
190+
human(head_bytes[key]),
191+
delta(head_bytes[key], base_bytes[key]);
192+
}
193+
}
194+
195+
print "";
196+
print "_Measured from the PR base SHA and PR head SHA. This comment reports package artifact sizes only; it is not a final app binary-size report._";
197+
}
198+
' "$base_file" "$head_file" > "$comment_file"
199+
}
200+
201+
command=${1:-comment}
202+
203+
case "$command" in
204+
collect)
205+
collect_measurements "$2"
206+
;;
207+
render)
208+
render_comment "$2" "$3" "$4"
209+
;;
210+
comment)
211+
output_file=${2:-"$repo_root/package-size-comment.md"}
212+
rows_file=$(mktemp "${TMPDIR:-/tmp}/checkout-kit-package-size-rows.XXXXXX")
213+
trap 'rm -f "$rows_file"' EXIT
214+
collect_measurements "$rows_file"
215+
render_comment /dev/null "$rows_file" "$output_file"
216+
;;
217+
*)
218+
echo "Usage: measure-package-size {collect <output-file>|render <base-file> <head-file> <comment-file>|comment [comment-file]}" >&2
219+
exit 1
220+
;;
221+
esac

.github/workflows/package-size.yml

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
name: Package Size
2+
3+
on:
4+
pull_request:
5+
types: [opened, synchronize, reopened, ready_for_review]
6+
7+
permissions:
8+
contents: read
9+
pull-requests: write
10+
11+
concurrency:
12+
group: package-size-${{ github.ref }}
13+
cancel-in-progress: true
14+
15+
jobs:
16+
changes:
17+
name: Detect Changed Packages
18+
if: github.event_name == 'pull_request' && github.event.pull_request.draft == false
19+
runs-on: ubuntu-latest
20+
outputs:
21+
android: ${{ steps.filter.outputs.android }}
22+
reactNative: ${{ steps.filter.outputs.reactNative }}
23+
web: ${{ steps.filter.outputs.web }}
24+
steps:
25+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
26+
27+
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
28+
id: filter
29+
with:
30+
filters: |
31+
android:
32+
- 'platforms/android/**'
33+
- 'protocol/**'
34+
- '.github/scripts/measure-package-size'
35+
- '.github/workflows/package-size.yml'
36+
reactNative:
37+
- 'platforms/react-native/**'
38+
- 'protocol/languages/typescript/**'
39+
- '.github/actions/setup/**'
40+
- '.github/scripts/measure-package-size'
41+
- '.github/workflows/package-size.yml'
42+
web:
43+
- 'platforms/web/**'
44+
- '.github/actions/setup/**'
45+
- '.github/scripts/measure-package-size'
46+
- '.github/workflows/package-size.yml'
47+
48+
measure:
49+
name: Measure Package Size
50+
needs: changes
51+
if: |
52+
needs.changes.outputs.android == 'true' ||
53+
needs.changes.outputs.reactNative == 'true' ||
54+
needs.changes.outputs.web == 'true'
55+
runs-on: ubuntu-latest
56+
timeout-minutes: 30
57+
steps:
58+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
59+
with:
60+
submodules: true
61+
62+
- name: Checkout PR base
63+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
64+
with:
65+
ref: ${{ github.event.pull_request.base.sha }}
66+
path: .package-size-base
67+
submodules: true
68+
69+
- name: Setup base Web dependencies
70+
if: needs.changes.outputs.web == 'true'
71+
uses: ./.github/actions/setup
72+
with:
73+
node-version-file: .package-size-base/platforms/web/package.json
74+
cache-dependency-path: .package-size-base/platforms/web/pnpm-lock.yaml
75+
package-json-file: .package-size-base/platforms/web/package.json
76+
working-directory: .package-size-base/platforms/web
77+
78+
- name: Setup head Web dependencies
79+
if: needs.changes.outputs.web == 'true'
80+
uses: ./.github/actions/setup
81+
with:
82+
node-version-file: platforms/web/package.json
83+
cache-dependency-path: platforms/web/pnpm-lock.yaml
84+
package-json-file: platforms/web/package.json
85+
working-directory: platforms/web
86+
87+
- name: Setup base React Native dependencies
88+
if: needs.changes.outputs.reactNative == 'true'
89+
uses: ./.github/actions/setup
90+
with:
91+
node-version-file: .package-size-base/platforms/react-native/package.json
92+
cache-dependency-path: .package-size-base/platforms/react-native/pnpm-lock.yaml
93+
package-json-file: .package-size-base/platforms/react-native/package.json
94+
working-directory: .package-size-base/platforms/react-native
95+
96+
- name: Setup head React Native dependencies
97+
if: needs.changes.outputs.reactNative == 'true'
98+
uses: ./.github/actions/setup
99+
with:
100+
node-version-file: platforms/react-native/package.json
101+
cache-dependency-path: platforms/react-native/pnpm-lock.yaml
102+
package-json-file: platforms/react-native/package.json
103+
working-directory: platforms/react-native
104+
105+
- name: Install JDK
106+
if: needs.changes.outputs.android == 'true'
107+
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
108+
with:
109+
distribution: zulu
110+
java-version: 17
111+
cache: gradle
112+
113+
- name: Measure base package sizes
114+
continue-on-error: true
115+
env:
116+
MEASURE_ANDROID: ${{ needs.changes.outputs.android }}
117+
MEASURE_REACT_NATIVE: ${{ needs.changes.outputs.reactNative }}
118+
MEASURE_WEB: ${{ needs.changes.outputs.web }}
119+
PACKAGE_SIZE_REPO_ROOT: ${{ github.workspace }}/.package-size-base
120+
run: .github/scripts/measure-package-size collect /tmp/package-size-base.tsv
121+
122+
- name: Measure head package sizes
123+
env:
124+
MEASURE_ANDROID: ${{ needs.changes.outputs.android }}
125+
MEASURE_REACT_NATIVE: ${{ needs.changes.outputs.reactNative }}
126+
MEASURE_WEB: ${{ needs.changes.outputs.web }}
127+
run: .github/scripts/measure-package-size collect /tmp/package-size-head.tsv
128+
129+
- name: Render package size comment
130+
run: |
131+
touch /tmp/package-size-base.tsv
132+
.github/scripts/measure-package-size render /tmp/package-size-base.tsv /tmp/package-size-head.tsv /tmp/package-size-comment.md
133+
134+
- name: Comment on PR
135+
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
136+
env:
137+
COMMENT_FILE: /tmp/package-size-comment.md
138+
PR_NUMBER: ${{ github.event.pull_request.number }}
139+
with:
140+
script: |
141+
const fs = require('fs');
142+
143+
const marker = '<!-- checkout-kit-package-size -->';
144+
const body = fs.readFileSync(process.env.COMMENT_FILE, 'utf8');
145+
const issue_number = Number(process.env.PR_NUMBER);
146+
const {owner, repo} = context.repo;
147+
148+
const comments = await github.paginate(github.rest.issues.listComments, {
149+
owner,
150+
repo,
151+
issue_number,
152+
per_page: 100,
153+
});
154+
155+
const existing = comments.find((comment) => comment.body?.includes(marker));
156+
157+
if (existing) {
158+
await github.rest.issues.updateComment({
159+
owner,
160+
repo,
161+
comment_id: existing.id,
162+
body,
163+
});
164+
} else {
165+
await github.rest.issues.createComment({
166+
owner,
167+
repo,
168+
issue_number,
169+
body,
170+
});
171+
}

0 commit comments

Comments
 (0)