527 lines
18 KiB
YAML
527 lines
18 KiB
YAML
name: Build & Release Client (Windows & Android)
|
|
|
|
on:
|
|
push:
|
|
tags:
|
|
- 'v*'
|
|
|
|
workflow_dispatch:
|
|
inputs:
|
|
tag_name:
|
|
description: 'Release tag (e.g. v1.0.0). Leave empty to use pubspec.yaml version.'
|
|
required: false
|
|
default: ''
|
|
type: string
|
|
|
|
jobs:
|
|
build-and-release:
|
|
name: Build Windows & Android Artifacts
|
|
runs-on: windows-latest
|
|
|
|
permissions:
|
|
contents: write
|
|
|
|
steps:
|
|
# ------------------------------------------------------------
|
|
# Checkout
|
|
# ------------------------------------------------------------
|
|
- name: Checkout Repository
|
|
uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
# ------------------------------------------------------------
|
|
# Java
|
|
# ------------------------------------------------------------
|
|
- name: Setup Java
|
|
uses: actions/setup-java@v4
|
|
with:
|
|
distribution: temurin
|
|
java-version: '17'
|
|
cache: gradle
|
|
|
|
- name: Verify Java
|
|
shell: pwsh
|
|
run: |
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
Write-Host "JAVA_HOME = $env:JAVA_HOME"
|
|
|
|
if (-not $env:JAVA_HOME) {
|
|
throw "JAVA_HOME is not set."
|
|
}
|
|
|
|
if (-not (Test-Path "$env:JAVA_HOME\bin\java.exe")) {
|
|
throw "Java executable was not found at $env:JAVA_HOME\bin\java.exe"
|
|
}
|
|
|
|
& "$env:JAVA_HOME\bin\java.exe" -version
|
|
|
|
# ------------------------------------------------------------
|
|
# .NET
|
|
# ------------------------------------------------------------
|
|
- name: Setup .NET
|
|
uses: actions/setup-dotnet@v4
|
|
with:
|
|
dotnet-version: '8.0.x'
|
|
|
|
- name: Verify .NET
|
|
shell: pwsh
|
|
run: |
|
|
$ErrorActionPreference = 'Stop'
|
|
dotnet --info
|
|
|
|
# ------------------------------------------------------------
|
|
# Flutter
|
|
# ------------------------------------------------------------
|
|
- name: Setup Flutter
|
|
uses: subosito/flutter-action@v2
|
|
with:
|
|
channel: stable
|
|
cache: true
|
|
|
|
- name: Flutter Doctor
|
|
working-directory: frontend
|
|
shell: pwsh
|
|
run: |
|
|
$ErrorActionPreference = 'Stop'
|
|
flutter doctor -v
|
|
|
|
# ------------------------------------------------------------
|
|
# Android / Gradle Java Fix
|
|
# ------------------------------------------------------------
|
|
- name: Fix Android Gradle Java Configuration
|
|
working-directory: frontend
|
|
shell: pwsh
|
|
run: |
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
$gradleProperties = "android\gradle.properties"
|
|
|
|
if (Test-Path $gradleProperties) {
|
|
Write-Host "Checking $gradleProperties"
|
|
|
|
$content = Get-Content $gradleProperties
|
|
|
|
$filtered = $content | Where-Object {
|
|
$_ -notmatch '^\s*org\.gradle\.java\.home\s*='
|
|
}
|
|
|
|
if ($filtered.Count -ne $content.Count) {
|
|
Write-Host "Removing stale org.gradle.java.home configuration..."
|
|
Set-Content `
|
|
-Path $gradleProperties `
|
|
-Value $filtered `
|
|
-Encoding utf8
|
|
}
|
|
else {
|
|
Write-Host "No org.gradle.java.home override found."
|
|
}
|
|
}
|
|
else {
|
|
Write-Host "$gradleProperties does not exist."
|
|
}
|
|
|
|
Write-Host ""
|
|
Write-Host "Java used by GitHub Actions:"
|
|
Write-Host "JAVA_HOME = $env:JAVA_HOME"
|
|
|
|
& "$env:JAVA_HOME\bin\java.exe" -version
|
|
|
|
# ------------------------------------------------------------
|
|
# Google Services
|
|
# ------------------------------------------------------------
|
|
- name: Restore google-services.json from Secret
|
|
env:
|
|
GOOGLE_SERVICES_JSON: ${{ secrets.GOOGLE_SERVICES_JSON }}
|
|
if: ${{ env.GOOGLE_SERVICES_JSON != '' }}
|
|
working-directory: frontend/android/app
|
|
shell: pwsh
|
|
run: |
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
Set-Content `
|
|
-Path "google-services.json" `
|
|
-Value $env:GOOGLE_SERVICES_JSON `
|
|
-Encoding utf8
|
|
|
|
Write-Host "google-services.json restored."
|
|
|
|
# ------------------------------------------------------------
|
|
# Android Release Keystore & Signing Configuration
|
|
# ------------------------------------------------------------
|
|
- name: Restore Android Release Keystore from Secret
|
|
env:
|
|
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
|
|
if: ${{ env.ANDROID_KEYSTORE_BASE64 != '' }}
|
|
working-directory: frontend/android/app
|
|
shell: pwsh
|
|
run: |
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
$base64 = $env:ANDROID_KEYSTORE_BASE64.Trim()
|
|
$bytes = [Convert]::FromBase64String($base64)
|
|
[IO.File]::WriteAllBytes("$PWD/tis_rms_release.keystore", $bytes)
|
|
|
|
Write-Host "tis_rms_release.keystore successfully restored from secret."
|
|
|
|
- name: Configure Android key.properties
|
|
env:
|
|
KEY_STORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_STORE_PASSWORD }}
|
|
KEY_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_KEY_PASSWORD }}
|
|
KEY_ALIAS: ${{ secrets.ANDROID_KEYSTORE_KEY_ALIAS }}
|
|
working-directory: frontend/android
|
|
shell: pwsh
|
|
run: |
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
$keystoreFile = "app/tis_rms_release.keystore"
|
|
if (Test-Path $keystoreFile) {
|
|
$storePass = if ($env:KEY_STORE_PASSWORD) { $env:KEY_STORE_PASSWORD } else { "android" }
|
|
$keyPass = if ($env:KEY_PASSWORD) { $env:KEY_PASSWORD } else { "android" }
|
|
$alias = if ($env:KEY_ALIAS) { $env:KEY_ALIAS } else { "androiddebugkey" }
|
|
|
|
@(
|
|
"storePassword=$storePass",
|
|
"keyPassword=$keyPass",
|
|
"keyAlias=$alias",
|
|
"storeFile=tis_rms_release.keystore"
|
|
) | Set-Content -Path "key.properties" -Encoding ascii
|
|
Write-Host "Android key.properties successfully created."
|
|
} else {
|
|
Write-Warning "Keystore file not found at $keystoreFile. Build will use debug signing."
|
|
}
|
|
|
|
|
|
# ------------------------------------------------------------
|
|
# Determine Release Tag & Version
|
|
# ------------------------------------------------------------
|
|
- name: Determine Release Tag & Version
|
|
id: release_info
|
|
shell: pwsh
|
|
run: |
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
$inputTag = '${{ github.event.inputs.tag_name }}'.Trim()
|
|
$runNumber = '${{ github.run_number }}'
|
|
|
|
$versionLine = Get-Content frontend/pubspec.yaml |
|
|
Select-String '^version:' |
|
|
Select-Object -First 1
|
|
|
|
if (-not $versionLine) {
|
|
throw "Could not find version: in frontend/pubspec.yaml"
|
|
}
|
|
|
|
$rawVer = ($versionLine.Line -split ':', 2)[1].Trim()
|
|
$verParts = $rawVer.Split('+')
|
|
$semVer = $verParts[0]
|
|
|
|
$semParts = $semVer.Split('.')
|
|
$major = if ($semParts.Length -ge 1) { $semParts[0] } else { "1" }
|
|
$minor = if ($semParts.Length -ge 2) { $semParts[1] } else { "0" }
|
|
|
|
$isTagPush = '${{ github.event_name }}' -eq 'push' -and '${{ github.ref }}'.StartsWith('refs/tags/')
|
|
|
|
if ($isTagPush) {
|
|
$refTag = '${{ github.ref_name }}'
|
|
$tag = if ($refTag.StartsWith('v')) { $refTag } else { "v$refTag" }
|
|
$verName = $tag.TrimStart('v')
|
|
}
|
|
elseif ($inputTag) {
|
|
$tag = if ($inputTag.StartsWith('v')) { $inputTag } else { "v$inputTag" }
|
|
$verName = $tag.TrimStart('v')
|
|
}
|
|
else {
|
|
# Auto-increment patch using GitHub Actions run_number
|
|
$verName = "$major.$minor.$runNumber"
|
|
$tag = "v$verName"
|
|
}
|
|
|
|
$buildNumber = $runNumber
|
|
|
|
if ($tag -notmatch '^v\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$') {
|
|
throw "Invalid release tag '$tag'. Expected format like v1.0.0."
|
|
}
|
|
|
|
Write-Host "Release tag : $tag"
|
|
Write-Host "Version name : $verName"
|
|
Write-Host "Build number : $buildNumber"
|
|
|
|
"tag=$tag" |
|
|
Out-File `
|
|
-FilePath $env:GITHUB_OUTPUT `
|
|
-Encoding utf8 `
|
|
-Append
|
|
|
|
"version_name=$verName" |
|
|
Out-File `
|
|
-FilePath $env:GITHUB_OUTPUT `
|
|
-Encoding utf8 `
|
|
-Append
|
|
|
|
"build_number=$buildNumber" |
|
|
Out-File `
|
|
-FilePath $env:GITHUB_OUTPUT `
|
|
-Encoding utf8 `
|
|
-Append
|
|
|
|
# Sync calculated version and build number into pubspec.yaml
|
|
$pubspecPath = "frontend/pubspec.yaml"
|
|
$pubContent = Get-Content $pubspecPath
|
|
$updatedPub = $pubContent -replace '^version:.*', "version: $verName+$buildNumber"
|
|
Set-Content -Path $pubspecPath -Value $updatedPub -Encoding utf8
|
|
Write-Host "Updated $pubspecPath to version: $verName+$buildNumber"
|
|
|
|
# ------------------------------------------------------------
|
|
# Manual Release Tag
|
|
# ------------------------------------------------------------
|
|
- name: Create Git Tag for Manual Release
|
|
if: ${{ github.event_name == 'workflow_dispatch' }}
|
|
shell: pwsh
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
RELEASE_TAG: ${{ steps.release_info.outputs.tag }}
|
|
run: |
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
Write-Host "Checking whether tag $env:RELEASE_TAG exists..."
|
|
|
|
gh api `
|
|
"repos/${{ github.repository }}/git/ref/tags/$env:RELEASE_TAG" `
|
|
--silent 2>$null
|
|
|
|
if ($LASTEXITCODE -eq 0) {
|
|
Write-Host "Tag $env:RELEASE_TAG already exists."
|
|
}
|
|
else {
|
|
Write-Host "Creating tag $env:RELEASE_TAG..."
|
|
|
|
gh api `
|
|
--method POST `
|
|
"repos/${{ github.repository }}/git/refs" `
|
|
-f "ref=refs/tags/$env:RELEASE_TAG" `
|
|
-f "sha=${{ github.sha }}"
|
|
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "Failed to create tag $env:RELEASE_TAG"
|
|
}
|
|
|
|
Write-Host "Tag $env:RELEASE_TAG created successfully."
|
|
}
|
|
|
|
# ------------------------------------------------------------
|
|
# Windows Build
|
|
# ------------------------------------------------------------
|
|
- name: Build Windows Desktop App
|
|
working-directory: frontend
|
|
shell: pwsh
|
|
run: |
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
flutter pub get
|
|
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "flutter pub get failed."
|
|
}
|
|
|
|
flutter build windows `
|
|
--release `
|
|
--obfuscate `
|
|
--split-debug-info=build/windows/symbols `
|
|
--build-name="${{ steps.release_info.outputs.version_name }}" `
|
|
--build-number="${{ steps.release_info.outputs.build_number }}"
|
|
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "Flutter Windows build failed."
|
|
}
|
|
|
|
Write-Host ""
|
|
Write-Host "Verifying compiled Windows release binaries and plugin DLLs:"
|
|
$releaseDir = "build\windows\x64\runner\Release"
|
|
Get-ChildItem -Path "$releaseDir\*.dll" | Select-Object Name, Length
|
|
|
|
# ------------------------------------------------------------
|
|
# Visual C++ Redistributable Dependency
|
|
# ------------------------------------------------------------
|
|
- name: Download Microsoft Visual C++ Redistributable
|
|
working-directory: frontend
|
|
shell: pwsh
|
|
run: |
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
$vcRedistUrl = "https://aka.ms/vs/17/release/vc_redist.x64.exe"
|
|
$vcRedistFile = "vc_redist.x64.exe"
|
|
|
|
Write-Host "Downloading Microsoft Visual C++ 2015-2022 Redistributable (x64) from $vcRedistUrl..."
|
|
$ProgressPreference = 'SilentlyContinue'
|
|
Invoke-WebRequest -Uri $vcRedistUrl -OutFile $vcRedistFile
|
|
|
|
if (-not (Test-Path $vcRedistFile)) {
|
|
throw "Downloaded vc_redist.x64.exe was not found."
|
|
}
|
|
|
|
$fileSize = (Get-Item $vcRedistFile).Length
|
|
Write-Host "vc_redist.x64.exe downloaded successfully. Size: $fileSize bytes."
|
|
|
|
if ($fileSize -lt 10000000) {
|
|
throw "Downloaded vc_redist.x64.exe appears incomplete or corrupted (size: $fileSize bytes)."
|
|
}
|
|
|
|
# ------------------------------------------------------------
|
|
# Windows Installer
|
|
# ------------------------------------------------------------
|
|
- name: Compile Inno Setup Windows Installer
|
|
working-directory: frontend
|
|
shell: pwsh
|
|
run: |
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
New-Item `
|
|
-ItemType Directory `
|
|
-Force `
|
|
-Path ..\installers |
|
|
Out-Null
|
|
|
|
& "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" `
|
|
"/DMyAppVersion=${{ steps.release_info.outputs.version_name }}" `
|
|
"/DOutputBaseFilename=TIS_RMS_Client_Setup_${{ steps.release_info.outputs.tag }}" `
|
|
TIS_RMS_Client.iss
|
|
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "Inno Setup compilation failed."
|
|
}
|
|
|
|
# ------------------------------------------------------------
|
|
# Android Build (Split 32-bit, 64-bit & Universal APKs)
|
|
# ------------------------------------------------------------
|
|
- name: Build Android APKs (Split 32-bit, 64-bit & Universal)
|
|
working-directory: frontend
|
|
shell: pwsh
|
|
run: |
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
Write-Host "Using JAVA_HOME:"
|
|
Write-Host $env:JAVA_HOME
|
|
|
|
& "$env:JAVA_HOME\bin\java.exe" -version
|
|
|
|
$tag = "${{ steps.release_info.outputs.tag }}"
|
|
$verName = "${{ steps.release_info.outputs.version_name }}"
|
|
$buildNum = "${{ steps.release_info.outputs.build_number }}"
|
|
|
|
# 1. Build Split-per-ABI APKs (32-bit armeabi-v7a & 64-bit arm64-v8a)
|
|
Write-Host ""
|
|
Write-Host "Building Android Split-per-ABI APKs (32-bit & 64-bit)..."
|
|
|
|
flutter build apk `
|
|
--release `
|
|
--split-per-abi `
|
|
--obfuscate `
|
|
--split-debug-info=build/app/outputs/symbols `
|
|
--build-name="$verName" `
|
|
--build-number="$buildNum"
|
|
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "Flutter Android split-per-abi build failed."
|
|
}
|
|
|
|
$apk32 = "build\app\outputs\flutter-apk\app-armeabi-v7a-release.apk"
|
|
$apk64 = "build\app\outputs\flutter-apk\app-arm64-v8a-release.apk"
|
|
|
|
$output32 = "..\installers\TIS_RMS_32bit_$tag.apk"
|
|
$output64 = "..\installers\TIS_RMS_64_$tag.apk"
|
|
|
|
if (-not (Test-Path $apk32)) {
|
|
throw "Split build completed without producing $apk32"
|
|
}
|
|
if (-not (Test-Path $apk64)) {
|
|
throw "Split build completed without producing $apk64"
|
|
}
|
|
|
|
Copy-Item $apk32 $output32 -Force
|
|
Copy-Item $apk64 $output64 -Force
|
|
Copy-Item $apk32 "..\installers\TIS_RMS_32bit.apk" -Force
|
|
Copy-Item $apk64 "..\installers\TIS_RMS_64.apk" -Force
|
|
|
|
Write-Host "32-bit APK copied to $output32 and ..\installers\TIS_RMS_32bit.apk"
|
|
Write-Host "64-bit APK copied to $output64 and ..\installers\TIS_RMS_64.apk"
|
|
|
|
# 2. Build Universal APK (All ABIs bundled)
|
|
Write-Host ""
|
|
Write-Host "Building Android Universal APK..."
|
|
|
|
flutter build apk `
|
|
--release `
|
|
--obfuscate `
|
|
--split-debug-info=build/app/outputs/symbols `
|
|
--build-name="$verName" `
|
|
--build-number="$buildNum"
|
|
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "Flutter Android universal APK build failed."
|
|
}
|
|
|
|
$apkUniversal = "build\app\outputs\flutter-apk\app-release.apk"
|
|
$outputUniversal = "..\installers\TIS_RMS_Universal_$tag.apk"
|
|
|
|
if (-not (Test-Path $apkUniversal)) {
|
|
throw "Universal build completed without producing $apkUniversal"
|
|
}
|
|
|
|
Copy-Item $apkUniversal $outputUniversal -Force
|
|
Copy-Item $apkUniversal "..\installers\TIS_RMS_Universal.apk" -Force
|
|
|
|
Write-Host "Universal APK copied to $outputUniversal and ..\installers\TIS_RMS_Universal.apk"
|
|
|
|
# ------------------------------------------------------------
|
|
# Verify Artifacts
|
|
# ------------------------------------------------------------
|
|
- name: Verify Release Artifacts
|
|
shell: pwsh
|
|
run: |
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
$tag = "${{ steps.release_info.outputs.tag }}"
|
|
$files = @(
|
|
"installers\TIS_RMS_Client_Setup_$tag.exe",
|
|
"installers\TIS_RMS_32bit_$tag.apk",
|
|
"installers\TIS_RMS_64_$tag.apk",
|
|
"installers\TIS_RMS_Universal_$tag.apk"
|
|
)
|
|
|
|
foreach ($file in $files) {
|
|
if (-not (Test-Path $file)) {
|
|
throw "Missing release artifact: $file"
|
|
}
|
|
|
|
$size = (Get-Item $file).Length
|
|
|
|
if ($size -le 0) {
|
|
throw "Release artifact is empty: $file"
|
|
}
|
|
|
|
Write-Host "Found: $file"
|
|
Write-Host "Size: $size bytes"
|
|
}
|
|
|
|
# ------------------------------------------------------------
|
|
# GitHub Release
|
|
# ------------------------------------------------------------
|
|
- name: Create GitHub Release and Upload Artifacts
|
|
uses: softprops/action-gh-release@v2
|
|
with:
|
|
tag_name: ${{ steps.release_info.outputs.tag }}
|
|
target_commitish: ${{ github.sha }}
|
|
name: Release ${{ steps.release_info.outputs.tag }}
|
|
files: |
|
|
installers/TIS_RMS_Client_Setup_${{ steps.release_info.outputs.tag }}.exe
|
|
installers/TIS_RMS_32bit_${{ steps.release_info.outputs.tag }}.apk
|
|
installers/TIS_RMS_64_${{ steps.release_info.outputs.tag }}.apk
|
|
installers/TIS_RMS_Universal_${{ steps.release_info.outputs.tag }}.apk
|
|
generate_release_notes: true
|
|
draft: false
|
|
prerelease: false
|
|
env:
|
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|