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 # ------------------------------------------------------------ # 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." # ------------------------------------------------------------ # 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." } # ------------------------------------------------------------ # 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 }}" ` TIS_RMS_Client.iss if ($LASTEXITCODE -ne 0) { throw "Inno Setup compilation failed." } # ------------------------------------------------------------ # Android Build # ------------------------------------------------------------ - name: Build Android Universal APK 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 Write-Host "" Write-Host "Building Android APK..." flutter build apk ` --release ` --obfuscate ` --split-debug-info=build/app/outputs/symbols ` --build-name="${{ steps.release_info.outputs.version_name }}" ` --build-number="${{ steps.release_info.outputs.build_number }}" if ($LASTEXITCODE -ne 0) { throw "Flutter Android APK build failed." } $apkPath = "build\app\outputs\flutter-apk\app-release.apk" $outputPath = "..\installers\TIS_RMS_Android_Universal.apk" if (-not (Test-Path $apkPath)) { throw "Android build completed without producing $apkPath" } Copy-Item ` $apkPath ` $outputPath ` -Force Write-Host "Android APK copied to $outputPath" # ------------------------------------------------------------ # Verify Artifacts # ------------------------------------------------------------ - name: Verify Release Artifacts shell: pwsh run: | $ErrorActionPreference = 'Stop' $files = @( "installers\TIS_RMS_Client_Setup.exe", "installers\TIS_RMS_Android_Universal.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.exe installers/TIS_RMS_Android_Universal.apk generate_release_notes: true draft: false prerelease: false env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}