1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
|
#!/usr/bin/env bash
# publish-apk <apk> <published-name> [--host H] [--dir D] [--keep N]
#
# From another machine: PUBLISH_SSH=user@tungsten publish-apk <apk> <name>
#
# Publishes a built APK to the static site, verifying every step that has gone wrong before:
# the host, that the file is really an APK, that what landed is byte-identical to what was
# tested, and that the web server actually serves it.
#
# Exists because publishing is where a silent mistake is most expensive. A stale published APK
# once sat for eight weeks while the repo moved on; installing it over current data threw
# "A migration from 11 to 6 was required but not found" and the app refused to start. The cost
# wasn't the bug, it was that nothing in the publish step had ever checked.
set -uo pipefail
die() { echo "publish-apk: $1" >&2; exit 2; }
EXPECT_HOST="tungsten"
DEST_DIR="/site/static.terst.org/public/files"
# Each backup is a full copy of a large APK, in a public directory. Four publishes in one
# evening left 448MB of them. Keep enough to roll back, not enough to become the problem.
KEEP_BACKUPS=2
[ $# -ge 2 ] || die "usage: publish-apk <apk> <published-name> [--host H] [--dir D]"
APK="$1"; NAME="$2"; shift 2
while [ $# -gt 0 ]; do
case "$1" in
--host) EXPECT_HOST="$2"; shift 2 ;;
--dir) DEST_DIR="$2"; shift 2 ;;
--keep) KEEP_BACKUPS="$2"; shift 2 ;;
*) die "unknown option '$1'" ;;
esac
done
case "$NAME" in
*/*) die "published name must be a bare filename, got '$NAME'" ;;
*.apk) ;;
*) die "published name should end in .apk, got '$NAME'" ;;
esac
# Publishing from a box that merely resembles the deploy host is how a previous session
# "deployed" to a sandbox and had to unwind it.
ACTUAL_HOST=$(hostname)
REMOTE=0
if [ "$ACTUAL_HOST" != "$EXPECT_HOST" ]; then
# Publishing from elsewhere is allowed, but only by sending the file to the real host --
# never by writing to a local path that merely looks like it. A previous session
# "deployed" to a sandbox exactly that way and had to unwind it.
if [ -n "${PUBLISH_SSH:-}" ]; then
REMOTE=1
else
die "host is '$ACTUAL_HOST', expected '$EXPECT_HOST'.
To publish from here, send it over ssh instead:
PUBLISH_SSH=<user@$EXPECT_HOST> publish-apk <apk> <name>
That copies to the real host rather than writing to a local path of the same name."
fi
fi
[ -f "$APK" ] || die "no such file: $APK"
# An .apk is a zip; an HTML error page or a truncated copy is not.
#
# Deliberately NOT `... | grep -q`: under `set -o pipefail` grep exits on its first match and
# SIGPIPEs the producer, so the pipeline reports failure on a file that is perfectly valid.
# This script hit exactly that on a real 112MB APK -- the trap is in this repo's own README,
# and it still caught me. Capture first, test second.
MAGIC=$(head -c 2 "$APK")
[ "$MAGIC" = "PK" ] || die "$APK is not a zip archive — not a real APK"
LISTING=$(unzip -l "$APK" 2>/dev/null)
case "$LISTING" in
*AndroidManifest.xml*) ;;
*) die "$APK has no AndroidManifest.xml — not an APK" ;;
esac
if [ $REMOTE -eq 0 ]; then
[ -d "$DEST_DIR" ] || die "no such directory: $DEST_DIR"
fi
DEST="$DEST_DIR/$NAME"
echo "=== publish-apk ==="
echo "host : $ACTUAL_HOST$([ $REMOTE -eq 1 ] && echo " (publishing to $PUBLISH_SSH over ssh)")"
echo "source : $APK ($(stat -c%s "$APK") bytes)"
echo "dest : $DEST"
if [ $REMOTE -eq 1 ]; then
echo "source : $APK ($(stat -c%s "$APK") bytes)"
LOCAL_SHA=$(sha256sum "$APK" | awk '{print $1}')
# Copy to a temp name first, so a half-transferred file is never the published one.
scp -q "$APK" "$PUBLISH_SSH:/tmp/$NAME.incoming" || die "scp failed"
ssh "$PUBLISH_SSH" "bash -s" <<REMOTE_SCRIPT || die "remote publish failed"
set -euo pipefail
dest="$DEST_DIR/$NAME"
[ -d "$DEST_DIR" ] || { echo "no such directory: $DEST_DIR" >&2; exit 1; }
[ -f "\$dest" ] && cp "\$dest" "\$dest.\$(date +%Y%m%d-%H%M%S).bak"
ls -t "\$dest".*.bak 2>/dev/null | tail -n +$((KEEP_BACKUPS + 1)) | while read -r old; do rm -f "\$old"; done
mv "/tmp/$NAME.incoming" "\$dest"
chmod 644 "\$dest"
chown root:root "\$dest" 2>/dev/null || true
sha256sum "\$dest" | awk '{print \$1}'
REMOTE_SCRIPT
REMOTE_SHA=$(ssh "$PUBLISH_SSH" "sha256sum '$DEST_DIR/$NAME'" 2>/dev/null | awk '{print $1}')
if [ "$LOCAL_SHA" = "$REMOTE_SHA" ]; then
echo "identity: published copy is byte-identical to the source"
else
die "published copy differs from the source (local $LOCAL_SHA, remote $REMOTE_SHA)"
fi
URL="https://static.terst.org/files/$NAME"
REMOTE_LEN=$(curl -sI "$URL" 2>/dev/null | tr -d '\r' | awk 'tolower($1)=="content-length:"{print $2}')
LOCAL_LEN=$(stat -c%s "$APK")
echo "url : $URL"
if [ "$REMOTE_LEN" = "$LOCAL_LEN" ]; then
echo "result : PASS -- served bytes match the tested build"
exit 0
fi
echo "result : FAIL -- server reports ${REMOTE_LEN:-nothing}, built $LOCAL_LEN"
exit 1
fi
if [ -f "$DEST" ]; then
BACKUP="$DEST.$(date +%Y%m%d-%H%M%S).bak"
cp "$DEST" "$BACKUP" || die "could not back up the existing file"
echo "backup : $BACKUP"
else
echo "backup : none needed (new file)"
fi
# Prune before copying, so a failed publish doesn't leave the pruning undone too.
PRUNED=$(ls -t "$DEST".*.bak 2>/dev/null | tail -n +$((KEEP_BACKUPS + 1)))
if [ -n "$PRUNED" ]; then
echo "$PRUNED" | while read -r old; do rm -f "$old"; done
echo "pruned : $(echo "$PRUNED" | wc -l) old backup(s), keeping the newest $KEEP_BACKUPS"
fi
cp "$APK" "$DEST" || die "copy failed"
chmod 644 "$DEST" || die "chmod failed"
chown root:root "$DEST" 2>/dev/null || true
# The whole point: what is served must be what was tested.
if cmp -s "$APK" "$DEST"; then
echo "identity: published copy is byte-identical to the source"
else
die "published copy differs from the source — do not trust it"
fi
URL="https://static.terst.org/files/$NAME"
REMOTE_LEN=$(curl -sI "$URL" 2>/dev/null | tr -d '\r' | awk 'tolower($1)=="content-length:"{print $2}')
LOCAL_LEN=$(stat -c%s "$DEST")
if [ -z "$REMOTE_LEN" ]; then
echo "serving : COULD NOT VERIFY (no content-length from $URL)"
echo "result : PARTIAL — the file is in place but the web server did not confirm it"
exit 1
fi
echo "url : $URL"
echo "serving : $REMOTE_LEN bytes"
if [ "$REMOTE_LEN" = "$LOCAL_LEN" ]; then
echo "result : PASS — served bytes match the tested build"
else
echo "result : FAIL — server reports $REMOTE_LEN, on disk $LOCAL_LEN"
exit 1
fi
|