2 Commits
Author SHA1 Message Date
unplayable 118d5ae93c Merge pull request #1 from DefiantWanderer/ci/jenkins-pipeline
Add Jenkinsfile for CI build pipeline
2026-09-17 12:28:44 +08:00
harineandClaude Sonnet 5 bf090b2526 Add Jenkinsfile for CI build pipeline
Builds the Debug APK and runs unit tests on push, using the Android
SDK/NDK toolchain installed on the team19 VM (Goal 2 of the course's
VM onboarding). Runs on Jenkins at http://51.79.242.163:5001.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 11:21:58 +08:00
6 changed files with 56 additions and 10 deletions
Vendored
+54
View File
@@ -0,0 +1,54 @@
// CI pipeline for BowlEye (PinPoint). Builds the Debug APK and runs unit
// tests on every push. Runs on the team19 VM's Jenkins install, using the
// Android SDK at /opt/android-sdk (see docs/ for the VM setup this depends on).
pipeline {
agent any
options {
timeout(time: 45, unit: 'MINUTES')
disableConcurrentBuilds()
buildDiscarder(logRotator(numToKeepStr: '20'))
}
environment {
ANDROID_HOME = '/opt/android-sdk'
ANDROID_SDK_ROOT = '/opt/android-sdk'
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Prepare SDK pointer') {
steps {
// AGP will also pick up ANDROID_HOME from the environment, but
// writing local.properties explicitly keeps this working even
// if the job ever runs on a different agent/environment setup.
sh 'echo "sdk.dir=/opt/android-sdk" > local.properties'
}
}
stage('Build Debug APK') {
steps {
sh 'chmod +x ./gradlew'
sh './gradlew assembleDebug --no-daemon --stacktrace'
}
}
stage('Unit Tests') {
steps {
sh './gradlew testDebugUnitTest --no-daemon --stacktrace'
}
}
}
post {
always {
junit allowEmptyResults: true, testResults: '**/build/test-results/**/*.xml'
archiveArtifacts artifacts: 'app/build/outputs/apk/**/*.apk', allowEmptyArchive: true
}
}
}
@@ -445,10 +445,6 @@ class BowlingCameraActivity : AppCompatActivity(), CameraXController.Callback {
binding.textPoseFeedback.text = getString(R.string.pose_phase_slide_and_release) binding.textPoseFeedback.text = getString(R.string.pose_phase_slide_and_release)
binding.textPoseFeedback.setBackgroundColor(ContextCompat.getColor(this, R.color.Slide_and_release_ready)) binding.textPoseFeedback.setBackgroundColor(ContextCompat.getColor(this, R.color.Slide_and_release_ready))
} }
BowlingPhase.FOLLOW_THROUGH -> {
binding.textPoseFeedback.text = getString(R.string.pose_phase_follow_through)
binding.textPoseFeedback.setBackgroundColor(ContextCompat.getColor(this, R.color.Follow_through_ready))
}
else -> { else -> {
binding.textPoseFeedback.text = correction ?: getString(R.string.pose_phase_waiting) binding.textPoseFeedback.text = correction ?: getString(R.string.pose_phase_waiting)
binding.textPoseFeedback.setBackgroundColor(ContextCompat.getColor(this, R.color.Starting_stance_waiting)) binding.textPoseFeedback.setBackgroundColor(ContextCompat.getColor(this, R.color.Starting_stance_waiting))
@@ -406,7 +406,6 @@ class CameraXController(
BowlingPhase.BACK_SWING -> "Backswing" BowlingPhase.BACK_SWING -> "Backswing"
BowlingPhase.POWER_STEP -> "Power Step" BowlingPhase.POWER_STEP -> "Power Step"
BowlingPhase.SLIDE_AND_RELEASE -> "Slide & Release" BowlingPhase.SLIDE_AND_RELEASE -> "Slide & Release"
BowlingPhase.FOLLOW_THROUGH -> "Follow Through"
} }
val colorRes = when (phase) { val colorRes = when (phase) {
BowlingPhase.STARTING_STANCE -> R.color.Starting_stance_ready BowlingPhase.STARTING_STANCE -> R.color.Starting_stance_ready
@@ -415,7 +414,6 @@ class CameraXController(
BowlingPhase.BACK_SWING -> R.color.Back_swing_ready BowlingPhase.BACK_SWING -> R.color.Back_swing_ready
BowlingPhase.POWER_STEP -> R.color.Power_step_ready BowlingPhase.POWER_STEP -> R.color.Power_step_ready
BowlingPhase.SLIDE_AND_RELEASE -> R.color.Slide_and_release_ready BowlingPhase.SLIDE_AND_RELEASE -> R.color.Slide_and_release_ready
BowlingPhase.FOLLOW_THROUGH -> R.color.Follow_through_ready
} }
val textPaint = overlayTextPaint.apply { textSize = 14f * scale } val textPaint = overlayTextPaint.apply { textSize = 14f * scale }
val textWidth = textPaint.measureText(label) val textWidth = textPaint.measureText(label)
-1
View File
@@ -21,6 +21,5 @@
<color name="Back_swing_ready">#CC9C27B0</color> <!-- purple --> <color name="Back_swing_ready">#CC9C27B0</color> <!-- purple -->
<color name="Power_step_ready">#CCFF9800</color> <!-- orange/amber --> <color name="Power_step_ready">#CCFF9800</color> <!-- orange/amber -->
<color name="Slide_and_release_ready">#CCE91E63</color> <!-- pink/red --> <color name="Slide_and_release_ready">#CCE91E63</color> <!-- pink/red -->
<color name="Follow_through_ready">#CC00BCD4</color> <!-- cyan -->
<color name="final_position_highlight">#FFFFD600</color> <color name="final_position_highlight">#FFFFD600</color>
</resources> </resources>
+1 -2
View File
@@ -55,8 +55,7 @@
<string name="pose_phase_pushaway">Pushaway</string> <string name="pose_phase_pushaway">Pushaway</string>
<string name="pose_phase_back_swing">Backswing</string> <string name="pose_phase_back_swing">Backswing</string>
<string name="pose_phase_power_step">Power Step</string> <string name="pose_phase_power_step">Power Step</string>
<string name="pose_phase_slide_and_release">Slide and Release</string> <string name="pose_phase_slide_and_release">Slide &amp; Release</string>
<string name="pose_phase_follow_through">Follow Through</string>
<string name="pose_phase_waiting">Waiting for stance…</string> <string name="pose_phase_waiting">Waiting for stance…</string>
<string name="pose_metrics_format">Torso: %1$s · Knee L: %2$s R: %3$s\nElbow L: %4$s R: %5$s</string> <string name="pose_metrics_format">Torso: %1$s · Knee L: %2$s R: %3$s\nElbow L: %4$s R: %5$s</string>
</resources> </resources>
+1 -1
View File
@@ -1,5 +1,5 @@
[versions] [versions]
agp = "9.3.0" agp = "9.4.0"
coreKtx = "1.18.0" coreKtx = "1.18.0"
junit = "4.13.2" junit = "4.13.2"
junitVersion = "1.3.0" junitVersion = "1.3.0"