Merge pull request #19 from Naveax/pr/add-android-supply-chain-ci

build: add reproducible Android release verification

Resolved a conflict in android/app/build.gradle.kts. #19 was written against
the pre-#10 file and re-added the pub-cache patch tasks that #10 deleted; only
`sha256Hex()` was kept from that block, since `verifyReleaseAppConfig` needs it.

Also restored `import java.io.File`, which #10 removed along with the patch
tasks. #19's `sha256Hex(file: File)` names the type in its signature, and
Kotlin does not auto-import `java.io.File`, so the release build would have
failed at Gradle configuration without it.

Verified after resolution: no orphaned references to `projectRootDir`,
`localProperties`, `flutterSdkPath` or `patchFlutterVless`; both tests in
test/release_verification_regression_test.dart pass; `flutter analyze` reports
no errors or warnings.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_019HT2x2JvJNg7XNsJubfHFf
This commit was merged in pull request #19.
This commit is contained in:
esefxdzandClaude Opus 5 committed 2026-09-06 11:27:28 +03:00
commit 061bd41920
10 files changed
+9499 -17

No files matched your search

+74 -5
View File
@@ -1,11 +1,18 @@
import java.util.Properties
import java.io.File
import java.io.FileInputStream
import java.security.MessageDigest
plugins {
id("com.android.application")
id("dev.flutter.flutter-gradle-plugin")
}
fun sha256Hex(file: File): String {
val digest = MessageDigest.getInstance("SHA-256").digest(file.readBytes())
return digest.joinToString("") { "%02x".format(it.toInt() and 0xff) }
}
android {
namespace = "com.paladinvpn.app"
compileSdk = flutter.compileSdkVersion
@@ -17,10 +24,6 @@ android {
targetCompatibility = JavaVersion.VERSION_17
}
packaging {
resources.excludes.add("META-INF/**")
}
val keystorePropertiesFile = rootProject.file("key.properties")
val keystoreProperties = Properties()
if (keystorePropertiesFile.exists()) {
@@ -33,6 +36,68 @@ android {
admobProperties.load(FileInputStream(admobPropertiesFile))
}
// Explicit opt-in used only by GitHub Actions to exercise the real
// release/R8 graph. Production release signing and config pinning remain
// fail-closed outside that CI smoke path.
val ciReleaseSmoke =
System.getenv("GITHUB_ACTIONS") == "true" &&
System.getenv("REVOLT_CI_RELEASE_SMOKE") == "true"
val appConfigFile = rootProject.file("../lib/logic/app_config.dart")
val verifyReleaseAppConfig = tasks.register("verifyReleaseAppConfig") {
group = "verification"
description = "Verify the production Dart app config against REVOLT_APP_CONFIG_SHA256."
doLast {
if (ciReleaseSmoke) {
logger.lifecycle("Skipping production app-config pin for CI release smoke build")
return@doLast
}
val expected = System.getenv("REVOLT_APP_CONFIG_SHA256")?.trim()?.lowercase()
if (expected.isNullOrEmpty()) {
throw org.gradle.api.GradleException(
"REVOLT_APP_CONFIG_SHA256 is required for production release builds"
)
}
if (!Regex("[0-9a-f]{64}").matches(expected)) {
throw org.gradle.api.GradleException(
"REVOLT_APP_CONFIG_SHA256 must be exactly 64 lowercase hexadecimal characters"
)
}
if (!appConfigFile.isFile) {
throw org.gradle.api.GradleException(
"Production app config is missing: ${appConfigFile.absolutePath}"
)
}
val configText = appConfigFile.readText()
val templateMarkers = listOf(
"'0.0.0.0'",
"YOUR_DOMAIN",
"YOUR_GITHUB_USERNAME",
"YOUR_REPO_NAME",
"ca-app-pub-0000000000000000/0000000000",
)
val remainingMarker = templateMarkers.firstOrNull(configText::contains)
if (remainingMarker != null) {
throw org.gradle.api.GradleException(
"Production app config still contains template marker: $remainingMarker"
)
}
val actual = sha256Hex(appConfigFile)
if (actual != expected) {
throw org.gradle.api.GradleException(
"Production app config SHA-256 mismatch: expected $expected, got $actual"
)
}
}
}
tasks.matching { it.name == "preReleaseBuild" }.configureEach {
dependsOn(verifyReleaseAppConfig)
}
defaultConfig {
applicationId = "com.paladinvpn.app"
minSdk = flutter.minSdkVersion
@@ -53,7 +118,11 @@ android {
buildTypes {
release {
signingConfig = signingConfigs.getByName("release")
signingConfig = if (ciReleaseSmoke) {
signingConfigs.getByName("debug")
} else {
signingConfigs.getByName("release")
}
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(