Author SHA1 Message Date
yilsem 47a7e35fea Run Android builds on the new server Linux runner
Android acceptance / android (push) Successful in 26m25s
2026-10-11 04:10:51 +03:00
yilsemandClaude Opus 5.5 8b485c0bad ci: check out from the new Gitea (gitea.turkservis.online)
Android acceptance / android (push) Canceled after 0s
TurkServis moved to the new server on 11 Oct 2026; CI now runs on its runner.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 03:53:15 +03:00
yilsem 6c17c3f04b test(cron): verify phone editor save/edit/run over real HTTP; clarify server timezone 2026-10-08 04:28:34 +03:00
6 changed files with 160 additions and 17 deletions

No files matched your search

+23 -15
View File
@@ -7,30 +7,38 @@ on:
jobs:
android:
runs-on: windows-latest
runs-on: ubuntu-24.04
timeout-minutes: 40
steps:
- uses: actions/checkout@v4
with:
github-server-url: https://gitea.twinpay.one
github-server-url: https://gitea.turkservis.online
- name: Set up Java 21
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
- name: Set up Android SDK
shell: powershell
run: ./scripts/setup-android-ci.ps1
shell: bash
run: bash scripts/setup-android-ci.sh
- name: Test and build exact commit
shell: powershell
shell: bash
run: |
$ErrorActionPreference = 'Stop'
if ((git rev-parse HEAD).Trim() -ne $env:GITHUB_SHA) { throw 'Checkout SHA mismatch' }
Get-CimInstance Win32_OperatingSystem | Select-Object TotalVisibleMemorySize, FreePhysicalMemory
# A fresh runner compiles both variants from scratch. Keep their compiler
# heaps separate in time; the previous shared 2 GiB heap exhausted itself.
$buildArgs = @('--no-daemon', '--console=plain', '--max-workers=1', '-Pkotlin.daemon.jvmargs=-Xmx4096m')
.\gradlew.bat testDebugUnitTest assembleDebug @buildArgs
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
.\gradlew.bat compileReleaseKotlin @buildArgs
exit $LASTEXITCODE
set -euo pipefail
test "$(git rev-parse HEAD)" = "$GITHUB_SHA"
uname -a
free -m
# One compiler process inside the server's bounded, serial CI runner.
build_args=(--no-daemon --console=plain --max-workers=1
'-Dorg.gradle.jvmargs=-Xmx2560m -Dfile.encoding=UTF-8'
'-Pkotlin.compiler.execution.strategy=in-process')
bash gradlew testDebugUnitTest assembleDebug "${build_args[@]}"
bash gradlew compileReleaseKotlin "${build_args[@]}"
- name: Keep exact-commit APK and test reports
uses: actions/upload-artifact@v3
with:
name: aiturk-android-${{ github.sha }}
if-no-files-found: error
path: |
app/build/outputs/apk/debug/app-debug.apk
app/build/reports/tests/testDebugUnitTest
@@ -0,0 +1,115 @@
package com.m57.hermescontrol.ui.cron
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.test.hasContentDescription
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollTo
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelStore
import androidx.lifecycle.ViewModelStoreOwner
import androidx.test.platform.app.InstrumentationRegistry
import com.m57.hermescontrol.BuildConfig
import com.m57.hermescontrol.R
import com.m57.hermescontrol.data.local.AuthManager
import com.m57.hermescontrol.data.remote.ApiClient
import java.net.URL
import org.json.JSONObject
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Assume.assumeTrue
import org.junit.Rule
import org.junit.Test
/** Opt-in test against a fresh, isolated real cron backend, never a user server. */
class ScheduleHttpAcceptanceTest {
@get:Rule val compose = createComposeRule()
@Test
fun editorCreatesEditsAndScheduledJobExecutesViaHttp() {
val args = InstrumentationRegistry.getArguments()
assumeTrue(args.getString("isolatedCronAcceptance") == "true")
val context = InstrumentationRegistry.getInstrumentation().targetContext
check(BuildConfig.DEBUG && context.packageName.endsWith(".dev"))
val url = "http://127.0.0.1:18765"
AuthManager.init(context)
kotlinx.coroutines.runBlocking { AuthManager.awaitInitialization() }
val oldUrl = AuthManager.getBaseUrl()
val oldToken = AuthManager.getToken()
val store = ViewModelStore()
val owner = object : ViewModelStoreOwner { override val viewModelStore = store }
try {
AuthManager.setBaseUrl(url)
AuthManager.setToken(null)
ApiClient.rebuild()
val vm = ViewModelProvider(owner)[CronJobsViewModel::class.java]
compose.setContent {
val state by vm.uiState.collectAsState()
MaterialTheme {
if (state.editorState.isOpen) {
CronJobEditorDialog(
state = state.editorState,
onFieldChange = vm::updateEditorField,
onToggleNoAgent = vm::toggleNoAgent,
onToggleRunContinuity = vm::toggleRunContinuity,
onSetMonitorMode = vm::setMonitorMode,
onSelectBlueprint = vm::selectBlueprint,
onBlueprintFieldChange = vm::updateBlueprintValue,
onSave = vm::saveEditor,
onDismiss = vm::closeEditor,
onClearToast = vm::clearEditorToast,
)
}
}
}
// Harmless execution fixture. Schedule is changed using the real clock below.
compose.runOnIdle {
vm.openNewJobDialog()
vm.updateEditorField("name", "Android HTTP clock acceptance")
vm.updateEditorField("script", "probe.py")
vm.updateEditorField("schedule", "59 23 * * *")
vm.toggleNoAgent()
}
compose.onNodeWithText("23:59").performScrollTo().performClick()
compose.onNodeWithText("59").performClick()
compose.onNode(hasContentDescription("55", substring = true)).performClick()
compose.onNodeWithText(context.getString(R.string.action_ok)).performClick()
compose.onNodeWithText(context.getString(R.string.cron_edit_action_save)).performClick()
compose.waitUntil(30_000) { !vm.uiState.value.editorState.isOpen }
compose.waitUntil(30_000) { vm.uiState.value.jobs.size == 1 }
val id = vm.uiState.value.jobs.single().id
compose.runOnIdle { vm.openEditJobDialog(id) }
compose.waitUntil(30_000) { !vm.uiState.value.editorState.isLoading }
compose.runOnIdle { assertEquals("55 23 * * *", vm.uiState.value.editorState.schedule) }
compose.onNodeWithText("23:55").performScrollTo()
compose.onNodeWithText(context.getString(R.string.cron_repeat_label)).performClick()
compose.onAllNodesWithText(context.getString(R.string.cron_repeat_minutely))[0].performClick()
compose.onNodeWithText(context.getString(R.string.cron_edit_action_save)).performClick()
compose.waitUntil(30_000) { !vm.uiState.value.editorState.isOpen }
val deadline = android.os.SystemClock.elapsedRealtime() + 90_000
var evidence = JSONObject()
while (android.os.SystemClock.elapsedRealtime() < deadline) {
evidence = JSONObject(URL("$url/acceptance/evidence").readText())
if (evidence.optString("result") == "PASS") break
android.os.SystemClock.sleep(500)
}
assertEquals(evidence.toString(), "PASS", evidence.optString("result"))
assertEquals(id, evidence.getString("job_id"))
assertEquals(1, evidence.getInt("runs"))
val requests = evidence.getJSONArray("requests")
assertEquals(2, requests.length())
assertEquals("55 23 * * *", requests.getJSONObject(0).getJSONObject("body").getString("schedule"))
assertEquals("* * * * *", requests.getJSONObject(1).getJSONObject("body").getJSONObject("updates").getString("schedule"))
assertTrue(requests.getJSONObject(1).getString("path").endsWith(id))
} finally {
compose.runOnIdle { store.clear() }
AuthManager.setBaseUrl(oldUrl)
AuthManager.setToken(oldToken)
ApiClient.rebuild()
}
}
}
+1 -1
View File
@@ -1950,5 +1950,5 @@
<string name="cron_repeat_calendar_note">Seçilen günün olmadığı ay veya yıllarda çalışmaz.</string>
<string name="cron_repeat_count">Aralık</string>
<string name="cron_repeat_unit">Birim</string>
<string name="cron_schedule_timezone_note">Saatler bağlı AITURK sunucusunda ayarlı saat dilimine göredir.</string>
<string name="cron_schedule_timezone_note">Saatler bağlı AITURK sunucusunun saat dilimine göredir. Sunucunun dilimi burada okunamıyor; telefonunuzdan farklı olabilir.</string>
</resources>
+1 -1
View File
@@ -1950,5 +1950,5 @@
<string name="cron_repeat_calendar_note">Skips months or years without the selected date.</string>
<string name="cron_repeat_count">Interval</string>
<string name="cron_repeat_unit">Unit</string>
<string name="cron_schedule_timezone_note">Times use the time zone configured on the connected AITURK server.</string>
<string name="cron_schedule_timezone_note">Times use the connected AITURK server’s time zone. Its zone cannot be read here and may differ from your phone’s.</string>
</resources>
@@ -15,6 +15,8 @@ class ScheduleSelectionTest {
"59 23 29 2 *" to ScheduleMode.YEARLY,
"0 0 1 1 *" to ScheduleMode.YEARLY,
"every 120m" to ScheduleMode.INTERVAL,
"every 2h" to ScheduleMode.INTERVAL,
"every 3d" to ScheduleMode.INTERVAL,
)
cases.forEach { (expression, mode) ->
val parsed = parseSchedule(expression)
+18
View File
@@ -0,0 +1,18 @@
#!/usr/bin/env bash
# Linux CI on the TurkServis server; no workstation Android installation required.
set -euo pipefail
sdk_root="${RUNNER_TEMP:?}/android-sdk"
archive="${RUNNER_TEMP}/commandlinetools-linux-16111833.zip"
mkdir -p "$sdk_root/cmdline-tools" "$sdk_root/licenses"
curl --fail --location --retry 3 --max-time 300 \
https://dl.google.com/android/repository/commandlinetools-linux-16111833_latest.zip -o "$archive"
# Official checksum from Google's repository2-3.xml, cmdline-tools;latest, linux.
printf '%s %s\n' e025545c62a8e64c7559119566a569fb1dec5f60 "$archive" | sha1sum -c -
unzip -q "$archive" -d "$sdk_root/cmdline-tools"
mv "$sdk_root/cmdline-tools/cmdline-tools" "$sdk_root/cmdline-tools/latest"
cp scripts/android-licenses/android-sdk-license "$sdk_root/licenses/"
"$sdk_root/cmdline-tools/latest/bin/sdkmanager" --sdk_root="$sdk_root" \
'platform-tools' 'platforms;android-37.0' 'build-tools;37.0.0'
test -f "$sdk_root/platforms/android-37.0/android.jar"
test -x "$sdk_root/build-tools/37.0.0/aapt2"
printf 'ANDROID_HOME=%s\nANDROID_SDK_ROOT=%s\n' "$sdk_root" "$sdk_root" >> "$GITHUB_ENV"