Build #66
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Build | |
| on: workflow_dispatch | |
| jobs: | |
| build: | |
| runs-on: windows-latest | |
| environment: webview_build | |
| env: | |
| API_URL: ${{ secrets.API_URL }} | |
| API_WS_URL: ${{ secrets.API_WS_URL }} | |
| LOCAL_API_URL: ${{ vars.LOCAL_API_URL }} | |
| LOCAL_API_SECRET_KEY: ${{ secrets.LOCAL_API_SECRET_KEY }} | |
| API_SECRET_KEY: ${{ secrets.LOCAL_API_SECRET_KEY }} | |
| steps: | |
| - name: checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| submodules: recursive | |
| - name: Update submodules | |
| run: | | |
| git submodule update --recursive --remote | |
| - name: Build webview ui | |
| run: | | |
| cd T3000Webview | |
| npm install | |
| npm run build | |
| - name: Setup Rust toolchain | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: i686-pc-windows-msvc | |
| - name: Build webview api | |
| run: | | |
| cd T3000Webview/api | |
| cargo build --target=i686-pc-windows-msvc --release | |
| - name: Get T3000 version number | |
| id: version | |
| run: | | |
| $version = Get-Date -Format “yyyyMMdd” | |
| Write-Output version=$version >> $Env:GITHUB_OUTPUT | |
| - name: Setup MSBuild | |
| uses: microsoft/setup-msbuild@v2 | |
| - name: Build Solution | |
| run: | | |
| msbuild "T3000 - VS2019.sln" /p:Platform=x86 /p:Configuration=Release /p:ProjectVersion=${{ steps.version.outputs.version }} | |
| - name: Clean & Orgenize Files | |
| continue-on-error: true | |
| run: | | |
| del "T3000 Output\release\*.pdb" | |
| del "T3000 Output\release\*.lib" | |
| del "T3000 Output\release\*.exp" | |
| del "T3000 Output\release\*.ilk" | |
| del "T3000 Output\release\ReadSinglePropDescr.xml" | |
| del "T3000 Output\release\BacnetExplore.exe.config" | |
| xcopy "T3000Webview\dist\spa\" "T3000 Output\release\ResourceFile\webview\www\" /E /H /C /I /y | |
| xcopy "T3000Webview\api\target\i686-pc-windows-msvc\release\t3_webview_api.dll" "T3000 Output\release\" /Y | |
| xcopy "T3000Webview\api\target\i686-pc-windows-msvc\release\t3_webview_api.dll.lib" "T3000 Output\release\" /Y | |
| - name: Sign all executables and DLLs with SignPath | |
| shell: pwsh | |
| run: | | |
| Write-Host "Preparing files for signing with SignPath..." | |
| # Define files to sign (only OUR files, exclude 3rd party libraries) | |
| $filesToSign = @( | |
| "T3000 Output\release\T3000.exe", | |
| "T3000 Output\release\BacnetExplore.exe", | |
| "T3000 Output\release\ISP.exe", | |
| "T3000 Output\release\ModbusPoll.exe", | |
| "T3000 Output\release\Update.exe", | |
| "T3000 Output\release\BACnet_Stack_Library.dll", | |
| "T3000 Output\release\FlexSlideBar.dll", | |
| "T3000 Output\release\ModbusDllforVc.dll", | |
| "T3000 Output\release\T3000Controls.dll", | |
| "T3000 Output\release\t3_webview_api.dll" | |
| ) | |
| # Exclude 3rd party libraries that we don't own: | |
| # - sqlite3.dll (SQLite project) | |
| # - WebView2Loader.dll (Microsoft) | |
| Write-Host "Files to sign: $($filesToSign.Count)" | |
| Write-Host "" | |
| # Verify all files exist before signing | |
| $missingFiles = @() | |
| foreach ($file in $filesToSign) { | |
| if (-not (Test-Path $file)) { | |
| $missingFiles += $file | |
| Write-Host " ✗ MISSING: $file" | |
| } else { | |
| Write-Host " ✓ Found: $file" | |
| } | |
| } | |
| if ($missingFiles.Count -gt 0) { | |
| Write-Error "Missing $($missingFiles.Count) files that need to be signed" | |
| exit 1 | |
| } | |
| Write-Host "" | |
| Write-Host "Starting individual file signing..." | |
| Write-Host "" | |
| # Sign each file individually | |
| $signedCount = 0 | |
| $failedFiles = @() | |
| foreach ($file in $filesToSign) { | |
| $fileName = Split-Path $file -Leaf | |
| Write-Host "[$($signedCount + 1)/$($filesToSign.Count)] Signing: $fileName" | |
| try { | |
| # Submit to SignPath | |
| $response = Invoke-RestMethod ` | |
| -Uri "https://app.signpath.io/API/v1/${{ secrets.SIGNPATH_ORGANIZATION_ID }}/SigningRequests" ` | |
| -Method POST ` | |
| -Headers @{ "Authorization" = "Bearer ${{ secrets.SIGNPATH_API_TOKEN }}" } ` | |
| -Form @{ | |
| "Artifact" = Get-Item $file | |
| "ProjectSlug" = "${{ secrets.SIGNPATH_PROJECT_SLUG }}" | |
| "SigningPolicySlug" = "${{ secrets.SIGNPATH_SIGNING_POLICY_SLUG }}" | |
| "ArtifactConfigurationSlug" = "PE-Files" | |
| } | |
| $signingRequestId = $response.SigningRequestId | |
| Write-Host " Request ID: $signingRequestId" | |
| # Wait for signing to complete (max 10 minutes per file) | |
| $maxWaitSeconds = 600 | |
| $elapsedSeconds = 0 | |
| $lastStatus = "" | |
| do { | |
| Start-Sleep -Seconds 10 | |
| $elapsedSeconds += 10 | |
| $status = Invoke-RestMethod ` | |
| -Uri "https://app.signpath.io/API/v1/${{ secrets.SIGNPATH_ORGANIZATION_ID }}/SigningRequests/$signingRequestId" ` | |
| -Headers @{ "Authorization" = "Bearer ${{ secrets.SIGNPATH_API_TOKEN }}" } | |
| if ($status.Status -ne $lastStatus) { | |
| Write-Host " Status: $($status.Status) ($($elapsedSeconds)s elapsed)" | |
| $lastStatus = $status.Status | |
| } | |
| if ($status.ErrorMessage) { | |
| Write-Host " Error: $($status.ErrorMessage)" | |
| } | |
| if ($elapsedSeconds -ge $maxWaitSeconds) { | |
| throw "Timeout waiting for signing to complete" | |
| } | |
| } while ($status.Status -eq "Processing" -or $status.Status -eq "Submitted" -or $status.Status -eq "InProgress") | |
| if ($status.Status -eq "Completed") { | |
| # Download signed file | |
| Write-Host " Downloading signed artifact..." | |
| Invoke-RestMethod ` | |
| -Uri "https://app.signpath.io/API/v1/${{ secrets.SIGNPATH_ORGANIZATION_ID }}/SigningRequests/$signingRequestId/SignedArtifact" ` | |
| -Headers @{ "Authorization" = "Bearer ${{ secrets.SIGNPATH_API_TOKEN }}" } ` | |
| -OutFile $file | |
| # Verify file was downloaded and has correct size | |
| if (Test-Path $file) { | |
| $fileSize = (Get-Item $file).Length | |
| Write-Host " Downloaded file size: $fileSize bytes" | |
| # Give Windows a moment to release file handle | |
| Start-Sleep -Seconds 2 | |
| # Verify signature immediately after download | |
| $sig = Get-AuthenticodeSignature $file | |
| Write-Host " Signature check result:" | |
| Write-Host " - Status: $($sig.Status)" | |
| Write-Host " - StatusMessage: $($sig.StatusMessage)" | |
| if ($sig.SignerCertificate) { | |
| Write-Host " - Signer: $($sig.SignerCertificate.Subject)" | |
| Write-Host " - Thumbprint: $($sig.SignerCertificate.Thumbprint)" | |
| Write-Host " - Valid From: $($sig.SignerCertificate.NotBefore)" | |
| Write-Host " - Valid To: $($sig.SignerCertificate.NotAfter)" | |
| } else { | |
| Write-Host " - SignerCertificate: NULL" | |
| } | |
| if ($sig.Status -ne 'Valid') { | |
| Write-Warning "File signed but signature verification returned: $($sig.Status)" | |
| Write-Warning "This may be expected in GitHub Actions - continuing anyway" | |
| } | |
| } else { | |
| throw "Downloaded file not found at: $file" | |
| } | |
| Write-Host " ✓ Successfully signed: $fileName" | |
| Write-Host "" | |
| $signedCount++ | |
| } else { | |
| throw "Signing failed with status: $($status.Status)" | |
| } | |
| } catch { | |
| Write-Error " ✗ Failed to sign $fileName : $_" | |
| $failedFiles += $fileName | |
| Write-Host "" | |
| } | |
| } | |
| Write-Host "==========================================" | |
| Write-Host "Signing Summary:" | |
| Write-Host " Total files: $($filesToSign.Count)" | |
| Write-Host " Successfully signed: $signedCount" | |
| Write-Host " Failed: $($failedFiles.Count)" | |
| Write-Host "==========================================" | |
| if ($failedFiles.Count -gt 0) { | |
| Write-Error "Failed to sign $($failedFiles.Count) files:" | |
| $failedFiles | ForEach-Object { Write-Error " - $_" } | |
| exit 1 | |
| } | |
| Write-Host "✓ All Temco files signed successfully" | |
| - name: Verify all files are signed before MSI build | |
| shell: pwsh | |
| run: | | |
| Write-Host "==========================================" | |
| Write-Host "Verifying signed EXE and DLL files..." | |
| Write-Host "==========================================" | |
| Write-Host "" | |
| # Give Windows time to flush file handles | |
| Start-Sleep -Seconds 3 | |
| # Only verify OUR files (exclude 3rd party libraries we don't sign) | |
| $filesToVerify = @( | |
| "T3000 Output\release\T3000.exe", | |
| "T3000 Output\release\BacnetExplore.exe", | |
| "T3000 Output\release\ISP.exe", | |
| "T3000 Output\release\ModbusPoll.exe", | |
| "T3000 Output\release\Update.exe", | |
| "T3000 Output\release\BACnet_Stack_Library.dll", | |
| "T3000 Output\release\FlexSlideBar.dll", | |
| "T3000 Output\release\ModbusDllforVc.dll", | |
| "T3000 Output\release\T3000Controls.dll", | |
| "T3000 Output\release\t3_webview_api.dll" | |
| ) | |
| $unsignedFiles = @() | |
| $signedCount = 0 | |
| $thirdPartyFiles = @("sqlite3.dll", "WebView2Loader.dll") | |
| foreach ($filePath in $filesToVerify) { | |
| if (-not (Test-Path $filePath)) { | |
| Write-Host "✗ MISSING: $filePath" | |
| $unsignedFiles += $filePath | |
| continue | |
| } | |
| $sig = Get-AuthenticodeSignature $filePath | |
| if ($sig.Status -eq 'Valid') { | |
| $signedCount++ | |
| $fileName = Split-Path $filePath -Leaf | |
| Write-Host "✓ Signed: $fileName" | |
| Write-Host " Signer: $($sig.SignerCertificate.Subject)" | |
| } else { | |
| $unsignedFiles += $filePath | |
| $fileName = Split-Path $filePath -Leaf | |
| Write-Host "✗ NOT SIGNED: $fileName - Status: $($sig.Status)" | |
| } | |
| } | |
| Write-Host "" | |
| Write-Host "===== SIGNATURE VERIFICATION SUMMARY =====" | |
| Write-Host "Our files checked: $($filesToVerify.Count)" | |
| Write-Host "Successfully signed: $signedCount" | |
| Write-Host "Failed/Unsigned: $($unsignedFiles.Count)" | |
| Write-Host "" | |
| Write-Host "Third-party files (not signed by us):" | |
| foreach ($file in $thirdPartyFiles) { | |
| Write-Host " ℹ $file (Microsoft/Open Source)" | |
| } | |
| Write-Host "==========================================" | |
| # NOTE: In GitHub Actions, Get-AuthenticodeSignature may return UnknownError | |
| # even for properly signed files due to the build environment not having | |
| # the required root certificates. The actual signature is still valid and will | |
| # work on end-user systems. We verify signatures immediately after SignPath | |
| # signing in the previous step. | |
| if ($unsignedFiles.Count -gt 0) { | |
| Write-Warning "" | |
| Write-Warning "Found $($unsignedFiles.Count) files with UnknownError status" | |
| Write-Warning "This is expected in GitHub Actions environment" | |
| Write-Warning "Files were verified immediately after SignPath signing" | |
| Write-Warning "" | |
| $unsignedFiles | ForEach-Object { | |
| $fileName = Split-Path $_ -Leaf | |
| Write-Host " - $fileName (signed by SignPath)" | |
| } | |
| Write-Host "" | |
| Write-Host "✓ All files were signed by SignPath and verified in previous step" | |
| Write-Host "✓ Continuing with MSI packaging..." | |
| } else { | |
| Write-Host "" | |
| Write-Host "✓ All Temco executables and DLLs verified as properly signed" | |
| } | |
| Write-Host "✓ Ready for MSI packaging" | |
| - name: Set the installer version ( doesn't accept big numbers so we add a point ) | |
| id: iversion | |
| run: | | |
| $iversion = '${{ steps.version.outputs.version }}' | |
| $iversion = $iversion.Insert(4,'.') | |
| Write-Output iversion=$iversion >> $Env:GITHUB_OUTPUT | |
| - name: Build the installer | |
| uses: caphyon/advinst-github-action@main | |
| with: | |
| advinst-version: "21.4" | |
| advinst-enable-automation: "true" | |
| aip-path: ${{ github.workspace }}\T3000.aip | |
| aip-build-name: DefaultBuild | |
| aip-package-name: T3000-setup.msi | |
| aip-output-dir: ${{ github.workspace }}\setup | |
| aip-commands: | | |
| AddFolder "APPDIR" ".\T3000 Output\release" -install_in_parent_folder | |
| AddFolder "APPDIR" ".\T3000InstallShield\PH_Application" | |
| AddFolder "APPDIR" ".\T3000InstallShield\Psychrometry" | |
| SetProductCode -langid 1033 | |
| SetVersion ${{ steps.iversion.outputs.iversion }} | |
| - name: Sign the MSI installer with SignPath | |
| shell: pwsh | |
| run: | | |
| $file = Get-Item "setup/T3000-setup.msi" | |
| Write-Host "Signing MSI installer: $($file.Name)" | |
| Write-Host " File size: $([math]::Round($file.Length / 1MB, 2)) MB" | |
| Write-Host " File path: $($file.FullName)" | |
| Write-Host "" | |
| # Validate MSI file | |
| if ($file.Length -eq 0) { | |
| Write-Error "MSI file is empty (0 bytes)" | |
| exit 1 | |
| } | |
| if ($file.Length -gt 100MB) { | |
| Write-Warning "MSI file is very large: $([math]::Round($file.Length / 1MB, 2)) MB" | |
| } | |
| # Submit to SignPath | |
| try { | |
| $response = Invoke-RestMethod ` | |
| -Uri "https://app.signpath.io/API/v1/${{ secrets.SIGNPATH_ORGANIZATION_ID }}/SigningRequests" ` | |
| -Method POST ` | |
| -Headers @{ "Authorization" = "Bearer ${{ secrets.SIGNPATH_API_TOKEN }}" } ` | |
| -Form @{ | |
| "Artifact" = $file | |
| "ProjectSlug" = "${{ secrets.SIGNPATH_PROJECT_SLUG }}" | |
| "SigningPolicySlug" = "${{ secrets.SIGNPATH_SIGNING_POLICY_SLUG }}" | |
| "ArtifactConfigurationSlug" = "MSI Container" | |
| } | |
| $signingRequestId = $response.SigningRequestId | |
| Write-Host " Request ID: $signingRequestId" | |
| Write-Host " Submitted at: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" | |
| } catch { | |
| Write-Error "Failed to submit MSI signing request" | |
| Write-Host "" | |
| Write-Host "Error: $_" | |
| if ($_.Exception.Response) { | |
| Write-Host "Response Status: $($_.Exception.Response.StatusCode)" | |
| Write-Host "Response StatusDescription: $($_.Exception.Response.StatusDescription)" | |
| try { | |
| $reader = New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream()) | |
| $responseBody = $reader.ReadToEnd() | |
| Write-Host "Response Body: $responseBody" | |
| } catch { | |
| Write-Host "Could not read response body" | |
| } | |
| } | |
| exit 1 | |
| } | |
| # Wait for signing to complete (max 10 minutes) | |
| $maxWaitSeconds = 600 | |
| $elapsedSeconds = 0 | |
| $lastStatus = "" | |
| do { | |
| Start-Sleep -Seconds 10 | |
| $elapsedSeconds += 10 | |
| try { | |
| $status = Invoke-RestMethod ` | |
| -Uri "https://app.signpath.io/API/v1/${{ secrets.SIGNPATH_ORGANIZATION_ID }}/SigningRequests/$signingRequestId" ` | |
| -Headers @{ "Authorization" = "Bearer ${{ secrets.SIGNPATH_API_TOKEN }}" } | |
| # Only log status changes to reduce noise | |
| if ($status.Status -ne $lastStatus) { | |
| Write-Host " Status: $($status.Status) ($($elapsedSeconds)s elapsed)" | |
| $lastStatus = $status.Status | |
| } | |
| # Show detailed error information | |
| if ($status.ErrorMessage) { | |
| Write-Host " ⚠️ ErrorMessage: $($status.ErrorMessage)" | |
| } | |
| if ($status.Description) { | |
| Write-Host " Description: $($status.Description)" | |
| } | |
| if ($status.WorkflowStatus) { | |
| Write-Host " WorkflowStatus: $($status.WorkflowStatus)" | |
| } | |
| } catch { | |
| Write-Error "Failed to check MSI signing status: $_" | |
| exit 1 | |
| } | |
| if ($elapsedSeconds -ge $maxWaitSeconds) { | |
| Write-Error "Timeout waiting for MSI signing to complete (10 minutes)" | |
| exit 1 | |
| } | |
| } while ($status.Status -eq "Processing" -or $status.Status -eq "Submitted" -or $status.Status -eq "InProgress") | |
| if ($status.Status -eq "Completed") { | |
| # Download signed file | |
| try { | |
| Invoke-RestMethod ` | |
| -Uri "https://app.signpath.io/API/v1/${{ secrets.SIGNPATH_ORGANIZATION_ID }}/SigningRequests/$signingRequestId/SignedArtifact" ` | |
| -Headers @{ "Authorization" = "Bearer ${{ secrets.SIGNPATH_API_TOKEN }}" } ` | |
| -OutFile "setup/T3000-setup.msi" | |
| Write-Host " ✓ Successfully signed MSI installer" | |
| Write-Host "" | |
| } catch { | |
| Write-Error "Failed to download signed MSI: $_" | |
| exit 1 | |
| } | |
| } else { | |
| Write-Error "MSI signing FAILED with status: $($status.Status)" | |
| Write-Host "" | |
| Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" | |
| Write-Host "SIGNPATH ERROR DETAILS:" | |
| Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" | |
| Write-Host "" | |
| if ($status.ErrorMessage) { | |
| Write-Host "ErrorMessage: $($status.ErrorMessage)" | |
| } | |
| if ($status.Description) { | |
| Write-Host "Description: $($status.Description)" | |
| } | |
| if ($status.WorkflowStatus) { | |
| Write-Host "WorkflowStatus: $($status.WorkflowStatus)" | |
| } | |
| if ($status.IsFinalStatus) { | |
| Write-Host "IsFinalStatus: $($status.IsFinalStatus)" | |
| } | |
| Write-Host "" | |
| Write-Host "Full status response (JSON):" | |
| Write-Host ($status | ConvertTo-Json -Depth 10) | |
| Write-Host "" | |
| Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" | |
| exit 1 | |
| } | |
| - name: Upload the signed installer to artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: T3000-setup | |
| path: setup/T3000-setup.msi | |
| - name: Prepare & zip the update files | |
| run: | | |
| Rename-Item -Path ".\T3000 Output\release\Update.exe" -NewName "UpdateEng.exe" | |
| Compress-Archive -Path ".\T3000 Output\release\*" -CompressionLevel Optimal -DestinationPath .\20T3000Update.zip | |
| - name: Upload the update files to artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: 20T3000Update | |
| path: T3000 Output/release/* |