Skip to content

Commit a67be87

Browse files
committed
Enhance CI workflow with performance regression checks and AGP compatibility testing: add a new job to measure build performance for assembleDebug and ensure compatibility across multiple AGP versions. Update ci.yml to include performance checks and improve build stability. Remove obsolete refactor plan checklist document to streamline project documentation.
1 parent e81dd4b commit a67be87

21 files changed

Lines changed: 565 additions & 216 deletions

.github/workflows/ci.yml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,42 @@ jobs:
2929

3030
- name: Sample app assembleDebug
3131
run: ./gradlew :app:assembleDebug --no-daemon
32+
33+
- name: Performance non-regression check (assembleDebug)
34+
env:
35+
PERF_MAX_SECONDS: "240"
36+
run: |
37+
./gradlew :app:clean --no-daemon
38+
./gradlew :app:assembleDebug --no-daemon
39+
start=$(date +%s)
40+
./gradlew :app:assembleDebug --no-daemon
41+
end=$(date +%s)
42+
elapsed=$((end - start))
43+
echo "Measured assembleDebug (warm) time: ${elapsed}s"
44+
if [ "$elapsed" -gt "$PERF_MAX_SECONDS" ]; then
45+
echo "Performance regression: ${elapsed}s > ${PERF_MAX_SECONDS}s"
46+
exit 1
47+
fi
48+
49+
agp-compat:
50+
runs-on: ubuntu-latest
51+
strategy:
52+
fail-fast: false
53+
matrix:
54+
agp-version: ["8.0.2", "8.7.3", "9.0.0"]
55+
steps:
56+
- uses: actions/checkout@v4
57+
58+
- uses: actions/setup-java@v4
59+
with:
60+
distribution: temurin
61+
java-version: 17
62+
63+
- name: Grant execute permission for gradlew
64+
run: chmod +x gradlew
65+
66+
- name: Set AGP version in version catalog
67+
run: sed -i.bak 's/^agp = ".*"/agp = "${{ matrix.agp-version }}"/' gradle/libs.versions.toml
68+
69+
- name: AGP compatibility check (assembleDebug)
70+
run: ./gradlew :app:assembleDebug --no-daemon

docs/refactor-plan-checklist.md

Lines changed: 0 additions & 119 deletions
This file was deleted.
Lines changed: 97 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package dev.vyp.stringcare.plugin.domain.models
22

33
import java.io.File
4+
import java.io.RandomAccessFile
45
import java.nio.file.AtomicMoveNotSupportedException
56
import java.nio.file.Files
67
import java.nio.file.StandardCopyOption
8+
import java.security.MessageDigest
79

810
/**
911
* Copies [source] to [dest] via a temp file in the same directory, then replaces [dest].
@@ -13,24 +15,105 @@ internal fun copyToBackupAtomically(
1315
source: File,
1416
dest: File,
1517
) {
16-
dest.parentFile?.mkdirs()
17-
val parent = dest.parentFile ?: error("Backup destination has no parent: ${dest.absolutePath}")
18-
val tmp = File.createTempFile("sc-backup-", ".tmp", parent)
19-
try {
20-
Files.copy(source.toPath(), tmp.toPath(), StandardCopyOption.REPLACE_EXISTING)
18+
withFileLock(dest) {
19+
dest.parentFile?.mkdirs()
20+
val parent = dest.parentFile ?: error("Backup destination has no parent: ${dest.absolutePath}")
21+
val tmp = File.createTempFile("sc-backup-", ".tmp", parent)
2122
try {
22-
Files.move(
23-
tmp.toPath(),
24-
dest.toPath(),
25-
StandardCopyOption.ATOMIC_MOVE,
23+
Files.copy(source.toPath(), tmp.toPath(), StandardCopyOption.REPLACE_EXISTING)
24+
try {
25+
Files.move(
26+
tmp.toPath(),
27+
dest.toPath(),
28+
StandardCopyOption.ATOMIC_MOVE,
29+
StandardCopyOption.REPLACE_EXISTING,
30+
)
31+
} catch (_: AtomicMoveNotSupportedException) {
32+
Files.move(tmp.toPath(), dest.toPath(), StandardCopyOption.REPLACE_EXISTING)
33+
}
34+
val sourceChecksum = source.sha256Hex()
35+
val backupChecksum = dest.sha256Hex()
36+
check(sourceChecksum == backupChecksum) {
37+
"Backup checksum mismatch for ${source.absolutePath} -> ${dest.absolutePath}"
38+
}
39+
} finally {
40+
if (tmp.exists()) {
41+
tmp.delete()
42+
}
43+
}
44+
}
45+
}
46+
47+
internal inline fun <T> withFileLock(
48+
file: File,
49+
action: () -> T,
50+
): T {
51+
file.parentFile?.mkdirs()
52+
if (!file.exists()) {
53+
file.createNewFile()
54+
}
55+
RandomAccessFile(file, "rw").use { raf ->
56+
raf.channel.use { channel ->
57+
channel.lock().use {
58+
return action()
59+
}
60+
}
61+
}
62+
}
63+
64+
internal inline fun <T> withBackup(
65+
backupFile: File,
66+
targetFile: File,
67+
action: () -> T,
68+
): T {
69+
return try {
70+
action()
71+
} catch (error: Throwable) {
72+
withFileLock(targetFile) {
73+
Files.copy(
74+
backupFile.toPath(),
75+
targetFile.toPath(),
2676
StandardCopyOption.REPLACE_EXISTING,
2777
)
28-
} catch (_: AtomicMoveNotSupportedException) {
29-
Files.move(tmp.toPath(), dest.toPath(), StandardCopyOption.REPLACE_EXISTING)
3078
}
31-
} finally {
32-
if (tmp.exists()) {
33-
tmp.delete()
79+
throw error
80+
}
81+
}
82+
83+
/**
84+
* Shared backup/rollback workflow for any [Backupable] item.
85+
*
86+
* Backups are created into [tempRoot], then [action] is executed for each item.
87+
* If [action] fails for an item, that item is restored from its backup and the error is rethrown.
88+
*/
89+
internal inline fun <T : Backupable> forEachWithBackup(
90+
items: Iterable<T>,
91+
tempRoot: String,
92+
action: (T) -> Unit,
93+
) {
94+
items.forEach { item ->
95+
val backup = item.backup(tempRoot)
96+
val target =
97+
when (item) {
98+
is ResourceFile -> item.file
99+
is AssetsFile -> item.file
100+
else -> error("Unsupported Backupable type: ${item::class.qualifiedName}")
101+
}
102+
withBackup(backup, target) {
103+
action(item)
104+
}
105+
}
106+
}
107+
108+
private fun File.sha256Hex(): String {
109+
val digest = MessageDigest.getInstance("SHA-256")
110+
inputStream().use { stream ->
111+
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
112+
while (true) {
113+
val read = stream.read(buffer)
114+
if (read <= 0) break
115+
digest.update(buffer, 0, read)
34116
}
35117
}
118+
return digest.digest().joinToString(separator = "") { b -> "%02x".format(b) }
36119
}

plugin/src/main/kotlin/dev/vyp/stringcare/plugin/domain/models/ResourceFile.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,24 @@ package dev.vyp.stringcare.plugin.domain.models
22

33
import java.io.File
44

5+
/**
6+
* String XML resource file selected for obfuscation and backup.
7+
*
8+
* Example:
9+
* `ResourceFile(File("src/main/res/values/strings.xml"), "src/main", "app")`
10+
*
11+
* @property file Physical file in the project.
12+
* @property sourceFolder Source-set root used to rebuild relative backup paths.
13+
* @property module Gradle module name that owns the resource file.
14+
*/
515
data class ResourceFile(
616
val file: File,
717
val sourceFolder: String,
818
val module: String,
919
) : Backupable {
20+
/**
21+
* Creates a backup copy of this resource file inside [tempRoot], preserving module/source relative path.
22+
*/
1023
override fun backup(tempRoot: String): File {
1124
val cleanPath =
1225
"$tempRoot${File.separator}$module${File.separator}$sourceFolder${file.absolutePath.split(sourceFolder)[1]}"

plugin/src/main/kotlin/dev/vyp/stringcare/plugin/domain/models/StringEntity.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,15 @@ package dev.vyp.stringcare.plugin.domain.models
33
/**
44
* A `<string>` resource entry marked for obfuscation (`hidden` not false).
55
*
6+
* Example:
7+
* `StringEntity("welcome_title", attributes, "Welcome", "string", 0, androidTreatment = true)`
8+
*
9+
* @param name XML `name` attribute.
10+
* @param attributes Original XML attributes copied from the source node.
11+
* @param value Current string value (plain text or obfuscated payload).
12+
* @param tag XML tag name, usually `string`.
613
* @param index DOM order index among all `<string>` nodes (used when rewriting XML).
14+
* @param androidTreatment Whether Android-style whitespace treatment should be applied before obfuscation.
715
*/
816
data class StringEntity(
917
val name: String,

plugin/src/main/kotlin/dev/vyp/stringcare/plugin/infrastructure/crypto/ObfuscationService.kt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,30 @@ import dev.vyp.stringcare.plugin.internal.Stark
88
* @throws UnsatisfiedLinkError if native code is not loaded on this host.
99
*/
1010
class ObfuscationService {
11+
/**
12+
* Obfuscates a payload using the JNI runtime.
13+
*
14+
* @param key SHA-1 signing fingerprint used as obfuscation key.
15+
* @param payload Raw bytes to obfuscate.
16+
* @param applicationId Android application id mixed in native transformation.
17+
* @return Obfuscated bytes to persist in resources/assets.
18+
* @throws UnsatisfiedLinkError if native code is unavailable for this host.
19+
*/
1120
fun obfuscate(
1221
key: String,
1322
payload: ByteArray,
1423
applicationId: String,
1524
): ByteArray = Stark.obfuscate(key, payload, applicationId)
1625

26+
/**
27+
* Reverts bytes previously obfuscated by [obfuscate].
28+
*
29+
* @param key SHA-1 signing fingerprint used during obfuscation.
30+
* @param payload Obfuscated bytes.
31+
* @param applicationId Android application id used by native transformation.
32+
* @return Plain decoded bytes.
33+
* @throws UnsatisfiedLinkError if native code is unavailable for this host.
34+
*/
1735
fun reveal(
1836
key: String,
1937
payload: ByteArray,

plugin/src/main/kotlin/dev/vyp/stringcare/plugin/infrastructure/parsers/XmlParser.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ import java.io.File
55

66
/** Public entry for `strings.xml` parsing (SAX + optional DOM fallback). */
77
object XmlParser {
8+
/**
9+
* Parses all obfuscatable string entries from [file].
10+
*
11+
* @param file XML file to parse.
12+
* @param domFallback Parser to use when SAX cannot preserve nested markup semantics.
13+
* @return Ordered list of obfuscatable [StringEntity] values.
14+
*/
815
fun parse(
916
file: File,
1017
domFallback: (File) -> List<StringEntity>,
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package dev.vyp.stringcare.plugin.internal
2+
3+
@Suppress("NOTHING_TO_INLINE")
4+
inline fun ByteArray.toReadableString(): String = joinToString(separator = ", ") { it.toString() }

0 commit comments

Comments
 (0)