From 7c190f3c4e991ed0292768f8269facd643799b29 Mon Sep 17 00:00:00 2001 From: WizDom13 Date: Wed, 30 Sep 2026 23:00:35 +0300 Subject: [PATCH] Fix collection of all three signed release APKs --- .github/workflows/release-apks.yml | 3 -- .github/workflows/release.yml | 4 -- scripts/collect-signed-release-apks.sh | 7 ++- scripts/tests/test-release-apks.py | 73 ++++++++++++++++++++++++++ 4 files changed, 78 insertions(+), 9 deletions(-) mode change 100644 => 100755 scripts/collect-signed-release-apks.sh mode change 100644 => 100755 scripts/tests/test-release-apks.py diff --git a/.github/workflows/release-apks.yml b/.github/workflows/release-apks.yml index acb30dc..4d688b6 100644 --- a/.github/workflows/release-apks.yml +++ b/.github/workflows/release-apks.yml @@ -56,7 +56,6 @@ jobs: echo "FILE_TAG=v$VERSION" >> "$GITHUB_ENV" - name: Exercise production multi-APK signing with CI key uses: r0adkll/sign-android-release@v1 - id: sign_app env: BUILD_TOOLS_VERSION: '36.0.0' with: @@ -66,8 +65,6 @@ jobs: keyStorePassword: ci-testing keyPassword: ci-testing - name: Collect and verify signed APKs - env: - SIGNED_RELEASE_FILES: ${{ steps.sign_app.outputs.signedReleaseFiles }} run: bash scripts/collect-signed-release-apks.sh - uses: actions/upload-artifact@v5 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 805bd19..185691e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -99,7 +99,6 @@ jobs: - name: Sign all three Release APKs uses: r0adkll/sign-android-release@v1 - id: sign_app env: BUILD_TOOLS_VERSION: "36.0.0" with: @@ -110,8 +109,6 @@ jobs: keyPassword: ${{ secrets.KEY_PASSWORD }} - name: Collect and verify signed Release APKs - env: - SIGNED_RELEASE_FILES: ${{ steps.sign_app.outputs.signedReleaseFiles }} run: bash scripts/collect-signed-release-apks.sh - name: Upload Signed Artifact @@ -135,4 +132,3 @@ jobs: fail_on_unmatched_files: true body_path: app/build/outputs/release-apks/signed/apk-sizes.md generate_release_notes: true - diff --git a/scripts/collect-signed-release-apks.sh b/scripts/collect-signed-release-apks.sh old mode 100644 new mode 100755 index b0c9661..9a9790e --- a/scripts/collect-signed-release-apks.sh +++ b/scripts/collect-signed-release-apks.sh @@ -4,10 +4,13 @@ set -euo pipefail cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." : "${FILE_TAG:?FILE_TAG is required}" -: "${SIGNED_RELEASE_FILES:?SIGNED_RELEASE_FILES is required}" [[ "$FILE_TAG" =~ ^v[0-9][0-9A-Za-z._-]*$ ]] || exit 1 -IFS=: read -r -a SIGNED_FILES <<< "$SIGNED_RELEASE_FILES" +# The v1 action signs every APK, but its bundled runtime only exports the last +# signedReleaseFile. Collect its on-disk outputs from the freshly built staging +# directory; unsigned/aligned APKs and the signing key must never be published. +shopt -s nullglob +SIGNED_FILES=(app/build/outputs/release-apks/unsigned/*-signed.apk) if [[ ${#SIGNED_FILES[@]} -ne 3 ]]; then echo 'Expected exactly three signed release APKs' >&2 exit 1 diff --git a/scripts/tests/test-release-apks.py b/scripts/tests/test-release-apks.py old mode 100644 new mode 100755 index b0dfa07..37b6947 --- a/scripts/tests/test-release-apks.py +++ b/scripts/tests/test-release-apks.py @@ -3,7 +3,11 @@ # SPDX-License-Identifier: GPL-3.0-only """Regression checks for malformed native payloads at the release publication gate.""" import importlib.util +import json +import os from pathlib import Path +import shutil +import subprocess import tempfile import unittest import warnings @@ -74,5 +78,74 @@ def test_rejects_duplicate_zip_entries(self): RELEASE.inspect_payload(self.apk, ('arm64-v8a',)) +class SignedReleaseCollectionTest(unittest.TestCase): + def setUp(self): + temporary = tempfile.TemporaryDirectory() + self.addCleanup(temporary.cleanup) + self.root = Path(temporary.name) + scripts = self.root / 'scripts' + scripts.mkdir() + self.collector = scripts / 'collect-signed-release-apks.sh' + shutil.copyfile(Path(__file__).resolve().parents[1] / self.collector.name, self.collector) + # Exercise the real shell collector without requiring Android SDK tools. + # The full APK and signature verifier still runs on real APKs in CI. + (scripts / 'verify-release-apks.py').write_text( + 'import json, os, sys\nfrom pathlib import Path\n' + 'Path("verification.json").write_text(json.dumps(sys.argv[1:]))\n' + 'sys.exit(int(os.environ.get("TEST_VERIFIER_EXIT", "0")))\n') + self.input = self.root / 'app/build/outputs/release-apks/unsigned' + self.input.mkdir(parents=True) + self.output = self.input.parent / 'signed' + self.names = ('WizeFiles_v1.2.0', 'WizeFiles_v1.2.0_arm64-v8a', + 'WizeFiles_v1.2.0_armeabi-v7a') + self.env = dict(os.environ, FILE_TAG='v1.2.0') + self.env.pop('SIGNED_RELEASE_FILES', None) + self.env.pop('SIGNED_RELEASE_FILE', None) + for name in self.names: + (self.input / f'{name}-signed.apk').write_bytes(name.encode()) + + def collect(self): + return subprocess.run(['bash', str(self.collector)], env=self.env, + text=True, capture_output=True) + + def test_collects_three_apks_without_action_outputs_and_excludes_signing_material(self): + for name in self.names: + (self.input / f'{name}.apk').write_bytes(b'unsigned') + (self.input / f'{name}-aligned.apk').write_bytes(b'aligned') + (self.input / 'signingKey.jks').write_bytes(b'test key placeholder') + result = self.collect() + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual({p.name for p in self.output.iterdir()}, {f'{n}.apk' for n in self.names}) + for name in self.names: + self.assertEqual((self.output / f'{name}.apk').read_bytes(), name.encode()) + self.assertEqual(json.loads((self.root / 'verification.json').read_text()), + ['app/build/outputs/release-apks/signed', '--file-tag', 'v1.2.0', '--signed']) + + def test_rejects_missing_signed_apk(self): + (self.input / f'{self.names[1]}-signed.apk').unlink() + result = self.collect() + self.assertNotEqual(result.returncode, 0) + self.assertIn('Expected exactly three', result.stderr) + + def test_rejects_no_signed_apks(self): + for apk in self.input.iterdir(): + apk.unlink() + self.assertNotEqual(self.collect().returncode, 0) + + def test_rejects_unexpected_signed_apk(self): + (self.input / f'{self.names[1]}-signed.apk').rename(self.input / 'wrong-signed.apk') + result = self.collect() + self.assertNotEqual(result.returncode, 0) + self.assertIn('Unexpected signed APK', result.stderr) + + def test_rejects_extra_stale_signed_apk(self): + (self.input / 'WizeFiles_v1.1.0-signed.apk').write_bytes(b'stale') + self.assertNotEqual(self.collect().returncode, 0) + + def test_propagates_signature_verification_failure(self): + self.env['TEST_VERIFIER_EXIT'] = '29' + self.assertEqual(self.collect().returncode, 29) + + if __name__ == '__main__': unittest.main()