Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions .github/workflows/release-apk.yml
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,47 @@ jobs:
grep -F "versionName='$RELEASE_TAG'" "$RUNNER_TEMP/apk-badging.txt"
cp "$APK" "minimum-${RELEASE_TAG}-foss.apk"
sha256sum "minimum-${RELEASE_TAG}-foss.apk" > "minimum-${RELEASE_TAG}-foss.apk.sha256"
- name: Build temporary Wi-Fi provisioner
run: |
./gradlew -p tools/t99-wifi-provisioner :app:assembleDebug --no-daemon --stacktrace
test -f tools/t99-wifi-provisioner/app/build/outputs/apk/debug/app-debug.apk
- name: Package standalone Windows provisioning bundle
env:
RELEASE_TAG: ${{ inputs.tag }}
run: |
BUNDLE_NAME="minimum-provisioning-${RELEASE_TAG}"
BUNDLE_DIR="$RUNNER_TEMP/$BUNDLE_NAME"
mkdir -p "$BUNDLE_DIR/scripts" "$BUNDLE_DIR/assets"
cp "Provision Minimum Device.cmd" "$BUNDLE_DIR/"
cp "minimum-${RELEASE_TAG}-foss.apk" "$BUNDLE_DIR/minimum-foss.apk"
cp scripts/provision-minimum-device.ps1 "$BUNDLE_DIR/scripts/"
cp scripts/prepare-t99.ps1 "$BUNDLE_DIR/scripts/"
cp scripts/prepare-t56.ps1 "$BUNDLE_DIR/scripts/"
cp tools/t99-wifi-provisioner/app/build/outputs/apk/debug/app-debug.apk \
"$BUNDLE_DIR/assets/t99-wifi-provisioner.apk"
cp docs/PROVISIONING_BUNDLE_README.txt "$BUNDLE_DIR/README.txt"
printf '%s\n' "$RELEASE_TAG" > "$BUNDLE_DIR/VERSION.txt"
(
cd "$RUNNER_TEMP"
zip -r "$GITHUB_WORKSPACE/$BUNDLE_NAME.zip" "$BUNDLE_NAME"
)
unzip -l "$BUNDLE_NAME.zip" | tee "$RUNNER_TEMP/provisioning-bundle-files.txt"
for required in \
"$BUNDLE_NAME/Provision Minimum Device.cmd" \
"$BUNDLE_NAME/minimum-foss.apk" \
"$BUNDLE_NAME/scripts/provision-minimum-device.ps1" \
"$BUNDLE_NAME/scripts/prepare-t99.ps1" \
"$BUNDLE_NAME/scripts/prepare-t56.ps1" \
"$BUNDLE_NAME/assets/t99-wifi-provisioner.apk" \
"$BUNDLE_NAME/README.txt" \
"$BUNDLE_NAME/VERSION.txt"; do
grep -F "$required" "$RUNNER_TEMP/provisioning-bundle-files.txt"
done
if unzip -l "$BUNDLE_NAME.zip" | grep -E '(^|/)(\.secrets|signing\.gradle|.*\.jks)(/|$)'; then
echo "Provisioning bundle contains a forbidden secret/signing path." >&2
exit 1
fi
sha256sum "$BUNDLE_NAME.zip" > "$BUNDLE_NAME.zip.sha256"
- name: Publish GitHub release
env:
GH_TOKEN: ${{ github.token }}
Expand All @@ -137,4 +178,6 @@ jobs:
gh release create "$RELEASE_TAG" \
"minimum-${RELEASE_TAG}-foss.apk" \
"minimum-${RELEASE_TAG}-foss.apk.sha256" \
"minimum-provisioning-${RELEASE_TAG}.zip" \
"minimum-provisioning-${RELEASE_TAG}.zip.sha256" \
--verify-tag --generate-notes --title "Minimum $RELEASE_TAG" "${prerelease_flag[@]}"
5 changes: 5 additions & 0 deletions docs/DEVELOPMENT_RUNBOOK.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,11 @@ guided flow detects the active ADB port, explains how to authorize USB debugging
device menu when several radios are attached, and shows recommended/custom setup choices. It keeps
the window open on PASS or failure so the result is not lost.

For a field workstation without a source checkout, download
`minimum-provisioning-<tag>.zip` from the GitHub Release, verify its `.sha256`, extract the complete
folder and double-click the launcher there. That bundle includes the signed APK and temporary Wi-Fi
helper, so Gradle and the project source are not required on the operator workstation.

Connect only one unit of a given model for the final reboot check. The workflow verifies the exact hardware,
builds the FOSS debug APK when requested or when the default APK is missing, installs it without
clearing app data, runs the guarded model preparation, opens the Portal, installs the one-time device
Expand Down
6 changes: 5 additions & 1 deletion docs/GITHUB_RELEASE_WORKFLOW.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,11 @@ APRS passcodes and cached positions.

The manual `.github/workflows/release-apk.yml` workflow checks out an existing numeric `x.y.z...`
tag, requires the reviewed Android `versionCode`, builds `:app:assembleFossRelease`, verifies the
package/version/signature, creates a SHA-256 file and publishes the tagged GitHub Release. Its
package/version/signature, creates SHA-256 files and publishes the tagged GitHub Release. The same
release also contains `minimum-provisioning-<tag>.zip`, a standalone Windows bundle with the signed
APK, double-click launcher, guarded T99/T56 scripts, prebuilt temporary Wi-Fi helper and operator
README. The bundle uses the included APK/helper and does not require a source checkout or Gradle on
the field workstation. Its
protected `release` environment must provide:

- `MINIMUM_RELEASE_KEYSTORE_BASE64`
Expand Down
48 changes: 48 additions & 0 deletions docs/PROVISIONING_BUNDLE_README.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
Minimum device provisioning bundle
==================================

This bundle prepares one supported T99 or T56 radio on Windows.

Requirements
------------

- Windows 10 or Windows 11
- Android Platform Tools (adb.exe) available in PATH
- Internet access to https://minimum.vra.or.th/
- A Minimum Portal administrator account
- USB debugging enabled and authorized on the radio

Use
---

1. Extract the complete ZIP. Do not run files from inside the ZIP preview.
2. Connect and unlock one radio, then authorize USB debugging.
3. Double-click "Provision Minimum Device.cmd".
4. Press Enter for the recommended setup, or choose the custom menu.
5. Register the displayed Device ID and model in the Minimum Portal.
6. Copy the one-time device token and paste it into the hidden prompt.
7. Wait for PASS before disconnecting the radio.

ADB ports
---------

Port 5037 is the Android standard. Port 5041 is the Minimum lab alternative. The launcher detects
the port with an authorized device and presents a menu only when it cannot make a safe choice.

Security and safety
-------------------

- The device token is never printed or placed in an ADB command argument.
- Temporary token files are removed from Windows and the radio.
- The lab Wi-Fi credential is protected by Windows DPAPI for the current Windows account.
- Unknown hardware is inventory-reported and rejected before APK installation or provisioning.
- PASS requires managed config activation and Ready both before and after reboot.

An existing debug-signed Minimum APK cannot be upgraded in place by the release-signed APK. The
installer stops on a signature mismatch rather than clearing app data automatically. Preserve any
required device identity/config information and perform an explicitly approved uninstall before
switching a lab device from debug signing to release signing.

The included APK is signed and versioned by the GitHub Release workflow. Verify the SHA-256 files
on the Release page before use. This prerelease still requires physical acceptance on the target
T99/T56 hardware before it is promoted as a stable field release.
24 changes: 15 additions & 9 deletions scripts/prepare-t99.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -328,16 +328,22 @@ function Invoke-LabWifiProvisioning {
[Parameter(Mandatory)][System.Management.Automation.PSCredential]$Credential
)

$helperRoot = Join-Path $PSScriptRoot "..\tools\t99-wifi-provisioner"
$gradleWrapper = Join-Path $PSScriptRoot "..\gradlew.bat"
$helperApk = Join-Path $helperRoot "app\build\outputs\apk\debug\app-debug.apk"
if (-not (Test-Path -LiteralPath $gradleWrapper)) {
throw "Gradle wrapper is missing; cannot build the temporary Wi-Fi provisioner."
}
$bundledHelperApk = Join-Path $PSScriptRoot "..\assets\t99-wifi-provisioner.apk"
if (Test-Path -LiteralPath $bundledHelperApk -PathType Leaf) {
$helperApk = $bundledHelperApk
Write-Host "Using the Wi-Fi provisioner included in the Release bundle."
} else {
$helperRoot = Join-Path $PSScriptRoot "..\tools\t99-wifi-provisioner"
$gradleWrapper = Join-Path $PSScriptRoot "..\gradlew.bat"
$helperApk = Join-Path $helperRoot "app\build\outputs\apk\debug\app-debug.apk"
if (-not (Test-Path -LiteralPath $gradleWrapper)) {
throw "Neither the bundled Wi-Fi provisioner nor the Gradle wrapper is available."
}

& $gradleWrapper -p $helperRoot :app:assembleDebug
if ($LASTEXITCODE -ne 0 -or -not (Test-Path -LiteralPath $helperApk)) {
throw "Temporary Wi-Fi provisioner build failed."
& $gradleWrapper -p $helperRoot :app:assembleDebug
if ($LASTEXITCODE -ne 0 -or -not (Test-Path -LiteralPath $helperApk)) {
throw "Temporary Wi-Fi provisioner build failed."
}
}

$temporaryDirectory = Join-Path ([IO.Path]::GetTempPath()) (
Expand Down
62 changes: 57 additions & 5 deletions scripts/provision-minimum-device.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,14 @@ $IdentityReportAction = "se.lublin.mumla.action.PROVISION_REPORT_IDENTITY"
$ProvisionStatusAction = "se.lublin.mumla.action.PROVISION_REPORT_STATUS"
$CredentialProvisionAction = "se.lublin.mumla.action.PROVISION_DEVICE_CONFIG_CREDENTIAL"
$RepositoryRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
$DefaultApkPath = Join-Path $RepositoryRoot "app\build\outputs\apk\foss\debug\mumla-foss-debug.apk"
$BundledApkPath = Join-Path $RepositoryRoot "minimum-foss.apk"
$SourceBuildApkPath = Join-Path $RepositoryRoot "app\build\outputs\apk\foss\debug\mumla-foss-debug.apk"
$SourceBuildAvailable = Test-Path -LiteralPath (Join-Path $RepositoryRoot "gradlew.bat") -PathType Leaf
$DefaultApkPath = if (Test-Path -LiteralPath $BundledApkPath -PathType Leaf) {
$BundledApkPath
} else {
$SourceBuildApkPath
}
try {
$adbPath = (Get-Command adb -ErrorAction Stop).Source
} catch {
Expand Down Expand Up @@ -149,7 +156,11 @@ function Show-GuidedSetupMenu {

Write-Host ""
Write-Host "Recommended setup will:"
Write-Host " - build and install the latest Minimum test APK"
if ($SourceBuildAvailable) {
Write-Host " - build and install the latest Minimum test APK"
} else {
Write-Host " - install the signed Minimum APK included in this Release bundle"
}
Write-Host " - configure lab Wi-Fi and managed Location"
Write-Host " - remove Zello for Android user 0"
Write-Host " - open the Portal for registration and a hidden token prompt"
Expand All @@ -164,13 +175,17 @@ function Show-GuidedSetupMenu {
Write-Host "Cancelled. No APK was installed and no provisioning change was made."
exit 0
}
$script:BuildApk = $true
$script:BuildApk = $SourceBuildAvailable
if (-not $mode) {
return
}

$answer = (Read-Host "Build the latest APK? [Y/n]").Trim()
if ($answer -ieq "N") { $script:BuildApk = $false }
if ($SourceBuildAvailable) {
$answer = (Read-Host "Build the latest APK? [Y/n]").Trim()
if ($answer -ieq "N") { $script:BuildApk = $false }
} else {
Write-Host "Source build tools are not included; the bundled signed APK will be used."
}

$answer = (Read-Host "Configure/verify lab Wi-Fi? [Y/n]").Trim()
if ($answer -ieq "N") { $script:SkipLabWifi = $true }
Expand Down Expand Up @@ -441,6 +456,42 @@ function Build-MinimumApk {
}
}

function Test-TargetLabWifiConnected {
$connectivity = (Invoke-TargetAdb -Arguments @("shell", "dumpsys", "connectivity")) -join "`n"
$escapedSsid = [regex]::Escape($LabWifiSsid)
return $connectivity -match
"(?s)type:\s*WIFI.*?state:\s*CONNECTED/CONNECTED.*?extra:\s*`"$escapedSsid`""
}

function Ensure-LabWifiCredential {
param([Parameter(Mandatory)][string]$Profile)
if ($SkipLabWifi -or (Test-TargetLabWifiConnected)) {
return
}
if (-not $LabWifiCredentialPath) {
$credentialName = "{0}-lab-wifi.credential.xml" -f $Profile.ToLowerInvariant()
$script:LabWifiCredentialPath = Join-Path $PSScriptRoot ".secrets\$credentialName"
}
if (Test-Path -LiteralPath $LabWifiCredentialPath) {
return
}
if ($NonInteractive) {
throw "Lab Wi-Fi is not connected. Create the DPAPI credential or pass -SkipLabWifi."
}

Write-Host "Lab Wi-Fi '$LabWifiSsid' is not connected. Windows will request its password."
$credential = Get-Credential -UserName $LabWifiSsid `
-Message "Enter the Minimum lab Wi-Fi password"
if (-not $credential) {
throw "Lab Wi-Fi credential entry was cancelled."
}
$credentialDirectory = Split-Path -Parent $LabWifiCredentialPath
New-Item -ItemType Directory -Path $credentialDirectory -Force | Out-Null
$credential | Export-Clixml -LiteralPath $LabWifiCredentialPath
$credential = $null
Write-Host "Lab Wi-Fi credential saved with Windows DPAPI for this account."
}

function Invoke-ModelPreparation {
param([Parameter(Mandatory)][string]$Profile)
$prepareScript = if ($Profile -eq "T56") {
Expand Down Expand Up @@ -667,6 +718,7 @@ if (-not $installed) {
throw "Minimum package verification failed after APK installation."
}

Ensure-LabWifiCredential -Profile $target.Profile
Invoke-ModelPreparation -Profile $target.Profile
$deviceId = Get-MinimumDeviceId
Write-Host "Minimum Device ID: $deviceId"
Expand Down
Loading