Skip to content

Commit f2851f6

Browse files
feat: add automated localized test APK delivery with Telegram integration
1 parent e793f8c commit f2851f6

10 files changed

Lines changed: 260 additions & 10 deletions

File tree

.github/workflows/build.yml

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ on:
1010
build_type:
1111
description: 'Build Type'
1212
required: true
13-
default: 'debug'
13+
default: 'test'
1414
type: choice
1515
options:
16-
- debug
16+
- test
1717
- release
1818
push:
1919
branches:
@@ -27,7 +27,7 @@ jobs:
2727
contents: write
2828

2929
env:
30-
BUILD_TYPE: ${{ github.event.inputs.build_type == 'release' && 'release' || 'debug' }}
30+
BUILD_TYPE: ${{ github.event.inputs.build_type == 'release' && 'release' || 'releaseTesting' }}
3131

3232
steps:
3333
- name: Checkout code
@@ -36,6 +36,13 @@ jobs:
3636
submodules: 'recursive'
3737
fetch-depth: '0'
3838

39+
- name: Prepare commit metadata
40+
id: commit_metadata
41+
shell: bash
42+
run: |
43+
echo "message=$(git log -1 --pretty=%s)" >> "$GITHUB_OUTPUT"
44+
echo "sha=$(git rev-parse --short=7 HEAD)" >> "$GITHUB_OUTPUT"
45+
3946
- name: Setup Android SDK
4047
uses: android-actions/setup-android@v4
4148
with:
@@ -108,15 +115,15 @@ jobs:
108115
echo "sdk.dir=${ANDROID_HOME}" > local.properties
109116
chmod 755 gradlew
110117
111-
if [ "${{ env.BUILD_TYPE }}" == "debug" ]; then
112-
echo "Starting DEBUG build process..."
113-
./gradlew assembleDebug --console=plain \
118+
if [ "${{ env.BUILD_TYPE }}" == "releaseTesting" ]; then
119+
echo "Starting TEST build process with R8..."
120+
./gradlew assembleReleaseTesting --console=plain \
114121
-PKS_PATH="${{ steps.android_keystore.outputs.filePath }}" \
115122
-PKS_PASS="${{ secrets.APP_KEYSTORE_PASSWORD }}" \
116123
-PKS_ALIAS="${{ secrets.APP_KEYSTORE_ALIAS }}" \
117124
-PKEY_PASS="${{ secrets.APP_KEY_PASSWORD }}"
118125
else
119-
echo "Starting RELEASE build process..."
126+
echo "Starting RELEASE build process with R8..."
120127
./gradlew assembleRelease --console=plain \
121128
-PKS_PATH="${{ steps.android_keystore.outputs.filePath }}" \
122129
-PKS_PASS="${{ secrets.APP_KEYSTORE_PASSWORD }}" \
@@ -145,6 +152,34 @@ jobs:
145152
name: x86-apk-${{ env.BUILD_TYPE }}
146153
path: ${{ github.workspace }}/MikuRay/app/build/outputs/apk/${{ env.BUILD_TYPE }}/*x86*.apk
147154

155+
- name: Check Telegram configuration
156+
id: telegram-config
157+
if: ${{ success() && env.BUILD_TYPE == 'releaseTesting' }}
158+
shell: bash
159+
env:
160+
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
161+
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
162+
run: |
163+
if [[ -n "${TELEGRAM_BOT_TOKEN}" && -n "${TELEGRAM_CHAT_ID}" ]]; then
164+
echo "configured=true" >> "$GITHUB_OUTPUT"
165+
else
166+
echo "configured=false" >> "$GITHUB_OUTPUT"
167+
echo "Telegram upload skipped: Telegram secrets are not configured."
168+
fi
169+
170+
- name: Upload APKs to Telegram
171+
if: ${{ env.BUILD_TYPE == 'releaseTesting' && steps.telegram-config.outputs.configured == 'true' }}
172+
shell: bash
173+
env:
174+
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
175+
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
176+
APK_DIR: ${{ github.workspace }}/MikuRay/app/build/outputs/apk/${{ env.BUILD_TYPE }}
177+
BUILD_TYPE: ${{ env.BUILD_TYPE }}
178+
TELEGRAM_COMMIT_MESSAGE: ${{ steps.commit_metadata.outputs.message }}
179+
TELEGRAM_COMMIT_SHA: ${{ steps.commit_metadata.outputs.sha }}
180+
TELEGRAM_COMMIT_URL: ${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }}
181+
run: bash ./scripts/upload-apks-to-telegram.sh
182+
148183
- name: Upload to release
149184
uses: svenstaro/upload-release-action@v2
150185
if: github.event.inputs.release_tag != ''

MikuRay/app/build.gradle.kts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@ plugins {
88
alias(libs.plugins.aboutLibraries)
99
}
1010

11+
aboutLibraries {
12+
collect {
13+
includeTestVariants.set(true)
14+
filterVariants.addAll("debug", "release", "releaseTesting")
15+
}
16+
}
17+
1118
val appVersionName = "2.3.0"
1219
val appVersionCode = 740
1320

@@ -98,6 +105,17 @@ android {
98105
isMinifyEnabled = false
99106
isShrinkResources = false
100107
}
108+
109+
create("releaseTesting") {
110+
initWith(getByName("release"))
111+
matchingFallbacks += listOf("release")
112+
if (project.hasProperty("KS_PATH")) {
113+
signingConfig = signingConfigs.getByName("appSigning")
114+
}
115+
isDebuggable = false
116+
isMinifyEnabled = true
117+
isShrinkResources = true
118+
}
101119
}
102120

103121
sourceSets {
@@ -150,9 +168,10 @@ androidComponents {
150168
output.versionCode.set((1000000 * multiplier) + appVersionCode)
151169
}
152170

153-
if (variant.buildType == "debug") {
171+
if (variant.buildType == "debug" || variant.buildType == "releaseTesting") {
154172
val abiSuffix = if (abi != "universal") "-$abi" else ""
155-
output.outputFileName.set("MikuRay_$appVersionName$abiSuffix-debug-$apkBuildDate.apk")
173+
val buildLabel = if (variant.buildType == "releaseTesting") "test" else variant.buildType
174+
output.outputFileName.set("MikuRay_$appVersionName$abiSuffix-$buildLabel-$apkBuildDate.apk")
156175
}
157176
}
158177
}

MikuRay/app/proguard-rules.pro

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
-repackageclasses
22
-allowaccessmodification
3+
-dontobfuscate
34
-keep class com.miku.ray.** { *; }
45
-keep class com.yalantis.ucrop.** { *; }
56

MikuRay/app/src/main/java/com/miku/ray/AppConfig.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ object AppConfig {
185185
const val PREF_ACTION_CLEAR_TOTAL_TRAFFIC = "action_clear_total_traffic"
186186
const val PREF_SEARCH_CHIP_GRADIENT = "pref_search_chip_gradient"
187187
const val PREF_SHOW_REALTIME_TRAFFIC_IP = "pref_show_realtime_traffic_ip"
188+
const val PREF_DISMISS_TEST_BUILD_INFO = "pref_dismiss_test_build_info"
188189

189190
const val PREF_CUSTOM_PROFILE_NAME = "custom_profile_name"
190191
const val PREF_PROFILE_BANNER_URI = "profile_banner_uri"

MikuRay/app/src/main/java/com/miku/ray/ui/main/MainActivity.kt

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package com.miku.ray.ui.main
22

3-
43
import com.miku.ray.remixicon.R as RemixR
54
import android.content.BroadcastReceiver
65
import android.content.Context
@@ -10,6 +9,11 @@ import android.graphics.BitmapFactory
109
import android.graphics.Color
1110
import android.net.Uri
1211
import android.net.VpnService
12+
import android.text.style.ClickableSpan
13+
import android.text.SpannableString
14+
import android.text.Spanned
15+
import android.text.TextPaint
16+
import android.text.method.LinkMovementMethod
1317
import android.os.Build
1418
import android.os.Bundle
1519
import android.view.GestureDetector
@@ -171,6 +175,7 @@ class MainActivity : HelperBaseActivity(),
171175
override fun onCreate(savedInstanceState: Bundle?) {
172176
super.onCreate(savedInstanceState)
173177
setContentView(binding.root)
178+
showTestBuildInfoIfNeeded()
174179

175180
hideLoading()
176181
window.statusBarColor = Color.TRANSPARENT
@@ -193,6 +198,51 @@ class MainActivity : HelperBaseActivity(),
193198
checkAndRequestPermission(PermissionType.POST_NOTIFICATIONS) {}
194199
}
195200

201+
private fun showTestBuildInfoIfNeeded() {
202+
if (BuildConfig.BUILD_TYPE != "releaseTesting") return
203+
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_DISMISS_TEST_BUILD_INFO, false)) return
204+
205+
val channelLabel = getString(R.string.test_build_info_channel)
206+
val message = getString(R.string.test_build_info_message, channelLabel)
207+
val styledMessage = SpannableString(message)
208+
val channelStart = message.indexOf(channelLabel)
209+
if (channelStart >= 0) {
210+
styledMessage.setSpan(
211+
object : ClickableSpan() {
212+
override fun onClick(widget: View) {
213+
startActivity(
214+
Intent(
215+
Intent.ACTION_VIEW,
216+
Uri.parse("https://t.me/uwuowoumuchannel")
217+
)
218+
)
219+
}
220+
221+
override fun updateDrawState(ds: TextPaint) {
222+
super.updateDrawState(ds)
223+
ds.color = getColorAttr("colorPrimary")
224+
ds.isUnderlineText = false
225+
}
226+
},
227+
channelStart,
228+
channelStart + channelLabel.length,
229+
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
230+
)
231+
}
232+
233+
val dialog = MaterialAlertDialogBuilder(this)
234+
.setTitle(R.string.test_build_info_title)
235+
.setIcon(RemixR.drawable.rmx_system_alarm_warning_line)
236+
.setMessage(styledMessage)
237+
.setPositiveButton(android.R.string.ok, null)
238+
.setNegativeButton(R.string.test_build_info_dont_show_again) { _, _ ->
239+
MmkvManager.encodeSettings(AppConfig.PREF_DISMISS_TEST_BUILD_INFO, true)
240+
}
241+
.showBlur()
242+
243+
dialog.findViewById<TextView>(android.R.id.message)?.movementMethod = LinkMovementMethod.getInstance()
244+
}
245+
196246
override fun onResume() {
197247
super.onResume()
198248

MikuRay/app/src/main/res/values-in/strings.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<resources>
3+
<string name="test_build_info_title">Build pengujian</string>
4+
<string name="test_build_info_message">Ini adalah build pengujian MikuRay. Kalau menemukan bug, kabari owner atau laporkan di Telegram grup dengan men-tag owner.\n\nTelegram channel: %1$s</string>
5+
<string name="test_build_info_channel">@uwuowoumuchannel</string>
6+
<string name="test_build_info_dont_show_again">Jangan tampilkan lagi</string>
37
<string name="app_widget_name">Tombol Switch</string>
48
<string name="widget_status_connected">Konek</string>
59
<string name="widget_status_disconnected">Gak konek</string>

MikuRay/app/src/main/res/values-ru/strings.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<resources>
3+
<string name="test_build_info_title">Тестовая сборка</string>
4+
<string name="test_build_info_message">Это тестовая сборка MikuRay. Если вы нашли баг, пожалуйста, сообщите владельцу или напишите в группе Telegram, отметив владельца.\n\nTelegram-канал: %1$s</string>
5+
<string name="test_build_info_channel">@uwuowoumuchannel</string>
6+
<string name="test_build_info_dont_show_again">Больше не показывать</string>
37
<string name="app_widget_name">Переключатель</string>
48
<string name="widget_status_connected">Подключено</string>
59
<string name="widget_status_disconnected">Отключено</string>

MikuRay/app/src/main/res/values-zh-rCN/strings.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<resources>
3+
<string name="test_build_info_title">测试版本</string>
4+
<string name="test_build_info_message">这是 MikuRay 的测试构建版。如果发现错误,请通知群主或在 Telegram 群组中通过@群主进行反馈。\n\nTelegram 频道:%1$s</string>
5+
<string name="test_build_info_channel">@uwuowoumuchannel</string>
6+
<string name="test_build_info_dont_show_again">不再显示</string>
37
<string name="app_widget_name">开关</string>
48
<string name="widget_status_connected">已连接</string>
59
<string name="widget_status_disconnected">未连接</string>

MikuRay/app/src/main/res/values/strings.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<resources>
33
<string name="app_name" translatable="false">MikuRay</string>
4+
<string name="test_build_info_title">Test build</string>
5+
<string name="test_build_info_message">This is a test build of MikuRay. If you find a bug, please notify the owner or report it in the Telegram group by tagging the owner.\n\nTelegram channel: %1$s</string>
6+
<string name="test_build_info_channel">@uwuowoumuchannel</string>
7+
<string name="test_build_info_dont_show_again">Don’t show again</string>
48
<string name="app_widget_name">Switch</string>
59
<string name="widget_status_connected">Connected</string>
610
<string name="widget_status_disconnected">Disconnected</string>

scripts/upload-apks-to-telegram.sh

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
: "${TELEGRAM_BOT_TOKEN:?TELEGRAM_BOT_TOKEN is required}"
5+
: "${TELEGRAM_CHAT_ID:?TELEGRAM_CHAT_ID is required}"
6+
7+
APK_DIR="${APK_DIR:-MikuRay/app/build/outputs/apk/${BUILD_TYPE:-debug}}"
8+
COMMIT_MESSAGE="${TELEGRAM_COMMIT_MESSAGE:-${TELEGRAM_CAPTION:-MikuRay APK build}}"
9+
COMMIT_SHA="${TELEGRAM_COMMIT_SHA:-}"
10+
COMMIT_URL="${TELEGRAM_COMMIT_URL:-}"
11+
MAX_FILE_BYTES="${MAX_FILE_BYTES:-50000000}"
12+
13+
escape_html() {
14+
local value="$1"
15+
value="${value//&/&amp;}"
16+
value="${value//</\&lt;}"
17+
value="${value//>/\&gt;}"
18+
value="${value//\"/\&quot;}"
19+
printf '%s' "${value}"
20+
}
21+
22+
escaped_commit_message="$(escape_html "${COMMIT_MESSAGE}")"
23+
if [[ -n "${COMMIT_SHA}" ]]; then
24+
commit_label="${escaped_commit_message} (${COMMIT_SHA})"
25+
plain_commit_label="${COMMIT_MESSAGE} (${COMMIT_SHA})"
26+
else
27+
commit_label="${escaped_commit_message}"
28+
plain_commit_label="${COMMIT_MESSAGE}"
29+
fi
30+
if [[ -n "${COMMIT_URL}" ]]; then
31+
CAPTION="<a href=\"${COMMIT_URL}\">${commit_label}</a>"
32+
PLAIN_CAPTION="${plain_commit_label}"$'\n'"${COMMIT_URL}"
33+
else
34+
CAPTION="${commit_label}"
35+
PLAIN_CAPTION="${plain_commit_label}"
36+
fi
37+
38+
shopt -s nullglob
39+
apk_files=(
40+
"${APK_DIR}"/*arm64-v8a*.apk
41+
"${APK_DIR}"/*armeabi-v7a*.apk
42+
)
43+
44+
if (( ${#apk_files[@]} == 0 )); then
45+
echo "No ARM APK files found in ${APK_DIR}."
46+
exit 1
47+
fi
48+
49+
LAST_RESPONSE=""
50+
LAST_CURL_EXIT=0
51+
52+
send_document() {
53+
local apk_file="$1"
54+
local caption="$2"
55+
local use_html="$3"
56+
local response
57+
local curl_exit
58+
local -a curl_args
59+
60+
curl_args=(
61+
--fail-with-body
62+
--silent
63+
--show-error
64+
--retry 3
65+
--retry-delay 5
66+
--request POST
67+
"https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendDocument"
68+
--form "chat_id=${TELEGRAM_CHAT_ID}"
69+
--form "document=@${apk_file}"
70+
--form-string "caption=${caption}"
71+
--form-string "disable_notification=false"
72+
)
73+
if [[ "${use_html}" == "html" ]]; then
74+
curl_args+=(--form-string "parse_mode=HTML")
75+
fi
76+
77+
if response="$(curl "${curl_args[@]}" 2>&1)"; then
78+
curl_exit=0
79+
else
80+
curl_exit=$?
81+
fi
82+
LAST_RESPONSE="${response}"
83+
LAST_CURL_EXIT="${curl_exit}"
84+
85+
if [[ "${curl_exit}" -eq 0 ]] && grep -Eq '"ok"[[:space:]]*:[[:space:]]*true' <<<"${response}"; then
86+
return 0
87+
fi
88+
return 1
89+
}
90+
91+
is_html_error() {
92+
grep -Eiq "can't parse entities|unsupported start tag|unclosed tag" <<<"${LAST_RESPONSE}"
93+
}
94+
95+
failure_exit_code() {
96+
if [[ "${LAST_CURL_EXIT}" -ne 0 ]]; then
97+
printf '%s' "${LAST_CURL_EXIT}"
98+
else
99+
printf '1'
100+
fi
101+
}
102+
103+
for apk_file in "${apk_files[@]}"; do
104+
file_name="$(basename "${apk_file}")"
105+
file_size="$(stat -c '%s' "${apk_file}")"
106+
if (( file_size > MAX_FILE_BYTES )); then
107+
echo "Telegram upload skipped for ${file_name}: file size ${file_size} bytes exceeds ${MAX_FILE_BYTES}-byte limit." >&2
108+
exit 1
109+
fi
110+
111+
echo "Uploading ${file_name} to Telegram as an individual document..."
112+
if ! send_document "${apk_file}" "${CAPTION}" html; then
113+
if is_html_error; then
114+
echo "HTML caption was rejected; retrying ${file_name} with a plain-text caption." >&2
115+
if ! send_document "${apk_file}" "${PLAIN_CAPTION}" plain; then
116+
echo "Telegram upload failed for ${file_name} (curl exit ${LAST_CURL_EXIT})." >&2
117+
echo "Telegram response: ${LAST_RESPONSE}" >&2
118+
exit "$(failure_exit_code)"
119+
fi
120+
else
121+
echo "Telegram upload failed for ${file_name} (curl exit ${LAST_CURL_EXIT})." >&2
122+
echo "Telegram response: ${LAST_RESPONSE}" >&2
123+
exit "$(failure_exit_code)"
124+
fi
125+
fi
126+
done
127+
128+
echo "Uploaded ${#apk_files[@]} ARM APK file(s) to Telegram in separate messages."

0 commit comments

Comments
 (0)