Align reproducible build to F-Droid env (trixie + JDK 21) for v1.1.4 … #34
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release | |
| on: | |
| push: | |
| tags: | |
| - "v*" | |
| permissions: {} | |
| # NDK_VERSION / CARGO_NDK_VERSION / RUST_VERSION are not consumed directly by | |
| # the build below (the pinned Dockerfile.reproducible owns the 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: | |
| build: | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 90 | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| - name: Decode keystore | |
| env: | |
| KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }} | |
| KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} | |
| run: | | |
| if [ -z "$KEYSTORE_BASE64" ]; then | |
| echo "error: KEYSTORE_BASE64 secret is empty." >&2 | |
| exit 1 | |
| fi | |
| KEYSTORE_OUT="${{ runner.temp }}/release.keystore" | |
| printf '%s' "$KEYSTORE_BASE64" | base64 -d > "$KEYSTORE_OUT" | |
| if [ ! -s "$KEYSTORE_OUT" ]; then | |
| echo "error: decoded keystore is empty; check KEYSTORE_BASE64 encoding." >&2 | |
| exit 1 | |
| fi | |
| if ! keytool -list -keystore "$KEYSTORE_OUT" -storepass:env KEYSTORE_PASSWORD >/dev/null; then | |
| echo "error: keystore failed to load (corrupt base64 or wrong password)." >&2 | |
| exit 1 | |
| fi | |
| # Build the release APK inside the pinned, reproducible container | |
| # (Dockerfile.reproducible) instead of on the bare runner. A clean, pinned | |
| # environment is required for the native aws-lc artifacts to be | |
| # bit-reproducible by F-Droid: the bare GitHub runner produces a | |
| # non-standard aws-lc build that no clean environment (F-Droid's | |
| # buildserver, a local checkout, this container) reproduces. | |
| # | |
| # The release keystore + passwords are passed as BuildKit secrets so | |
| # Gradle signs the APK *during* the build. Signing during the build (not | |
| # re-signing the finished APK with apksigner) keeps the payload identical | |
| # to F-Droid's gradle-built rebuild — apksigner re-signing repacks the APK | |
| # and would break the payload comparison. Secrets never land in an image | |
| # layer or docker history. | |
| - name: Build release-signed APK in pinned container | |
| env: | |
| REF_NAME: ${{ github.ref_name }} | |
| KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} | |
| KEY_ALIAS: ${{ secrets.KEY_ALIAS }} | |
| KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} | |
| run: | | |
| DOCKER_BUILDKIT=1 docker build \ | |
| --build-arg KEEP_SHA="$(tr -d '[:space:]' < keep.version)" \ | |
| --secret id=keystore,src="${{ runner.temp }}/release.keystore" \ | |
| --secret id=keystore_pass,env=KEYSTORE_PASSWORD \ | |
| --secret id=key_alias,env=KEY_ALIAS \ | |
| --secret id=key_pass,env=KEY_PASSWORD \ | |
| --output type=local,dest=docker-out \ | |
| -f Dockerfile.reproducible . | |
| test -f docker-out/app-release.apk | |
| mv docker-out/app-release.apk "keep-android-${REF_NAME}.apk" | |
| - name: Setup Android SDK | |
| uses: android-actions/setup-android@40fd30fb8d7440372e1316f5d1809ec01dcd3699 # v4.0.1 | |
| - name: Install build-tools (for apksigner) | |
| run: sdkmanager --install "build-tools;${{ env.BUILD_TOOLS_VERSION }}" | |
| - name: Verify APK signing certificate | |
| env: | |
| REF_NAME: ${{ github.ref_name }} | |
| # SHA-256 of the release signing certificate (DER encoding). Must | |
| # equal the AllowedAPKSigningKeys value in | |
| # metadata/io.privkey.keep.yml; both are the digest apksigner reports | |
| # as "certificate SHA-256 digest". | |
| EXPECTED_CERT_SHA256: 3eb0e6f6f4e0962ebd717f84eea47428b73087b686c956633d6a9fcc4c854f50 | |
| run: | | |
| APKSIGNER="$ANDROID_HOME/build-tools/${{ env.BUILD_TOOLS_VERSION }}/apksigner" | |
| CERTS="$("$APKSIGNER" verify --print-certs "keep-android-${REF_NAME}.apk")" | |
| echo "$CERTS" | |
| ACTUAL="$(printf '%s\n' "$CERTS" \ | |
| | sed -nE 's/.*certificate SHA-256 digest:[[:space:]]*([0-9a-fA-F]+).*/\1/p' \ | |
| | head -1 | tr 'A-F' 'a-f')" | |
| if [ -z "$ACTUAL" ]; then | |
| echo "::error::could not parse signing certificate SHA-256 digest" >&2 | |
| exit 1 | |
| fi | |
| if [ "$ACTUAL" != "$EXPECTED_CERT_SHA256" ]; then | |
| echo "::error::signing cert SHA-256 $ACTUAL does not match expected release key $EXPECTED_CERT_SHA256" >&2 | |
| exit 1 | |
| fi | |
| echo "Signing certificate matches the expected release key." | |
| - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: apk | |
| path: keep-android-${{ github.ref_name }}.apk | |
| release: | |
| needs: [build] | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 10 | |
| permissions: | |
| contents: write | |
| steps: | |
| - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| path: artifacts | |
| merge-multiple: true | |
| - name: Generate checksums | |
| run: | | |
| cd artifacts | |
| sha256sum * > SHA256SUMS | |
| - name: Create release | |
| uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v2 | |
| with: | |
| files: artifacts/* | |
| generate_release_notes: true |