Skip to content

Commit 9939bae

Browse files
committed
feat: Add CI/CD pipeline and enhance URL cleaning with composite rules
- Add comprehensive GitHub Actions workflow with multi-API testing - Add Renovate bot configuration for automated dependency management - Enhance URL cleaning with composite rule application (applies all matching rules) - Improve share intent UX (no auto-clipboard copy, cleaner messages) - Add comprehensive test coverage with unit and instrumented tests - Add Jacoco test coverage reporting and Codecov integration - Update build configuration with desugaring and enhanced test support Breaking changes: - Share intents no longer auto-copy cleaned URLs to clipboard - Toast messages simplified ("Cleaned" instead of "Cleaned → copied") Tests: ✅ All unit and integration tests pass Build: ✅ Debug APK builds successfully Lint: ✅ 10 warnings (acceptable with baseline) Coverage: ✅ Jacoco reporting configured CI: ✅ GitHub Actions workflow validated
1 parent 2fa5ad2 commit 9939bae

File tree

11 files changed

+730
-57
lines changed

11 files changed

+730
-57
lines changed

.github/workflows/ci.yml

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [ main, develop ]
6+
tags:
7+
- 'v*' # Trigger on version tags like v1.0.0
8+
pull_request:
9+
branches: [ main, develop ]
10+
11+
env:
12+
# Set this to 'true' in repository environment variables to enable instrumented tests
13+
RUN_INSTRUMENTED_TESTS: ${{ vars.RUN_INSTRUMENTED_TESTS == 'true' || github.event_name == 'pull_request' }}
14+
15+
jobs:
16+
test:
17+
runs-on: ubuntu-latest
18+
19+
steps:
20+
- name: Checkout code
21+
uses: actions/checkout@v4
22+
23+
- name: Set up JDK 17
24+
uses: actions/setup-java@v4
25+
with:
26+
java-version: '17'
27+
distribution: 'temurin'
28+
29+
- name: Cache Gradle packages
30+
uses: actions/cache@v4
31+
with:
32+
path: |
33+
~/.gradle/caches
34+
~/.gradle/wrapper
35+
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
36+
restore-keys: |
37+
${{ runner.os }}-gradle-
38+
39+
- name: Grant execute permission for gradlew
40+
run: chmod +x gradlew
41+
42+
- name: Run unit tests
43+
run: ./gradlew test --continue
44+
45+
- name: Generate coverage report
46+
run: ./gradlew jacocoTestReport
47+
48+
- name: Upload coverage to Codecov
49+
uses: codecov/codecov-action@v4
50+
with:
51+
token: ${{ secrets.CODECOV_TOKEN }}
52+
files: ./app/build/reports/jacoco/test/jacocoTestReport.xml
53+
flags: unittests
54+
name: codecov-umbrella
55+
fail_ci_if_error: false
56+
57+
- name: Run lint
58+
run: ./gradlew lint
59+
60+
- name: Build debug APK
61+
run: ./gradlew assembleDebug
62+
63+
- name: Upload test results
64+
uses: actions/upload-artifact@v4
65+
if: always()
66+
with:
67+
name: test-results
68+
path: app/build/reports/tests/
69+
retention-days: 30
70+
71+
- name: Upload lint results
72+
uses: actions/upload-artifact@v4
73+
if: always()
74+
with:
75+
name: lint-results
76+
path: app/build/reports/lint-results-debug.html
77+
retention-days: 30
78+
79+
- name: Upload APK
80+
uses: actions/upload-artifact@v4
81+
with:
82+
name: debug-apk
83+
path: app/build/outputs/apk/debug/app-debug.apk
84+
retention-days: 7
85+
86+
# Matrix instrumented tests for minSdk and targetSdk
87+
instrumented-tests:
88+
runs-on: ubuntu-latest
89+
if: ${{ needs.test.result == 'success' && (vars.RUN_INSTRUMENTED_TESTS == 'true' || github.event_name == 'pull_request') }}
90+
needs: test
91+
strategy:
92+
fail-fast: false
93+
matrix:
94+
api-level: [29, 36] # minSdk and targetSdk
95+
include:
96+
- api-level: 29
97+
target: default
98+
arch: x86_64
99+
- api-level: 36
100+
target: google_apis
101+
arch: x86_64
102+
103+
steps:
104+
- name: Checkout code
105+
uses: actions/checkout@v4
106+
107+
- name: Set up JDK 17
108+
uses: actions/setup-java@v4
109+
with:
110+
java-version: '17'
111+
distribution: 'temurin'
112+
113+
- name: Cache Gradle packages
114+
uses: actions/cache@v4
115+
with:
116+
path: |
117+
~/.gradle/caches
118+
~/.gradle/wrapper
119+
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
120+
restore-keys: |
121+
${{ runner.os }}-gradle-
122+
123+
- name: Grant execute permission for gradlew
124+
run: chmod +x gradlew
125+
126+
- name: Enable KVM group perms
127+
run: |
128+
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
129+
sudo udevadm control --reload-rules
130+
sudo udevadm trigger --name-match=kvm
131+
132+
- name: Set up Android SDK
133+
uses: android-actions/setup-android@v3
134+
135+
- name: AVD cache
136+
uses: actions/cache@v4
137+
id: avd-cache
138+
with:
139+
path: |
140+
~/.android/avd/*
141+
~/.android/adb*
142+
key: avd-${{ matrix.api-level }}-${{ matrix.target }}-${{ matrix.arch }}
143+
144+
- name: Create AVD and generate snapshot for caching
145+
if: steps.avd-cache.outputs.cache-hit != 'true'
146+
uses: reactivecircus/android-emulator-runner@v2
147+
with:
148+
api-level: ${{ matrix.api-level }}
149+
target: ${{ matrix.target }}
150+
arch: ${{ matrix.arch }}
151+
force-avd-creation: false
152+
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
153+
disable-animations: false
154+
script: echo "Generated AVD snapshot for caching."
155+
156+
- name: Run instrumented tests
157+
uses: reactivecircus/android-emulator-runner@v2
158+
with:
159+
api-level: ${{ matrix.api-level }}
160+
target: ${{ matrix.target }}
161+
arch: ${{ matrix.arch }}
162+
force-avd-creation: false
163+
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
164+
disable-animations: true
165+
script: ./gradlew connectedAndroidTest
166+
167+
- name: Upload instrumented test results
168+
uses: actions/upload-artifact@v4
169+
if: always()
170+
with:
171+
name: instrumented-test-results-api-${{ matrix.api-level }}
172+
path: app/build/reports/androidTests/
173+
retention-days: 30
174+
175+
# Tag-triggered release job
176+
release:
177+
runs-on: ubuntu-latest
178+
if: startsWith(github.ref, 'refs/tags/v')
179+
needs: test
180+
181+
steps:
182+
- name: Checkout code
183+
uses: actions/checkout@v4
184+
185+
- name: Set up JDK 17
186+
uses: actions/setup-java@v4
187+
with:
188+
java-version: '17'
189+
distribution: 'temurin'
190+
191+
- name: Cache Gradle packages
192+
uses: actions/cache@v4
193+
with:
194+
path: |
195+
~/.gradle/caches
196+
~/.gradle/wrapper
197+
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
198+
restore-keys: |
199+
${{ runner.os }}-gradle-
200+
201+
- name: Grant execute permission for gradlew
202+
run: chmod +x gradlew
203+
204+
- name: Build release APK
205+
run: ./gradlew assembleRelease
206+
207+
- name: Get tag name
208+
id: tag
209+
run: echo "tag=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
210+
211+
- name: Create GitHub Release
212+
uses: softprops/action-gh-release@v2
213+
with:
214+
files: |
215+
app/build/outputs/apk/release/app-release-unsigned.apk
216+
name: Release ${{ steps.tag.outputs.tag }}
217+
body: |
218+
Release ${{ steps.tag.outputs.tag }}
219+
220+
## What's Changed
221+
- See commit history for detailed changes
222+
223+
## Installation
224+
Download the APK and install on your Android device (API 29+)
225+
draft: false
226+
prerelease: ${{ contains(steps.tag.outputs.tag, 'beta') || contains(steps.tag.outputs.tag, 'alpha') || contains(steps.tag.outputs.tag, 'rc') }}
227+
generate_release_notes: true

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# Detracktor
2+
![vibe-coded](https://img.shields.io/badge/vibe--coded-✨-blue)
23

34
A tiny Android app that cleans URLs by removing tracking parameters on demand.
45

app/build.gradle.kts

Lines changed: 99 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
1+
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
12
plugins {
23
alias(libs.plugins.android.application)
34
alias(libs.plugins.kotlin.android)
45
alias(libs.plugins.kotlin.compose)
6+
id("jacoco")
7+
}
8+
9+
kotlin {
10+
compilerOptions {
11+
jvmTarget.set(JvmTarget.JVM_11)
12+
}
513
}
614

715
android {
@@ -19,6 +27,10 @@ android {
1927
}
2028

2129
buildTypes {
30+
debug {
31+
enableUnitTestCoverage = true // Enable coverage for debug builds
32+
enableAndroidTestCoverage = true
33+
}
2234
release {
2335
isMinifyEnabled = false
2436
proguardFiles(
@@ -33,19 +45,25 @@ android {
3345
// Enable desugaring for newer Java features
3446
isCoreLibraryDesugaringEnabled = true
3547
}
36-
kotlinOptions {
37-
jvmTarget = "11"
38-
}
48+
3949
buildFeatures {
4050
compose = true
4151
}
52+
testOptions {
53+
animationsDisabled = true
54+
unitTests {
55+
isReturnDefaultValues = true
56+
all {
57+
it.useJUnitPlatform()
58+
}
59+
}
60+
}
4261
lint {
4362
baseline = file("lint-baseline.xml")
4463
}
4564
}
4665

4766
dependencies {
48-
4967
implementation(libs.androidx.core.ktx)
5068
implementation(libs.androidx.lifecycle.runtime.ktx)
5169
implementation(libs.androidx.activity.compose)
@@ -80,3 +98,80 @@ dependencies {
8098
debugImplementation(libs.androidx.ui.tooling)
8199
debugImplementation(libs.androidx.ui.test.manifest)
82100
}
101+
102+
// Jacoco configuration
103+
jacoco {
104+
toolVersion = "0.8.13"
105+
}
106+
107+
tasks.register<JacocoReport>("jacocoTestReport") {
108+
dependsOn("testDebugUnitTest")
109+
110+
reports {
111+
xml.required.set(true)
112+
html.required.set(true)
113+
csv.required.set(false)
114+
}
115+
116+
val fileFilter = listOf(
117+
"**/R.class",
118+
"**/R$*.class",
119+
"**/BuildConfig.*",
120+
"**/Manifest*.*",
121+
"**/*Test*.*",
122+
"android/**/*.*",
123+
"**/databinding/**/*.*",
124+
"**/generated/**/*.*",
125+
"**/compose/**/*.*" // Exclude Compose generated classes
126+
)
127+
128+
val debugTree = fileTree("${layout.buildDirectory.get()}/tmp/kotlin-classes/debug") {
129+
exclude(fileFilter)
130+
}
131+
val mainSrc = "${project.projectDir}/src/main/java"
132+
val kotlinSrc = "${project.projectDir}/src/main/kotlin"
133+
134+
sourceDirectories.setFrom(files(mainSrc, kotlinSrc))
135+
classDirectories.setFrom(files(debugTree))
136+
executionData.setFrom(fileTree(layout.buildDirectory.get()) {
137+
include("jacoco/testDebugUnitTest.exec")
138+
})
139+
}
140+
141+
// Optional: Task to combine unit and instrumented test coverage
142+
tasks.register<JacocoReport>("jacocoFullReport") {
143+
dependsOn("testDebugUnitTest", "createDebugCoverageReport")
144+
145+
reports {
146+
xml.required.set(true)
147+
html.required.set(true)
148+
csv.required.set(false)
149+
}
150+
151+
val fileFilter = listOf(
152+
"**/R.class",
153+
"**/R$*.class",
154+
"**/BuildConfig.*",
155+
"**/Manifest*.*",
156+
"**/*Test*.*",
157+
"android/**/*.*",
158+
"**/databinding/**/*.*",
159+
"**/generated/**/*.*",
160+
"**/compose/**/*.*"
161+
)
162+
163+
val debugTree = fileTree("${layout.buildDirectory.get()}/tmp/kotlin-classes/debug") {
164+
exclude(fileFilter)
165+
}
166+
val mainSrc = "${project.projectDir}/src/main/java"
167+
val kotlinSrc = "${project.projectDir}/src/main/kotlin"
168+
169+
sourceDirectories.setFrom(files(mainSrc, kotlinSrc))
170+
classDirectories.setFrom(files(debugTree))
171+
executionData.setFrom(fileTree(layout.buildDirectory.get()) {
172+
include(
173+
"jacoco/testDebugUnitTest.exec",
174+
"outputs/code_coverage/debugAndroidTest/connected/coverage.ec"
175+
)
176+
})
177+
}

0 commit comments

Comments
 (0)