Files
TIS_RMS/.github/workflows/release.yml
T

500 lines
17 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
# ------------------------------------------------------------
# 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 }}