Skip to content

Move DKG roster fingerprint, validation and assembly into Rust FFI #595

Move DKG roster fingerprint, validation and assembly into Rust FFI

Move DKG roster fingerprint, validation and assembly into Rust FFI #595

Workflow file for this run

name: Reproducibility
on:
workflow_dispatch:
schedule:
- cron: '0 6 * * 1'
push:
tags:
- "v*"
pull_request:
types: [opened, synchronize, reopened, labeled]
permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
# These pins are not consumed by the steps below (the build runs inside
# Dockerfile.reproducible, which owns its own pinned toolchain), but
# scripts/check-toolchain-pins.sh cross-validates them against the Dockerfile
# and build.gradle.kts, so they must stay in sync.
env:
ANDROID_HOME: /usr/local/lib/android/sdk
NDK_VERSION: "29.0.14206865"
CARGO_NDK_VERSION: "4.1.2"
RUST_VERSION: "1.89.0"
BUILD_TOOLS_VERSION: "37.0.0"
jobs:
verify:
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'reproducibility')
runs-on: ubuntu-24.04
timeout-minutes: 90
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# Resolve the release tag whose published APK we compare against, then
# build from that exact tag (below) so the build ref and keep.version
# match the published artifact. On schedule/dispatch the runner checked
# out HEAD, which may have advanced past the latest release tag.
- name: Resolve release version to verify
id: ver
if: github.event_name != 'pull_request'
env:
EVENT_NAME: ${{ github.event_name }}
REF_NAME: ${{ github.ref_name }}
REPOSITORY: ${{ github.repository }}
GH_TOKEN: ${{ github.token }}
run: |
if [ "$EVENT_NAME" = "push" ]; then
VERSION="$REF_NAME"
else
# Authenticated + retried lookup. An unauthenticated api.github.com
# request shares the runner's egress IP and can hit the 60/hr limit,
# which -eo pipefail turns into an immediate step failure (the weekly
# cron went red this way). The workflow token raises the limit to
# 1000/hr, and the retry rides out any transient API blip.
VERSION=""
for attempt in 1 2 3 4 5; do
if VERSION="$(gh api "repos/${REPOSITORY}/releases/latest" --jq .tag_name)" \
&& [ -n "$VERSION" ] && [ "$VERSION" != "null" ]; then
break
fi
VERSION=""
if [ "$attempt" -lt 5 ]; then
echo "releases/latest lookup failed (attempt $attempt/5); retrying in 15s..." >&2
sleep 15
fi
done
fi
if [ -z "$VERSION" ] || [ "$VERSION" = "null" ]; then
echo "::error::could not determine release version to verify" >&2
exit 1
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "Will verify reproduction of $VERSION"
# Build from the published tag (not HEAD) so the source tree and
# keep.version match the APK we downloaded. Skipped on PRs, which build
# the PR head to prove the reproducible path still builds.
- name: Check out published release tag
if: github.event_name != 'pull_request'
env:
VERSION: ${{ steps.ver.outputs.version }}
run: |
git fetch --no-tags --depth 1 origin "+refs/tags/${VERSION}:refs/tags/${VERSION}"
git checkout -f "refs/tags/${VERSION}"
echo "Building from tag ${VERSION} (keep.version $(tr -d '[:space:]' < keep.version))"
# Build the APK exactly as release.yml does: inside the pinned
# Dockerfile.reproducible container. This verifies that the container
# build is deterministic and reproduces the published release payload
# when run here. It is NOT an independent cross-environment rebuild: it
# runs on the same GitHub runner with the same Dockerfile, so it does not
# by itself prove that F-Droid's buildserver will reproduce the bytes. The
# bare GitHub runner produces a non-standard aws-lc build, which is why
# this builds in the pinned container rather than directly on the runner.
- name: Build APK in pinned container
run: |
DOCKER_BUILDKIT=1 docker build \
--build-arg KEEP_SHA="$(tr -d '[:space:]' < keep.version)" \
--output type=local,dest=out \
-f Dockerfile.reproducible .
# ABI splits produce one APK per architecture.
ls out/app-*-release.apk
test "$(ls out/app-*-release.apk | wc -l)" -ge 2
- name: Setup Android SDK
if: github.event_name != 'pull_request'
uses: android-actions/setup-android@40fd30fb8d7440372e1316f5d1809ec01dcd3699 # v4.0.1
- name: Install build-tools and put apksigner on PATH
if: github.event_name != 'pull_request'
run: |
sdkmanager --install "build-tools;${{ env.BUILD_TOOLS_VERSION }}"
# apksigcopier invokes `apksigner` from PATH; build-tools is not on
# PATH by default, so add it.
echo "${{ env.ANDROID_HOME }}/build-tools/${{ env.BUILD_TOOLS_VERSION }}" >> "$GITHUB_PATH"
- name: Install apksigcopier
if: github.event_name != 'pull_request'
run: pipx install 'apksigcopier==1.1.1'
# Verify the container build reproduces the published, developer-signed
# release APK. apksigcopier compares the zip payload while ignoring the
# signing block, exactly the comparison F-Droid performs. Skipped on PRs,
# which have no matching published APK (the container build above already
# proves the reproducible path still builds).
- name: Reproduce published release
if: github.event_name != 'pull_request'
env:
VERSION: ${{ steps.ver.outputs.version }}
REPOSITORY: ${{ github.repository }}
run: |
echo "Verifying reproduction of $VERSION"
# One published APK per architecture; compare each to its container build.
for built in out/app-*-release.apk; do
abi="$(basename "$built" | sed -E 's/^app-(.*)-release\.apk$/\1/')"
URL="https://github.com/${REPOSITORY}/releases/download/${VERSION}/keep-android-${VERSION}-${abi}.apk"
# On a tag push this runs concurrently with release.yml, so the asset
# may not be published yet. Wait for it (up to ~20 min).
ok=""
for i in $(seq 1 40); do
if curl -fsSL -o "published-${abi}.apk" "$URL"; then ok=1; break; fi
echo "Published $abi APK not available yet (attempt $i/40); waiting 30s..."
sleep 30
done
if [ -z "$ok" ] || ! test -s "published-${abi}.apk"; then
echo "::error::published $abi APK never became available at $URL" >&2
exit 1
fi
apksigcopier compare "published-${abi}.apk" "$built"
echo "Container build reproduces the published $VERSION $abi APK (payload identical)."
done