mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-10-19 22:17:29 +02:00
ci: provision pinned Discord SDK for Android and iOS
This commit is contained in:
1 parent
5bcbb029c9
commit
3191ff926f
8 files changed
+412
-15
No files matched your search
@@ -0,0 +1,31 @@
|
||||
name: Prepare Discord Social SDK
|
||||
description: Decrypt, verify and stage the pinned Android and iOS SDK.
|
||||
inputs:
|
||||
passphrase:
|
||||
description: Decryption key supplied from an Actions repository secret.
|
||||
required: true
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Ensure GnuPG is available
|
||||
shell: bash
|
||||
run: |
|
||||
if ! command -v gpg >/dev/null 2>&1; then
|
||||
if [[ "$RUNNER_OS" == "macOS" ]]; then
|
||||
brew install gnupg
|
||||
else
|
||||
echo "GnuPG is required to prepare the Discord SDK." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
- name: Decrypt, verify and stage SDK
|
||||
shell: bash
|
||||
env:
|
||||
DISCORD_MOBILE_SDK_PASSPHRASE: ${{ inputs.passphrase }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
umask 077
|
||||
sdk_dir="$(python3 scripts/prepare_discord_sdk.py)"
|
||||
bash scripts/setup_discord_sdk.sh "$sdk_dir"
|
||||
printf 'SPOTIFLAC_DISCORD_SDK=1\n' >> "$GITHUB_ENV"
|
||||
@@ -53,6 +53,10 @@ jobs:
|
||||
- 'third_party/audio_service/**'
|
||||
- 'third_party/spotiflac_discord/**'
|
||||
- 'scripts/setup_discord_sdk.sh'
|
||||
- 'scripts/prepare_discord_sdk.py'
|
||||
- 'scripts/test_prepare_discord_sdk.py'
|
||||
- '.github/actions/discord-sdk/**'
|
||||
- 'third_party/discord/*.zip.gpg'
|
||||
- 'ios/**'
|
||||
- 'scripts/build_ios.sh'
|
||||
- 'scripts/check_backend_ios.py'
|
||||
@@ -68,6 +72,10 @@ jobs:
|
||||
- 'third_party/audio_service/**'
|
||||
- 'third_party/spotiflac_discord/**'
|
||||
- 'scripts/setup_discord_sdk.sh'
|
||||
- 'scripts/prepare_discord_sdk.py'
|
||||
- 'scripts/test_prepare_discord_sdk.py'
|
||||
- '.github/actions/discord-sdk/**'
|
||||
- 'third_party/discord/*.zip.gpg'
|
||||
- 'android/**'
|
||||
- 'rust_backend/**'
|
||||
- 'scripts/build_android.sh'
|
||||
@@ -151,7 +159,7 @@ jobs:
|
||||
run: bash scripts/check_rust_backend.sh
|
||||
|
||||
- name: Install Android NDK
|
||||
run: $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager "ndk;29.0.14206865"
|
||||
run: '"$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" "ndk;29.0.14206865"'
|
||||
|
||||
- name: Build Android ARM32 and ARM64 libraries
|
||||
run: ANDROID_NDK_HOME="$ANDROID_HOME/ndk/29.0.14206865" bash scripts/build_rust_backend.sh android
|
||||
@@ -161,6 +169,8 @@ jobs:
|
||||
runs-on: macos-latest
|
||||
needs: changes
|
||||
if: needs.changes.outputs.rust == 'true' || needs.changes.outputs.ios == 'true'
|
||||
env:
|
||||
SPOTIFLAC_DISCORD_SDK: "0"
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
@@ -180,6 +190,12 @@ jobs:
|
||||
flutter-version-file: .fvmrc
|
||||
cache: true
|
||||
|
||||
- name: Prepare pinned Discord SDK
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
||||
uses: ./.github/actions/discord-sdk
|
||||
with:
|
||||
passphrase: ${{ secrets.DISCORD_MOBILE_SDK_PASSPHRASE }}
|
||||
|
||||
- name: Configure Rust application pods
|
||||
run: |
|
||||
flutter pub get
|
||||
@@ -190,6 +206,10 @@ jobs:
|
||||
run: |
|
||||
python3 -m unittest discover -s scripts -p 'test_check_backend_ios.py'
|
||||
flutter build ios --release --no-codesign --target lib/main.dart
|
||||
if [[ "$SPOTIFLAC_DISCORD_SDK" == "1" ]]; then
|
||||
test -f build/ios/iphoneos/Runner.app/Frameworks/discord_partner_sdk.framework/discord_partner_sdk
|
||||
test -f build/ios/iphoneos/Runner.app/Discord-License-Notices.txt
|
||||
fi
|
||||
python3 scripts/check_backend_ios.py build/ios/iphoneos/Runner.app \
|
||||
--backend rust --archs arm64 --platform ios --release
|
||||
|
||||
@@ -198,6 +218,8 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
if: needs.changes.outputs.android == 'true'
|
||||
env:
|
||||
SPOTIFLAC_DISCORD_SDK: "0"
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
@@ -235,10 +257,19 @@ jobs:
|
||||
"cmake;3.22.1"
|
||||
echo "ANDROID_NDK_HOME=$ANDROID_HOME/ndk/29.0.14206865" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Test APK audits
|
||||
- name: Test SDK preparation, release selection and APK audits
|
||||
run: |
|
||||
python3 -m unittest discover -s scripts -p 'test_prepare_discord_sdk.py'
|
||||
python3 -m unittest discover -s scripts -p 'test_build_android.py'
|
||||
python3 -m unittest discover -s scripts -p 'test_check_backend_apk.py'
|
||||
|
||||
# Fork PRs cannot access repository secrets and exercise the Lite build.
|
||||
- name: Prepare pinned Discord SDK
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
||||
uses: ./.github/actions/discord-sdk
|
||||
with:
|
||||
passphrase: ${{ secrets.DISCORD_MOBILE_SDK_PASSPHRASE }}
|
||||
|
||||
- name: Get Flutter dependencies
|
||||
run: flutter pub get
|
||||
|
||||
@@ -258,16 +289,25 @@ jobs:
|
||||
run: gradle -p android :app:assembleDebug :app:testDebugUnitTest
|
||||
|
||||
- name: Build Rust release application
|
||||
run: bash scripts/build_android.sh --target lib/main.dart
|
||||
run: |
|
||||
if [[ "$SPOTIFLAC_DISCORD_SDK" == "1" ]]; then
|
||||
bash scripts/build_android.sh --production --target lib/main.dart
|
||||
else
|
||||
bash scripts/build_android.sh --lite --target lib/main.dart
|
||||
fi
|
||||
|
||||
- name: Verify Rust release APK payloads
|
||||
run: |
|
||||
audit_flags=()
|
||||
if [[ "$SPOTIFLAC_DISCORD_SDK" == "1" ]]; then
|
||||
audit_flags+=(--discord-sdk)
|
||||
fi
|
||||
python3 scripts/check_backend_apk.py \
|
||||
build/app/outputs/flutter-apk/app-arm64-v8a-release.apk \
|
||||
--backend rust --abis arm64-v8a
|
||||
--backend rust --abis arm64-v8a "${audit_flags[@]}"
|
||||
python3 scripts/check_backend_apk.py \
|
||||
build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk \
|
||||
--backend rust --abis armeabi-v7a
|
||||
--backend rust --abis armeabi-v7a "${audit_flags[@]}"
|
||||
# PR builds use Gradle's test key; production signing stays in Release.
|
||||
for abi in arm64-v8a armeabi-v7a; do
|
||||
"$ANDROID_HOME/build-tools/37.0.0/apksigner" verify --verbose \
|
||||
|
||||
@@ -70,6 +70,11 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
|
||||
|
||||
- name: Prepare pinned Discord SDK
|
||||
uses: ./.github/actions/discord-sdk
|
||||
with:
|
||||
passphrase: ${{ secrets.DISCORD_MOBILE_SDK_PASSPHRASE }}
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5
|
||||
with:
|
||||
@@ -240,6 +245,11 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
|
||||
|
||||
- name: Prepare pinned Discord SDK
|
||||
uses: ./.github/actions/discord-sdk
|
||||
with:
|
||||
passphrase: ${{ secrets.DISCORD_MOBILE_SDK_PASSPHRASE }}
|
||||
|
||||
- name: Select Xcode 26.1.1
|
||||
run: |
|
||||
sudo xcode-select -s /Applications/Xcode_26.1.1.app
|
||||
@@ -334,7 +344,7 @@ jobs:
|
||||
mkdir Payload
|
||||
cp -r Runner.app Payload/
|
||||
# Use absolute path to avoid relative path issues
|
||||
zip -r $GITHUB_WORKSPACE/build/ios/ipa/SpotiFLAC-${VERSION}-ios-unsigned.ipa Payload
|
||||
zip -r "$GITHUB_WORKSPACE/build/ios/ipa/SpotiFLAC-${VERSION}-ios-unsigned.ipa" Payload
|
||||
rm -rf Payload
|
||||
|
||||
- name: Verify IPA created
|
||||
@@ -517,8 +527,12 @@ jobs:
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add apps.json
|
||||
git diff --cached --quiet && echo "No changes to commit" || \
|
||||
(git commit -m "chore: update AltStore source to ${VERSION}" && git push)
|
||||
if git diff --cached --quiet; then
|
||||
echo "No changes to commit"
|
||||
else
|
||||
git commit -m "chore: update AltStore source to ${VERSION}"
|
||||
git push
|
||||
fi
|
||||
|
||||
notify-telegram:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
+2
-1
@@ -53,8 +53,9 @@ AGENTS.md
|
||||
/android/*.keystore
|
||||
/android/app/*.jks
|
||||
|
||||
# Keep obsolete local SDK copies out of the checkout.
|
||||
# Only the pinned encrypted CI payload belongs in the checkout.
|
||||
/third_party/discord/*
|
||||
!/third_party/discord/discord-mobile-1.10.19337.zip.gpg
|
||||
|
||||
# iOS generated state
|
||||
/ios/build/
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Verify and prepare the pinned official Discord mobile SDK for CI."""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
|
||||
VERSION = "1.10.19337"
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||||
DEFAULT_OUTPUT = PROJECT_ROOT / ".dart_tool" / "discord-social-sdk" / VERSION
|
||||
ENCRYPTED_ARCHIVE = PROJECT_ROOT / "third_party" / "discord" / f"discord-mobile-{VERSION}.zip.gpg"
|
||||
XCFRAMEWORK = "lib/release/discord_partner_sdk.xcframework"
|
||||
FILES = {
|
||||
"License-Notices.txt":
|
||||
"e8afa66340c225431e69768543cc34a7240f3494a9d759189fe118620ea8eebf",
|
||||
"include/discordpp.h":
|
||||
"822e71509bf11b7155becca5fb00d215a7db7b305f357c0f3b5f6e27ff45b6ab",
|
||||
"lib/release/discord_partner_sdk.aar":
|
||||
"b1b2491f1e1848c79fd6f1986d5aa1e0c8019e88a6e62c7be06b89e8e4870933",
|
||||
f"{XCFRAMEWORK}/Info.plist":
|
||||
"bb227b40a5adb4e4a02447b46a91b7a0fe28c9d238fbb80e19b7215c7ae3f431",
|
||||
}
|
||||
for slice_name, plist, binary in (
|
||||
("ios-arm64",
|
||||
"41bc76fed4b9d3adb7d711e5b44848b87f775598ed24173abfd1f4a6a0656a90",
|
||||
"8d86063b1a6c5c562b822378d34f55d335b37358e20f3425d446cc335ffae8bd"),
|
||||
("ios-arm64-simulator",
|
||||
"a3e162d5587897f9435e7897dbcde4d5b32c6dd445a0c8ff897157f5f340e4a1",
|
||||
"eddc98dcb0e5614d64bef4c17476e5bb1ad53b3a7e8d6d8fc550b3abfc067f98"),
|
||||
):
|
||||
framework = f"{XCFRAMEWORK}/{slice_name}/discord_partner_sdk.framework"
|
||||
FILES.update({
|
||||
f"{framework}/Info.plist": plist,
|
||||
f"{framework}/discord_partner_sdk": binary,
|
||||
f"{framework}/Headers/cdiscord.h":
|
||||
"86ef9280ab8432493952bb974bd18c5a25d7725011393b5dba05162bcfdb3371",
|
||||
f"{framework}/Headers/discord_partner_sdk.h":
|
||||
"b049099dab197146011b3ceecc823ec25fdbbda7a37ea1584be8e472501a390b",
|
||||
f"{framework}/Headers/discordpp.h": FILES["include/discordpp.h"],
|
||||
f"{framework}/Modules/module.modulemap":
|
||||
"48879037011d015ad2c9802fdc478862785ba073e7f96dfdf6c0865b6075a542",
|
||||
})
|
||||
MAX_FILE_BYTES = 64 * 1024 * 1024
|
||||
|
||||
|
||||
class SdkError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def verify(directory: Path) -> None:
|
||||
for relative, expected in FILES.items():
|
||||
path = directory / relative
|
||||
if not path.is_file() or path.is_symlink():
|
||||
raise SdkError("Missing or invalid Discord SDK file: " + relative)
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
if digest.hexdigest() != expected:
|
||||
raise SdkError(f"Discord SDK {VERSION} checksum mismatch: {relative}")
|
||||
|
||||
|
||||
def unpack(archive: Path, destination: Path) -> None:
|
||||
with zipfile.ZipFile(archive) as zf:
|
||||
for relative in FILES:
|
||||
matches = [info for info in zf.infolist() if not info.is_dir() and
|
||||
(info.filename == relative or info.filename.endswith("/" + relative))]
|
||||
if len(matches) != 1:
|
||||
raise SdkError("SDK archive must contain exactly one " + relative)
|
||||
if matches[0].file_size > MAX_FILE_BYTES:
|
||||
raise SdkError("SDK archive entry exceeds size limit: " + relative)
|
||||
path = destination / relative
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
# Write only pinned destinations, never paths supplied by the ZIP.
|
||||
with zf.open(matches[0]) as source, path.open("wb") as target:
|
||||
shutil.copyfileobj(source, target)
|
||||
if relative.endswith(".framework/discord_partner_sdk"):
|
||||
path.chmod(0o755)
|
||||
|
||||
|
||||
def prepare(output: Path, source_dir: Optional[Path] = None,
|
||||
archive: Optional[Path] = None, passphrase: Optional[str] = None) -> Path:
|
||||
output = output.expanduser().resolve()
|
||||
if source_dir is None and archive is None and output.is_dir():
|
||||
verify(output)
|
||||
return output
|
||||
if source_dir is None and archive is None and not passphrase:
|
||||
raise SdkError(
|
||||
f"Discord SDK {VERSION} is required. Supply --source-dir, --archive, "
|
||||
"or the Actions secret DISCORD_MOBILE_SDK_PASSPHRASE."
|
||||
)
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
with tempfile.TemporaryDirectory(prefix="discord-sdk-", dir=output.parent) as temporary:
|
||||
staging = Path(temporary) / "sdk"
|
||||
if source_dir is not None:
|
||||
source_dir = source_dir.expanduser().resolve()
|
||||
verify(source_dir)
|
||||
for relative in FILES:
|
||||
target = staging / relative
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(source_dir / relative, target)
|
||||
else:
|
||||
if archive is None:
|
||||
archive = Path(temporary) / "sdk.zip"
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["gpg", "--batch", "--quiet", "--no-symkey-cache",
|
||||
"--pinentry-mode", "loopback",
|
||||
"--passphrase-fd", "0", "--output", str(archive),
|
||||
"--decrypt", str(ENCRYPTED_ARCHIVE)],
|
||||
input=passphrase.encode(), stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL, check=False,
|
||||
)
|
||||
except FileNotFoundError as exc:
|
||||
raise SdkError("Install GnuPG to decrypt the Discord SDK") from exc
|
||||
if result.returncode != 0:
|
||||
raise SdkError("SDK decryption failed; check DISCORD_MOBILE_SDK_PASSPHRASE")
|
||||
unpack(archive, staging)
|
||||
verify(staging)
|
||||
for relative in FILES:
|
||||
target = output / relative
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
(staging / relative).replace(target)
|
||||
verify(output)
|
||||
return output
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT)
|
||||
parser.add_argument("--source-dir", type=Path)
|
||||
parser.add_argument("--archive", type=Path, help="official SDK ZIP for offline setup")
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
output = prepare(args.output, args.source_dir, args.archive,
|
||||
os.environ.get("DISCORD_MOBILE_SDK_PASSPHRASE"))
|
||||
except (SdkError, OSError, zipfile.BadZipFile) as exc:
|
||||
print(f"error: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
# stdout contains only the verified directory, suitable for setup_discord_sdk.sh.
|
||||
print(output)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,147 @@
|
||||
"""Check SDK integrity, bounded extraction and encrypted CI preparation."""
|
||||
|
||||
import hashlib
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
import prepare_discord_sdk as sdk
|
||||
|
||||
|
||||
class PrepareDiscordSdkTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
directory = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(directory.cleanup)
|
||||
self.root = Path(directory.name).resolve()
|
||||
self.output = self.root / "prepared"
|
||||
self.archive = self.root / "sdk.zip"
|
||||
self.contents = {
|
||||
"lib/release/discord_partner_sdk.aar": b"test Android library",
|
||||
f"{sdk.XCFRAMEWORK}/ios-arm64/discord_partner_sdk.framework/discord_partner_sdk":
|
||||
b"test iOS device library",
|
||||
f"{sdk.XCFRAMEWORK}/ios-arm64-simulator/discord_partner_sdk.framework/discord_partner_sdk":
|
||||
b"test iOS simulator library",
|
||||
"License-Notices.txt": b"test license notices",
|
||||
"include/discordpp.h": b"test C++ header",
|
||||
}
|
||||
checksums = {name: hashlib.sha256(data).hexdigest()
|
||||
for name, data in self.contents.items()}
|
||||
patch = mock.patch.object(sdk, "FILES", checksums)
|
||||
patch.start()
|
||||
self.addCleanup(patch.stop)
|
||||
|
||||
def make_archive(self, prefix="", contents=None):
|
||||
with zipfile.ZipFile(self.archive, "w") as zf:
|
||||
for name, data in (self.contents if contents is None else contents).items():
|
||||
zf.writestr(prefix + name, data)
|
||||
zf.writestr("../../outside.txt", b"must not be extracted")
|
||||
zf.writestr("lib/release/desktop.dll", b"unused")
|
||||
|
||||
def assert_prepared(self):
|
||||
files = {str(path.relative_to(self.output)): path.read_bytes()
|
||||
for path in self.output.rglob("*") if path.is_file()}
|
||||
self.assertEqual(files, self.contents)
|
||||
|
||||
def test_root_and_prefixed_archives_extract_only_mobile_files(self):
|
||||
for prefix in ("", "discord_social_sdk/"):
|
||||
with self.subTest(prefix=prefix):
|
||||
self.make_archive(prefix)
|
||||
sdk.prepare(self.output, archive=self.archive)
|
||||
self.assert_prepared()
|
||||
self.assertFalse((self.root / "outside.txt").exists())
|
||||
|
||||
def test_source_directory_and_verified_cache(self):
|
||||
source = self.root / "download"
|
||||
for name, data in self.contents.items():
|
||||
path = source / name
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_bytes(data)
|
||||
sdk.prepare(self.output, source_dir=source)
|
||||
shutil.rmtree(source)
|
||||
self.assertEqual(sdk.prepare(self.output), self.output)
|
||||
self.assert_prepared()
|
||||
|
||||
def test_corrupt_cache_is_rejected(self):
|
||||
self.make_archive()
|
||||
sdk.prepare(self.output, archive=self.archive)
|
||||
(self.output / "License-Notices.txt").write_bytes(b"tampered")
|
||||
with self.assertRaisesRegex(sdk.SdkError, "checksum mismatch"):
|
||||
sdk.prepare(self.output)
|
||||
|
||||
def test_bad_archive_does_not_replace_verified_sdk(self):
|
||||
self.make_archive()
|
||||
sdk.prepare(self.output, archive=self.archive)
|
||||
contents = self.contents.copy()
|
||||
contents["include/discordpp.h"] = b"wrong SDK version"
|
||||
self.make_archive(contents=contents)
|
||||
with self.assertRaisesRegex(sdk.SdkError, "checksum mismatch"):
|
||||
sdk.prepare(self.output, archive=self.archive)
|
||||
self.assert_prepared()
|
||||
|
||||
def test_each_missing_mobile_component_is_rejected(self):
|
||||
for name in self.contents:
|
||||
with self.subTest(name=name):
|
||||
self.make_archive(contents={key: value for key, value in self.contents.items()
|
||||
if key != name})
|
||||
with self.assertRaisesRegex(sdk.SdkError, "exactly one"):
|
||||
sdk.prepare(self.output, archive=self.archive)
|
||||
self.assertFalse(self.output.exists())
|
||||
|
||||
def test_duplicate_file_is_rejected(self):
|
||||
self.make_archive()
|
||||
with zipfile.ZipFile(self.archive, "a") as zf:
|
||||
zf.writestr("other/License-Notices.txt", b"duplicate")
|
||||
with self.assertRaisesRegex(sdk.SdkError, "exactly one"):
|
||||
sdk.prepare(self.output, archive=self.archive)
|
||||
|
||||
def test_oversized_file_is_rejected_before_writing_output(self):
|
||||
self.make_archive()
|
||||
with mock.patch.object(sdk, "MAX_FILE_BYTES", 1):
|
||||
with self.assertRaisesRegex(sdk.SdkError, "size limit"):
|
||||
sdk.prepare(self.output, archive=self.archive)
|
||||
self.assertFalse(self.output.exists())
|
||||
|
||||
def test_missing_configuration_names_the_required_secret(self):
|
||||
with self.assertRaisesRegex(sdk.SdkError, "DISCORD_MOBILE_SDK_PASSPHRASE"):
|
||||
sdk.prepare(self.output)
|
||||
|
||||
def test_decryption_failure_does_not_echo_key(self):
|
||||
key = "private-test-passphrase"
|
||||
with mock.patch.object(sdk.subprocess, "run", return_value=mock.Mock(returncode=2)):
|
||||
with self.assertRaises(sdk.SdkError) as error:
|
||||
sdk.prepare(self.output, passphrase=key)
|
||||
self.assertNotIn(key, str(error.exception))
|
||||
self.assertFalse(self.output.exists())
|
||||
|
||||
def test_missing_gpg_is_actionable(self):
|
||||
with mock.patch.object(sdk.subprocess, "run", side_effect=FileNotFoundError):
|
||||
with self.assertRaisesRegex(sdk.SdkError, "Install GnuPG"):
|
||||
sdk.prepare(self.output, passphrase="test")
|
||||
|
||||
@unittest.skipUnless(shutil.which("gpg"), "GnuPG is required for encrypted archive test")
|
||||
def test_encrypted_archive_roundtrip_and_wrong_key(self):
|
||||
self.make_archive()
|
||||
encrypted = self.root / "sdk.zip.gpg"
|
||||
key = "test-fixture-passphrase"
|
||||
gnupg_home = self.root / "gnupg"
|
||||
gnupg_home.mkdir(mode=0o700)
|
||||
with mock.patch.dict(sdk.os.environ, {"GNUPGHOME": str(gnupg_home)}):
|
||||
subprocess.run(
|
||||
["gpg", "--batch", "--quiet", "--pinentry-mode", "loopback",
|
||||
"--passphrase-fd", "0", "--cipher-algo", "AES256", "--symmetric",
|
||||
"--output", str(encrypted), str(self.archive)],
|
||||
input=key.encode(), check=True, capture_output=True,
|
||||
)
|
||||
with mock.patch.object(sdk, "ENCRYPTED_ARCHIVE", encrypted):
|
||||
with self.assertRaisesRegex(sdk.SdkError, "decryption failed"):
|
||||
sdk.prepare(self.output, passphrase="incorrect")
|
||||
sdk.prepare(self.output, passphrase=key)
|
||||
self.assert_prepared()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Binary file not shown.
+16
-6
@@ -14,10 +14,19 @@ bash scripts/setup_discord_sdk.sh /path/to/discord_social_sdk
|
||||
flutter pub get
|
||||
```
|
||||
|
||||
On CI, provide the official SDK through the build environment and run the same
|
||||
script before building. Vendor binaries and their license notices stay under
|
||||
the ignored `sdk/` and `ios/Frameworks/` directories; do not commit developer credentials or rehost SDK
|
||||
downloads. Distributions containing the SDK must include its license notices
|
||||
CI prepares the pinned mobile SDK with `.github/actions/discord-sdk` before
|
||||
building. Its AES-256 encrypted payload contains only the Android AAR, iOS
|
||||
XCFramework, C++ header and license notices from the official download. Set the
|
||||
Actions repository secret `DISCORD_MOBILE_SDK_PASSPHRASE` to the payload's key.
|
||||
The helper verifies SHA-256 checksums for all 16 required files before staging
|
||||
them. A missing key, failed decryption or changed SDK stops the build.
|
||||
|
||||
Decrypted vendor binaries and notices stay under the ignored `.dart_tool/`,
|
||||
`sdk/` and `ios/Frameworks/` directories. Do not commit the encryption key,
|
||||
developer credentials or unencrypted SDK downloads. This uses GitHub's
|
||||
[large-secret storage pattern](https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/use-secrets#storing-large-secrets);
|
||||
the portal's signed download link expires after one hour and is unsuitable for
|
||||
release automation. Distributions containing the SDK must include its license notices
|
||||
and follow Discord's SDK terms. The setup script stages notices for inclusion
|
||||
in Android assets and iOS resources. Without these files the app still builds, but
|
||||
the presence setting reports that the SDK is unavailable. A clean clone alone
|
||||
@@ -31,8 +40,9 @@ as unavailable; the staged SDK files are retained for subsequent full builds.
|
||||
Production Android builds use `bash scripts/build_android.sh --production`,
|
||||
which forces Discord on, checks the staged SDK and audits it in every APK ABI.
|
||||
`--production --lite` is rejected. Release CI requires the SDK on both platforms
|
||||
and stops before publication if the SDK is absent. Provision the official SDK
|
||||
in the build environment before running that workflow.
|
||||
and stops before publication if the SDK is absent. CI on trusted branches also
|
||||
builds with Discord; fork PRs run native/debug checks and explicit Lite builds
|
||||
without receiving the decryption key.
|
||||
|
||||
Android uses the installed Discord app's signed-in session (Social SDK 1.10+).
|
||||
The AAR and CMake use the same staged native library; the host packages one copy
|
||||
|
||||
Reference in new issue
Block a user