From 648a9c39b4954797e94fc09e3d58a6cde8dc8182 Mon Sep 17 00:00:00 2001 From: Jason Naylor Date: Wed, 18 Feb 2026 12:30:23 -0800 Subject: [PATCH 01/11] Revert the github actions corrupted in the Modernization --- .github/workflows/CI.yml | 233 ++++------ .github/workflows/CommitMessage.yml | 94 ++-- .github/workflows/base-installer-cd.yml | 558 ++++++++++++----------- .github/workflows/patch-installer-cd.yml | 128 +++--- 4 files changed, 483 insertions(+), 530 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 531b9ebdc1..29c4b2aff2 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -1,160 +1,85 @@ name: Flex CI on: - push: - branches: ["release/**", "main", "feature/PubSub"] - pull_request: - branches: ["release/**", "main", "feature/PubSub"] - workflow_dispatch: - inputs: - run_wix6_installer_build: - description: "Run WiX 6 installer build (opt-in)" - required: false - default: "false" + push: + branches: ["release/**", "main", "feature/PubSub"] + pull_request: + branches: ["release/**", "main", "feature/PubSub"] + workflow_dispatch: concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true jobs: - debug_build_and_test: - env: - CROWDIN_API_KEY: ${{ secrets.FLEX_CROWDIN_API }} - name: Build Debug and run Tests (x64 only) - runs-on: windows-latest - steps: - - name: Checkout Files - uses: actions/checkout@v4 - id: checkout - - - name: Download 481 targeting pack - uses: suisei-cn/actions-download-file@818d6b7dc8fe73f2f924b6241f2b1134ca1377d9 # 1.6.0 - id: downloadfile # Remember to give an ID if you need the output filename - with: - url: "https://download.microsoft.com/download/8/1/8/81877d8b-a9b2-4153-9ad2-63a6441d11dd/NDP481-DevPack-ENU.exe" - target: public/ - - - name: Install targeting pack - shell: cmd - working-directory: public - run: NDP481-DevPack-ENU.exe /q - - - name: Setup dotnet - uses: actions/setup-dotnet@v4 - with: - dotnet-version: | - 2.1.x - 3.1.x - 5.0.x - - - name: Add NETFX tools to PATH - shell: powershell - run: | - 'C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8.1 Tools' | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - - name: Setup MSBuild - uses: microsoft/setup-msbuild@v2 - - name: Build Debug and run tests - id: build_and_test - shell: powershell - run: | - $ErrorActionPreference = 'Stop' - ./build.ps1 -Configuration Debug -Platform x64 -MsBuildArgs @('/m', '/p:action=test', '/p:desktopNotAvailable=true') -LogFile Build/build.log - - - name: Scan Debug Build Output - shell: powershell - working-directory: Build - run: | - $results = Select-String -Path "build.log" -Pattern "^\s*[1-9][0-9]* Error\(s\)" - if ($results) { - foreach ($result in $results) { - Write-Host "Found errors in build.log $($result.LineNumber): $($result.Line)" -ForegroundColor red - } - exit 1 - } else { - Write-Host "No errors found" -ForegroundColor green - exit 0 - } - - - name: Capture Test Results - shell: powershell - working-directory: Build - run: .\NUnitReport /a ^| tee-object -FilePath test-results.log - - - name: Report Test Results - uses: sillsdev/fw-nunitreport-action@v2.0.0 - with: - log-path: Build/test-results.log - token: ${{ secrets.GITHUB_TOKEN }} - - # Upload build and test artifacts - - uses: actions/upload-artifact@v4 - with: - name: build-logs - path: Build/*.log - - # Upload generated manifests for inspection (registration-free COM) - - uses: actions/upload-artifact@v4 - with: - name: regfree-manifests - path: | - Output/Debug/FieldWorks.exe.manifest - if-no-files-found: warn - - # Smoke test: Verify registration-free COM activation (no registry writes) - - name: Smoke test - Reg-free COM activation - shell: powershell - run: | - Write-Host "Testing registration-free COM activation..." -ForegroundColor Cyan - $exitCode = & "Output\Debug\ComManifestTestHost.exe" - if ($LASTEXITCODE -ne 0) { - Write-Host "❌ COM activation test failed with exit code $LASTEXITCODE" -ForegroundColor Red - exit $LASTEXITCODE - } else { - Write-Host "✅ COM activation test passed" -ForegroundColor Green - } - - wix6_installer_build: - if: github.event_name == 'workflow_dispatch' && github.event.inputs.run_wix6_installer_build == 'true' - name: Build WiX 6 installer (opt-in) - runs-on: windows-latest - steps: - - name: Checkout Files - uses: actions/checkout@v4 - - - name: Download 481 targeting pack - uses: suisei-cn/actions-download-file@818d6b7dc8fe73f2f924b6241f2b1134ca1377d9 # 1.6.0 - id: downloadfile - with: - url: "https://download.microsoft.com/download/8/1/8/81877d8b-a9b2-4153-9ad2-63a6441d11dd/NDP481-DevPack-ENU.exe" - target: public/ - - - name: Install targeting pack - shell: cmd - working-directory: public - run: NDP481-DevPack-ENU.exe /q - - - name: Setup dotnet - uses: actions/setup-dotnet@v4 - with: - dotnet-version: | - 2.1.x - 3.1.x - 5.0.x - - - name: Add NETFX tools to PATH - shell: powershell - run: | - 'C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8.1 Tools' | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - - - name: Setup MSBuild - uses: microsoft/setup-msbuild@v2 - - - name: Build WiX 6 installer - shell: powershell - run: | - $ErrorActionPreference = 'Stop' - ./build.ps1 -BuildInstaller -InstallerToolset Wix6 -Configuration Debug -Platform x64 -LogFile Build/wix6-installer.log - - - uses: actions/upload-artifact@v4 - with: - name: wix6-installer-logs - path: Build/wix6-installer.log + debug_build_and_test: + env: + CROWDIN_API_KEY: ${{ secrets.FLEX_CROWDIN_API }} + name: Build Debug and run Tests + runs-on: windows-latest + steps: + - name: Checkout Files + uses: actions/checkout@v4 + id: checkout + + - name: Download 461 targeting pack + uses: suisei-cn/actions-download-file@818d6b7dc8fe73f2f924b6241f2b1134ca1377d9 # 1.6.0 + id: downloadfile # Remember to give an ID if you need the output filename + with: + url: "https://download.microsoft.com/download/F/1/D/F1DEB8DB-D277-4EF9-9F48-3A65D4D8F965/NDP461-DevPack-KB3105179-ENU.exe" + target: public/ + + - name: Install targeting pack + shell: cmd + working-directory: public + run: NDP461-DevPack-KB3105179-ENU.exe /q + + - name: Setup dotnet + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 2.1.x + 3.1.x + 5.0.x + + - name: Prepare for build + shell: cmd + working-directory: Build + run: build64.bat /t:WriteNonlocalDevelopmentPropertiesFile + + - name: Build Debug and run tests + id: build_and_test + shell: powershell + run: | + cd Build + .\build64.bat /t:remakefw-jenkins /p:action=test /p:desktopNotAvailable=true ^| tee-object -FilePath build.log + + - name: Scan Debug Build Output + shell: powershell + working-directory: Build + run: | + $results = Select-String -Path "build.log" -Pattern "^\s*[1-9][0-9]* Error\(s\)" + if ($results) { + foreach ($result in $results) { + Write-Host "Found errors in build.log $($result.LineNumber): $($result.Line)" -ForegroundColor red + } + exit 1 + } else { + Write-Host "No errors found" -ForegroundColor green + exit 0 + } + + - name: Capture Test Results + shell: powershell + working-directory: Build + run: .\NUnitReport /a ^| tee-object -FilePath test-results.log + + - name: Report Test Results + uses: sillsdev/fw-nunitreport-action@v2.0.0 + with: + log-path: Build/test-results.log + token: ${{ secrets.GITHUB_TOKEN }} + + - uses: actions/upload-artifact@v4 + with: + name: build-logs + path: Build/*.log diff --git a/.github/workflows/CommitMessage.yml b/.github/workflows/CommitMessage.yml index 68f86b638a..28dced92fa 100644 --- a/.github/workflows/CommitMessage.yml +++ b/.github/workflows/CommitMessage.yml @@ -1,50 +1,52 @@ name: Commit messages check on: - pull_request: - workflow_call: + pull_request: + workflow_call: jobs: - gitlint: - name: Check commit messages - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Install dependencies - run: | - pip install --upgrade gitlint - - name: Lint git commit messages - shell: bash - run: bash ./Build/Agent/commit-messages.sh - - name: Propegate Error Summary - if: always() - shell: bash - # put the output of the commit message linting into the summary for the job and in an environment variable - run: | - # Change the commit part of the log into a markdown link to the commit - commitsUrl="https:\/\/github.com\/${{ github.repository_owner }}\/${{ github.event.repository.name }}\/commit\/" - sed -i "s/Commit \([0-9a-f]\{7,40\}\)/[commit \1]($commitsUrl\1)/g" check_results.log - # Put the results into the job summary - cat check_results.log >> "$GITHUB_STEP_SUMMARY" - # Put the results into a multi-line environment variable to use in the next step - echo "check_results<<###LINT_DELIMITER###" >> "$GITHUB_ENV" - echo "$(cat check_results.log)" >> "$GITHUB_ENV" - echo "###LINT_DELIMITER###" >> "$GITHUB_ENV" - # add a comment on the PR if the commit message linting failed - - name: Comment on PR - if: failure() - uses: marocchino/sticky-pull-request-comment@v2 - with: - header: Commit Comment - message: | - ⚠️ Commit Message Format Issues ⚠️ - ${{ env.check_results }} - - name: Clear PR Comment - if: success() - uses: marocchino/sticky-pull-request-comment@v2 - with: - header: Commit Comment - hide: true - hide_classify: "RESOLVED" + gitlint: + name: Check commit messages + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Install dependencies + run: | + pip install --upgrade gitlint + - name: Lint git commit messages + shell: bash + # run the linter and tee the output to a file, this will make the check fail but allow us to use the results in summary + run: gitlint --ignore body-is-missing --commits origin/$GITHUB_BASE_REF.. 2>&1 | tee check_results.log + - name: Propegate Error Summary + if: always() + shell: bash + # put the output of the commit message linting into the summary for the job and in an environment variable + run: | + # Change the commit part of the log into a markdown link to the commit + commitsUrl="https:\/\/github.com\/${{ github.repository_owner }}\/${{ github.event.repository.name }}\/commit\/" + sed -i "s/Commit \([0-9a-f]\{7,40\}\)/[commit \1]($commitsUrl\1)/g" check_results.log + # Put the results into the job summary + cat check_results.log >> "$GITHUB_STEP_SUMMARY" + # Put the results into a multi-line environment variable to use in the next step + echo "check_results<<###LINT_DELIMITER###" >> "$GITHUB_ENV" + echo "$(cat check_results.log)" >> "$GITHUB_ENV" + echo "###LINT_DELIMITER###" >> "$GITHUB_ENV" + # add a comment on the PR if the commit message linting failed + - name: Comment on PR + if: failure() + uses: marocchino/sticky-pull-request-comment@v2 + with: + header: Commit Comment + message: | + ⚠️ Commit Message Format Issues ⚠️ + ${{ env.check_results }} + - name: Clear PR Comment + if: success() + uses: marocchino/sticky-pull-request-comment@v2 + with: + header: Commit Comment + hide: true + hide_classify: "RESOLVED" + \ No newline at end of file diff --git a/.github/workflows/base-installer-cd.yml b/.github/workflows/base-installer-cd.yml index 5423854c09..aab44b5abf 100644 --- a/.github/workflows/base-installer-cd.yml +++ b/.github/workflows/base-installer-cd.yml @@ -9,278 +9,294 @@ name: Base Installer # Note: FW_BUILD_NUMBER is higher than GITHUB_RUN_NUMBER because it needs to be higher than the build number on artifacts from our previous system. on: - schedule: - # Runs every Monday at 02:30 UTC (which is 6:30pm PST / 7:30pm PDT Sunday evening) - - cron: "30 2 * * 1" - workflow_dispatch: - inputs: - fw_ref: - description: "Commit-ish (branch, tag, SHA) to checkout for the main repository" - required: false - default: "" - helps_ref: - description: "Commit-ish for helps repository" - required: false - default: "develop" - localizations_ref: - description: "Commit-ish for localization repository" - required: false - default: "develop" - lcm_ref: - description: "Commit-ish for liblcm repository" - required: false - default: "master" - make_release: - description: "Should the build archive a release, false by default, should be set to true on a release build." - required: false - default: "false" + schedule: + # Runs every Monday at 02:30 UTC (which is 6:30pm PST / 7:30pm PDT Sunday evening) + - cron: "30 2 * * 1" + workflow_dispatch: + inputs: + fw_ref: + description: 'Commit-ish (branch, tag, SHA) to checkout for the main repository' + required: false + default: '' + helps_ref: + description: 'Commit-ish for helps repository' + required: false + default: 'develop' + installer_ref: + description: 'Commit-ish for PatchableInstaller repository' + required: false + default: 'master' + localizations_ref: + description: 'Commit-ish for localization repository' + required: false + default: 'develop' + lcm_ref: + description: 'Commit-ish for liblcm repository' + required: false + default: 'master' + make_release: + description: 'Should the build archive a release, false by default, should be set to true on a release build.' + required: false + default: 'false' concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true jobs: - debug_build_and_test: - env: - CROWDIN_API_KEY: ${{ secrets.FLEX_CROWDIN_API }} - LcmRootDir: ${{ github.workspace }}/Localizations/LCM - FILESTOSIGNLATER: ./signExternally - name: Build Debug and run Tests - runs-on: windows-latest - steps: - - name: Compute build number - id: build_number - run: | - $lastJenkins = 1100 # The last base build from jenkins, rounded to the next hundred - $githubRun = $env:GITHUB_RUN_NUMBER - $combined = $lastJenkins + $githubRun - echo "Calculated build number: $combined" - echo "FW_BUILD_NUMBER=$combined" >> $env:GITHUB_ENV - - - name: Checkout Files - uses: actions/checkout@v4 - id: checkout - with: - ref: ${{ github.event.inputs.fw_ref || github.ref }} - fetch-depth: 0 - - - name: Checkout Helps - uses: actions/checkout@v4 - id: helps-checkout - with: - repository: "sillsdev/FwHelps" - ref: ${{ github.event.inputs.helps_ref || 'develop' }} - fetch-depth: 0 - path: "DistFiles/Helps" - - - name: Checkout Localizations - uses: actions/checkout@v4 - id: loc-checkout - with: - repository: "sillsdev/FwLocalizations" - ref: ${{ github.event.inputs.localizations_ref || 'develop' }} - fetch-depth: 0 - path: "Localizations" - - name: Checkout liblcm - uses: actions/checkout@v4 - id: liblcm-checkout - with: - repository: "sillsdev/liblcm" - ref: ${{ github.event.inputs.lcm_ref || 'master' }} - fetch-depth: 0 - path: "Localizations/LCM" - - - name: Download .NET 481 targeting pack - uses: suisei-cn/actions-download-file@818d6b7dc8fe73f2f924b6241f2b1134ca1377d9 # 1.6.0 - id: downloadfile # Remember to give an ID if you need the output filename - with: - url: "https://download.microsoft.com/download/8/1/8/81877d8b-a9b2-4153-9ad2-63a6441d11dd/NDP481-DevPack-ENU.exe" - target: public/ - - - name: Install .NET 481 targeting pack - shell: cmd - working-directory: public - run: NDP481-DevPack-ENU.exe /q - - - name: Setup dotnet - uses: actions/setup-dotnet@v4 - with: - dotnet-version: | - 3.1.x - 5.0.x - - - name: Install Overcrowdin - shell: cmd - run: | - dotnet tool update -g overcrowdin || dotnet tool install -g overcrowdin - - - name: Add NETFX tools to PATH - shell: powershell - run: | - 'C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8.1 Tools' | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - - - name: Setup MSBuild - uses: microsoft/setup-msbuild@v2 - - - name: Build and run tests - id: build_installer - shell: powershell - run: | - $ErrorActionPreference = 'Stop' - # Build base installer (uses traversal build internally via Installer.targets) - dotnet restore FieldWorks.sln /p:NoWarn=NU1903 /p:DisableWarnForInvalidRestoreProjects=true - msbuild Build/InstallerBuild.proj /t:BuildBaseInstaller /p:Configuration=Debug /p:Platform=x64 /p:config=release /p:action=test /p:desktopNotAvailable=true /m /v:d /bl | Tee-Object -FilePath Build/build.log - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - Set-Location BuildDir - md5sum *.exe > md5.txt - - - name: Scan Build Output - shell: powershell - working-directory: Build - run: | - $results = Select-String -Path "build.log" -Pattern "^\s*[1-9][0-9]* Error\(s\)" - if ($results) { - foreach ($result in $results) { - Write-Host "Found errors in build.log $($result.LineNumber): $($result.Line)" -ForegroundColor red - } - exit 1 - } else { - Write-Host "No errors found" -ForegroundColor green - exit 0 - } - - - name: Move installers to staging folder - id: stage_installers - shell: pwsh - run: | - $staging = Join-Path $pwd "Installers" - if (-not (Test-Path $staging)) { New-Item -ItemType Directory -Path $staging | Out-Null } - - $offlineExe = Get-ChildItem -Path BuildDir -Filter "FieldWorks_*_Offline_x64.exe" | Select-Object -First 1 - $onlineExe = Get-ChildItem -Path BuildDir -Filter "FieldWorks_*_Online_x64.exe" | Select-Object -First 1 - - if (-not $offlineExe) { throw "FieldWorks_VERSION_Offline_x64.exe not found in BuildDir" } - if (-not $onlineExe) { throw "FieldWorks_VERSION_Online_x64.exe not found in BuildDir" } - - Move-Item -Path $offlineExe.FullName -Destination $staging - Move-Item -Path $onlineExe.FullName -Destination $staging - - $offlinePath = Join-Path $staging $offlineExe.Name - $onlinePath = Join-Path $staging $onlineExe.Name - - "offline_exe=$offlinePath" >> $env:GITHUB_OUTPUT - "online_exe=$onlinePath" >> $env:GITHUB_OUTPUT - - - name: Extract burn engines from bundles - shell: cmd - working-directory: Installers - run: | - insignia -ib "${{ steps.stage_installers.outputs.offline_exe }}" -o offline-engine.exe - insignia -ib "${{ steps.stage_installers.outputs.online_exe }}" -o online-engine.exe - - - name: Sign Engines - if: github.event_name != 'pull_request' - uses: sillsdev/codesign/trusted-signing-action@v3 - with: - credentials: ${{ secrets.TRUSTED_SIGNING_CREDENTIALS }} - files-folder: Installers - files-folder-filter: "*-engine.exe" - description: "FieldWorks Installer" - description-url: "https://software.sil.org/fieldworks/" - - - name: Reattach Engines - if: github.event_name != 'pull_request' - working-directory: Installers - shell: cmd - run: | - insignia -ab offline-engine.exe "${{ steps.stage_installers.outputs.offline_exe }}" -o "${{ steps.stage_installers.outputs.offline_exe }}" - insignia -ab online-engine.exe "${{ steps.stage_installers.outputs.online_exe }}" -o "${{ steps.stage_installers.outputs.online_exe }}" - - - name: Sign Bundles - if: github.event_name != 'pull_request' - uses: sillsdev/codesign/trusted-signing-action@v3 - with: - credentials: ${{ secrets.TRUSTED_SIGNING_CREDENTIALS }} - files-folder: Installers - files-folder-filter: FieldWorks_*.exe - description: "FieldWorks Installer" - description-url: "https://software.sil.org/fieldworks/" - - - name: Configure AWS credentials - if: ${{ inputs.make_release == 'true' }} - uses: aws-actions/configure-aws-credentials@v4 - with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: us-east-1 - - - name: Upload Installers to S3 - if: ${{ inputs.make_release == 'true' }} - shell: pwsh - run: | - $offlineExe = "${{ steps.stage_installers.outputs.offline_exe }}" - $onlineExe = "${{ steps.stage_installers.outputs.online_exe }}" - - if (-not (Test-Path $offlineExe)) { - throw "Offline installer not found at $offlineExe" - } - if (-not (Test-Path $onlineExe)) { - throw "Online installer not found at $onlineExe" - } - - $offlineFile = Split-Path $offlineExe -Leaf - $onlineFile = Split-Path $onlineExe -Leaf - - $s3BasePath = "jobs/FieldWorks-Win-all-Release-Base/$($env:FW_BUILD_NUMBER)" - $offlineS3Key = "$s3BasePath/$offlineFile" - $onlineS3Key = "$s3BasePath/$onlineFile" - - aws s3 cp $offlineExe "s3://flex-updates/$offlineS3Key" - Write-Host "Uploaded offline installer to s3://flex-updates/$offlineS3Key" - - aws s3 cp $onlineExe "s3://flex-updates/$onlineS3Key" - Write-Host "Uploaded online installer to s3://flex-updates/$onlineS3Key" - - - name: Compress Build Artifacts - id: compress_artifacts - if: ${{ inputs.make_release == 'true' }} - run: | - [System.IO.Compression.ZipFile]::CreateFromDirectory( - "${{ github.workspace }}\BuildDir", - "${{ github.workspace }}\BuildDir.zip", - [System.IO.Compression.CompressionLevel]::Optimal, - $false - ) - [System.IO.Compression.ZipFile]::CreateFromDirectory( - "${{ github.workspace }}\FLExInstaller\Shared\ProcRunner\ProcRunner\bin\Release\net48", - "${{ github.workspace }}\ProcRunner.zip", - [System.IO.Compression.CompressionLevel]::Optimal, - $false - ) - Get-ChildItem "${{ github.workspace }}" -Filter "*.zip" | Select-Object Name, Length - "build_dir_zip=$(("${{ github.workspace }}/BuildDir.zip") -replace '\\','/')" >> $env:GITHUB_OUTPUT - "proc_runner_zip=$(("${{ github.workspace }}/ProcRunner.zip") -replace '\\','/')" >> $env:GITHUB_OUTPUT - - - name: Tag, Create Release, and Upload artifacts - if: ${{ inputs.make_release == 'true' }} - uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 - with: - target_commitish: ${{ github.event.inputs.fw_ref || github.ref }} - tag_name: build-${{ env.FW_BUILD_NUMBER }} - name: "FieldWorks Base Build #${{ env.FW_BUILD_NUMBER }}" - draft: false - prerelease: true - fail_on_unmatched_files: true - files: | - ${{ steps.compress_artifacts.outputs.build_dir_zip }} - ${{ steps.compress_artifacts.outputs.proc_runner_zip }} - - - name: Upload Build Logs - uses: actions/upload-artifact@v4 - if: always() - with: - if-no-files-found: warn - name: build-logs - path: | - Build/*.log - Build/*.binlog + debug_build_and_test: + env: + CROWDIN_API_KEY: ${{ secrets.FLEX_CROWDIN_API }} + LcmRootDir: ${{ github.workspace }}/Localizations/LCM + FILESTOSIGNLATER: ./signExternally + name: Build Debug and run Tests + runs-on: windows-latest + steps: + - name: Compute build number + id: build_number + run: | + $lastJenkins = 1100 # The last base build from jenkins, rounded to the next hundred + $githubRun = $env:GITHUB_RUN_NUMBER + $combined = $lastJenkins + $githubRun + echo "Calculated build number: $combined" + echo "FW_BUILD_NUMBER=$combined" >> $env:GITHUB_ENV + + - name: Checkout Files + uses: actions/checkout@v4 + id: checkout + with: + ref: ${{ github.event.inputs.fw_ref || github.ref }} + fetch-depth: 0 + + - name: Checkout Helps + uses: actions/checkout@v4 + id: helps-checkout + with: + repository: 'sillsdev/FwHelps' + ref: ${{ github.event.inputs.helps_ref || 'develop' }} + fetch-depth: 0 + path: 'DistFiles/Helps' + + - name: Checkout PatchableInstaller + uses: actions/checkout@v4 + id: installer-checkout + with: + repository: 'sillsdev/genericinstaller' + ref: ${{ github.event.inputs.installer_ref || 'master' }} + fetch-depth: 0 + path: 'PatchableInstaller' + + - name: Checkout Localizations + uses: actions/checkout@v4 + id: loc-checkout + with: + repository: 'sillsdev/FwLocalizations' + ref: ${{ github.event.inputs.localizations_ref || 'develop' }} + fetch-depth: 0 + path: 'Localizations' + - name: Checkout liblcm + uses: actions/checkout@v4 + id: liblcm-checkout + with: + repository: 'sillsdev/liblcm' + ref: ${{ github.event.inputs.installer_ref || 'master' }} + fetch-depth: 0 + path: 'Localizations/LCM' + + - name: Download .NET 461 targeting pack + uses: suisei-cn/actions-download-file@818d6b7dc8fe73f2f924b6241f2b1134ca1377d9 # 1.6.0 + id: downloadfile # Remember to give an ID if you need the output filename + with: + url: "https://download.microsoft.com/download/F/1/D/F1DEB8DB-D277-4EF9-9F48-3A65D4D8F965/NDP461-DevPack-KB3105179-ENU.exe" + target: public/ + + - name: Install .NET 461 targeting pack + shell: cmd + working-directory: public + run: NDP461-DevPack-KB3105179-ENU.exe /q + + - name: Setup dotnet + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 3.1.x + 5.0.x + + - name: Install Overcrowdin + shell: cmd + run: | + dotnet tool update -g overcrowdin || dotnet tool install -g overcrowdin + + - name: Downgrade Wix Toolset - remove when runner has 3.14.2 + run: | + choco uninstall wixtoolset + choco install wixtoolset --version 3.11.2 --allow-downgrade --force + echo "C:\Program Files (x86)\WiX Toolset v3.11\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + if: github.event_name != 'pull_request' + + - name: Prepare for build + shell: cmd + working-directory: Build + run: build64.bat /t:WriteNonlocalDevelopmentPropertiesFile + + - name: Build and run tests + id: build_installer + shell: powershell + run: | + cd Build + .\build64.bat /t:BuildBaseInstaller "/property:config=release;action=test;desktopNotAvailable=true" /v:d /bl ^| tee-object -FilePath build.log + cd .. + cd BuildDir + md5sum *.exe > md5.txt + + - name: Scan Build Output + shell: powershell + working-directory: Build + run: | + $results = Select-String -Path "build.log" -Pattern "^\s*[1-9][0-9]* Error\(s\)" + if ($results) { + foreach ($result in $results) { + Write-Host "Found errors in build.log $($result.LineNumber): $($result.Line)" -ForegroundColor red + } + exit 1 + } else { + Write-Host "No errors found" -ForegroundColor green + exit 0 + } + + - name: Move installers to staging folder + id: stage_installers + shell: pwsh + run: | + $staging = Join-Path $pwd "Installers" + if (-not (Test-Path $staging)) { New-Item -ItemType Directory -Path $staging | Out-Null } + + $offlineExe = Get-ChildItem -Path BuildDir -Filter "FieldWorks_*_Offline_x64.exe" | Select-Object -First 1 + $onlineExe = Get-ChildItem -Path BuildDir -Filter "FieldWorks_*_Online_x64.exe" | Select-Object -First 1 + + if (-not $offlineExe) { throw "FieldWorks_VERSION_Offline_x64.exe not found in BuildDir" } + if (-not $onlineExe) { throw "FieldWorks_VERSION_Online_x64.exe not found in BuildDir" } + + Move-Item -Path $offlineExe.FullName -Destination $staging + Move-Item -Path $onlineExe.FullName -Destination $staging + + $offlinePath = Join-Path $staging $offlineExe.Name + $onlinePath = Join-Path $staging $onlineExe.Name + + "offline_exe=$offlinePath" >> $env:GITHUB_OUTPUT + "online_exe=$onlinePath" >> $env:GITHUB_OUTPUT + + - name: Extract burn engines from bundles + shell: cmd + working-directory: Installers + run: | + insignia -ib "${{ steps.stage_installers.outputs.offline_exe }}" -o offline-engine.exe + insignia -ib "${{ steps.stage_installers.outputs.online_exe }}" -o online-engine.exe + + - name: Sign Engines + if: github.event_name != 'pull_request' + uses: sillsdev/codesign/trusted-signing-action@v3 + with: + credentials: ${{ secrets.TRUSTED_SIGNING_CREDENTIALS }} + files-folder: Installers + files-folder-filter: '*-engine.exe' + description: 'FieldWorks Installer' + description-url: 'https://software.sil.org/fieldworks/' + + - name: Reattach Engines + if: github.event_name != 'pull_request' + working-directory: Installers + shell: cmd + run: | + insignia -ab offline-engine.exe "${{ steps.stage_installers.outputs.offline_exe }}" -o "${{ steps.stage_installers.outputs.offline_exe }}" + insignia -ab online-engine.exe "${{ steps.stage_installers.outputs.online_exe }}" -o "${{ steps.stage_installers.outputs.online_exe }}" + + - name: Sign Bundles + if: github.event_name != 'pull_request' + uses: sillsdev/codesign/trusted-signing-action@v3 + with: + credentials: ${{ secrets.TRUSTED_SIGNING_CREDENTIALS }} + files-folder: Installers + files-folder-filter: FieldWorks_*.exe + description: 'FieldWorks Installer' + description-url: 'https://software.sil.org/fieldworks/' + + - name: Configure AWS credentials + if: ${{ inputs.make_release == 'true' }} + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: us-east-1 + + - name: Upload Installers to S3 + if: ${{ inputs.make_release == 'true' }} + shell: pwsh + run: | + $offlineExe = "${{ steps.stage_installers.outputs.offline_exe }}" + $onlineExe = "${{ steps.stage_installers.outputs.online_exe }}" + + if (-not (Test-Path $offlineExe)) { + throw "Offline installer not found at $offlineExe" + } + if (-not (Test-Path $onlineExe)) { + throw "Online installer not found at $onlineExe" + } + + $offlineFile = Split-Path $offlineExe -Leaf + $onlineFile = Split-Path $onlineExe -Leaf + + $s3BasePath = "jobs/FieldWorks-Win-all-Release-Base/$($env:FW_BUILD_NUMBER)" + $offlineS3Key = "$s3BasePath/$offlineFile" + $onlineS3Key = "$s3BasePath/$onlineFile" + + aws s3 cp $offlineExe "s3://flex-updates/$offlineS3Key" + Write-Host "Uploaded offline installer to s3://flex-updates/$offlineS3Key" + + aws s3 cp $onlineExe "s3://flex-updates/$onlineS3Key" + Write-Host "Uploaded online installer to s3://flex-updates/$onlineS3Key" + + - name: Compress Build Artifacts + id: compress_artifacts + if: ${{ inputs.make_release == 'true' }} + run: | + [System.IO.Compression.ZipFile]::CreateFromDirectory( + "${{ github.workspace }}\BuildDir", + "${{ github.workspace }}\BuildDir.zip", + [System.IO.Compression.CompressionLevel]::Optimal, + $false + ) + [System.IO.Compression.ZipFile]::CreateFromDirectory( + "${{ github.workspace }}\PatchableInstaller\ProcRunner\ProcRunner\bin\Release\net48", + "${{ github.workspace }}\ProcRunner.zip", + [System.IO.Compression.CompressionLevel]::Optimal, + $false + ) + Get-ChildItem "${{ github.workspace }}" -Filter "*.zip" | Select-Object Name, Length + "build_dir_zip=$(("${{ github.workspace }}/BuildDir.zip") -replace '\\','/')" >> $env:GITHUB_OUTPUT + "proc_runner_zip=$(("${{ github.workspace }}/ProcRunner.zip") -replace '\\','/')" >> $env:GITHUB_OUTPUT + + + - name: Tag, Create Release, and Upload artifacts + if: ${{ inputs.make_release == 'true' }} + uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 + with: + target_commitish: ${{ github.event.inputs.fw_ref || github.ref }} + tag_name: build-${{ env.FW_BUILD_NUMBER }} + name: "FieldWorks Base Build #${{ env.FW_BUILD_NUMBER }}" + draft: false + prerelease: true + fail_on_unmatched_files: true + files: | + ${{ steps.compress_artifacts.outputs.build_dir_zip }} + ${{ steps.compress_artifacts.outputs.proc_runner_zip }} + + - name: Upload Build Logs + uses: actions/upload-artifact@v4 + if: always() + with: + if-no-files-found: warn + name: build-logs + path: | + Build/*.log + Build/*.binlog diff --git a/.github/workflows/patch-installer-cd.yml b/.github/workflows/patch-installer-cd.yml index 8a9c314bcf..0b89bc051a 100644 --- a/.github/workflows/patch-installer-cd.yml +++ b/.github/workflows/patch-installer-cd.yml @@ -15,38 +15,42 @@ on: workflow_dispatch: inputs: fw_ref: - description: "Commit-ish (branch, tag, SHA) to checkout for the main repository" + description: 'Commit-ish (branch, tag, SHA) to checkout for the main repository' required: false - default: "" + default: '' helps_ref: - description: "Commit-ish for helps repository" + description: 'Commit-ish for helps repository' required: false - default: "develop" + default: 'develop' + installer_ref: + description: 'Commit-ish for PatchableInstaller repository' + required: false + default: 'master' localizations_ref: - description: "Commit-ish for localization repository" + description: 'Commit-ish for localization repository' required: false - default: "develop" + default: 'develop' lcm_ref: - description: "Commit-ish for liblcm repository" + description: 'Commit-ish for liblcm repository' required: false - default: "master" - # default should be changed to the most recent base release for the release cycle. - # After 9.3 is stable, base_release can be computed from base_build_number, but they need to be independent for bootstrapping patches - # on Jenkins-built bases. + default: 'master' +# default should be changed to the most recent base release for the release cycle. +# After 9.3 is stable, base_release can be computed from base_build_number, but they need to be independent for bootstrapping patches +# on Jenkins-built bases. base_release: - description: "The github release for the base build artifacts (separate only for bootstrapping; should be removed after 9.3 is the stable)" - default: "build-1379" # When updating this, update base_build_number and their fallbacks below, too. + description: 'The github release for the base build artifacts (separate only for bootstrapping; should be removed after 9.3 is the stable)' + default: 'build-1379' # When updating this, update base_build_number and their fallbacks below, too. base_build_number: - description: "The base build number" + description: 'The base build number' required: false default: '1379' make_release: - description: "Should the release be pushed to s3 - use false for test builds" + description: 'Should the release be pushed to s3 - use false for test builds' required: false - default: "true" + default: 'true' concurrency: - group: ${{ github.workflow }}-${{ github.ref }} + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true jobs: @@ -83,45 +87,53 @@ jobs: uses: actions/checkout@v4 id: helps-checkout with: - repository: "sillsdev/FwHelps" + repository: 'sillsdev/FwHelps' ref: ${{ github.event.inputs.helps_ref || 'develop' }} fetch-depth: 0 - path: "DistFiles/Helps" + path: 'DistFiles/Helps' + - name: Checkout PatchableInstaller + uses: actions/checkout@v4 + id: installer-checkout + with: + repository: 'sillsdev/genericinstaller' + ref: ${{ github.event.inputs.installer_ref || 'master' }} + fetch-depth: 0 + path: 'PatchableInstaller' - name: Checkout Localizations uses: actions/checkout@v4 id: loc-checkout with: - repository: "sillsdev/FwLocalizations" + repository: 'sillsdev/FwLocalizations' ref: ${{ github.event.inputs.localizations_ref || 'develop' }} fetch-depth: 0 - path: "Localizations" + path: 'Localizations' - name: Checkout liblcm uses: actions/checkout@v4 id: liblcm-checkout with: - repository: "sillsdev/liblcm" + repository: 'sillsdev/liblcm' ref: ${{ github.event.inputs.lcm_ref || 'master' }} fetch-depth: 0 - path: "Localizations/LCM" + path: 'Localizations/LCM' - - name: Download .NET 481 targeting pack + - name: Download .NET 461 targeting pack uses: suisei-cn/actions-download-file@818d6b7dc8fe73f2f924b6241f2b1134ca1377d9 # 1.6.0 - id: downloadfile # Remember to give an ID if you need the output filename + id: downloadfile # Remember to give an ID if you need the output filename with: - url: "https://download.microsoft.com/download/8/1/8/81877d8b-a9b2-4153-9ad2-63a6441d11dd/NDP481-DevPack-ENU.exe" - target: public/ + url: "https://download.microsoft.com/download/F/1/D/F1DEB8DB-D277-4EF9-9F48-3A65D4D8F965/NDP461-DevPack-KB3105179-ENU.exe" + target: public/ - - name: Install .NET 481 targeting pack + - name: Install .NET 461 targeting pack shell: cmd working-directory: public - run: NDP481-DevPack-ENU.exe /q + run: NDP461-DevPack-KB3105179-ENU.exe /q - name: Setup dotnet uses: actions/setup-dotnet@v4 with: - dotnet-version: | + dotnet-version: | 3.1.x 5.0.x @@ -130,14 +142,13 @@ jobs: run: | dotnet tool update -g overcrowdin || dotnet tool install -g overcrowdin - - name: Add NETFX tools to PATH - shell: powershell + - name: Downgrade Wix Toolset - remove when runner has 3.14.2 run: | - 'C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8.1 Tools' | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - - - name: Setup MSBuild - uses: microsoft/setup-msbuild@v2 - + choco uninstall wixtoolset + choco install wixtoolset --version 3.11.2 --allow-downgrade --force + echo "C:\Program Files (x86)\WiX Toolset v3.11\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + if: github.event_name != 'pull_request' + - name: Import Base Build Artifacts shell: pwsh run: | @@ -163,7 +174,7 @@ jobs: Write-Host "Expanded BuildDir.zip -> ./BuildDir" # Ensure ProcRunner target path exists - $procTarget = "FLExInstaller/Shared/ProcRunner/ProcRunner/bin/Release/net48" + $procTarget = "PatchableInstaller/ProcRunner/ProcRunner/bin/Release/net48" if (-not (Test-Path $procTarget)) { New-Item -ItemType Directory -Path $procTarget -Force | Out-Null } @@ -175,9 +186,8 @@ jobs: # Write out the properties file (a first build on a system requires a prompt response otherwise) # and set an OS feature in the registry that will allow Wix v3 to use temporary files without error - name: Prepare for build - shell: powershell + working-directory: Build run: | - $ErrorActionPreference = 'Stop' # Define paths and the key/value to set $regPaths = @( "HKLM:\SOFTWARE\Microsoft\.NETFramework\AppContext", @@ -185,26 +195,26 @@ jobs: ) $valueName = "Switch.System.DisableTempFileCollectionDirectoryFeature" $expectedValue = "true" - + foreach ($path in $regPaths) { Write-Host "Adding or updating registry value in $path..." if (-not (Test-Path $path)) { - New-Item -Path $path -Force | Out-Null + New-Item -Path $path -Force | Out-Null } New-ItemProperty -Path $path -Name $valueName -Value $expectedValue -Type String -Force } + + .\build64.bat /t:WriteNonlocalDevelopmentPropertiesFile - name: Build Debug and run tests id: build_installer shell: powershell run: | - $ErrorActionPreference = 'Stop' Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\.NETFramework\AppContext" - # Build patch installer (uses traversal build internally via Installer.targets) - dotnet restore FieldWorks.sln /p:NoWarn=NU1903 /p:DisableWarnForInvalidRestoreProjects=true - msbuild Build/InstallerBuild.proj /t:BuildPatchInstaller /p:Configuration=Debug /p:Platform=x64 /p:config=release /p:action=test /p:desktopNotAvailable=true /m /v:d /bl | Tee-Object -FilePath Build/build.log - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - Set-Location BuildDir + cd Build + .\build64.bat /t:BuildPatchInstaller "/property:config=release;action=test;desktopNotAvailable=true" /v:d /bl ^| tee-object -FilePath build.log + cd .. + cd BuildDir - name: Scan Debug Build Output shell: powershell @@ -213,11 +223,11 @@ jobs: $results = Select-String -Path "build.log" -Pattern "^\s*[1-9][0-9]* Error\(s\)" if ($results) { foreach ($result in $results) { - Write-Host "Found errors in build.log $($result.LineNumber): $($result.Line)" -ForegroundColor red + Write-Host "Found errors in build.log $($result.LineNumber): $($result.Line)" -ForegroundColor red } exit 1 } else { - Write-Host "No errors found" -ForegroundColor green + Write-Host "No errors found" -ForegroundColor green exit 0 } @@ -225,21 +235,21 @@ jobs: id: find_patch shell: pwsh run: | - $patch = Get-ChildItem -Path BuildDir -Filter "FieldWorks_*.msp" | Select-Object -First 1 - if (-not $patch) { throw "Patch (.msp) file not found in BuildDir" } + $patch = Get-ChildItem -Path BuildDir -Filter "FieldWorks_*.msp" | Select-Object -First 1 + if (-not $patch) { throw "Patch (.msp) file not found in BuildDir" } - $patchPath = $patch.FullName - "patch_file=$patchPath" >> $env:GITHUB_OUTPUT + $patchPath = $patch.FullName + "patch_file=$patchPath" >> $env:GITHUB_OUTPUT - name: Sign Patch if: github.event_name != 'pull_request' uses: sillsdev/codesign/trusted-signing-action@v3 with: - credentials: ${{ secrets.TRUSTED_SIGNING_CREDENTIALS }} - files-folder: BuildDir - files-folder-filter: "*.msp" - description: "FieldWorks Patch Installer" - description-url: "https://software.sil.org/fieldworks/" + credentials: ${{ secrets.TRUSTED_SIGNING_CREDENTIALS }} + files-folder: BuildDir + files-folder-filter: '*.msp' + description: 'FieldWorks Patch Installer' + description-url: 'https://software.sil.org/fieldworks/' - name: Configure AWS credentials # Use inputs if defined but default to true From d0560d8f49d7b8d33e29e51a7723201e711a67d0 Mon Sep 17 00:00:00 2001 From: Jason Naylor Date: Wed, 18 Feb 2026 13:34:19 -0800 Subject: [PATCH 02/11] Update CI with current build steps (LT-22420) * Call test.ps1 and report on results * Fix lint error in copilot-docs.detect.yml --- .github/workflows/CI.yml | 63 +++++------------------ .github/workflows/copilot-docs-detect.yml | 59 ++++++++++----------- 2 files changed, 44 insertions(+), 78 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 29c4b2aff2..e31a2f9ba7 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -6,14 +6,16 @@ on: branches: ["release/**", "main", "feature/PubSub"] workflow_dispatch: +permissions: + contents: read + checks: write + concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true jobs: debug_build_and_test: - env: - CROWDIN_API_KEY: ${{ secrets.FLEX_CROWDIN_API }} name: Build Debug and run Tests runs-on: windows-latest steps: @@ -21,65 +23,28 @@ jobs: uses: actions/checkout@v4 id: checkout - - name: Download 461 targeting pack - uses: suisei-cn/actions-download-file@818d6b7dc8fe73f2f924b6241f2b1134ca1377d9 # 1.6.0 - id: downloadfile # Remember to give an ID if you need the output filename - with: - url: "https://download.microsoft.com/download/F/1/D/F1DEB8DB-D277-4EF9-9F48-3A65D4D8F965/NDP461-DevPack-KB3105179-ENU.exe" - target: public/ - - - name: Install targeting pack - shell: cmd - working-directory: public - run: NDP461-DevPack-KB3105179-ENU.exe /q - - name: Setup dotnet uses: actions/setup-dotnet@v4 with: dotnet-version: | - 2.1.x - 3.1.x 5.0.x - - name: Prepare for build - shell: cmd - working-directory: Build - run: build64.bat /t:WriteNonlocalDevelopmentPropertiesFile - - name: Build Debug and run tests id: build_and_test shell: powershell - run: | - cd Build - .\build64.bat /t:remakefw-jenkins /p:action=test /p:desktopNotAvailable=true ^| tee-object -FilePath build.log - - - name: Scan Debug Build Output - shell: powershell - working-directory: Build - run: | - $results = Select-String -Path "build.log" -Pattern "^\s*[1-9][0-9]* Error\(s\)" - if ($results) { - foreach ($result in $results) { - Write-Host "Found errors in build.log $($result.LineNumber): $($result.Line)" -ForegroundColor red - } - exit 1 - } else { - Write-Host "No errors found" -ForegroundColor green - exit 0 - } - - - name: Capture Test Results - shell: powershell - working-directory: Build - run: .\NUnitReport /a ^| tee-object -FilePath test-results.log + run: .\test.ps1 -TestFilter "TestCategory!=LongRunning&TestCategory!=ByHand&TestCategory!=SmokeTest&TestCategory!=DesktopRequired" | tee-object -FilePath build.log - - name: Report Test Results - uses: sillsdev/fw-nunitreport-action@v2.0.0 + - name: Publish Test Results + uses: EnricoMi/publish-unit-test-result-action@27d65e188ec43221b20d26de30f4892fad91df2f + if: ${{ !cancelled() }} with: - log-path: Build/test-results.log - token: ${{ secrets.GITHUB_TOKEN }} + files: '**/*.trx' + check_name: NUnit Tests + comment_mode: always + job_summary: true + fail_on: test failures - uses: actions/upload-artifact@v4 with: name: build-logs - path: Build/*.log + path: ./*.log diff --git a/.github/workflows/copilot-docs-detect.yml b/.github/workflows/copilot-docs-detect.yml index e251f5dfa6..532ecc75a2 100644 --- a/.github/workflows/copilot-docs-detect.yml +++ b/.github/workflows/copilot-docs-detect.yml @@ -1,34 +1,35 @@ name: Agent docs detection on: - pull_request: - types: [opened, synchronize, reopened] + pull_request: + types: [opened, synchronize, reopened] jobs: - detect: - runs-on: windows-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: "3.x" - - name: Detect folders needing agent-doc updates - shell: pwsh - run: | - python .github/detect_copilot_needed.py --base "origin/$env:GITHUB_BASE_REF" --strict - - name: Scaffold changed agent docs (advisory) - if: ${{ always() }} - shell: pwsh - run: | - python .github/scaffold_copilot_markdown.py --base "origin/$env:GITHUB_BASE_REF" - continue-on-error: true - - name: Validate agent-doc structure (optional) - if: ${{ always() }} - shell: pwsh - run: | - python .github/check_copilot_docs.py --only-changed --base "origin/$env:GITHUB_BASE_REF" --fail - continue-on-error: true + detect: + runs-on: windows-latest + steps: + - name: Checkout Files + uses: actions/checkout@v4 + id: checkout + with: + fetch-depth: 0 + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + - name: Detect folders needing agent-doc updates + shell: pwsh + run: | + python .github/detect_copilot_needed.py --base "origin/$env:GITHUB_BASE_REF" --strict + - name: Scaffold changed agent docs (advisory) + if: ${{ always() }} + shell: pwsh + run: | + python .github/scaffold_copilot_markdown.py --base "origin/$env:GITHUB_BASE_REF" + continue-on-error: true + - name: Validate agent-doc structure (optional) + if: ${{ always() }} + shell: pwsh + run: | + python .github/check_copilot_docs.py --only-changed --base "origin/$env:GITHUB_BASE_REF" --fail + continue-on-error: true From 109103f5c710eefc89719f765139c645175335b4 Mon Sep 17 00:00:00 2001 From: John Lambert Date: Thu, 19 Feb 2026 11:44:56 -0500 Subject: [PATCH 03/11] ci: use modern FieldWorks scripts in GitHub Actions - run build.ps1 -Configuration Debug -Platform x64 -BuildTests - run managed tests via test.ps1 -NoBuild with existing test filter - run native tests via test.ps1 -Native -NoBuild - remove outdated actions/setup-dotnet (5.0.x) step - upload logs from repo root and Output/** - Need to build for native to get executables - Use windows-compatible test uploader - Add longer timeout for fonts - can take a while - Timeout for native tests hanging was too long. --- .github/workflows/CI.yml | 66 ++++++++++++++----- Build/scripts/Invoke-CppTest.ps1 | 51 ++++++++++++-- .../DefaultFontsControlTests.cs | 1 + 3 files changed, 94 insertions(+), 24 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index e31a2f9ba7..c0c67c757d 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -16,35 +16,67 @@ concurrency: jobs: debug_build_and_test: - name: Build Debug and run Tests + name: Build Debug and run managed/native tests runs-on: windows-latest steps: - name: Checkout Files uses: actions/checkout@v4 id: checkout - - name: Setup dotnet - uses: actions/setup-dotnet@v4 - with: - dotnet-version: | - 5.0.x - - - name: Build Debug and run tests - id: build_and_test + - name: Build Debug x64 (with tests) + id: build_debug + shell: powershell + run: | + .\build.ps1 -Configuration Debug -Platform x64 -BuildTests + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + + - name: Run managed tests (filtered) + id: test_managed + shell: powershell + run: | + .\test.ps1 -Configuration Debug -NoBuild -TestFilter "TestCategory!=LongRunning&TestCategory!=ByHand&TestCategory!=SmokeTest&TestCategory!=DesktopRequired" + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + + - name: Run native tests + id: test_native shell: powershell - run: .\test.ps1 -TestFilter "TestCategory!=LongRunning&TestCategory!=ByHand&TestCategory!=SmokeTest&TestCategory!=DesktopRequired" | tee-object -FilePath build.log + run: | + .\test.ps1 -Configuration Debug -Native + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + + - name: Upload TRX test results + if: ${{ !cancelled() }} + uses: actions/upload-artifact@v4 + with: + name: trx-results + path: | + **/*.trx + if-no-files-found: warn + + - uses: actions/upload-artifact@v4 + with: + name: build-logs + path: | + ./*.log + ./Output/**/*.log + + publish_test_results: + name: Publish Test Results + if: ${{ !cancelled() }} + needs: debug_build_and_test + runs-on: ubuntu-latest + steps: + - name: Download TRX artifacts + uses: actions/download-artifact@v4 + with: + name: trx-results + path: test-results - name: Publish Test Results uses: EnricoMi/publish-unit-test-result-action@27d65e188ec43221b20d26de30f4892fad91df2f - if: ${{ !cancelled() }} with: - files: '**/*.trx' + files: 'test-results/**/*.trx' check_name: NUnit Tests comment_mode: always job_summary: true fail_on: test failures - - - uses: actions/upload-artifact@v4 - with: - name: build-logs - path: ./*.log diff --git a/Build/scripts/Invoke-CppTest.ps1 b/Build/scripts/Invoke-CppTest.ps1 index 69e0354e50..ee0a5637f2 100644 --- a/Build/scripts/Invoke-CppTest.ps1 +++ b/Build/scripts/Invoke-CppTest.ps1 @@ -52,6 +52,8 @@ param( [int]$TimeoutSeconds = 300, + [int]$PostCompletionGraceSeconds = 5, + [string[]]$TestArguments, [string]$LogPath @@ -458,13 +460,45 @@ function Invoke-Run { $process = Start-Process @startInfo $timedOut = $false - try { - Wait-Process -Id $process.Id -Timeout $TimeoutSeconds -ErrorAction Stop - } - catch { - $timedOut = $true - Write-Host "Test run exceeded timeout (${TimeoutSeconds}s); terminating process..." -ForegroundColor Red - try { Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue } catch {} + $terminatedAfterCompletion = $false + $summarySeenAt = $null + $startTime = Get-Date + $summaryPattern = 'Tests \[Ok-Fail-Error\]: \[\d+-\d+-\d+\]' + + while ($true) { + $process.Refresh() + if ($process.HasExited) { + break + } + + $elapsedSeconds = ((Get-Date) - $startTime).TotalSeconds + if ($elapsedSeconds -ge $TimeoutSeconds) { + $timedOut = $true + Write-Host "Test run exceeded timeout (${TimeoutSeconds}s); terminating process..." -ForegroundColor Red + try { Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue } catch {} + break + } + + if (Test-Path $LogPath) { + $recentOutput = Get-Content -Path $LogPath -Tail 25 -ErrorAction SilentlyContinue + if (($recentOutput -join "`n") -match $summaryPattern) { + if (-not $summarySeenAt) { + $summarySeenAt = Get-Date + Write-Host "Detected Unit++ completion summary; waiting up to ${PostCompletionGraceSeconds}s for process exit..." -ForegroundColor Yellow + } + elseif (((Get-Date) - $summarySeenAt).TotalSeconds -ge $PostCompletionGraceSeconds) { + $terminatedAfterCompletion = $true + Write-Host "Process did not exit ${PostCompletionGraceSeconds}s after completion summary; terminating hung process..." -ForegroundColor Yellow + try { Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue } catch {} + break + } + } + else { + $summarySeenAt = $null + } + } + + Start-Sleep -Milliseconds 250 } $process.Refresh() @@ -486,6 +520,9 @@ function Invoke-Run { if ($timedOut) { Write-Host "Tests terminated due to timeout (${TimeoutSeconds}s)." -ForegroundColor Red } + elseif ($terminatedAfterCompletion) { + Write-Host "Tests reported completion but process hung; terminated after ${PostCompletionGraceSeconds}s grace period." -ForegroundColor Yellow + } elseif ($exitCode -eq 0) { Write-Host "All tests passed! (exit code: 0)" -ForegroundColor Green } diff --git a/Src/FwCoreDlgs/FwCoreDlgControls/FwCoreDlgControlsTests/DefaultFontsControlTests.cs b/Src/FwCoreDlgs/FwCoreDlgControls/FwCoreDlgControlsTests/DefaultFontsControlTests.cs index 8bb702da49..b5b46bc05d 100644 --- a/Src/FwCoreDlgs/FwCoreDlgControls/FwCoreDlgControlsTests/DefaultFontsControlTests.cs +++ b/Src/FwCoreDlgs/FwCoreDlgControls/FwCoreDlgControlsTests/DefaultFontsControlTests.cs @@ -51,6 +51,7 @@ public override void TestTearDown() /// /// ---------------------------------------------------------------------------------------- [Test] + [Timeout(180000)] public void FontsAreAlphabeticallySorted() { ComboBox.ObjectCollection fontNamesNormal = m_fontsControl.DefaultFontComboBox.Items; From a5d82f3a8674e27fab66261fa0d657eb48e2d6b6 Mon Sep 17 00:00:00 2001 From: John Lambert Date: Thu, 19 Feb 2026 13:15:50 -0500 Subject: [PATCH 04/11] No PR comment --- .github/workflows/CI.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index c0c67c757d..70d08c038e 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -77,6 +77,6 @@ jobs: with: files: 'test-results/**/*.trx' check_name: NUnit Tests - comment_mode: always + comment_mode: off job_summary: true fail_on: test failures From e71ab05c19fd5d8de4b147b3e2ddc41b7145bb97 Mon Sep 17 00:00:00 2001 From: John Lambert Date: Thu, 19 Feb 2026 14:04:38 -0500 Subject: [PATCH 05/11] Only run native tests when changed. Make heartbeat --- .github/workflows/CI.yml | 146 ++++++++++++++++++++++++++++++- Build/scripts/Invoke-CppTest.ps1 | 7 ++ 2 files changed, 151 insertions(+), 2 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 70d08c038e..de6faa7687 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -1,3 +1,23 @@ +# CI Flow (high-level) +# +# ```mermaid +# flowchart TD +# A[Checkout] --> B[Detect native-related changes] +# B --> C[Build Debug x64 with tests] +# C --> D[Run managed tests] +# C --> E{Native changes?} +# E -->|yes| F[Build native test exes in parallel with managed tests] +# D --> G{Native changes?} +# F --> G +# G -->|yes| H[Run native tests -NoBuild] +# G -->|no| I[Write native-skip summary] +# H --> J[Summarize native results] +# I --> K[Upload TRX] +# J --> K +# K --> L[Upload logs on failure only] +# L --> M[Publish test results] +# ``` +# name: Flex CI on: push: @@ -23,6 +43,26 @@ jobs: uses: actions/checkout@v4 id: checkout + - name: Detect native-related changes + id: changes + uses: dorny/paths-filter@v3 + with: + filters: | + native: + - 'Src/**/*.cpp' + - 'Src/**/*.h' + - 'Src/**/*.hpp' + - 'Src/**/*.cc' + - 'Src/**/*.ixx' + - 'Src/**/*.def' + - 'Src/**/*.vcxproj' + - 'Src/**/*.vcxproj.filters' + - 'Src/**/*.mak' + - 'Lib/src/unit++/**' + - 'Build/Src/NativeBuild/**' + - 'Build/scripts/Invoke-CppTest.ps1' + - 'test.ps1' + - name: Build Debug x64 (with tests) id: build_debug shell: powershell @@ -34,16 +74,117 @@ jobs: id: test_managed shell: powershell run: | + $nativeChanged = '${{ steps.changes.outputs.native }}' -eq 'true' + $nativeBuildJob = $null + + if ($nativeChanged) { + Write-Host "Native-related changes detected; building native test executables in parallel." -ForegroundColor Cyan + $repoRoot = $PWD.Path + $nativeBuildJob = Start-Job -Name NativeBuild -ScriptBlock { + param($repoRoot) + Set-Location $repoRoot + + .\Build\scripts\Invoke-CppTest.ps1 -Action Build -TestProject TestGeneric -Configuration Debug + if ($LASTEXITCODE -ne 0) { + throw "TestGeneric native build failed with exit code $LASTEXITCODE" + } + + .\Build\scripts\Invoke-CppTest.ps1 -Action Build -TestProject TestViews -Configuration Debug + if ($LASTEXITCODE -ne 0) { + throw "TestViews native build failed with exit code $LASTEXITCODE" + } + } -ArgumentList $repoRoot + } + else { + Write-Host "No native-related changes detected; running managed tests only." -ForegroundColor Cyan + } + .\test.ps1 -Configuration Debug -NoBuild -TestFilter "TestCategory!=LongRunning&TestCategory!=ByHand&TestCategory!=SmokeTest&TestCategory!=DesktopRequired" - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + $managedExitCode = $LASTEXITCODE + + if ($nativeBuildJob) { + Wait-Job -Job $nativeBuildJob | Out-Null + Receive-Job -Job $nativeBuildJob + $nativeBuildCompleted = $nativeBuildJob.State -eq 'Completed' + Remove-Job -Job $nativeBuildJob -Force + + if (-not $nativeBuildCompleted) { + Write-Host "[ERROR] Native test executable build did not complete successfully." -ForegroundColor Red + exit 1 + } + } + + if ($managedExitCode -ne 0) { + exit $managedExitCode + } - name: Run native tests id: test_native shell: powershell + if: ${{ steps.changes.outputs.native == 'true' }} run: | - .\test.ps1 -Configuration Debug -Native + .\test.ps1 -Configuration Debug -Native -NoBuild if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + - name: Report native test skip + if: ${{ steps.changes.outputs.native != 'true' }} + shell: powershell + run: | + Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value '### Native test summary' + Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value '' + Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value 'Native tests skipped: no native-related file changes detected.' + + - name: Summarize native test results + if: ${{ always() && steps.changes.outputs.native == 'true' }} + shell: powershell + run: | + $nativeLogs = @( + 'Output/Debug/testGenericLib.exe.log', + 'Output/Debug/TestViews.exe.log' + ) + + $tableRows = @() + foreach ($logPath in $nativeLogs) { + if (-not (Test-Path $logPath)) { + Write-Host "::warning title=Native log missing::$logPath not found" + $tableRows += "| $logPath | - | - | - | MISSING |" + continue + } + + $summaryLine = Get-Content -Path $logPath -ErrorAction SilentlyContinue | + Select-String -Pattern 'Tests \[Ok-Fail-Error\]: \[(\d+)-(\d+)-(\d+)\]' | + Select-Object -Last 1 + + if (-not $summaryLine) { + Write-Host "::warning title=Native summary missing::$logPath does not contain Unit++ summary line" + $tableRows += "| $logPath | - | - | - | UNKNOWN |" + continue + } + + $match = [regex]::Match($summaryLine.Line, 'Tests \[Ok-Fail-Error\]: \[(\d+)-(\d+)-(\d+)\]') + $okCount = [int]$match.Groups[1].Value + $failCount = [int]$match.Groups[2].Value + $errorCount = [int]$match.Groups[3].Value + $status = if ($failCount -gt 0 -or $errorCount -gt 0) { 'FAIL' } else { 'PASS' } + + if ($status -eq 'FAIL') { + Write-Host "::error title=Native test failure::$logPath => Ok=$okCount Fail=$failCount Error=$errorCount" + } + else { + Write-Host "::notice title=Native test pass::$logPath => Ok=$okCount Fail=$failCount Error=$errorCount" + } + + $tableRows += "| $logPath | $okCount | $failCount | $errorCount | $status |" + } + + Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value '### Native test summary' + Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value '' + Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value '| Log | Ok | Fail | Error | Status |' + Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value '|---|---:|---:|---:|---|' + foreach ($row in $tableRows) { + Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value $row + } + - name: Upload TRX test results if: ${{ !cancelled() }} uses: actions/upload-artifact@v4 @@ -54,6 +195,7 @@ jobs: if-no-files-found: warn - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() && failure() }} with: name: build-logs path: | diff --git a/Build/scripts/Invoke-CppTest.ps1 b/Build/scripts/Invoke-CppTest.ps1 index ee0a5637f2..5674ba50a8 100644 --- a/Build/scripts/Invoke-CppTest.ps1 +++ b/Build/scripts/Invoke-CppTest.ps1 @@ -464,6 +464,8 @@ function Invoke-Run { $summarySeenAt = $null $startTime = Get-Date $summaryPattern = 'Tests \[Ok-Fail-Error\]: \[\d+-\d+-\d+\]' + $heartbeatIntervalSeconds = 30 + $nextHeartbeatAt = $startTime.AddSeconds($heartbeatIntervalSeconds) while ($true) { $process.Refresh() @@ -479,6 +481,11 @@ function Invoke-Run { break } + if ((Get-Date) -ge $nextHeartbeatAt) { + Write-Host ("[HEARTBEAT] {0} still running after {1:N0}s (pid {2})" -f $config.ExeName, $elapsedSeconds, $process.Id) -ForegroundColor DarkGray + $nextHeartbeatAt = (Get-Date).AddSeconds($heartbeatIntervalSeconds) + } + if (Test-Path $LogPath) { $recentOutput = Get-Content -Path $LogPath -Tail 25 -ErrorAction SilentlyContinue if (($recentOutput -join "`n") -match $summaryPattern) { From 666505b84c5b5d57f8588da0947e4a11c4d1adc1 Mon Sep 17 00:00:00 2001 From: John Lambert Date: Thu, 19 Feb 2026 14:31:13 -0500 Subject: [PATCH 06/11] Only key on changes; Refactor CI --- .github/instructions/terminal.instructions.md | 5 +- .github/workflows/CI.yml | 124 +++--------------- AGENTS.md | 3 +- Build/Agent/Build-NativeTestExecutables.ps1 | 24 ++++ Build/Agent/Detect-NativeChanges.ps1 | 77 +++++++++++ ...ke-ManagedTestsWithOptionalNativeBuild.ps1 | 72 ++++++++++ Build/Agent/README.md | 21 +++ Build/Agent/Summarize-NativeTestResults.ps1 | 59 +++++++++ 8 files changed, 273 insertions(+), 112 deletions(-) create mode 100644 Build/Agent/Build-NativeTestExecutables.ps1 create mode 100644 Build/Agent/Detect-NativeChanges.ps1 create mode 100644 Build/Agent/Invoke-ManagedTestsWithOptionalNativeBuild.ps1 create mode 100644 Build/Agent/README.md create mode 100644 Build/Agent/Summarize-NativeTestResults.ps1 diff --git a/.github/instructions/terminal.instructions.md b/.github/instructions/terminal.instructions.md index 9d91a98ba6..1226d90c3d 100644 --- a/.github/instructions/terminal.instructions.md +++ b/.github/instructions/terminal.instructions.md @@ -6,7 +6,10 @@ description: "Terminal command patterns for auto-approval in FieldWorks" # Terminal Commands -Commands with pipes (`|`), `&&`, or `2>&1` require manual approval. Use `scripts/Agent/` wrappers instead. +Commands with pipes (`|`), `&&`, or `2>&1` require manual approval. Use repository wrapper scripts instead. + +Placement policy for wrappers: +- Prefer `Build/Agent/` for new build/test/CI orchestration scripts. **MCP-first:** When the `ps-tools` MCP server is running, prefer MCP tools (`Git-Search`, `Read-FileContent`, `Invoke-AgentTask`, `build`, `test`, agent tools) instead of direct terminal commands. Use wrappers only when MCP is unavailable. diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index de6faa7687..5330246694 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -4,9 +4,9 @@ # flowchart TD # A[Checkout] --> B[Detect native-related changes] # B --> C[Build Debug x64 with tests] -# C --> D[Run managed tests] +# C --> D[Run managed tests wrapper] # C --> E{Native changes?} -# E -->|yes| F[Build native test exes in parallel with managed tests] +# E -->|yes| F[Wrapper starts native build in parallel] # D --> G{Native changes?} # F --> G # G -->|yes| H[Run native tests -NoBuild] @@ -42,26 +42,17 @@ jobs: - name: Checkout Files uses: actions/checkout@v4 id: checkout + with: + fetch-depth: 0 - - name: Detect native-related changes + - name: Detect native source changes id: changes - uses: dorny/paths-filter@v3 - with: - filters: | - native: - - 'Src/**/*.cpp' - - 'Src/**/*.h' - - 'Src/**/*.hpp' - - 'Src/**/*.cc' - - 'Src/**/*.ixx' - - 'Src/**/*.def' - - 'Src/**/*.vcxproj' - - 'Src/**/*.vcxproj.filters' - - 'Src/**/*.mak' - - 'Lib/src/unit++/**' - - 'Build/Src/NativeBuild/**' - - 'Build/scripts/Invoke-CppTest.ps1' - - 'test.ps1' + shell: powershell + run: | + .\Build\Agent\Detect-NativeChanges.ps1 ` + -EventName '${{ github.event_name }}' ` + -PullRequestBaseRef '${{ github.event.pull_request.base.ref }}' ` + -EventBefore '${{ github.event.before }}' - name: Build Debug x64 (with tests) id: build_debug @@ -74,49 +65,9 @@ jobs: id: test_managed shell: powershell run: | - $nativeChanged = '${{ steps.changes.outputs.native }}' -eq 'true' - $nativeBuildJob = $null - - if ($nativeChanged) { - Write-Host "Native-related changes detected; building native test executables in parallel." -ForegroundColor Cyan - $repoRoot = $PWD.Path - $nativeBuildJob = Start-Job -Name NativeBuild -ScriptBlock { - param($repoRoot) - Set-Location $repoRoot - - .\Build\scripts\Invoke-CppTest.ps1 -Action Build -TestProject TestGeneric -Configuration Debug - if ($LASTEXITCODE -ne 0) { - throw "TestGeneric native build failed with exit code $LASTEXITCODE" - } - - .\Build\scripts\Invoke-CppTest.ps1 -Action Build -TestProject TestViews -Configuration Debug - if ($LASTEXITCODE -ne 0) { - throw "TestViews native build failed with exit code $LASTEXITCODE" - } - } -ArgumentList $repoRoot - } - else { - Write-Host "No native-related changes detected; running managed tests only." -ForegroundColor Cyan - } - - .\test.ps1 -Configuration Debug -NoBuild -TestFilter "TestCategory!=LongRunning&TestCategory!=ByHand&TestCategory!=SmokeTest&TestCategory!=DesktopRequired" - $managedExitCode = $LASTEXITCODE - - if ($nativeBuildJob) { - Wait-Job -Job $nativeBuildJob | Out-Null - Receive-Job -Job $nativeBuildJob - $nativeBuildCompleted = $nativeBuildJob.State -eq 'Completed' - Remove-Job -Job $nativeBuildJob -Force - - if (-not $nativeBuildCompleted) { - Write-Host "[ERROR] Native test executable build did not complete successfully." -ForegroundColor Red - exit 1 - } - } - - if ($managedExitCode -ne 0) { - exit $managedExitCode - } + .\Build\Agent\Invoke-ManagedTestsWithOptionalNativeBuild.ps1 ` + -Configuration Debug ` + -NativeChanged '${{ steps.changes.outputs.native }}' - name: Run native tests id: test_native @@ -138,52 +89,7 @@ jobs: if: ${{ always() && steps.changes.outputs.native == 'true' }} shell: powershell run: | - $nativeLogs = @( - 'Output/Debug/testGenericLib.exe.log', - 'Output/Debug/TestViews.exe.log' - ) - - $tableRows = @() - foreach ($logPath in $nativeLogs) { - if (-not (Test-Path $logPath)) { - Write-Host "::warning title=Native log missing::$logPath not found" - $tableRows += "| $logPath | - | - | - | MISSING |" - continue - } - - $summaryLine = Get-Content -Path $logPath -ErrorAction SilentlyContinue | - Select-String -Pattern 'Tests \[Ok-Fail-Error\]: \[(\d+)-(\d+)-(\d+)\]' | - Select-Object -Last 1 - - if (-not $summaryLine) { - Write-Host "::warning title=Native summary missing::$logPath does not contain Unit++ summary line" - $tableRows += "| $logPath | - | - | - | UNKNOWN |" - continue - } - - $match = [regex]::Match($summaryLine.Line, 'Tests \[Ok-Fail-Error\]: \[(\d+)-(\d+)-(\d+)\]') - $okCount = [int]$match.Groups[1].Value - $failCount = [int]$match.Groups[2].Value - $errorCount = [int]$match.Groups[3].Value - $status = if ($failCount -gt 0 -or $errorCount -gt 0) { 'FAIL' } else { 'PASS' } - - if ($status -eq 'FAIL') { - Write-Host "::error title=Native test failure::$logPath => Ok=$okCount Fail=$failCount Error=$errorCount" - } - else { - Write-Host "::notice title=Native test pass::$logPath => Ok=$okCount Fail=$failCount Error=$errorCount" - } - - $tableRows += "| $logPath | $okCount | $failCount | $errorCount | $status |" - } - - Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value '### Native test summary' - Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value '' - Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value '| Log | Ok | Fail | Error | Status |' - Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value '|---|---:|---:|---:|---|' - foreach ($row in $tableRows) { - Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value $row - } + .\Build\Agent\Summarize-NativeTestResults.ps1 -Configuration Debug - name: Upload TRX test results if: ${{ !cancelled() }} diff --git a/AGENTS.md b/AGENTS.md index b3882fb003..f0a083ebd2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,8 +9,7 @@ This document explains how AI agents (coding and chat modes) should operate insi - Run on Windows (`windows-latest` runners or local VS Code workspaces). Do not create new worktrees or branches unless explicitly requested. - Always build through the traversal script: `.\build.ps1` (sets configuration, cleans stale obj, enforces native-first order). - Always test through `.\test.ps1` (dispatches managed/natives tests, applies VS test settings). -- Use `scripts/Agent/*.ps1` wrappers whenever a command would normally need pipes/filters (`Git-Search`, `Read-FileContent`, etc.). -- Keep localization in `.resx` and respect `crowdin.json`; do not introduce new ad-hoc localization flows. +- Use repository wrapper scripts whenever a command would normally need pipes/filters (`Git-Search`, `Read-FileContent`, etc.); prefer `Build/Agent/*.ps1` for new build/test/CI wrappers.- Keep localization in `.resx` and respect `crowdin.json`; do not introduce new ad-hoc localization flows. ## Agent Catalog diff --git a/Build/Agent/Build-NativeTestExecutables.ps1 b/Build/Agent/Build-NativeTestExecutables.ps1 new file mode 100644 index 0000000000..32f4418f13 --- /dev/null +++ b/Build/Agent/Build-NativeTestExecutables.ps1 @@ -0,0 +1,24 @@ +[CmdletBinding()] +param( + [string]$Configuration = 'Debug' +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path +Push-Location $repoRoot +try { + .\Build\scripts\Invoke-CppTest.ps1 -Action Build -TestProject TestGeneric -Configuration $Configuration + if ($LASTEXITCODE -ne 0) { + throw "TestGeneric native build failed with exit code $LASTEXITCODE" + } + + .\Build\scripts\Invoke-CppTest.ps1 -Action Build -TestProject TestViews -Configuration $Configuration + if ($LASTEXITCODE -ne 0) { + throw "TestViews native build failed with exit code $LASTEXITCODE" + } +} +finally { + Pop-Location +} diff --git a/Build/Agent/Detect-NativeChanges.ps1 b/Build/Agent/Detect-NativeChanges.ps1 new file mode 100644 index 0000000000..d64690eee7 --- /dev/null +++ b/Build/Agent/Detect-NativeChanges.ps1 @@ -0,0 +1,77 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)][string]$EventName, + [string]$PullRequestBaseRef, + [string]$EventBefore, + [string]$RepoRoot = ".", + [string]$OutputPath = $env:GITHUB_OUTPUT, + [string]$StepSummaryPath = $env:GITHUB_STEP_SUMMARY +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +Push-Location $RepoRoot +try { + $headRef = 'HEAD' + + if ($EventName -eq 'pull_request') { + if ([string]::IsNullOrWhiteSpace($PullRequestBaseRef)) { + throw 'PullRequestBaseRef is required when EventName is pull_request.' + } + + git fetch --no-tags --prune --depth=1 origin $PullRequestBaseRef + if ($LASTEXITCODE -ne 0) { + throw "Failed to fetch base branch origin/$PullRequestBaseRef" + } + + $baseRef = "origin/$PullRequestBaseRef" + $mergeBase = (git merge-base $headRef $baseRef).Trim() + if ([string]::IsNullOrWhiteSpace($mergeBase)) { + throw "Failed to resolve merge-base between $headRef and $baseRef" + } + + $diffRange = "$mergeBase..$headRef" + } + else { + if (-not [string]::IsNullOrWhiteSpace($EventBefore) -and $EventBefore -ne '0000000000000000000000000000000000000000') { + $diffRange = "$EventBefore..$headRef" + } + else { + $diffRange = 'HEAD~1..HEAD' + } + } + + Write-Host "Detecting native changes in range: $diffRange" -ForegroundColor Cyan + $changedFiles = @(git diff --name-only $diffRange) + if ($LASTEXITCODE -ne 0) { + throw "git diff failed for range $diffRange" + } + + $nativePattern = '^(Src/.*\.(cpp|h|hpp|cc|ixx|def|vcxproj|vcxproj\.filters|mak)|Lib/src/unit\+\+/|Build/Src/NativeBuild/)' + $nativeChangedFiles = @($changedFiles | Where-Object { $_ -match $nativePattern }) + $nativeChanged = $nativeChangedFiles.Count -gt 0 + $nativeChangedValue = if ($nativeChanged) { 'true' } else { 'false' } + + if (-not [string]::IsNullOrWhiteSpace($OutputPath)) { + "native=$nativeChangedValue" | Add-Content -Path $OutputPath + } + + if (-not [string]::IsNullOrWhiteSpace($StepSummaryPath)) { + Add-Content -Path $StepSummaryPath -Value '### Native change detection' + Add-Content -Path $StepSummaryPath -Value '' + Add-Content -Path $StepSummaryPath -Value "Diff range: $diffRange" + Add-Content -Path $StepSummaryPath -Value "Native changes detected: $nativeChangedValue" + + if ($nativeChangedFiles.Count -gt 0) { + Add-Content -Path $StepSummaryPath -Value '' + Add-Content -Path $StepSummaryPath -Value 'Native-changed files:' + foreach ($file in $nativeChangedFiles) { + Add-Content -Path $StepSummaryPath -Value "- $file" + } + } + } +} +finally { + Pop-Location +} diff --git a/Build/Agent/Invoke-ManagedTestsWithOptionalNativeBuild.ps1 b/Build/Agent/Invoke-ManagedTestsWithOptionalNativeBuild.ps1 new file mode 100644 index 0000000000..23388a9649 --- /dev/null +++ b/Build/Agent/Invoke-ManagedTestsWithOptionalNativeBuild.ps1 @@ -0,0 +1,72 @@ +[CmdletBinding()] +param( + [string]$Configuration = 'Debug', + [string]$NativeChanged = 'false', + [string]$ManagedTestFilter = 'TestCategory!=LongRunning&TestCategory!=ByHand&TestCategory!=SmokeTest&TestCategory!=DesktopRequired' +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path +$nativeChangedBool = $NativeChanged -eq 'true' + +$nativeBuildProcess = $null +$nativeBuildLogPath = Join-Path $repoRoot "Output/$Configuration/native-test-build.log" +$nativeBuildErrorPath = "$nativeBuildLogPath.stderr" + +Push-Location $repoRoot +try { + if ($nativeChangedBool) { + Write-Host 'Native-related changes detected; building native test executables in parallel.' -ForegroundColor Cyan + + New-Item -Path (Split-Path $nativeBuildLogPath -Parent) -ItemType Directory -Force | Out-Null + if (Test-Path $nativeBuildLogPath) { Remove-Item $nativeBuildLogPath -Force -ErrorAction SilentlyContinue } + if (Test-Path $nativeBuildErrorPath) { Remove-Item $nativeBuildErrorPath -Force -ErrorAction SilentlyContinue } + + $nativeBuildScript = Join-Path $repoRoot 'Build\Agent\Build-NativeTestExecutables.ps1' + $nativeBuildProcess = Start-Process -FilePath 'powershell.exe' ` + -ArgumentList @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $nativeBuildScript, '-Configuration', $Configuration) ` + -WorkingDirectory $repoRoot ` + -NoNewWindow ` + -PassThru ` + -RedirectStandardOutput $nativeBuildLogPath ` + -RedirectStandardError $nativeBuildErrorPath + } + else { + Write-Host 'No native-related changes detected; running managed tests only.' -ForegroundColor Cyan + } + + .\test.ps1 -Configuration $Configuration -NoBuild -TestFilter $ManagedTestFilter + $managedExitCode = $LASTEXITCODE + + $nativeBuildExitCode = 0 + if ($nativeBuildProcess) { + $nativeBuildProcess.WaitForExit() + $nativeBuildExitCode = $nativeBuildProcess.ExitCode + if ($nativeBuildExitCode -ne 0) { + Write-Host "[ERROR] Native test executable build failed with exit code $nativeBuildExitCode." -ForegroundColor Red + if (Test-Path $nativeBuildLogPath) { + Write-Host '--- Native build output (last 60 lines) ---' -ForegroundColor Yellow + Get-Content -Path $nativeBuildLogPath -Tail 60 -ErrorAction SilentlyContinue | ForEach-Object { Write-Host $_ } + Write-Host '--- end output ---' -ForegroundColor Yellow + } + if (Test-Path $nativeBuildErrorPath) { + Write-Host '--- Native build stderr (last 60 lines) ---' -ForegroundColor Yellow + Get-Content -Path $nativeBuildErrorPath -Tail 60 -ErrorAction SilentlyContinue | ForEach-Object { Write-Host $_ } + Write-Host '--- end stderr ---' -ForegroundColor Yellow + } + } + } + + if ($managedExitCode -ne 0) { + exit $managedExitCode + } + + if ($nativeBuildExitCode -ne 0) { + exit $nativeBuildExitCode + } +} +finally { + Pop-Location +} diff --git a/Build/Agent/README.md b/Build/Agent/README.md new file mode 100644 index 0000000000..1daa8b62d0 --- /dev/null +++ b/Build/Agent/README.md @@ -0,0 +1,21 @@ +# Build/Agent Scripts + +PowerShell scripts for build, test, and CI orchestration. + +## Placement policy + +- Put new build/test/CI orchestration scripts in `Build/Agent/`. +- Keep `scripts/Agent/` for existing helper wrappers and installer/ops-oriented scripts. + +## CI helper scripts + +| Script | Purpose | +|--------|---------| +| `Detect-NativeChanges.ps1` | Detects native-related changes using git diff (PR merge-base aware) and writes `native=true/false` to GitHub step outputs. | +| `Build-NativeTestExecutables.ps1` | Builds native test executables (`TestGeneric`, `TestViews`) via `Build/scripts/Invoke-CppTest.ps1`. | +| `Invoke-ManagedTestsWithOptionalNativeBuild.ps1` | Runs managed tests and, when native changes are present, builds native test executables in parallel in a separate process. | +| `Summarize-NativeTestResults.ps1` | Parses native Unit++ logs and appends a pass/fail summary table to GitHub step summary. | + +## GitHub Actions usage + +These scripts are used by `.github/workflows/CI.yml` to keep workflow YAML concise and keep CI behavior testable/reusable in script files. diff --git a/Build/Agent/Summarize-NativeTestResults.ps1 b/Build/Agent/Summarize-NativeTestResults.ps1 new file mode 100644 index 0000000000..9e1600d6a0 --- /dev/null +++ b/Build/Agent/Summarize-NativeTestResults.ps1 @@ -0,0 +1,59 @@ +[CmdletBinding()] +param( + [string]$Configuration = 'Debug', + [string]$StepSummaryPath = $env:GITHUB_STEP_SUMMARY +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path +$nativeLogs = @( + (Join-Path $repoRoot "Output/$Configuration/testGenericLib.exe.log"), + (Join-Path $repoRoot "Output/$Configuration/TestViews.exe.log") +) + +$tableRows = @() +foreach ($logPath in $nativeLogs) { + $displayPath = Resolve-Path -LiteralPath $logPath -ErrorAction SilentlyContinue + if (-not $displayPath) { + Write-Host "::warning title=Native log missing::$logPath not found" + $tableRows += "| $logPath | - | - | - | MISSING |" + continue + } + + $summaryLine = Get-Content -Path $logPath -ErrorAction SilentlyContinue | + Select-String -Pattern 'Tests \[Ok-Fail-Error\]: \[(\d+)-(\d+)-(\d+)\]' | + Select-Object -Last 1 + + if (-not $summaryLine) { + Write-Host "::warning title=Native summary missing::$logPath does not contain Unit++ summary line" + $tableRows += "| $logPath | - | - | - | UNKNOWN |" + continue + } + + $match = [regex]::Match($summaryLine.Line, 'Tests \[Ok-Fail-Error\]: \[(\d+)-(\d+)-(\d+)\]') + $okCount = [int]$match.Groups[1].Value + $failCount = [int]$match.Groups[2].Value + $errorCount = [int]$match.Groups[3].Value + $status = if ($failCount -gt 0 -or $errorCount -gt 0) { 'FAIL' } else { 'PASS' } + + if ($status -eq 'FAIL') { + Write-Host "::error title=Native test failure::$logPath => Ok=$okCount Fail=$failCount Error=$errorCount" + } + else { + Write-Host "::notice title=Native test pass::$logPath => Ok=$okCount Fail=$failCount Error=$errorCount" + } + + $tableRows += "| $logPath | $okCount | $failCount | $errorCount | $status |" +} + +if (-not [string]::IsNullOrWhiteSpace($StepSummaryPath)) { + Add-Content -Path $StepSummaryPath -Value '### Native test summary' + Add-Content -Path $StepSummaryPath -Value '' + Add-Content -Path $StepSummaryPath -Value '| Log | Ok | Fail | Error | Status |' + Add-Content -Path $StepSummaryPath -Value '|---|---:|---:|---:|---|' + foreach ($row in $tableRows) { + Add-Content -Path $StepSummaryPath -Value $row + } +} From fcea94dc4e591e5c0a47421a802dd4cceaea9bbc Mon Sep 17 00:00:00 2001 From: John Lambert Date: Thu, 19 Feb 2026 15:03:08 -0500 Subject: [PATCH 07/11] Pull out native CI to it's own piece --- .github/workflows/CI-native.yml | 83 +++++++++++++++++++ .github/workflows/CI.yml | 69 +-------------- Build/Agent/Detect-NativeChanges.ps1 | 77 ----------------- ...ke-ManagedTestsWithOptionalNativeBuild.ps1 | 72 ---------------- Build/Agent/README.md | 4 +- Build/Agent/Run-ManagedCi.ps1 | 27 ++++++ Build/Agent/Run-NativeCi.ps1 | 31 +++++++ 7 files changed, 147 insertions(+), 216 deletions(-) create mode 100644 .github/workflows/CI-native.yml delete mode 100644 Build/Agent/Detect-NativeChanges.ps1 delete mode 100644 Build/Agent/Invoke-ManagedTestsWithOptionalNativeBuild.ps1 create mode 100644 Build/Agent/Run-ManagedCi.ps1 create mode 100644 Build/Agent/Run-NativeCi.ps1 diff --git a/.github/workflows/CI-native.yml b/.github/workflows/CI-native.yml new file mode 100644 index 0000000000..4098e5bca8 --- /dev/null +++ b/.github/workflows/CI-native.yml @@ -0,0 +1,83 @@ +name: Flex CI Native +on: + push: + branches: ["release/**", "main", "feature/PubSub"] + paths: + - 'Src/**/*.cpp' + - 'Src/**/*.h' + - 'Src/**/*.hpp' + - 'Src/**/*.cc' + - 'Src/**/*.ixx' + - 'Src/**/*.def' + - 'Src/**/*.vcxproj' + - 'Src/**/*.vcxproj.filters' + - 'Src/**/*.mak' + - 'Lib/**/*.cpp' + - 'Lib/**/*.h' + - 'Lib/**/*.hpp' + - 'Lib/**/*.cc' + - 'Lib/**/*.ixx' + - 'Lib/**/*.def' + - 'Build/Src/NativeBuild/**' + - 'Build/scripts/Invoke-CppTest.ps1' + - 'test.ps1' + pull_request: + branches: ["release/**", "main", "feature/PubSub"] + paths: + - 'Src/**/*.cpp' + - 'Src/**/*.h' + - 'Src/**/*.hpp' + - 'Src/**/*.cc' + - 'Src/**/*.ixx' + - 'Src/**/*.def' + - 'Src/**/*.vcxproj' + - 'Src/**/*.vcxproj.filters' + - 'Src/**/*.mak' + - 'Lib/**/*.cpp' + - 'Lib/**/*.h' + - 'Lib/**/*.hpp' + - 'Lib/**/*.cc' + - 'Lib/**/*.ixx' + - 'Lib/**/*.def' + - 'Build/Src/NativeBuild/**' + - 'Build/scripts/Invoke-CppTest.ps1' + - 'test.ps1' + workflow_dispatch: + +permissions: + contents: read + checks: write + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + native_build_and_test: + name: Build Debug and run native tests + runs-on: windows-latest + steps: + - name: Checkout Files + uses: actions/checkout@v4 + id: checkout + + - name: Run native CI pipeline + id: native_ci + shell: powershell + run: | + .\Build\Agent\Run-NativeCi.ps1 -Configuration Debug -Platform x64 + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + + - name: Summarize native test results + if: ${{ always() }} + shell: powershell + run: | + .\Build\Agent\Summarize-NativeTestResults.ps1 -Configuration Debug + + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() && failure() }} + with: + name: native-build-logs + path: | + ./*.log + ./Output/**/*.log diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 5330246694..6001093f5c 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -1,23 +1,3 @@ -# CI Flow (high-level) -# -# ```mermaid -# flowchart TD -# A[Checkout] --> B[Detect native-related changes] -# B --> C[Build Debug x64 with tests] -# C --> D[Run managed tests wrapper] -# C --> E{Native changes?} -# E -->|yes| F[Wrapper starts native build in parallel] -# D --> G{Native changes?} -# F --> G -# G -->|yes| H[Run native tests -NoBuild] -# G -->|no| I[Write native-skip summary] -# H --> J[Summarize native results] -# I --> K[Upload TRX] -# J --> K -# K --> L[Upload logs on failure only] -# L --> M[Publish test results] -# ``` -# name: Flex CI on: push: @@ -36,61 +16,20 @@ concurrency: jobs: debug_build_and_test: - name: Build Debug and run managed/native tests + name: Build Debug and run managed tests runs-on: windows-latest steps: - name: Checkout Files uses: actions/checkout@v4 id: checkout - with: - fetch-depth: 0 - - name: Detect native source changes - id: changes + - name: Run managed CI pipeline + id: managed_ci shell: powershell run: | - .\Build\Agent\Detect-NativeChanges.ps1 ` - -EventName '${{ github.event_name }}' ` - -PullRequestBaseRef '${{ github.event.pull_request.base.ref }}' ` - -EventBefore '${{ github.event.before }}' - - - name: Build Debug x64 (with tests) - id: build_debug - shell: powershell - run: | - .\build.ps1 -Configuration Debug -Platform x64 -BuildTests + .\Build\Agent\Run-ManagedCi.ps1 -Configuration Debug -Platform x64 if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - - name: Run managed tests (filtered) - id: test_managed - shell: powershell - run: | - .\Build\Agent\Invoke-ManagedTestsWithOptionalNativeBuild.ps1 ` - -Configuration Debug ` - -NativeChanged '${{ steps.changes.outputs.native }}' - - - name: Run native tests - id: test_native - shell: powershell - if: ${{ steps.changes.outputs.native == 'true' }} - run: | - .\test.ps1 -Configuration Debug -Native -NoBuild - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - - - name: Report native test skip - if: ${{ steps.changes.outputs.native != 'true' }} - shell: powershell - run: | - Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value '### Native test summary' - Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value '' - Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value 'Native tests skipped: no native-related file changes detected.' - - - name: Summarize native test results - if: ${{ always() && steps.changes.outputs.native == 'true' }} - shell: powershell - run: | - .\Build\Agent\Summarize-NativeTestResults.ps1 -Configuration Debug - - name: Upload TRX test results if: ${{ !cancelled() }} uses: actions/upload-artifact@v4 diff --git a/Build/Agent/Detect-NativeChanges.ps1 b/Build/Agent/Detect-NativeChanges.ps1 deleted file mode 100644 index d64690eee7..0000000000 --- a/Build/Agent/Detect-NativeChanges.ps1 +++ /dev/null @@ -1,77 +0,0 @@ -[CmdletBinding()] -param( - [Parameter(Mandatory = $true)][string]$EventName, - [string]$PullRequestBaseRef, - [string]$EventBefore, - [string]$RepoRoot = ".", - [string]$OutputPath = $env:GITHUB_OUTPUT, - [string]$StepSummaryPath = $env:GITHUB_STEP_SUMMARY -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -Push-Location $RepoRoot -try { - $headRef = 'HEAD' - - if ($EventName -eq 'pull_request') { - if ([string]::IsNullOrWhiteSpace($PullRequestBaseRef)) { - throw 'PullRequestBaseRef is required when EventName is pull_request.' - } - - git fetch --no-tags --prune --depth=1 origin $PullRequestBaseRef - if ($LASTEXITCODE -ne 0) { - throw "Failed to fetch base branch origin/$PullRequestBaseRef" - } - - $baseRef = "origin/$PullRequestBaseRef" - $mergeBase = (git merge-base $headRef $baseRef).Trim() - if ([string]::IsNullOrWhiteSpace($mergeBase)) { - throw "Failed to resolve merge-base between $headRef and $baseRef" - } - - $diffRange = "$mergeBase..$headRef" - } - else { - if (-not [string]::IsNullOrWhiteSpace($EventBefore) -and $EventBefore -ne '0000000000000000000000000000000000000000') { - $diffRange = "$EventBefore..$headRef" - } - else { - $diffRange = 'HEAD~1..HEAD' - } - } - - Write-Host "Detecting native changes in range: $diffRange" -ForegroundColor Cyan - $changedFiles = @(git diff --name-only $diffRange) - if ($LASTEXITCODE -ne 0) { - throw "git diff failed for range $diffRange" - } - - $nativePattern = '^(Src/.*\.(cpp|h|hpp|cc|ixx|def|vcxproj|vcxproj\.filters|mak)|Lib/src/unit\+\+/|Build/Src/NativeBuild/)' - $nativeChangedFiles = @($changedFiles | Where-Object { $_ -match $nativePattern }) - $nativeChanged = $nativeChangedFiles.Count -gt 0 - $nativeChangedValue = if ($nativeChanged) { 'true' } else { 'false' } - - if (-not [string]::IsNullOrWhiteSpace($OutputPath)) { - "native=$nativeChangedValue" | Add-Content -Path $OutputPath - } - - if (-not [string]::IsNullOrWhiteSpace($StepSummaryPath)) { - Add-Content -Path $StepSummaryPath -Value '### Native change detection' - Add-Content -Path $StepSummaryPath -Value '' - Add-Content -Path $StepSummaryPath -Value "Diff range: $diffRange" - Add-Content -Path $StepSummaryPath -Value "Native changes detected: $nativeChangedValue" - - if ($nativeChangedFiles.Count -gt 0) { - Add-Content -Path $StepSummaryPath -Value '' - Add-Content -Path $StepSummaryPath -Value 'Native-changed files:' - foreach ($file in $nativeChangedFiles) { - Add-Content -Path $StepSummaryPath -Value "- $file" - } - } - } -} -finally { - Pop-Location -} diff --git a/Build/Agent/Invoke-ManagedTestsWithOptionalNativeBuild.ps1 b/Build/Agent/Invoke-ManagedTestsWithOptionalNativeBuild.ps1 deleted file mode 100644 index 23388a9649..0000000000 --- a/Build/Agent/Invoke-ManagedTestsWithOptionalNativeBuild.ps1 +++ /dev/null @@ -1,72 +0,0 @@ -[CmdletBinding()] -param( - [string]$Configuration = 'Debug', - [string]$NativeChanged = 'false', - [string]$ManagedTestFilter = 'TestCategory!=LongRunning&TestCategory!=ByHand&TestCategory!=SmokeTest&TestCategory!=DesktopRequired' -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path -$nativeChangedBool = $NativeChanged -eq 'true' - -$nativeBuildProcess = $null -$nativeBuildLogPath = Join-Path $repoRoot "Output/$Configuration/native-test-build.log" -$nativeBuildErrorPath = "$nativeBuildLogPath.stderr" - -Push-Location $repoRoot -try { - if ($nativeChangedBool) { - Write-Host 'Native-related changes detected; building native test executables in parallel.' -ForegroundColor Cyan - - New-Item -Path (Split-Path $nativeBuildLogPath -Parent) -ItemType Directory -Force | Out-Null - if (Test-Path $nativeBuildLogPath) { Remove-Item $nativeBuildLogPath -Force -ErrorAction SilentlyContinue } - if (Test-Path $nativeBuildErrorPath) { Remove-Item $nativeBuildErrorPath -Force -ErrorAction SilentlyContinue } - - $nativeBuildScript = Join-Path $repoRoot 'Build\Agent\Build-NativeTestExecutables.ps1' - $nativeBuildProcess = Start-Process -FilePath 'powershell.exe' ` - -ArgumentList @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $nativeBuildScript, '-Configuration', $Configuration) ` - -WorkingDirectory $repoRoot ` - -NoNewWindow ` - -PassThru ` - -RedirectStandardOutput $nativeBuildLogPath ` - -RedirectStandardError $nativeBuildErrorPath - } - else { - Write-Host 'No native-related changes detected; running managed tests only.' -ForegroundColor Cyan - } - - .\test.ps1 -Configuration $Configuration -NoBuild -TestFilter $ManagedTestFilter - $managedExitCode = $LASTEXITCODE - - $nativeBuildExitCode = 0 - if ($nativeBuildProcess) { - $nativeBuildProcess.WaitForExit() - $nativeBuildExitCode = $nativeBuildProcess.ExitCode - if ($nativeBuildExitCode -ne 0) { - Write-Host "[ERROR] Native test executable build failed with exit code $nativeBuildExitCode." -ForegroundColor Red - if (Test-Path $nativeBuildLogPath) { - Write-Host '--- Native build output (last 60 lines) ---' -ForegroundColor Yellow - Get-Content -Path $nativeBuildLogPath -Tail 60 -ErrorAction SilentlyContinue | ForEach-Object { Write-Host $_ } - Write-Host '--- end output ---' -ForegroundColor Yellow - } - if (Test-Path $nativeBuildErrorPath) { - Write-Host '--- Native build stderr (last 60 lines) ---' -ForegroundColor Yellow - Get-Content -Path $nativeBuildErrorPath -Tail 60 -ErrorAction SilentlyContinue | ForEach-Object { Write-Host $_ } - Write-Host '--- end stderr ---' -ForegroundColor Yellow - } - } - } - - if ($managedExitCode -ne 0) { - exit $managedExitCode - } - - if ($nativeBuildExitCode -ne 0) { - exit $nativeBuildExitCode - } -} -finally { - Pop-Location -} diff --git a/Build/Agent/README.md b/Build/Agent/README.md index 1daa8b62d0..ccda43c1ec 100644 --- a/Build/Agent/README.md +++ b/Build/Agent/README.md @@ -11,9 +11,9 @@ PowerShell scripts for build, test, and CI orchestration. | Script | Purpose | |--------|---------| -| `Detect-NativeChanges.ps1` | Detects native-related changes using git diff (PR merge-base aware) and writes `native=true/false` to GitHub step outputs. | +| `Run-ManagedCi.ps1` | Orchestrates managed CI path: build (`build.ps1 -BuildTests`) then filtered managed tests (`test.ps1 -NoBuild`). | +| `Run-NativeCi.ps1` | Orchestrates native CI path: build (`build.ps1 -BuildTests`), build native test exes, then run native tests (`test.ps1 -Native -NoBuild`). | | `Build-NativeTestExecutables.ps1` | Builds native test executables (`TestGeneric`, `TestViews`) via `Build/scripts/Invoke-CppTest.ps1`. | -| `Invoke-ManagedTestsWithOptionalNativeBuild.ps1` | Runs managed tests and, when native changes are present, builds native test executables in parallel in a separate process. | | `Summarize-NativeTestResults.ps1` | Parses native Unit++ logs and appends a pass/fail summary table to GitHub step summary. | ## GitHub Actions usage diff --git a/Build/Agent/Run-ManagedCi.ps1 b/Build/Agent/Run-ManagedCi.ps1 new file mode 100644 index 0000000000..a06316d859 --- /dev/null +++ b/Build/Agent/Run-ManagedCi.ps1 @@ -0,0 +1,27 @@ +[CmdletBinding()] +param( + [string]$Configuration = 'Debug', + [string]$Platform = 'x64', + [string]$ManagedTestFilter = 'TestCategory!=LongRunning&TestCategory!=ByHand&TestCategory!=SmokeTest&TestCategory!=DesktopRequired' +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path + +Push-Location $repoRoot +try { + .\build.ps1 -Configuration $Configuration -Platform $Platform -BuildTests + if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE + } + + .\test.ps1 -Configuration $Configuration -NoBuild -TestFilter $ManagedTestFilter + if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE + } +} +finally { + Pop-Location +} diff --git a/Build/Agent/Run-NativeCi.ps1 b/Build/Agent/Run-NativeCi.ps1 new file mode 100644 index 0000000000..b958c2536f --- /dev/null +++ b/Build/Agent/Run-NativeCi.ps1 @@ -0,0 +1,31 @@ +[CmdletBinding()] +param( + [string]$Configuration = 'Debug', + [string]$Platform = 'x64' +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path + +Push-Location $repoRoot +try { + .\build.ps1 -Configuration $Configuration -Platform $Platform -BuildTests + if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE + } + + .\Build\Agent\Build-NativeTestExecutables.ps1 -Configuration $Configuration + if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE + } + + .\test.ps1 -Configuration $Configuration -Native -NoBuild + if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE + } +} +finally { + Pop-Location +} From 666b1e1b515786ffd23a80f1e18bd0bc55ffadff Mon Sep 17 00:00:00 2001 From: Jason Naylor Date: Thu, 19 Feb 2026 12:52:54 -0800 Subject: [PATCH 08/11] Eliminate sloppy powershell scripts --- .github/workflows/CI-native.yml | 9 +++++- .github/workflows/CI.yml | 13 +++++++-- Build/Agent/Build-NativeTestExecutables.ps1 | 24 ---------------- Build/Agent/README.md | 3 -- Build/Agent/Run-ManagedCi.ps1 | 27 ------------------ Build/Agent/Run-NativeCi.ps1 | 31 --------------------- 6 files changed, 18 insertions(+), 89 deletions(-) delete mode 100644 Build/Agent/Build-NativeTestExecutables.ps1 delete mode 100644 Build/Agent/Run-ManagedCi.ps1 delete mode 100644 Build/Agent/Run-NativeCi.ps1 diff --git a/.github/workflows/CI-native.yml b/.github/workflows/CI-native.yml index 4098e5bca8..0410ac85cc 100644 --- a/.github/workflows/CI-native.yml +++ b/.github/workflows/CI-native.yml @@ -65,7 +65,14 @@ jobs: id: native_ci shell: powershell run: | - .\Build\Agent\Run-NativeCi.ps1 -Configuration Debug -Platform x64 + .\build.ps1 -Configuration Debug -Platform x64 -Project Build/Src/NativeBuild/NativeBuild.csproj + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + + - name: Build and run native tests + id: native_test + shell: powershell + run: | + .\test.ps1 -Configuration Debug -Native if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - name: Summarize native test results diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 6001093f5c..a217b0ea77 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -23,11 +23,18 @@ jobs: uses: actions/checkout@v4 id: checkout - - name: Run managed CI pipeline - id: managed_ci + - name: Build managed (with tests) + id: managed_build shell: powershell run: | - .\Build\Agent\Run-ManagedCi.ps1 -Configuration Debug -Platform x64 + .\build.ps1 -Configuration Debug -Platform x64 -BuildTests + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + + - name: Run managed tests + id: managed_test + shell: powershell + run: | + .\test.ps1 -Configuration Debug -NoBuild -TestFilter 'TestCategory!=LongRunning&TestCategory!=ByHand&TestCategory!=SmokeTest&TestCategory!=DesktopRequired' if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - name: Upload TRX test results diff --git a/Build/Agent/Build-NativeTestExecutables.ps1 b/Build/Agent/Build-NativeTestExecutables.ps1 deleted file mode 100644 index 32f4418f13..0000000000 --- a/Build/Agent/Build-NativeTestExecutables.ps1 +++ /dev/null @@ -1,24 +0,0 @@ -[CmdletBinding()] -param( - [string]$Configuration = 'Debug' -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path -Push-Location $repoRoot -try { - .\Build\scripts\Invoke-CppTest.ps1 -Action Build -TestProject TestGeneric -Configuration $Configuration - if ($LASTEXITCODE -ne 0) { - throw "TestGeneric native build failed with exit code $LASTEXITCODE" - } - - .\Build\scripts\Invoke-CppTest.ps1 -Action Build -TestProject TestViews -Configuration $Configuration - if ($LASTEXITCODE -ne 0) { - throw "TestViews native build failed with exit code $LASTEXITCODE" - } -} -finally { - Pop-Location -} diff --git a/Build/Agent/README.md b/Build/Agent/README.md index ccda43c1ec..b7f4e6b0a1 100644 --- a/Build/Agent/README.md +++ b/Build/Agent/README.md @@ -11,9 +11,6 @@ PowerShell scripts for build, test, and CI orchestration. | Script | Purpose | |--------|---------| -| `Run-ManagedCi.ps1` | Orchestrates managed CI path: build (`build.ps1 -BuildTests`) then filtered managed tests (`test.ps1 -NoBuild`). | -| `Run-NativeCi.ps1` | Orchestrates native CI path: build (`build.ps1 -BuildTests`), build native test exes, then run native tests (`test.ps1 -Native -NoBuild`). | -| `Build-NativeTestExecutables.ps1` | Builds native test executables (`TestGeneric`, `TestViews`) via `Build/scripts/Invoke-CppTest.ps1`. | | `Summarize-NativeTestResults.ps1` | Parses native Unit++ logs and appends a pass/fail summary table to GitHub step summary. | ## GitHub Actions usage diff --git a/Build/Agent/Run-ManagedCi.ps1 b/Build/Agent/Run-ManagedCi.ps1 deleted file mode 100644 index a06316d859..0000000000 --- a/Build/Agent/Run-ManagedCi.ps1 +++ /dev/null @@ -1,27 +0,0 @@ -[CmdletBinding()] -param( - [string]$Configuration = 'Debug', - [string]$Platform = 'x64', - [string]$ManagedTestFilter = 'TestCategory!=LongRunning&TestCategory!=ByHand&TestCategory!=SmokeTest&TestCategory!=DesktopRequired' -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path - -Push-Location $repoRoot -try { - .\build.ps1 -Configuration $Configuration -Platform $Platform -BuildTests - if ($LASTEXITCODE -ne 0) { - exit $LASTEXITCODE - } - - .\test.ps1 -Configuration $Configuration -NoBuild -TestFilter $ManagedTestFilter - if ($LASTEXITCODE -ne 0) { - exit $LASTEXITCODE - } -} -finally { - Pop-Location -} diff --git a/Build/Agent/Run-NativeCi.ps1 b/Build/Agent/Run-NativeCi.ps1 deleted file mode 100644 index b958c2536f..0000000000 --- a/Build/Agent/Run-NativeCi.ps1 +++ /dev/null @@ -1,31 +0,0 @@ -[CmdletBinding()] -param( - [string]$Configuration = 'Debug', - [string]$Platform = 'x64' -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path - -Push-Location $repoRoot -try { - .\build.ps1 -Configuration $Configuration -Platform $Platform -BuildTests - if ($LASTEXITCODE -ne 0) { - exit $LASTEXITCODE - } - - .\Build\Agent\Build-NativeTestExecutables.ps1 -Configuration $Configuration - if ($LASTEXITCODE -ne 0) { - exit $LASTEXITCODE - } - - .\test.ps1 -Configuration $Configuration -Native -NoBuild - if ($LASTEXITCODE -ne 0) { - exit $LASTEXITCODE - } -} -finally { - Pop-Location -} From eb6f8b473c65deca9c82092060733c2ce555482b Mon Sep 17 00:00:00 2001 From: John Lambert Date: Thu, 19 Feb 2026 16:16:09 -0500 Subject: [PATCH 09/11] Ensure unit++.lib --- Build/scripts/Invoke-CppTest.ps1 | 34 +++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/Build/scripts/Invoke-CppTest.ps1 b/Build/scripts/Invoke-CppTest.ps1 index 5674ba50a8..35f3ac7fdf 100644 --- a/Build/scripts/Invoke-CppTest.ps1 +++ b/Build/scripts/Invoke-CppTest.ps1 @@ -186,6 +186,36 @@ function Build-ViewsInterfacesArtifacts { if ($process.ExitCode -ne 0) { throw "ViewsInterfaces build failed with exit code $($process.ExitCode)" } } +function Ensure-UnitPlusPlusLibrary { + $unitLibPath = Join-Path $WorktreePath "Lib\$($Configuration.ToLower())\unit++.lib" + if (Test-Path $unitLibPath) { + return + } + + Write-Host "[INFO] Missing unit++.lib; building Unit++ library..." -ForegroundColor Yellow + $unitProj = Join-Path $WorktreePath 'Lib\src\unit++\VS\unit++.vcxproj' + if (-not (Test-Path $unitProj)) { + throw "Unit++ project not found: $unitProj" + } + + $msbuild = Get-MsBuildExecutable + $msbuildArgs = @( + $unitProj, + '/p:Configuration={0}' -f $Configuration, + '/p:Platform=x64', + '/v:minimal', + '/nologo' + ) + $process = Start-Process -FilePath $msbuild -ArgumentList $msbuildArgs -WorkingDirectory $WorktreePath -NoNewWindow -Wait -PassThru + if ($process.ExitCode -ne 0) { + throw "Unit++ build failed with exit code $($process.ExitCode)" + } + + if (-not (Test-Path $unitLibPath)) { + throw "Unit++ build completed but library is still missing: $unitLibPath" + } +} + function Ensure-TestViewsPrerequisites { if ($TestProject -ne 'TestViews') { return } $fwKernelHeader = Join-Path $WorktreePath "Output\$Configuration\Common\FwKernelTlb.h" @@ -203,12 +233,14 @@ function Invoke-Build { (Test-Path (Join-Path $outputDir 'DebugProcs.dll')), (Test-Path (Join-Path $outputDir 'icuin70.dll')), (Test-Path (Join-Path $outputDir 'icuuc70.dll')) - ) -notcontains $true + ) -contains $false if ($needsNative) { Write-Host "[INFO] Native runtime artifacts missing for $TestProject; building NativeBuild.csproj..." -ForegroundColor Yellow Build-NativeArtifacts } + Ensure-UnitPlusPlusLibrary + if ($BuildSystem -eq 'MSBuild') { $vcxproj = Join-Path $WorktreePath $config.VcxprojPath From 275d2634e512dacd0d17bd99b53c14261d0f570f Mon Sep 17 00:00:00 2001 From: Jason Naylor Date: Thu, 19 Feb 2026 14:47:37 -0800 Subject: [PATCH 10/11] Report test results in PR and fixup Invoke-CppTest.ps1 --- .github/workflows/CI.yml | 7 +- Build/scripts/Invoke-CppTest.ps1 | 936 ++++++++++++++++--------------- 2 files changed, 482 insertions(+), 461 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index a217b0ea77..7101ac04a2 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -59,6 +59,11 @@ jobs: if: ${{ !cancelled() }} needs: debug_build_and_test runs-on: ubuntu-latest + permissions: + contents: read + issues: read + checks: write + pull-requests: write steps: - name: Download TRX artifacts uses: actions/download-artifact@v4 @@ -71,6 +76,6 @@ jobs: with: files: 'test-results/**/*.trx' check_name: NUnit Tests - comment_mode: off + comment_mode: always job_summary: true fail_on: test failures diff --git a/Build/scripts/Invoke-CppTest.ps1 b/Build/scripts/Invoke-CppTest.ps1 index 35f3ac7fdf..2c349085e2 100644 --- a/Build/scripts/Invoke-CppTest.ps1 +++ b/Build/scripts/Invoke-CppTest.ps1 @@ -36,27 +36,27 @@ #> [CmdletBinding()] param( - [ValidateSet('Build', 'Run', 'BuildAndRun')] - [string]$Action = 'BuildAndRun', + [ValidateSet('Build', 'Run', 'BuildAndRun')] + [string]$Action = 'BuildAndRun', - [ValidateSet('TestGeneric', 'TestViews')] - [string]$TestProject = 'TestGeneric', + [ValidateSet('TestGeneric', 'TestViews')] + [string]$TestProject = 'TestGeneric', - [ValidateSet('Debug', 'Release')] - [string]$Configuration = 'Debug', + [ValidateSet('Debug', 'Release')] + [string]$Configuration = 'Debug', - [ValidateSet('MSBuild', 'NMake')] - [string]$BuildSystem = 'MSBuild', + [ValidateSet('MSBuild', 'NMake')] + [string]$BuildSystem = 'MSBuild', - [string]$WorktreePath, + [string]$WorktreePath, - [int]$TimeoutSeconds = 300, + [int]$TimeoutSeconds = 300, - [int]$PostCompletionGraceSeconds = 5, + [int]$PostCompletionGraceSeconds = 5, - [string[]]$TestArguments, + [string[]]$TestArguments, - [string]$LogPath + [string]$LogPath ) Set-StrictMode -Version Latest @@ -69,8 +69,8 @@ $script:LastLocalOutDir = $null $helpersPath = Join-Path $PSScriptRoot "../Agent/FwBuildHelpers.psm1" if (-not (Test-Path $helpersPath)) { - Write-Host "[ERROR] FwBuildHelpers.psm1 not found at $helpersPath" -ForegroundColor Red - exit 1 + Write-Host "[ERROR] FwBuildHelpers.psm1 not found at $helpersPath" -ForegroundColor Red + exit 1 } Import-Module $helpersPath -Force @@ -98,7 +98,7 @@ $oldMode = $Kernel32::SetErrorMode(0x8003) # Resolve worktree path if (-not $WorktreePath) { - $WorktreePath = (Get-Location).Path + $WorktreePath = (Get-Location).Path } $WorktreePath = (Resolve-Path $WorktreePath).Path $sourceWorktreePath = $WorktreePath @@ -108,18 +108,18 @@ $activeWorktreePath = $WorktreePath # Project configuration $projectConfig = @{ - TestGeneric = @{ - VcxprojPath = 'Src\Generic\Test\TestGeneric.vcxproj' - MakefilePath = 'Src\Generic\Test' - MakefileName = 'testGenericLib.mak' - ExeName = 'testGenericLib.exe' - } - TestViews = @{ - VcxprojPath = 'Src\views\Test\TestViews.vcxproj' - MakefilePath = 'Src\views\Test' - MakefileName = 'testViews.mak' - ExeName = 'TestViews.exe' - } + TestGeneric = @{ + VcxprojPath = 'Src\Generic\Test\TestGeneric.vcxproj' + MakefilePath = 'Src\Generic\Test' + MakefileName = 'testGenericLib.mak' + ExeName = 'testGenericLib.exe' + } + TestViews = @{ + VcxprojPath = 'Src\views\Test\TestViews.vcxproj' + MakefilePath = 'Src\views\Test' + MakefileName = 'testViews.mak' + ExeName = 'TestViews.exe' + } } $config = $projectConfig[$TestProject] @@ -134,466 +134,482 @@ Write-Host "Build System: $BuildSystem" -ForegroundColor Cyan Write-Host "======================================" -ForegroundColor Cyan function Get-MsBuildExecutable { - $buildToolsMsbuild = 'C:\BuildTools\MSBuild\Current\Bin\MSBuild.exe' - if (Test-Path $buildToolsMsbuild) { return $buildToolsMsbuild } - return 'msbuild' + $buildToolsMsbuild = 'C:\BuildTools\MSBuild\Current\Bin\MSBuild.exe' + if (Test-Path $buildToolsMsbuild) { return $buildToolsMsbuild } + return 'msbuild' } function Build-FwBuildTasks { - Write-Host "[INFO] Building FwBuildTasks helper assembly..." -ForegroundColor Yellow - $tasksProj = Join-Path $WorktreePath 'Build\Src\FwBuildTasks\FwBuildTasks.csproj' - $msbuild = Get-MsBuildExecutable - $args = @( - '/restore', - "$tasksProj", - '/p:Configuration={0}' -f $Configuration, - '/p:Platform=x64', - '/nologo' - ) - $process = Start-Process -FilePath $msbuild -ArgumentList $args -WorkingDirectory $WorktreePath -NoNewWindow -Wait -PassThru - if ($process.ExitCode -ne 0) { throw "FwBuildTasks build failed with exit code $($process.ExitCode)" } + Write-Host "[INFO] Building FwBuildTasks helper assembly..." -ForegroundColor Yellow + $tasksProj = Join-Path $WorktreePath 'Build\Src\FwBuildTasks\FwBuildTasks.csproj' + $msbuild = Get-MsBuildExecutable + $buildArgs = @( + '/restore', + "$tasksProj", + ('/p:Configuration={0}' -f $Configuration), + '/p:Platform=x64', + '/nologo' + ) + & $msbuild @buildArgs + if ($LASTEXITCODE -ne 0) { throw "FwBuildTasks build failed with exit code $LASTEXITCODE" } } function Build-NativeArtifacts { - Write-Host "[INFO] Building native artifacts (NativeBuild.csproj)..." -ForegroundColor Yellow - Build-FwBuildTasks - $nativeProj = Join-Path $WorktreePath 'Build\Src\NativeBuild\NativeBuild.csproj' - $msbuild = Get-MsBuildExecutable - $args = @( - "$nativeProj", - '/p:Configuration={0}' -f $Configuration, - '/p:Platform=x64', - '/nologo', - '/m' - ) - $process = Start-Process -FilePath $msbuild -ArgumentList $args -WorkingDirectory $WorktreePath -NoNewWindow -Wait -PassThru - if ($process.ExitCode -ne 0) { throw "Native build failed with exit code $($process.ExitCode)" } + Write-Host "[INFO] Building native artifacts (NativeBuild.csproj)..." -ForegroundColor Yellow + Build-FwBuildTasks + $nativeProj = Join-Path $WorktreePath 'Build\Src\NativeBuild\NativeBuild.csproj' + $msbuild = Get-MsBuildExecutable + $buildArgs = @( + "$nativeProj", + ('/p:Configuration={0}' -f $Configuration), + '/p:Platform=x64', + '/p:BuildNativeTests=true', + '/nologo', + '/m' + ) + & $msbuild @buildArgs + if ($LASTEXITCODE -ne 0) { throw "Native build failed with exit code $LASTEXITCODE" } } function Build-ViewsInterfacesArtifacts { - Write-Host "[INFO] Generating ViewsInterfaces artifacts..." -ForegroundColor Yellow - $viewsProj = Join-Path $WorktreePath 'Src\Common\ViewsInterfaces\ViewsInterfaces.csproj' - $msbuild = Get-MsBuildExecutable - $args = @( - '/restore', - "$viewsProj", - '/p:Configuration={0}' -f $Configuration, - '/p:Platform=x64', - '/nologo', - '/v:minimal' - ) - $process = Start-Process -FilePath $msbuild -ArgumentList $args -WorkingDirectory $WorktreePath -NoNewWindow -Wait -PassThru - if ($process.ExitCode -ne 0) { throw "ViewsInterfaces build failed with exit code $($process.ExitCode)" } + Write-Host "[INFO] Generating ViewsInterfaces artifacts..." -ForegroundColor Yellow + $viewsProj = Join-Path $WorktreePath 'Src\Common\ViewsInterfaces\ViewsInterfaces.csproj' + $msbuild = Get-MsBuildExecutable + $buildArgs = @( + '/restore', + "$viewsProj", + ('/p:Configuration={0}' -f $Configuration), + '/p:Platform=x64', + '/nologo', + '/v:minimal' + ) + & $msbuild @buildArgs + if ($LASTEXITCODE -ne 0) { throw "ViewsInterfaces build failed with exit code $LASTEXITCODE" } } function Ensure-UnitPlusPlusLibrary { - $unitLibPath = Join-Path $WorktreePath "Lib\$($Configuration.ToLower())\unit++.lib" - if (Test-Path $unitLibPath) { - return - } - - Write-Host "[INFO] Missing unit++.lib; building Unit++ library..." -ForegroundColor Yellow - $unitProj = Join-Path $WorktreePath 'Lib\src\unit++\VS\unit++.vcxproj' - if (-not (Test-Path $unitProj)) { - throw "Unit++ project not found: $unitProj" - } - - $msbuild = Get-MsBuildExecutable - $msbuildArgs = @( - $unitProj, - '/p:Configuration={0}' -f $Configuration, - '/p:Platform=x64', - '/v:minimal', - '/nologo' - ) - $process = Start-Process -FilePath $msbuild -ArgumentList $msbuildArgs -WorkingDirectory $WorktreePath -NoNewWindow -Wait -PassThru - if ($process.ExitCode -ne 0) { - throw "Unit++ build failed with exit code $($process.ExitCode)" - } - - if (-not (Test-Path $unitLibPath)) { - throw "Unit++ build completed but library is still missing: $unitLibPath" - } + $unitLibPath = Join-Path $WorktreePath "Lib\$($Configuration.ToLower())\unit++.lib" + if (Test-Path $unitLibPath) { + return + } + + Write-Host "[INFO] Missing unit++.lib; building Unit++ library..." -ForegroundColor Yellow + $unitProj = Join-Path $WorktreePath 'Lib\src\unit++\VS\unit++.vcxproj' + if (-not (Test-Path $unitProj)) { + throw "Unit++ project not found: $unitProj" + } + + $msbuild = Get-MsBuildExecutable + $msbuildArgs = @( + $unitProj, + ('/p:Configuration={0}' -f $Configuration), + '/p:Platform=x64', + '/v:minimal', + '/nologo' + ) + & $msbuild @msbuildArgs + if ($LASTEXITCODE -ne 0) { + throw "Unit++ build failed with exit code $LASTEXITCODE" + } + + if (-not (Test-Path $unitLibPath)) { + throw "Unit++ build completed but library is still missing: $unitLibPath" + } } function Ensure-TestViewsPrerequisites { - if ($TestProject -ne 'TestViews') { return } - $fwKernelHeader = Join-Path $WorktreePath "Output\$Configuration\Common\FwKernelTlb.h" - $viewsObj = Join-Path $WorktreePath "Obj\$Configuration\Views\autopch\VwRootBox.obj" - if ((Test-Path $fwKernelHeader) -and (Test-Path $viewsObj)) { return } - - Write-Host "[INFO] Missing native artifacts or generated headers required for TestViews." -ForegroundColor Yellow - Build-NativeArtifacts - Build-ViewsInterfacesArtifacts + if ($TestProject -ne 'TestViews') { return } + $fwKernelHeader = Join-Path $WorktreePath "Output\$Configuration\Common\FwKernelTlb.h" + $viewsObj = Join-Path $WorktreePath "Obj\$Configuration\Views\autopch\VwRootBox.obj" + if ((Test-Path $fwKernelHeader) -and (Test-Path $viewsObj)) { return } + + Write-Host "[INFO] Missing native artifacts or generated headers required for TestViews." -ForegroundColor Yellow + Build-NativeArtifacts + Build-ViewsInterfacesArtifacts } function Invoke-Build { - # Ensure native runtime dependencies (DebugProcs, ICU DLLs, etc.) exist before building the test exe - $needsNative = @( - (Test-Path (Join-Path $outputDir 'DebugProcs.dll')), - (Test-Path (Join-Path $outputDir 'icuin70.dll')), - (Test-Path (Join-Path $outputDir 'icuuc70.dll')) - ) -contains $false - if ($needsNative) { - Write-Host "[INFO] Native runtime artifacts missing for $TestProject; building NativeBuild.csproj..." -ForegroundColor Yellow - Build-NativeArtifacts - } - - Ensure-UnitPlusPlusLibrary - - if ($BuildSystem -eq 'MSBuild') { - $vcxproj = Join-Path $WorktreePath $config.VcxprojPath - - Write-Host "WorktreePath: $WorktreePath" -ForegroundColor Gray - Write-Host "vcxproj path: $vcxproj" -ForegroundColor Gray - if (-not (Test-Path $vcxproj)) { - Write-Host "[ERROR] vcxproj not found at resolved path" -ForegroundColor Red - } - - if (-not (Test-Path $vcxproj)) { - throw "vcxproj not found: $vcxproj. Has the project been converted from Makefile?" - } - - Ensure-TestViewsPrerequisites - - Write-Host "`nBuilding with MSBuild..." -ForegroundColor Yellow - - # Force x64 platform to avoid Win32 default when building from host - $env:Platform = 'x64' - - # Prefer the BuildTools MSBuild inside the container to match VCINSTALLDIR - $msbuild = "msbuild" - $buildToolsMsbuild = 'C:\BuildTools\MSBuild\Current\Bin\MSBuild.exe' - if (Test-Path $buildToolsMsbuild) { - $msbuild = $buildToolsMsbuild - } - - $msbuildOutArgs = @() - $localOutDir = $null - - $msbuildArgs = @( - $config.VcxprojPath, - '/p:Configuration={0}' -f $Configuration, - '/p:Platform=x64', - '/p:LinkIncremental=false', - '/v:minimal', - '/nologo' - ) + $msbuildOutArgs - $msbuildCmd = "$msbuild $($msbuildArgs -join ' ')" - Write-Host "(cd $WorktreePath) $msbuildCmd" -ForegroundColor Gray - - $process = Start-Process -FilePath $msbuild -ArgumentList $msbuildArgs -WorkingDirectory $WorktreePath -NoNewWindow -Wait -PassThru - if ($process.ExitCode -ne 0) { throw "MSBuild failed with exit code $($process.ExitCode)" } - - if ($localOutDir) { - $script:LastLocalOutDir = $localOutDir - # Ensure output directory exists in the local clone - if (-not (Test-Path $outputDir)) { - New-Item -ItemType Directory -Force -Path $outputDir | Out-Null - } - - $artifactNames = @( - $config.ExeName, - ($config.ExeName -replace '\.exe$', '.pdb'), - ($config.ExeName -replace '\.exe$', '.lib'), - ($config.ExeName -replace '\.exe$', '.exp') - ) - - foreach ($name in $artifactNames) { - $src = Join-Path $localOutDir $name - if (Test-Path $src) { - try { - Copy-Item -Path $src -Destination (Join-Path $outputDir $name) -Force - } - catch { - Write-Host "[WARN] Could not copy $name to Output (in use?): $_" -ForegroundColor Yellow - } - } - } - - # Also copy ICU DLLs from lib/x64 if they exist (required for runtime) - $localLibX64 = Join-Path $localOutDir "lib\x64" - if (Test-Path $localLibX64) { - $destLibX64 = Join-Path $outputDir "lib\x64" - if (-not (Test-Path $destLibX64)) { - New-Item -ItemType Directory -Force -Path $destLibX64 | Out-Null - } - try { - Copy-Item -Path (Join-Path $localLibX64 "icu*.dll") -Destination $destLibX64 -Force - } - catch { - Write-Host "[WARN] Could not copy ICU DLLs to Output (in use?): $_" -ForegroundColor Yellow - } - } - } - } - else { - # NMake build (legacy) - $makeDir = Join-Path $WorktreePath $config.MakefilePath - $makefile = $config.MakefileName - $buildType = if ($Configuration -eq 'Debug') { 'd' } else { 'r' } - - Write-Host "`nBuilding with NMake (legacy)..." -ForegroundColor Yellow - - # VsDevCmd is already initialized by Initialize-VsDevEnvironment - - $nmakeArgs = @( - '/nologo', - "BUILD_CONFIG=$Configuration", - "BUILD_TYPE=$buildType", - "BUILD_ROOT=$WorktreePath\", - 'BUILD_ARCH=x64', - '/f', - $makefile - ) - Write-Host "(cd $makeDir) nmake $($nmakeArgs -join ' ')" -ForegroundColor Gray - $process = Start-Process -FilePath 'nmake' -ArgumentList $nmakeArgs -WorkingDirectory $makeDir -NoNewWindow -Wait -PassThru - if ($process.ExitCode -ne 0) { throw "NMake failed with exit code $($process.ExitCode)" } - } - - Write-Host "`nBuild completed successfully." -ForegroundColor Green + # Ensure native runtime dependencies (DebugProcs, ICU DLLs, etc.) exist before building the test exe + $needsNative = @( + (Test-Path (Join-Path $outputDir 'DebugProcs.dll')), + (Test-Path (Join-Path $outputDir 'icuin70.dll')), + (Test-Path (Join-Path $outputDir 'icuuc70.dll')) + ) -contains $false + if ($needsNative) { + Write-Host "[INFO] Native runtime artifacts missing for $TestProject; building NativeBuild.csproj..." -ForegroundColor Yellow + Build-NativeArtifacts + } + + Ensure-UnitPlusPlusLibrary + + if ($BuildSystem -eq 'MSBuild') { + $vcxproj = Join-Path $WorktreePath $config.VcxprojPath + + Write-Host "WorktreePath: $WorktreePath" -ForegroundColor Gray + Write-Host "vcxproj path: $vcxproj" -ForegroundColor Gray + if (-not (Test-Path $vcxproj)) { + Write-Host "[ERROR] vcxproj not found at resolved path" -ForegroundColor Red + } + + if (-not (Test-Path $vcxproj)) { + throw "vcxproj not found: $vcxproj. Has the project been converted from Makefile?" + } + + Ensure-TestViewsPrerequisites + + Write-Host "`nBuilding with MSBuild..." -ForegroundColor Yellow + + # Force x64 platform to avoid Win32 default when building from host + $env:Platform = 'x64' + + # Prefer the BuildTools MSBuild inside the container to match VCINSTALLDIR + $msbuild = "msbuild" + $buildToolsMsbuild = 'C:\BuildTools\MSBuild\Current\Bin\MSBuild.exe' + if (Test-Path $buildToolsMsbuild) { + $msbuild = $buildToolsMsbuild + } + + $msbuildOutArgs = @() + $localOutDir = $null + + $msbuildArgs = @( + $config.VcxprojPath, + ('/p:Configuration={0}' -f $Configuration), + '/p:Platform=x64', + '/p:LinkIncremental=false', + '/v:minimal', + '/nologo' + ) + $msbuildOutArgs + $msbuildCmd = "$msbuild $($msbuildArgs -join ' ')" + Write-Host "(cd $WorktreePath) $msbuildCmd" -ForegroundColor Gray + + Push-Location $WorktreePath + try { + & $msbuild @msbuildArgs + if ($LASTEXITCODE -ne 0) { throw "MSBuild failed with exit code $LASTEXITCODE" } + } + finally { + Pop-Location + } + + if ($localOutDir) { + $script:LastLocalOutDir = $localOutDir + # Ensure output directory exists in the local clone + if (-not (Test-Path $outputDir)) { + New-Item -ItemType Directory -Force -Path $outputDir | Out-Null + } + + $artifactNames = @( + $config.ExeName, + ($config.ExeName -replace '\.exe$', '.pdb'), + ($config.ExeName -replace '\.exe$', '.lib'), + ($config.ExeName -replace '\.exe$', '.exp') + ) + + foreach ($name in $artifactNames) { + $src = Join-Path $localOutDir $name + if (Test-Path $src) { + try { + Copy-Item -Path $src -Destination (Join-Path $outputDir $name) -Force + } + catch { + Write-Host "[WARN] Could not copy $name to Output (in use?): $_" -ForegroundColor Yellow + } + } + } + + # Also copy ICU DLLs from lib/x64 if they exist (required for runtime) + $localLibX64 = Join-Path $localOutDir "lib\x64" + if (Test-Path $localLibX64) { + $destLibX64 = Join-Path $outputDir "lib\x64" + if (-not (Test-Path $destLibX64)) { + New-Item -ItemType Directory -Force -Path $destLibX64 | Out-Null + } + try { + Copy-Item -Path (Join-Path $localLibX64 "icu*.dll") -Destination $destLibX64 -Force + } + catch { + Write-Host "[WARN] Could not copy ICU DLLs to Output (in use?): $_" -ForegroundColor Yellow + } + } + } + } + else { + # NMake build (legacy) + $makeDir = Join-Path $WorktreePath $config.MakefilePath + $makefile = $config.MakefileName + $buildType = if ($Configuration -eq 'Debug') { 'd' } else { 'r' } + + Write-Host "`nBuilding with NMake (legacy)..." -ForegroundColor Yellow + + # VsDevCmd is already initialized by Initialize-VsDevEnvironment + + $nmakeArgs = @( + '/nologo', + "BUILD_CONFIG=$Configuration", + "BUILD_TYPE=$buildType", + "BUILD_ROOT=$WorktreePath\", + 'BUILD_ARCH=x64', + '/f', + $makefile + ) + Write-Host "(cd $makeDir) nmake $($nmakeArgs -join ' ')" -ForegroundColor Gray + $process = Start-Process -FilePath 'nmake' -ArgumentList $nmakeArgs -WorkingDirectory $makeDir -NoNewWindow -Wait -PassThru + if ($process.ExitCode -ne 0) { throw "NMake failed with exit code $($process.ExitCode)" } + } + + Write-Host "`nBuild completed successfully." -ForegroundColor Green } function Invoke-Run { - Write-Host "`nRunning $($config.ExeName)..." -ForegroundColor Yellow - - # Add Debug CRT to PATH if running in Debug configuration (required for VCRUNTIME140D.dll) - if ($Configuration -eq 'Debug') { - $debugCrt = Get-ChildItem -Path "C:\BuildTools\VC\Redist\MSVC" -Filter "Microsoft.VC*.DebugCRT" -Recurse -ErrorAction SilentlyContinue | - Where-Object { $_.FullName -like "*\x64\*" -and $_.FullName -like "*\debug_nonredist\*" } | - Select-Object -First 1 -ExpandProperty FullName - - if ($debugCrt) { - Write-Host "Adding Debug CRT to PATH: $debugCrt" -ForegroundColor Gray - $env:PATH = "$debugCrt;$env:PATH" - } - } - - # Ensure ICU DLLs are in place - $icuSource = Join-Path $outputDir "lib\x64" - $icuDlls = Get-ChildItem -Path $icuSource -Filter "icu*70.dll" -ErrorAction SilentlyContinue - if ($icuDlls) { - foreach ($dll in $icuDlls) { - $dest = Join-Path $outputDir $dll.Name - if (-not (Test-Path $dest)) { - Write-Host "Copying $($dll.Name) to output directory..." -ForegroundColor Gray - Copy-Item $dll.FullName $dest - } - } - } - - if (-not (Test-Path $exePath)) { - throw "Test executable not found: $exePath. Run with -Action Build first." - } - - if (-not $LogPath) { - $LogPath = Join-Path $outputDir "$($config.ExeName).log" - } - - $errorLogPath = "$LogPath.stderr" - - # Ensure native tests resolve paths consistently in containers/CI - $distFiles = Join-Path $activeWorktreePath 'DistFiles' - $sourceDistFiles = if ($sourceWorktreePath -and (Test-Path (Join-Path $sourceWorktreePath 'DistFiles'))) { Join-Path $sourceWorktreePath 'DistFiles' } else { $null } - - # Backfill DistFiles into the active clone when missing critical assets - $requiredAssets = @( - @{ Rel = 'XceedZip.dll' }, - @{ Rel = 'Templates\NewLangProj.fwdata' } - ) - - if (-not (Test-Path $distFiles) -and $sourceDistFiles) { - Write-Host "[INFO] DistFiles missing in active worktree; copying from source" -ForegroundColor Yellow - Copy-Item -Path $sourceDistFiles -Destination $activeWorktreePath -Recurse -Force - } - - foreach ($asset in $requiredAssets) { - $target = Join-Path $distFiles $asset.Rel - if (-not (Test-Path $target)) { - if ($sourceDistFiles) { - $sourceAsset = Join-Path $sourceDistFiles $asset.Rel - if (Test-Path $sourceAsset) { - New-Item -ItemType Directory -Force -Path (Split-Path $target) | Out-Null - Copy-Item $sourceAsset $target -Force - } - } - } - } - - if (-not (Test-Path $distFiles)) { - throw "DistFiles not found in active worktree ($activeWorktreePath) and no source copy available." - } - - $env:FW_ROOT_CODE_DIR = $distFiles - $icuDir = Join-Path $distFiles 'Icu70' - if (Test-Path $icuDir) { - $env:FW_ICU_DATA_DIR = $icuDir - $env:ICU_DATA = $icuDir - } - Write-Host "FW_ROOT_CODE_DIR=$($env:FW_ROOT_CODE_DIR)" -ForegroundColor Gray - if ($env:FW_ICU_DATA_DIR) { Write-Host "FW_ICU_DATA_DIR=$($env:FW_ICU_DATA_DIR)" -ForegroundColor Gray } - $zipPath = Join-Path $distFiles 'XceedZip.dll' - Write-Host "XceedZip present: $(Test-Path $zipPath)" -ForegroundColor Gray - - $runExePath = $exePath - if ($null -ne $script:LastLocalOutDir) { - $potentialPath = Join-Path $script:LastLocalOutDir $config.ExeName - if (Test-Path $potentialPath) { - $runExePath = $potentialPath - } - } - - if (Test-Path $LogPath) { - Remove-Item $LogPath -Force -ErrorAction SilentlyContinue - } - - if (Test-Path $errorLogPath) { - Remove-Item $errorLogPath -Force -ErrorAction SilentlyContinue - } - - # Ensure no stale test process is holding the exe open - $exeProcessName = [System.IO.Path]::GetFileNameWithoutExtension($config.ExeName) - try { - Get-Process -Name $exeProcessName -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue - } catch {} - - # Stage required runtime assets into the output directory for reliable lookup - $assetsToStage = @( - @{ Source = Join-Path $distFiles 'XceedZip.dll'; Destination = Join-Path $outputDir 'XceedZip.dll' }, - @{ Source = Join-Path $distFiles 'Templates\NewLangProj.fwdata'; Destination = Join-Path $outputDir 'Templates\NewLangProj.fwdata' } - ) - foreach ($asset in $assetsToStage) { - if (Test-Path $asset.Source) { - New-Item -ItemType Directory -Force -Path (Split-Path $asset.Destination) | Out-Null - Copy-Item $asset.Source $asset.Destination -Force - } - } - - $argumentList = @() - if ($TestArguments) { - $argumentList += $TestArguments - } - - Write-Host "Running: $runExePath $($argumentList -join ' ')" -ForegroundColor Gray - Write-Host "Logging to: $LogPath" -ForegroundColor Gray - - $startInfo = @{ - FilePath = $runExePath - WorkingDirectory = $outputDir - RedirectStandardOutput = $LogPath - RedirectStandardError = $errorLogPath - NoNewWindow = $true - PassThru = $true - } - - if ($argumentList.Count -gt 0) { - $startInfo.ArgumentList = $argumentList - } - - $process = Start-Process @startInfo - $timedOut = $false - $terminatedAfterCompletion = $false - $summarySeenAt = $null - $startTime = Get-Date - $summaryPattern = 'Tests \[Ok-Fail-Error\]: \[\d+-\d+-\d+\]' - $heartbeatIntervalSeconds = 30 - $nextHeartbeatAt = $startTime.AddSeconds($heartbeatIntervalSeconds) - - while ($true) { - $process.Refresh() - if ($process.HasExited) { - break - } - - $elapsedSeconds = ((Get-Date) - $startTime).TotalSeconds - if ($elapsedSeconds -ge $TimeoutSeconds) { - $timedOut = $true - Write-Host "Test run exceeded timeout (${TimeoutSeconds}s); terminating process..." -ForegroundColor Red - try { Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue } catch {} - break - } - - if ((Get-Date) -ge $nextHeartbeatAt) { - Write-Host ("[HEARTBEAT] {0} still running after {1:N0}s (pid {2})" -f $config.ExeName, $elapsedSeconds, $process.Id) -ForegroundColor DarkGray - $nextHeartbeatAt = (Get-Date).AddSeconds($heartbeatIntervalSeconds) - } - - if (Test-Path $LogPath) { - $recentOutput = Get-Content -Path $LogPath -Tail 25 -ErrorAction SilentlyContinue - if (($recentOutput -join "`n") -match $summaryPattern) { - if (-not $summarySeenAt) { - $summarySeenAt = Get-Date - Write-Host "Detected Unit++ completion summary; waiting up to ${PostCompletionGraceSeconds}s for process exit..." -ForegroundColor Yellow - } - elseif (((Get-Date) - $summarySeenAt).TotalSeconds -ge $PostCompletionGraceSeconds) { - $terminatedAfterCompletion = $true - Write-Host "Process did not exit ${PostCompletionGraceSeconds}s after completion summary; terminating hung process..." -ForegroundColor Yellow - try { Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue } catch {} - break - } - } - else { - $summarySeenAt = $null - } - } - - Start-Sleep -Milliseconds 250 - } - - $process.Refresh() - $exitCode = if ($process.HasExited) { $process.ExitCode } else { -1 } - - $logTail = @() - if (Test-Path $LogPath) { - if (Test-Path $errorLogPath) { - Add-Content -Path $LogPath -Value "`n--- stderr ---" - Get-Content -Path $errorLogPath -ErrorAction SilentlyContinue | Add-Content -Path $LogPath - } - $logTail = Get-Content -Path $LogPath -Tail 40 -ErrorAction SilentlyContinue - Write-Host "--- Test output (last 40 lines) ---" -ForegroundColor Yellow - $logTail | ForEach-Object { Write-Host $_ } - Write-Host "--- end output ---" -ForegroundColor Yellow - } - - Write-Host "" - if ($timedOut) { - Write-Host "Tests terminated due to timeout (${TimeoutSeconds}s)." -ForegroundColor Red - } - elseif ($terminatedAfterCompletion) { - Write-Host "Tests reported completion but process hung; terminated after ${PostCompletionGraceSeconds}s grace period." -ForegroundColor Yellow - } - elseif ($exitCode -eq 0) { - Write-Host "All tests passed! (exit code: 0)" -ForegroundColor Green - } - else { - Write-Host "Tests failed with exit code: $exitCode" -ForegroundColor Red - if ($exitCode -gt 0) { - Write-Host "($exitCode test(s) failed)" -ForegroundColor Red - } - } - - return $exitCode + Write-Host "`nRunning $($config.ExeName)..." -ForegroundColor Yellow + + # Add Debug CRT to PATH if running in Debug configuration (required for VCRUNTIME140D.dll) + if ($Configuration -eq 'Debug') { + $debugCrt = Get-ChildItem -Path "C:\BuildTools\VC\Redist\MSVC" -Filter "Microsoft.VC*.DebugCRT" -Recurse -ErrorAction SilentlyContinue | + Where-Object { $_.FullName -like "*\x64\*" -and $_.FullName -like "*\debug_nonredist\*" } | + Select-Object -First 1 -ExpandProperty FullName + + if ($debugCrt) { + Write-Host "Adding Debug CRT to PATH: $debugCrt" -ForegroundColor Gray + $env:PATH = "$debugCrt;$env:PATH" + } + } + + # Ensure ICU DLLs are in place + $icuSource = Join-Path $outputDir "lib\x64" + $icuDlls = Get-ChildItem -Path $icuSource -Filter "icu*70.dll" -ErrorAction SilentlyContinue + if ($icuDlls) { + foreach ($dll in $icuDlls) { + $dest = Join-Path $outputDir $dll.Name + if (-not (Test-Path $dest)) { + Write-Host "Copying $($dll.Name) to output directory..." -ForegroundColor Gray + Copy-Item $dll.FullName $dest + } + } + } + + if (-not (Test-Path $exePath)) { + throw "Test executable not found: $exePath. Run with -Action Build first." + } + + if (-not $LogPath) { + $LogPath = Join-Path $outputDir "$($config.ExeName).log" + } + + $errorLogPath = "$LogPath.stderr" + + # Ensure native tests resolve paths consistently in containers/CI + $distFiles = Join-Path $activeWorktreePath 'DistFiles' + $sourceDistFiles = if ($sourceWorktreePath -and (Test-Path (Join-Path $sourceWorktreePath 'DistFiles'))) { Join-Path $sourceWorktreePath 'DistFiles' } else { $null } + + # Backfill DistFiles into the active clone when missing critical assets + $requiredAssets = @( + @{ Rel = 'XceedZip.dll' }, + @{ Rel = 'Templates\NewLangProj.fwdata' } + ) + + if (-not (Test-Path $distFiles) -and $sourceDistFiles) { + Write-Host "[INFO] DistFiles missing in active worktree; copying from source" -ForegroundColor Yellow + Copy-Item -Path $sourceDistFiles -Destination $activeWorktreePath -Recurse -Force + } + + foreach ($asset in $requiredAssets) { + $target = Join-Path $distFiles $asset.Rel + if (-not (Test-Path $target)) { + if ($sourceDistFiles) { + $sourceAsset = Join-Path $sourceDistFiles $asset.Rel + if (Test-Path $sourceAsset) { + New-Item -ItemType Directory -Force -Path (Split-Path $target) | Out-Null + Copy-Item $sourceAsset $target -Force + } + } + } + } + + if (-not (Test-Path $distFiles)) { + throw "DistFiles not found in active worktree ($activeWorktreePath) and no source copy available." + } + + $env:FW_ROOT_CODE_DIR = $distFiles + $icuDir = Join-Path $distFiles 'Icu70' + if (Test-Path $icuDir) { + $env:FW_ICU_DATA_DIR = $icuDir + $env:ICU_DATA = $icuDir + } + Write-Host "FW_ROOT_CODE_DIR=$($env:FW_ROOT_CODE_DIR)" -ForegroundColor Gray + if ($env:FW_ICU_DATA_DIR) { Write-Host "FW_ICU_DATA_DIR=$($env:FW_ICU_DATA_DIR)" -ForegroundColor Gray } + $zipPath = Join-Path $distFiles 'XceedZip.dll' + Write-Host "XceedZip present: $(Test-Path $zipPath)" -ForegroundColor Gray + + $runExePath = $exePath + if ($null -ne $script:LastLocalOutDir) { + $potentialPath = Join-Path $script:LastLocalOutDir $config.ExeName + if (Test-Path $potentialPath) { + $runExePath = $potentialPath + } + } + + if (Test-Path $LogPath) { + Remove-Item $LogPath -Force -ErrorAction SilentlyContinue + } + + if (Test-Path $errorLogPath) { + Remove-Item $errorLogPath -Force -ErrorAction SilentlyContinue + } + + # Ensure no stale test process is holding the exe open + $exeProcessName = [System.IO.Path]::GetFileNameWithoutExtension($config.ExeName) + try { + Get-Process -Name $exeProcessName -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue + } + catch {} + + # Stage required runtime assets into the output directory for reliable lookup + $assetsToStage = @( + @{ Source = Join-Path $distFiles 'XceedZip.dll'; Destination = Join-Path $outputDir 'XceedZip.dll' }, + @{ Source = Join-Path $distFiles 'Templates\NewLangProj.fwdata'; Destination = Join-Path $outputDir 'Templates\NewLangProj.fwdata' } + ) + foreach ($asset in $assetsToStage) { + if (Test-Path $asset.Source) { + New-Item -ItemType Directory -Force -Path (Split-Path $asset.Destination) | Out-Null + Copy-Item $asset.Source $asset.Destination -Force + } + } + + $argumentList = @() + if ($TestArguments) { + $argumentList += $TestArguments + } + + Write-Host "Running: $runExePath $($argumentList -join ' ')" -ForegroundColor Gray + Write-Host "Logging to: $LogPath" -ForegroundColor Gray + + $startInfo = @{ + FilePath = $runExePath + WorkingDirectory = $outputDir + RedirectStandardOutput = $LogPath + RedirectStandardError = $errorLogPath + NoNewWindow = $true + PassThru = $true + } + + if ($argumentList.Count -gt 0) { + $startInfo.ArgumentList = $argumentList + } + + $process = Start-Process @startInfo + $timedOut = $false + $terminatedAfterCompletion = $false + $summarySeenAt = $null + $startTime = Get-Date + $summaryPattern = 'Tests \[Ok-Fail-Error\]: \[\d+-\d+-\d+\]' + $heartbeatIntervalSeconds = 30 + $nextHeartbeatAt = $startTime.AddSeconds($heartbeatIntervalSeconds) + + while ($true) { + $process.Refresh() + if ($process.HasExited) { + break + } + + $elapsedSeconds = ((Get-Date) - $startTime).TotalSeconds + if ($elapsedSeconds -ge $TimeoutSeconds) { + $timedOut = $true + Write-Host "Test run exceeded timeout (${TimeoutSeconds}s); terminating process..." -ForegroundColor Red + try { Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue } catch {} + break + } + + if ((Get-Date) -ge $nextHeartbeatAt) { + Write-Host ("[HEARTBEAT] {0} still running after {1:N0}s (pid {2})" -f $config.ExeName, $elapsedSeconds, $process.Id) -ForegroundColor DarkGray + $nextHeartbeatAt = (Get-Date).AddSeconds($heartbeatIntervalSeconds) + } + + if (Test-Path $LogPath) { + $recentOutput = Get-Content -Path $LogPath -Tail 25 -ErrorAction SilentlyContinue + if (($recentOutput -join "`n") -match $summaryPattern) { + if (-not $summarySeenAt) { + $summarySeenAt = Get-Date + Write-Host "Detected Unit++ completion summary; waiting up to ${PostCompletionGraceSeconds}s for process exit..." -ForegroundColor Yellow + } + elseif (((Get-Date) - $summarySeenAt).TotalSeconds -ge $PostCompletionGraceSeconds) { + $terminatedAfterCompletion = $true + Write-Host "Process did not exit ${PostCompletionGraceSeconds}s after completion summary; terminating hung process..." -ForegroundColor Yellow + try { Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue } catch {} + break + } + } + else { + $summarySeenAt = $null + } + } + + Start-Sleep -Milliseconds 250 + } + + $process.WaitForExit() + + $logTail = @() + if (Test-Path $LogPath) { + if (Test-Path $errorLogPath) { + Add-Content -Path $LogPath -Value "`n--- stderr ---" + Get-Content -Path $errorLogPath -ErrorAction SilentlyContinue | Add-Content -Path $LogPath + } + $logTail = Get-Content -Path $LogPath -Tail 40 -ErrorAction SilentlyContinue + Write-Host "--- Test output (last 40 lines) ---" -ForegroundColor Yellow + $logTail | ForEach-Object { Write-Host $_ } + Write-Host "--- end output ---" -ForegroundColor Yellow + } + + # Determine exit code: parse the Unit++ summary line from the log as the authoritative + # source. Start-Process -RedirectStandardOutput in PowerShell 5.1 can return a null + # ExitCode even after WaitForExit(), so the process exit code is not reliable here. + $exitCode = -1 + if (-not $timedOut) { + $summaryLine = $logTail | Where-Object { $_ -match 'Tests \[Ok-Fail-Error\]: \[\d+-\d+-\d+\]' } | Select-Object -Last 1 + if ($summaryLine) { + $m = [regex]::Match($summaryLine, 'Tests \[Ok-Fail-Error\]: \[(\d+)-(\d+)-(\d+)\]') + $exitCode = [int]$m.Groups[2].Value + [int]$m.Groups[3].Value + } + } + + Write-Host "" + if ($timedOut) { + Write-Host "Tests terminated due to timeout (${TimeoutSeconds}s)." -ForegroundColor Red + } + elseif ($terminatedAfterCompletion) { + Write-Host "Tests reported completion but process hung; terminated after ${PostCompletionGraceSeconds}s grace period." -ForegroundColor Yellow + } + elseif ($exitCode -eq 0) { + Write-Host "All tests passed!" -ForegroundColor Green + } + else { + Write-Host "Tests failed: $exitCode failure(s)/error(s)." -ForegroundColor Red + } + + return $exitCode } # Execute requested action $result = 0 try { - switch ($Action) { - 'Build' { - Invoke-Build - } - 'Run' { - $result = Invoke-Run - } - 'BuildAndRun' { - Invoke-Build - $result = Invoke-Run - } - } + switch ($Action) { + 'Build' { + Invoke-Build + } + 'Run' { + $result = Invoke-Run + } + 'BuildAndRun' { + Invoke-Build + $result = Invoke-Run + } + } } catch { - Write-Host "`nError: $_" -ForegroundColor Red - exit 1 + Write-Host "`nError: $_" -ForegroundColor Red + exit 1 } exit $result From f7e02acf4ae9b406da6e1395f06967cfb3a7eb3b Mon Sep 17 00:00:00 2001 From: Jason Naylor Date: Thu, 19 Feb 2026 14:50:26 -0800 Subject: [PATCH 11/11] Remove extra NativeProjects build --- .github/workflows/CI-native.yml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.github/workflows/CI-native.yml b/.github/workflows/CI-native.yml index 0410ac85cc..c4c98f4891 100644 --- a/.github/workflows/CI-native.yml +++ b/.github/workflows/CI-native.yml @@ -61,13 +61,6 @@ jobs: uses: actions/checkout@v4 id: checkout - - name: Run native CI pipeline - id: native_ci - shell: powershell - run: | - .\build.ps1 -Configuration Debug -Platform x64 -Project Build/Src/NativeBuild/NativeBuild.csproj - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - - name: Build and run native tests id: native_test shell: powershell