name: Build and Release on: push: branches: - main paths: - 'src/**' workflow_dispatch: permissions: contents: write pull-requests: read jobs: build: name: Build and Sign for Release runs-on: windows-latest outputs: new_tag: ${{ steps.bump.outputs.new_tag }} steps: - name: Checkout code uses: actions/checkout@v4 with: ref: main - name: Cache NuGet packages uses: actions/cache@v4 with: path: ~/.nuget/packages key: nuget-${{ hashFiles('**/*.csproj') }} restore-keys: | nuget- - name: Setup MSBuild uses: microsoft/setup-msbuild@v2 - name: Bump version tag id: bump shell: pwsh run: | git fetch --tags $currentTag = git tag --sort=-creatordate | Where-Object { $_ -match '^\d+\.\d+\.\d+$' } | Select-Object -First 1 if (-not $currentTag) { $major = 1 $minor = 0 $patch = 0 } else { $parts = $currentTag.Split('.') $major = [int]$parts[0] $minor = [int]$parts[1] $patch = [int]$parts[2] + 1 } $newTag = "$($major).$($minor).$($patch)" "new_tag=$newTag" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append Write-Host "New tag generated: $newTag" - name: Restore NuGet packages shell: pwsh run: nuget restore src\WinMemoryCleaner.sln - name: Build solution shell: pwsh run: msbuild src\WinMemoryCleaner.sln /m /p:Configuration=Release /p:Platform="Any CPU" - name: Capture window-only screenshot for docs shell: pwsh run: | $ErrorActionPreference = "Stop" $appPath = "src\bin\Release\WinMemoryCleaner.exe" if (-not (Test-Path $appPath)) { Write-Host "::error::Built EXE not found at $appPath" exit 1 } $windowScreenshotPath = "$pwd\main-window-raw.png" # Launch app and wait for main window $proc = Start-Process -FilePath $appPath -PassThru $maxWaitMs = 15000 $elapsed = 0 while ($proc.MainWindowHandle -eq 0 -and $elapsed -lt $maxWaitMs) { Start-Sleep -Milliseconds 200 $proc.Refresh() $elapsed += 200 } if ($proc.MainWindowHandle -eq 0) { Write-Host "::error::Main window not found." Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue exit 1 } Add-Type -AssemblyName System.Drawing # Define Win32 interop without a here-string to avoid YAML parsing issues $sig = 'using System; using System.Runtime.InteropServices; public static class Win32 { [StructLayout(LayoutKind.Sequential)] public struct RECT { public int Left; public int Top; public int Right; public int Bottom; } [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr hWnd, ref RECT rect); [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hWnd); }' Add-Type -TypeDefinition $sig -Language CSharp [void][Win32]::SetForegroundWindow([IntPtr]$proc.MainWindowHandle) $rect = New-Object Win32+RECT if (-not [Win32]::GetWindowRect([IntPtr]$proc.MainWindowHandle, [ref]$rect)) { Write-Host "::error::GetWindowRect failed." Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue exit 1 } $width = $rect.Right - $rect.Left $height = $rect.Bottom - $rect.Top if ($width -le 0 -or $height -le 0) { Write-Host "::error::Invalid window size: $width x $height" Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue exit 1 } $wbmp = New-Object System.Drawing.Bitmap $width, $height $wgfx = [System.Drawing.Graphics]::FromImage($wbmp) $wgfx.CopyFromScreen($rect.Left, $rect.Top, 0, 0, (New-Object System.Drawing.Size($width, $height))) $wbmp.Save($windowScreenshotPath, [System.Drawing.Imaging.ImageFormat]::Png) $wgfx.Dispose() $wbmp.Dispose() Stop-Process -Id $proc.Id -Force - name: Upload screenshot artifact if: always() uses: actions/upload-artifact@v4 with: name: release-window-screenshot path: main-window-raw.png retention-days: 7 - name: Upload unsigned EXE for signing id: upload-unsigned uses: actions/upload-artifact@v4 with: name: winmemorycleaner-${{ steps.bump.outputs.new_tag }} path: src\bin\Release\WinMemoryCleaner.exe if-no-files-found: error - name: Submit to SignPath (release cert) if: github.repository == 'IgorMundstein/WinMemoryCleaner' && github.ref == 'refs/heads/main' id: signpath uses: signpath/github-action-submit-signing-request@v1.1 with: api-token: ${{ secrets.SIGNPATH_API_TOKEN }} organization-id: ${{ secrets.SIGNPATH_ORGANIZATION_ID }} project-slug: WinMemoryCleaner signing-policy-slug: release-signing github-artifact-id: ${{ steps.upload-unsigned.outputs.artifact-id }} wait-for-completion: true output-artifact-directory: ./ - name: Validate signed EXE if: github.repository == 'IgorMundstein/WinMemoryCleaner' && github.ref == 'refs/heads/main' shell: pwsh run: | if ((Get-Item WinMemoryCleaner.exe).Length -lt 100000) { Write-Host "::error::Signed EXE is unexpectedly small, aborting release!" exit 1 } - name: Create ZIP archive if: github.repository == 'IgorMundstein/WinMemoryCleaner' && github.ref == 'refs/heads/main' shell: pwsh run: Compress-Archive -Path WinMemoryCleaner.exe -DestinationPath WinMemoryCleaner.zip - name: Upload release artifacts if: github.repository == 'IgorMundstein/WinMemoryCleaner' && github.ref == 'refs/heads/main' uses: actions/upload-artifact@v4 with: name: winmemorycleaner-release-${{ steps.bump.outputs.new_tag }} path: | WinMemoryCleaner.exe WinMemoryCleaner.zip retention-days: 30 release: name: Create GitHub Release needs: build runs-on: windows-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Download release artifacts uses: actions/download-artifact@v4 with: name: winmemorycleaner-release-${{ needs.build.outputs.new_tag }} path: release_artifacts - name: Create Checksums shell: pwsh working-directory: release_artifacts run: | Get-FileHash -Algorithm SHA256 -Path "WinMemoryCleaner.exe", "WinMemoryCleaner.zip" | ForEach-Object { $_.Hash.ToLower() + " " + (Split-Path -Leaf $_.Path) } | Out-File -FilePath "checksums.txt" -Encoding utf8 Write-Host "Generated checksums.txt:" cat checksums.txt - name: Create or reuse version tag shell: pwsh run: | $tag = "${{ needs.build.outputs.new_tag }}" git fetch --tags $tagExists = git tag -l $tag if (-not $tagExists) { git config user.name "github-actions" git config user.email "github-actions@github.com" git tag $tag git push origin $tag } else { Write-Host "Tag '$tag' already exists. Reusing..." } - name: Check if release already exists id: check_release shell: pwsh run: | $tag = "${{ needs.build.outputs.new_tag }}" $headers = @{ Authorization = "Bearer $env:GITHUB_TOKEN" } $uri = "https://api.github.com/repos/${{ github.repository }}/releases/tags/$tag" try { $response = Invoke-RestMethod -Uri $uri -Headers $headers -Method GET Write-Host "Release already exists for tag $tag." "skip=true" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append } catch { if ($_.Exception.Response.StatusCode.value__ -eq 404) { Write-Host "No release exists for tag $tag. Proceeding." "skip=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append } else { Write-Error "Unexpected error: $($_.Exception.Message)" exit 1 } } env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Get commit date id: commit_date shell: pwsh run: | $date = (git show -s --format=%cd --date=short ${{ github.event.head_commit.id }}).Trim() echo "date=$date" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append - name: Format commit message for release notes id: formatted_message shell: pwsh env: COMMIT_MESSAGE: ${{ github.event.head_commit.message }} run: | $msg = @() foreach ($line in $env:COMMIT_MESSAGE -split "`n") { $msg += "- $line" } $body = $msg -join "`n" echo "body<> $GITHUB_OUTPUT echo "IMAGE_HEIGHT=$H" >> $GITHUB_OUTPUT # Corner radius ~2% of min dimension, clamped to [10, 24] if [ "$W" -lt "$H" ]; then MIN=$W; else MIN=$H; fi R=$(( MIN / 50 )) if [ "$R" -lt 10 ]; then R=10; fi if [ "$R" -gt 24 ]; then R=24; fi magick "$FILE" \ \( -size "${W}x${H}" xc:none -fill white -draw "roundrectangle 0,0,$((W-1)),$((H-1)),$R,$R" \) \ -compose DstIn -composite \ docs/assets/images/main-window.png - name: Update docs/index.html shell: bash run: | set -euo pipefail INDEX="docs/index.html" IMG="docs/assets/images/main-window.png" if [ ! -f "$INDEX" ]; then echo "::error::$INDEX not found" exit 1 fi if [ ! -f "$IMG" ]; then echo "::error::$IMG not found" exit 1 fi # Update image dimensions WIDTH=${{ steps.image_dimensions.outputs.IMAGE_WIDTH }} HEIGHT=${{ steps.image_dimensions.outputs.IMAGE_HEIGHT }} sed -i "s|\"Appconst IMAGE_VERSION = "%s";\n' "$HASH" >> "$INDEX" fi echo "Updated IMAGE_VERSION to $HASH" - name: Commit and push if changed (with PAT) shell: bash run: | set -euo pipefail CHANGED=0 git add docs/assets/images/main-window.png docs/index.html || true if ! git diff --cached --quiet; then CHANGED=1 git commit -m "chore(docs): update main-window.png & cache bust token (image hash)" git push origin HEAD:main fi if [ $CHANGED -eq 0 ]; then echo "No changes to commit." fi