aboutsummaryrefslogtreecommitdiff
path: root/ci
diff options
context:
space:
mode:
Diffstat (limited to 'ci')
-rwxr-xr-xci/build.sh4
-rwxr-xr-xci/release.sh88
-rwxr-xr-xci/setup-toolchain.sh62
-rwxr-xr-xci/test.sh21
-rwxr-xr-xci/verify-repro.sh24
5 files changed, 140 insertions, 59 deletions
diff --git a/ci/build.sh b/ci/build.sh
index 5afbfd4..c99ec5f 100755
--- a/ci/build.sh
+++ b/ci/build.sh
@@ -5,8 +5,8 @@ set -eu
root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
-make -C "$root" native
make -C "$root" app
+git -C "$root" rev-parse HEAD > "$root/app/build/outputs/apk/release/.source-commit"
echo "ci: APK at app/build/outputs/apk/release/"
-ls -l "$root"/app/build/outputs/apk/release/*.apk 2>/dev/null || true
+ls -l "$root"/app/build/outputs/apk/release/*.apk
diff --git a/ci/release.sh b/ci/release.sh
index 260efba..1c96d1b 100755
--- a/ci/release.sh
+++ b/ci/release.sh
@@ -14,38 +14,90 @@ set -eu
: "${CODEBERG_REPO:?set CODEBERG_REPO as owner/name}"
tag=${TAG:-${CI_COMMIT_TAG:?set TAG or CI_COMMIT_TAG}}
base=${CODEBERG_URL:-https://codeberg.org}
-apk=app/build/outputs/apk/release/app-release.apk
+base=${base%/}
+root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
+apk="$root/app/build/outputs/apk/release/app-release.apk"
+source_commit="$root/app/build/outputs/apk/release/.source-commit"
[ -f "$apk" ] || { echo "release: $apk not found; run ci/build.sh first" >&2; exit 1; }
+[ -f "$source_commit" ] || { echo "release: build provenance not found; run ci/build.sh first" >&2; exit 1; }
+command -v apksigner >/dev/null 2>&1 || { echo "release: apksigner is required" >&2; exit 1; }
+apksigner verify --verbose --print-certs "$apk"
+
+[ -z "$(git -C "$root" status --porcelain --untracked-files=normal)" ] || {
+ echo "release: working tree is not clean" >&2
+ exit 1
+}
+
+case $CODEBERG_REPO in
+ *[!A-Za-z0-9._/-]*|*/*/*|/*|*/)
+ echo "release: invalid CODEBERG_REPO" >&2
+ exit 1
+ ;;
+ */*) ;;
+ *)
+ echo "release: CODEBERG_REPO must be owner/name" >&2
+ exit 1
+ ;;
+esac
+
+head=$(git -C "$root" rev-parse HEAD)
+IFS= read -r built_commit < "$source_commit"
+[ "$built_commit" = "$head" ] || { echo "release: APK was not built from HEAD" >&2; exit 1; }
+git check-ref-format "refs/tags/$tag" >/dev/null 2>&1 || { echo "release: invalid tag $tag" >&2; exit 1; }
+tag_head=$(git -C "$root" rev-parse --verify "$tag^{commit}")
+[ "$head" = "$tag_head" ] || { echo "release: $tag does not point to HEAD" >&2; exit 1; }
+version=$(python3 -c 'import re,sys
+s=open(sys.argv[1]).read()
+m=re.search(r"^\s*versionName [\x27\x22]([^\x27\x22]+)", s, re.M)
+print(m.group(1) if m else "")' "$root/app/build.gradle")
+[ "$tag" = "$version" ] || { echo "release: tag $tag does not match app version $version" >&2; exit 1; }
+
+quote() {
+ python3 -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=""))' "$1"
+}
api="$base/api/v1/repos/$CODEBERG_REPO"
auth="Authorization: token $CODEBERG_TOKEN"
name="rsend-$tag.apk"
+sum_name="$name.sha256"
+tag_url=$(quote "$tag")
+name_url=$(quote "$name")
+sum_name_url=$(quote "$sum_name")
+sum=$(mktemp)
+trap 'rm -f "$sum"' 0
+sha256sum "$apk" | { read -r hash _; printf '%s %s\n' "$hash" "$name"; } > "$sum"
-# Reuse the tag's release if it exists, else create it. Re-runs are idempotent:
-# same release, and a stale asset from an earlier run is replaced below.
-id=$(curl -fsS -H "$auth" "$api/releases/tags/$tag" 2>/dev/null \
+# Reuse the tag's release if it exists, else create it. Published assets are
+# immutable: a rerun uploads only missing assets and never replaces one.
+id=$(curl -fsS -H "$auth" "$api/releases/tags/$tag_url" 2>/dev/null \
| python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])' 2>/dev/null) || id=
if [ -n "$id" ]; then
- curl -fsS -H "$auth" "$api/releases/$id/assets" \
- | python3 -c 'import json,sys
-for a in json.load(sys.stdin):
- if a["name"] == sys.argv[1]:
- print(a["id"])' "$name" \
- | while read -r aid; do
- curl -fsS -X DELETE -H "$auth" "$api/releases/$id/assets/$aid"
- done
+ assets=$(curl -fsS -H "$auth" "$api/releases/$id/assets")
else
+ payload=$(python3 -c 'import json,sys; print(json.dumps({"tag_name":sys.argv[1],"name":sys.argv[1]}))' "$tag")
id=$(curl -fsS -X POST "$api/releases" \
-H "$auth" -H "Content-Type: application/json" \
- -d "{\"tag_name\":\"$tag\",\"name\":\"$tag\"}" \
+ -d "$payload" \
| python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])')
fi
[ -n "$id" ] || { echo "release: could not create release for $tag" >&2; exit 1; }
-# Upload the APK as a release asset.
-curl -fsS -X POST "$api/releases/$id/assets?name=$name" \
- -H "$auth" \
- -F "attachment=@$apk" >/dev/null
+has_asset() {
+ printf '%s' "${assets:-[]}" | python3 -c 'import json,sys
+name=sys.argv[1]
+raise SystemExit(0 if any(a["name"] == name for a in json.load(sys.stdin)) else 1)' "$1"
+}
+
+if ! has_asset "$name"; then
+ curl -fsS -X POST "$api/releases/$id/assets?name=$name_url" \
+ -H "$auth" \
+ -F "attachment=@$apk" >/dev/null
+fi
+if ! has_asset "$sum_name"; then
+ curl -fsS -X POST "$api/releases/$id/assets?name=$sum_name_url" \
+ -H "$auth" \
+ -F "attachment=@$sum" >/dev/null
+fi
-echo "release: uploaded $name to $CODEBERG_REPO ($tag)"
+echo "release: $name and $sum_name are present on $CODEBERG_REPO ($tag)"
diff --git a/ci/setup-toolchain.sh b/ci/setup-toolchain.sh
index 7aeaa42..6ed09c6 100755
--- a/ci/setup-toolchain.sh
+++ b/ci/setup-toolchain.sh
@@ -11,35 +11,47 @@ root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
dir=${TOOLCHAIN_DIR:-$HOME/toolchains}
sdk="$dir/android-sdk"
mkdir -p "$dir/dl"
+jdk="$dir/jdk-$JDK_RELEASE"
+cmdline="$sdk/cmdline-tools/$ANDROID_CMDLINE_TOOLS"
+goroot="$dir/go-$GO_VERSION"
+
+download() {
+ url=$1
+ file=$2
+ want=$3
+ curl -fsSL "$url" -o "$file"
+ printf '%s %s\n' "$want" "$file" | sha256sum -c -
+}
# JDK (Temurin, exact pinned release; + must be %2B in the URL).
-if [ ! -x "$dir/jdk17/bin/java" ]; then
+if [ ! -x "$jdk/bin/java" ]; then
echo "setup: JDK $JDK_RELEASE"
rel=$(printf '%s' "$JDK_RELEASE" | sed 's/+/%2B/')
- curl -fsSL "https://api.adoptium.net/v3/binary/version/jdk-${rel}/linux/x64/jdk/hotspot/normal/eclipse" -o "$dir/dl/jdk.tar.gz"
- mkdir -p "$dir/jdk17"
- tar xzf "$dir/dl/jdk.tar.gz" -C "$dir/jdk17" --strip-components=1
+ download "https://api.adoptium.net/v3/binary/version/jdk-${rel}/linux/x64/jdk/hotspot/normal/eclipse" \
+ "$dir/dl/jdk-$JDK_RELEASE.tar.gz" "$JDK_SHA256"
+ mkdir -p "$jdk"
+ tar xzf "$dir/dl/jdk-$JDK_RELEASE.tar.gz" -C "$jdk" --strip-components=1
fi
-export JAVA_HOME="$dir/jdk17"
+export JAVA_HOME="$jdk"
export PATH="$JAVA_HOME/bin:$PATH"
# Android cmdline-tools.
-if [ ! -x "$sdk/cmdline-tools/latest/bin/sdkmanager" ]; then
+if [ ! -x "$cmdline/bin/sdkmanager" ]; then
echo "setup: cmdline-tools $ANDROID_CMDLINE_TOOLS"
- curl -fsSL "https://dl.google.com/android/repository/commandlinetools-linux-${ANDROID_CMDLINE_TOOLS}_latest.zip" -o "$dir/dl/clt.zip"
- rm -rf "$dir/dl/clt"
- python3 -m zipfile -e "$dir/dl/clt.zip" "$dir/dl/clt"
+ download "https://dl.google.com/android/repository/commandlinetools-linux-${ANDROID_CMDLINE_TOOLS}_latest.zip" \
+ "$dir/dl/clt-$ANDROID_CMDLINE_TOOLS.zip" "$ANDROID_CMDLINE_TOOLS_SHA256"
+ rm -rf "$dir/dl/clt-$ANDROID_CMDLINE_TOOLS"
+ python3 -m zipfile -e "$dir/dl/clt-$ANDROID_CMDLINE_TOOLS.zip" "$dir/dl/clt-$ANDROID_CMDLINE_TOOLS"
mkdir -p "$sdk/cmdline-tools"
- rm -rf "$sdk/cmdline-tools/latest"
- mv "$dir/dl/clt/cmdline-tools" "$sdk/cmdline-tools/latest"
- chmod +x "$sdk/cmdline-tools/latest/bin/"*
+ rm -rf "$cmdline"
+ mv "$dir/dl/clt-$ANDROID_CMDLINE_TOOLS/cmdline-tools" "$cmdline"
+ chmod +x "$cmdline/bin/"*
fi
# SDK packages.
echo "setup: sdk packages"
-yes | "$sdk/cmdline-tools/latest/bin/sdkmanager" --licenses >/dev/null 2>&1 || true
-"$sdk/cmdline-tools/latest/bin/sdkmanager" \
- "platform-tools" \
+yes | "$cmdline/bin/sdkmanager" --licenses >/dev/null
+"$cmdline/bin/sdkmanager" \
"platforms;android-${ANDROID_PLATFORM}" \
"build-tools;${ANDROID_BUILD_TOOLS}" \
"ndk;${ANDROID_NDK}" >/dev/null
@@ -47,28 +59,30 @@ yes | "$sdk/cmdline-tools/latest/bin/sdkmanager" --licenses >/dev/null 2>&1 || t
# Gradle.
if [ ! -x "$dir/gradle-${GRADLE_VERSION}/bin/gradle" ]; then
echo "setup: Gradle $GRADLE_VERSION"
- curl -fsSL "https://services.gradle.org/distributions/gradle-${GRADLE_VERSION}-bin.zip" -o "$dir/dl/gradle.zip"
- python3 -m zipfile -e "$dir/dl/gradle.zip" "$dir"
+ download "https://services.gradle.org/distributions/gradle-${GRADLE_VERSION}-bin.zip" \
+ "$dir/dl/gradle-$GRADLE_VERSION.zip" "$GRADLE_SHA256"
+ python3 -m zipfile -e "$dir/dl/gradle-$GRADLE_VERSION.zip" "$dir"
chmod +x "$dir/gradle-${GRADLE_VERSION}/bin/gradle"
fi
# Go (pinned; builds rsh).
-if [ ! -x "$dir/go/bin/go" ]; then
+if [ ! -x "$goroot/bin/go" ]; then
echo "setup: Go $GO_VERSION"
- curl -fsSL "https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz" -o "$dir/dl/go.tar.gz"
- rm -rf "$dir/go"
- tar xzf "$dir/dl/go.tar.gz" -C "$dir"
+ download "https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz" \
+ "$dir/dl/go-$GO_VERSION.tar.gz" "$GO_SHA256"
+ mkdir -p "$goroot"
+ tar xzf "$dir/dl/go-$GO_VERSION.tar.gz" -C "$goroot" --strip-components=1
fi
# Emit the env to source before building.
cat > "$dir/env.sh" <<EOF
-export JAVA_HOME="$dir/jdk17"
-export GOROOT="$dir/go"
+export JAVA_HOME="$jdk"
+export GOROOT="$goroot"
export ANDROID_HOME="$sdk"
export ANDROID_SDK_ROOT="$sdk"
export ANDROID_NDK_HOME="$sdk/ndk/${ANDROID_NDK}"
export GRADLE_HOME="$dir/gradle-${GRADLE_VERSION}"
-export PATH="\$JAVA_HOME/bin:$dir/go/bin:\$GRADLE_HOME/bin:$sdk/cmdline-tools/latest/bin:$sdk/platform-tools:\$PATH"
+export PATH="\$JAVA_HOME/bin:\$GOROOT/bin:\$GRADLE_HOME/bin:$cmdline/bin:$sdk/build-tools/${ANDROID_BUILD_TOOLS}:\$PATH"
EOF
echo "setup: done; source $dir/env.sh"
diff --git a/ci/test.sh b/ci/test.sh
index 5c141e5..2e6b14d 100755
--- a/ci/test.sh
+++ b/ci/test.sh
@@ -1,12 +1,14 @@
#!/bin/sh
-# Host-verifiable checks: pinned source checksum, Go formatting, vet, and tests
-# (including the real-rsync-through-rsh end-to-end). Needs Go and rsync; no
-# Android toolchain. This is what runs on every push.
+# Full checks: source pins, Go formatting/vet/tests, real-rsync integration,
+# and Android JVM tests. Requires the provisioned toolchain and host rsync.
set -eu
root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
. "$root/versions"
+command -v rsync >/dev/null 2>&1 || { echo "ci: rsync is required" >&2; exit 1; }
+command -v gradle >/dev/null 2>&1 || { echo "ci: gradle is required" >&2; exit 1; }
+
if [ -f "$root/rsync/rsync-${RSYNC_VERSION}.tar.gz" ]; then
echo "ci: verifying pinned rsync checksum"
( cd "$root/rsync" && sha256sum -c "rsync-${RSYNC_VERSION}.tar.gz.sha256" )
@@ -16,15 +18,21 @@ fi
# a shell file); fail loud if they drift.
echo "ci: version pins"
pin() {
- grep -q "$2" "$root/$1" || { echo "ci: $1 does not pin '$2' (see versions)" >&2; exit 1; }
+ grep -Fq "$2" "$root/$1" || { echo "ci: $1 does not pin '$2' (see versions)" >&2; exit 1; }
}
pin build.gradle "version '${AGP_VERSION}'"
pin build.gradle "version '${KOTLIN_VERSION}'"
pin gradle/wrapper/gradle-wrapper.properties "gradle-${GRADLE_VERSION}-bin.zip"
pin app/build.gradle "compileSdk ${ANDROID_PLATFORM}"
+pin app/build.gradle "buildToolsVersion '${ANDROID_BUILD_TOOLS}'"
pin app/build.gradle "minSdk ${ANDROID_MIN_SDK}"
pin app/build.gradle "targetSdk ${ANDROID_TARGET_SDK}"
-pin rsh/go.mod "^go ${GO_VERSION}"
+pin rsh/go.mod "go ${GO_VERSION}"
+pin app/build.gradle "androidx.core:core-ktx:${ANDROIDX_CORE_VERSION}"
+pin app/build.gradle "androidx.appcompat:appcompat:${ANDROIDX_APPCOMPAT_VERSION}"
+pin app/build.gradle "androidx.work:work-runtime-ktx:${ANDROIDX_WORK_VERSION}"
+pin app/build.gradle "junit:junit:${JUNIT_VERSION}"
+pin app/build.gradle "org.json:json:${JSON_VERSION}"
echo "ci: gofmt"
unformatted=$(gofmt -l "$root/rsh")
@@ -40,4 +48,7 @@ echo "ci: go vet"
echo "ci: go test"
( cd "$root/rsh" && go test ./... )
+echo "ci: Android unit tests"
+( cd "$root" && gradle :app:testReleaseUnitTest )
+
echo "ci: ok"
diff --git a/ci/verify-repro.sh b/ci/verify-repro.sh
index d412713..580ccd0 100755
--- a/ci/verify-repro.sh
+++ b/ci/verify-repro.sh
@@ -1,22 +1,26 @@
#!/bin/sh
-# Build the native libs twice and diff them, catching nondeterminism early.
-# Requires the Android NDK and Go.
+# Build the complete unsigned APK twice and compare it byte for byte.
set -eu
root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
a=$(mktemp -d)
b=$(mktemp -d)
-trap 'rm -rf "$a" "$b"' EXIT
+trap 'rm -rf "$a" "$b"' 0
-make -C "$root" native
-cp -r "$root/app/src/main/jniLibs" "$a/jniLibs"
+[ ! -f "$root/keystore.properties" ] || {
+ echo "verify-repro: keystore.properties must be absent" >&2
+ exit 1
+}
+
+make -C "$root" app
+cp "$root/app/build/outputs/apk/release/app-release-unsigned.apk" "$a/app.apk"
-rm -rf "$root/app/src/main/jniLibs" "$root/rsync/work" "$root/out"
-make -C "$root" native
-cp -r "$root/app/src/main/jniLibs" "$b/jniLibs"
+rm -rf "$root/app/build" "$root/app/src/main/jniLibs" "$root/rsync/work" "$root/out"
+make -C "$root" app
+cp "$root/app/build/outputs/apk/release/app-release-unsigned.apk" "$b/app.apk"
-if diff -r "$a/jniLibs" "$b/jniLibs"; then
- echo "verify-repro: native libs are bit-identical"
+if cmp "$a/app.apk" "$b/app.apk"; then
+ echo "verify-repro: APKs are bit-identical"
else
echo "verify-repro: NONDETERMINISM detected" >&2
exit 1