From 8c6390571d28ff21a98ed3802458aceddbb6d2a0 Mon Sep 17 00:00:00 2001 From: Lena Date: Sun, 16 Aug 2026 00:00:00 +0000 Subject: build: pin and verify release inputs Support Alpine/musl and Debian/glibc hosts, lock downloaded and Gradle artifacts, and verify native and APK security properties. --- ci/test.sh | 152 +++++++++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 122 insertions(+), 30 deletions(-) (limited to 'ci/test.sh') diff --git a/ci/test.sh b/ci/test.sh index 2e6b14d..e3e728c 100755 --- a/ci/test.sh +++ b/ci/test.sh @@ -1,54 +1,146 @@ #!/bin/sh -# Full checks: source pins, Go formatting/vet/tests, real-rsync integration, -# and Android JVM tests. Requires the provisioned toolchain and host rsync. +# Full checks: exact source pins, shell syntax, Go analysis, integration tests, +# vulnerability scanning, Android JVM tests, and Android lint. set -eu -root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +root=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) +# shellcheck source=versions . "$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; } +for command in cmp git govulncheck gradle go gofmt rsync sha256sum shellcheck; do + command -v "$command" >/dev/null 2>&1 || { + echo "ci: $command is required" >&2 + exit 1 + } +done 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" ) fi -# The gradle files and go.mod repeat pins from ./versions (gradle cannot source -# a shell file); fail loud if they drift. +# These files repeat values from ./versions because Gradle and Go cannot source +# a POSIX shell file. Exact source lines keep comments from satisfying a pin. +line() { + grep -Fqx -e "$2" "$root/$1" || { + echo "ci: $1 does not contain exact pin: $2" >&2 + exit 1 + } +} + echo "ci: version pins" -pin() { - grep -Fq "$2" "$root/$1" || { echo "ci: $1 does not pin '$2' (see versions)" >&2; exit 1; } +line build.gradle " id 'com.android.application' version '${AGP_VERSION}' apply false" +line build.gradle " id 'org.jetbrains.kotlin.android' version '${KOTLIN_VERSION}' apply false" +line gradle/wrapper/gradle-wrapper.properties "distributionUrl=https\\://services.gradle.org/distributions/gradle-${GRADLE_VERSION}-bin.zip" +line gradle/wrapper/gradle-wrapper.properties "distributionSha256Sum=${GRADLE_SHA256}" +line app/build.gradle " compileSdk ${ANDROID_PLATFORM}" +line app/build.gradle " buildToolsVersion '${ANDROID_BUILD_TOOLS}'" +line app/build.gradle " minSdk ${ANDROID_MIN_SDK}" +line app/build.gradle " targetSdk ${ANDROID_TARGET_SDK}" +line app/build.gradle " versionCode ${APP_VERSION_CODE}" +line app/build.gradle " versionName '${APP_VERSION}'" +line app/build.gradle " implementation 'androidx.core:core-ktx:${ANDROIDX_CORE_VERSION}'" +line app/build.gradle " implementation 'androidx.appcompat:appcompat:${ANDROIDX_APPCOMPAT_VERSION}'" +line app/build.gradle " implementation 'androidx.work:work-runtime:${ANDROIDX_WORK_VERSION}'" +line app/build.gradle " testImplementation 'junit:junit:${JUNIT_VERSION}'" +line app/build.gradle " testImplementation 'org.json:json:${JSON_VERSION}'" +line rsh/go.mod "go ${GO_VERSION}" +line rsh/go.mod "require golang.org/x/crypto v${X_CRYPTO_VERSION}" +line rsh/go.mod "require golang.org/x/sys v${X_SYS_VERSION} // indirect" +line THIRD_PARTY "- rsync ${RSYNC_VERSION}, GPLv3. Source: https://download.samba.org/pub/rsync/src/." +line THIRD_PARTY "- The Go standard library from Go ${GO_VERSION}, BSD 3-clause." +line THIRD_PARTY "- golang.org/x/crypto ${X_CRYPTO_VERSION}, BSD 3-clause." +line THIRD_PARTY "APK. golang.org/x/sys ${X_SYS_VERSION} is an indirect module-graph dependency but no" +line THIRD_PARTY "- AndroidX Core ${ANDROIDX_CORE_VERSION}, AppCompat ${ANDROIDX_APPCOMPAT_VERSION}, and WorkManager ${ANDROIDX_WORK_VERSION}," +cmp "$root/APACHE-2.0" "$root/app/src/main/assets/APACHE-2.0" || { + echo "ci: packaged APACHE-2.0 differs from repository APACHE-2.0" >&2 + exit 1 +} +cmp "$root/LICENSE" "$root/app/src/main/assets/LICENSE" || { + echo "ci: packaged LICENSE differs from repository LICENSE" >&2 + exit 1 +} +cmp "$root/THIRD_PARTY" "$root/app/src/main/assets/THIRD_PARTY" || { + echo "ci: packaged THIRD_PARTY differs from repository THIRD_PARTY" >&2 + exit 1 +} +[ -s "$root/gradle/verification-metadata.xml" ] || { + echo "ci: Gradle dependency verification metadata is missing" >&2 + exit 1 +} +tracked_files=$(git -C "$root" ls-files) +tracked_secrets= +if tracked_secrets=$(printf '%s\n' "$tracked_files" | + grep -E '(^|/)(keystore\.properties|[^/]*\.(jks|keystore))$'); then + : +else + status=$? + [ "$status" -eq 1 ] || exit "$status" +fi +[ -z "$tracked_secrets" ] || { + echo "ci: signing secret is tracked:" >&2 + printf '%s\n' "$tracked_secrets" >&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 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}" +abi_filters= +for abi in $ABIS; do + if [ -z "$abi_filters" ]; then + abi_filters="'$abi'" + else + abi_filters="$abi_filters, '$abi'" + fi +done +line app/build.gradle " ndk { abiFilters $abi_filters }" -echo "ci: gofmt" -unformatted=$(gofmt -l "$root/rsh") -if [ -n "$unformatted" ]; then - echo "ci: gofmt needed for:" >&2 - echo "$unformatted" >&2 +checksum="$root/rsync/rsync-${RSYNC_VERSION}.tar.gz.sha256" +[ -f "$checksum" ] || { echo "ci: missing $checksum" >&2; exit 1; } +[ "$(awk 'NF == 2 { print $2 }' "$checksum")" = "rsync-${RSYNC_VERSION}.tar.gz" ] || { + echo "ci: checksum filename does not match rsync pin" >&2 + exit 1 +} + +changelog="fastlane/metadata/android/en-US/changelogs/${APP_VERSION_CODE}.txt" +[ -s "$root/$changelog" ] || { echo "ci: missing $changelog for version ${APP_VERSION}" >&2; exit 1; } +chars=$(wc -c < "$root/$changelog") +[ "$chars" -le 500 ] || { echo "ci: $changelog is $chars bytes, over F-Droid's 500 limit" >&2; exit 1; } + +echo "ci: shell syntax" +shell_scripts=$(git -C "$root" ls-files --cached --others --exclude-standard '*.sh') +printf '%s\n' "$shell_scripts" | +while IFS= read -r script; do + sh -n "$root/$script" +done +( cd "$root" && shellcheck -x ci/*.sh rsync/build.sh rsh/build.sh ) +em_dash=$(printf '\342\200\224') +if git -C "$root" grep --untracked -n -I "$em_dash"; then + echo "ci: em dash is not allowed" >&2 exit 1 +else + status=$? + [ "$status" -eq 1 ] || exit "$status" fi +echo "ci: gofmt" +unformatted=$(gofmt -l "$root/rsh") +[ -z "$unformatted" ] || { echo "ci: gofmt needed for:$unformatted" >&2; exit 1; } + +echo "ci: go module checksums" +( cd "$root/rsh" && go mod verify ) + echo "ci: go vet" ( cd "$root/rsh" && go vet ./... ) -echo "ci: go test" -( cd "$root/rsh" && go test ./... ) +echo "ci: go test (race detector)" +( cd "$root/rsh" && go test -race ./... ) + +# govulncheck queries vuln.go.dev and sends module paths, not source code. +echo "ci: govulncheck" +( cd "$root/rsh" && govulncheck ./... ) echo "ci: Android unit tests" -( cd "$root" && gradle :app:testReleaseUnitTest ) +( cd "$root" && gradle --no-daemon :app:testReleaseUnitTest ) + +echo "ci: Android lint" +( cd "$root" && gradle --no-daemon :app:lintRelease ) echo "ci: ok" -- cgit v1.2.3