This repository was archived by the owner on Apr 6, 2026. It is now read-only.
Build and Release #54
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Build and Release | |
| on: | |
| workflow_dispatch: | |
| env: | |
| DOTNET_VERSION: '9.0.x' | |
| jobs: | |
| build-and-release: | |
| runs-on: windows-latest | |
| if: github.event_name == 'workflow_dispatch' | |
| permissions: | |
| contents: write | |
| pull-requests: read | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| ref: ${{ github.ref }} | |
| - name: Setup .NET | |
| uses: actions/setup-dotnet@v4 | |
| with: | |
| dotnet-version: ${{ env.DOTNET_VERSION }} | |
| - name: Restore dependencies | |
| run: dotnet restore YAEP.sln | |
| - name: Build solution | |
| run: dotnet build YAEP.AvaloniaUI/YAEP.AvaloniaUI.csproj --configuration Release --runtime win-x64 --no-restore | |
| - name: Get current version | |
| id: get_version | |
| run: | | |
| $version = Select-String -Path "YAEP.AvaloniaUI\YAEP.AvaloniaUI.csproj" -Pattern '<Version>(\d+)\.(\d+)\.(\d+)</Version>' | ForEach-Object { $_.Matches.Groups[1..3].Value -join '.' } | |
| if ([string]::IsNullOrEmpty($version)) { | |
| $version = "1.0.0" | |
| } | |
| Write-Host "Current version: $version" | |
| echo "CURRENT_VERSION=$version" >> $env:GITHUB_ENV | |
| $parts = $version -split '\.' | |
| echo "MAJOR=$($parts[0])" >> $env:GITHUB_ENV | |
| echo "MINOR=$($parts[1])" >> $env:GITHUB_ENV | |
| echo "PATCH=$($parts[2])" >> $env:GITHUB_ENV | |
| - name: Get last tag | |
| id: get_last_tag | |
| run: | | |
| $ErrorActionPreference = "Continue" | |
| $lastTag = "" | |
| $hasChanges = "false" | |
| # Fetch all tags to ensure we have them | |
| Write-Host "Fetching all tags..." | |
| git fetch --tags --force | |
| # Get the most recent tag (sorted by version), won't fail if no tags exist | |
| $tagOutput = git tag --sort=-version:refname | Select-Object -First 1 | |
| if (-not [string]::IsNullOrWhiteSpace($tagOutput)) { | |
| $lastTag = $tagOutput.Trim() | |
| Write-Host "Found last tag: $lastTag" | |
| # Get the commit SHA that the tag points to (handles both annotated and lightweight tags) | |
| $tagCommit = git rev-parse "$lastTag^{commit}" 2>&1 | |
| if ($LASTEXITCODE -ne 0) { | |
| Write-Host "Warning: Could not resolve tag commit: $tagCommit" | |
| Write-Host "Assuming changes exist for safety" | |
| $hasChanges = "true" | |
| } else { | |
| $tagCommit = $tagCommit.Trim() | |
| Write-Host "Tag $lastTag points to commit: $tagCommit" | |
| # Get current HEAD commit | |
| $headCommit = git rev-parse HEAD 2>&1 | |
| if ($LASTEXITCODE -ne 0) { | |
| Write-Host "Warning: Could not get HEAD commit" | |
| Write-Host "Assuming changes exist for safety" | |
| $hasChanges = "true" | |
| } else { | |
| $headCommit = $headCommit.Trim() | |
| Write-Host "Current HEAD commit: $headCommit" | |
| # Check if there are commits between tag and HEAD | |
| # Use ^{commit} to dereference annotated tags to their commit | |
| Write-Host "Checking for commits between $lastTag and HEAD..." | |
| $commitCountOutput = git rev-list "$lastTag^{commit}..HEAD" --count 2>&1 | |
| if ($LASTEXITCODE -eq 0) { | |
| if (-not [string]::IsNullOrWhiteSpace($commitCountOutput)) { | |
| $commitCount = [int]$commitCountOutput.Trim() | |
| Write-Host "Commit count: $commitCount" | |
| if ($commitCount -gt 0) { | |
| $hasChanges = "true" | |
| Write-Host "Found $commitCount new commits since last tag" | |
| } else { | |
| Write-Host "No new commits since last tag, skipping version bump and release" | |
| } | |
| } else { | |
| Write-Host "No commit count output, checking if commits exist..." | |
| # Fallback: check if there are any commits at all | |
| $commitList = git rev-list "$lastTag^{commit}..HEAD" 2>&1 | |
| if (-not [string]::IsNullOrWhiteSpace($commitList)) { | |
| $hasChanges = "true" | |
| Write-Host "Found commits between tag and HEAD" | |
| } else { | |
| Write-Host "No new commits since last tag, skipping version bump and release" | |
| } | |
| } | |
| } else { | |
| Write-Host "Warning: git rev-list failed with exit code $LASTEXITCODE" | |
| Write-Host "Output: $commitCountOutput" | |
| Write-Host "Assuming changes exist for safety" | |
| $hasChanges = "true" | |
| } | |
| } | |
| } | |
| } else { | |
| Write-Host "No existing tags found - this will be the first release" | |
| $hasChanges = "true" | |
| } | |
| Write-Host "Last tag: $lastTag (empty if none)" | |
| Write-Host "Has changes: $hasChanges" | |
| echo "has_changes=$hasChanges" >> $env:GITHUB_OUTPUT | |
| echo "LAST_TAG=$lastTag" >> $env:GITHUB_ENV | |
| - name: Increment version | |
| id: increment_version | |
| if: steps.get_last_tag.outputs.has_changes == 'true' | |
| run: | | |
| $patch = [int]$env:PATCH + 1 | |
| $newVersion = "$env:MAJOR.$env:MINOR.$patch" | |
| Write-Host "New version: $newVersion" | |
| echo "version=$newVersion" >> $env:GITHUB_OUTPUT | |
| - name: Update version in project files | |
| if: steps.get_last_tag.outputs.has_changes == 'true' | |
| run: | | |
| $newVersion = "${{ steps.increment_version.outputs.version }}" | |
| $files = @("YAEP.AvaloniaUI\YAEP.AvaloniaUI.csproj", "YAEP.Interop\YAEP.Interop.csproj") | |
| foreach ($file in $files) { | |
| $content = Get-Content $file -Raw | |
| # Add or update Version property | |
| if ($content -match '<Version>(\d+\.\d+\.\d+)</Version>') { | |
| $content = $content -replace '<Version>(\d+\.\d+\.\d+)</Version>', "<Version>$newVersion</Version>" | |
| } else { | |
| # Add Version property after TargetFramework | |
| $content = $content -replace '(<TargetFramework>.*?</TargetFramework>)', "`$1`n <Version>$newVersion</Version>" | |
| } | |
| # Add or update AssemblyVersion | |
| if ($content -match '<AssemblyVersion>(\d+\.\d+\.\d+\.\d+)</AssemblyVersion>') { | |
| $content = $content -replace '<AssemblyVersion>(\d+\.\d+\.\d+\.\d+)</AssemblyVersion>', "<AssemblyVersion>$newVersion.0</AssemblyVersion>" | |
| } else { | |
| $content = $content -replace '(<Version>.*?</Version>)', "`$1`n <AssemblyVersion>$newVersion.0</AssemblyVersion>" | |
| } | |
| # Add or update FileVersion | |
| if ($content -match '<FileVersion>(\d+\.\d+\.\d+\.\d+)</FileVersion>') { | |
| $content = $content -replace '<FileVersion>(\d+\.\d+\.\d+\.\d+)</FileVersion>', "<FileVersion>$newVersion.0</FileVersion>" | |
| } else { | |
| $content = $content -replace '(<AssemblyVersion>.*?</AssemblyVersion>)', "`$1`n <FileVersion>$newVersion.0</FileVersion>" | |
| } | |
| # Add AssemblyName to YAEP.AvaloniaUI to make executable YAEP.exe | |
| if ($file -like "*YAEP.AvaloniaUI*") { | |
| if (-not ($content -match '<AssemblyName>')) { | |
| $content = $content -replace '(<FileVersion>.*?</FileVersion>)', "`$1`n <AssemblyName>YAEP</AssemblyName>" | |
| } else { | |
| $content = $content -replace '<AssemblyName>.*?</AssemblyName>', "<AssemblyName>YAEP</AssemblyName>" | |
| } | |
| } | |
| Set-Content $file -Value $content -NoNewline | |
| } | |
| - name: Rebuild with new version | |
| if: steps.get_last_tag.outputs.has_changes == 'true' | |
| run: | | |
| dotnet build YAEP.AvaloniaUI/YAEP.AvaloniaUI.csproj --configuration Release --runtime win-x64 --no-restore | |
| - name: Publish application | |
| if: steps.get_last_tag.outputs.has_changes == 'true' | |
| run: | | |
| dotnet publish YAEP.AvaloniaUI/YAEP.AvaloniaUI.csproj --configuration Release --runtime win-x64 --output ./publish --no-build | |
| - name: Configure Git | |
| if: steps.get_last_tag.outputs.has_changes == 'true' | |
| run: | | |
| git config --global user.name "github-actions[bot]" | |
| git config --global user.email "github-actions[bot]@users.noreply.github.com" | |
| - name: Commit version bump | |
| if: steps.get_last_tag.outputs.has_changes == 'true' | |
| run: | | |
| git add YAEP.AvaloniaUI/YAEP.AvaloniaUI.csproj YAEP.Interop/YAEP.Interop.csproj | |
| git commit -m "Bump version to ${{ steps.increment_version.outputs.version }}" || exit 0 | |
| - name: Generate release notes | |
| id: generate_release_notes | |
| if: steps.get_last_tag.outputs.has_changes == 'true' | |
| uses: actions/github-script@v7 | |
| env: | |
| LAST_TAG: ${{ env.LAST_TAG }} | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| const lastTag = process.env.LAST_TAG; | |
| const params = { | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| tag_name: `v${{ steps.increment_version.outputs.version }}`, | |
| }; | |
| // If there's a previous tag, use it for comparison (as per GitHub docs) | |
| if (lastTag && lastTag.trim() !== '') { | |
| params.previous_tag_name = lastTag; | |
| } | |
| const { data: releaseNotes } = await github.rest.repos.generateReleaseNotes(params); | |
| core.setOutput('body', releaseNotes.body); | |
| fs.writeFileSync('CHANGELOG.md', releaseNotes.body); | |
| - name: Create git tag | |
| if: steps.get_last_tag.outputs.has_changes == 'true' | |
| run: | | |
| $version = "${{ steps.increment_version.outputs.version }}" | |
| git tag -a "v$version" -m "Release v$version" | |
| git push origin "v$version" | |
| - name: Push version bump commit | |
| if: steps.get_last_tag.outputs.has_changes == 'true' | |
| run: | | |
| git push origin main | |
| - name: Create release package | |
| id: create_package | |
| if: steps.get_last_tag.outputs.has_changes == 'true' | |
| run: | | |
| $version = "${{ steps.increment_version.outputs.version }}" | |
| $zipName = "YAEP-v$version.zip" | |
| Compress-Archive -Path ./publish/* -DestinationPath $zipName -Force | |
| Write-Host "Created package: $zipName" | |
| echo "package_name=$zipName" >> $env:GITHUB_OUTPUT | |
| - name: Scan with VirusTotal | |
| id: virustotal_scan | |
| if: steps.get_last_tag.outputs.has_changes == 'true' | |
| continue-on-error: true | |
| env: | |
| VT_API_KEY: ${{ secrets.VT_API_KEY }} | |
| run: | | |
| $zipFile = "YAEP-v${{ steps.increment_version.outputs.version }}.zip" | |
| $apiKey = $env:VT_API_KEY | |
| if ([string]::IsNullOrWhiteSpace($apiKey)) { | |
| Write-Host "VT_API_KEY secret not set, skipping VirusTotal scan" | |
| echo "scan_url=" >> $env:GITHUB_OUTPUT | |
| echo "scan_id=" >> $env:GITHUB_OUTPUT | |
| exit 0 | |
| } | |
| Write-Host "Uploading $zipFile to VirusTotal..." | |
| # Check file size (32 MB limit for direct upload) | |
| $fileSize = (Get-Item $zipFile).Length | |
| $maxDirectUpload = 32 * 1024 * 1024 | |
| if ($fileSize -le $maxDirectUpload) { | |
| # Direct upload for files <= 32 MB | |
| $response = curl.exe -s -X POST "https://www.virustotal.com/api/v3/files" ` | |
| -H "x-apikey: $apiKey" ` | |
| -F "file=@$zipFile" | |
| } else { | |
| # Get upload URL for larger files | |
| Write-Host "File is larger than 32 MB, requesting upload URL..." | |
| $uploadUrlResponse = curl.exe -s -X GET "https://www.virustotal.com/api/v3/files/upload_url" ` | |
| -H "x-apikey: $apiKey" | |
| $uploadUrl = ($uploadUrlResponse | ConvertFrom-Json).data | |
| Write-Host "Upload URL obtained, uploading file..." | |
| $response = curl.exe -s -X POST "$uploadUrl" ` | |
| -H "x-apikey: $apiKey" ` | |
| -F "file=@$zipFile" | |
| } | |
| $jsonResponse = $response | ConvertFrom-Json | |
| $analysisId = $jsonResponse.data.id | |
| Write-Host "File uploaded. Analysis ID: $analysisId" | |
| # Wait for analysis to complete (poll every 10 seconds, max 5 minutes) | |
| $maxAttempts = 30 | |
| $attempt = 0 | |
| $analysisComplete = $false | |
| while ($attempt -lt $maxAttempts -and -not $analysisComplete) { | |
| Start-Sleep -Seconds 10 | |
| $attempt++ | |
| Write-Host "Checking analysis status (attempt $attempt/$maxAttempts)..." | |
| $analysisResponse = curl.exe -s -X GET "https://www.virustotal.com/api/v3/analyses/$analysisId" ` | |
| -H "x-apikey: $apiKey" | |
| $analysisData = $analysisResponse | ConvertFrom-Json | |
| if ($analysisData.data.attributes.status -eq "completed") { | |
| $analysisComplete = $true | |
| $stats = $analysisData.data.attributes.stats | |
| $harmless = $stats.harmless | |
| $malicious = $stats.malicious | |
| $suspicious = $stats.suspicious | |
| $undetected = $stats.undetected | |
| $total = $harmless + $malicious + $suspicious + $undetected | |
| # Get file hash for permalink - try relationships first, then attributes | |
| $fileHash = $null | |
| if ($analysisData.data.relationships.file.data.id) { | |
| $fileHash = $analysisData.data.relationships.file.data.id | |
| } elseif ($analysisData.data.attributes.sha256) { | |
| $fileHash = $analysisData.data.attributes.sha256 | |
| } | |
| if ([string]::IsNullOrWhiteSpace($fileHash)) { | |
| Write-Host "Warning: Could not extract file hash from analysis response" | |
| Write-Host "Analysis response structure:" | |
| $analysisResponse | ConvertFrom-Json | ConvertTo-Json -Depth 10 | |
| $fileHash = "unknown" | |
| } | |
| $permalink = "https://www.virustotal.com/gui/file/$fileHash" | |
| # Determine verdict | |
| if ($malicious -gt 0) { | |
| $verdict = "⚠️ $malicious/$total engines detected threats" | |
| } elseif ($suspicious -gt 0) { | |
| $verdict = "⚠️ $suspicious/$total engines flagged as suspicious" | |
| } else { | |
| $verdict = "✅ Clean ($harmless/$total engines found no threats)" | |
| } | |
| Write-Host "Analysis complete!" | |
| Write-Host "File Hash: $fileHash" | |
| Write-Host "Verdict: $verdict" | |
| Write-Host "Permalink: $permalink" | |
| echo "scan_url=$permalink" >> $env:GITHUB_OUTPUT | |
| echo "file_hash=$fileHash" >> $env:GITHUB_OUTPUT | |
| echo "scan_id=$analysisId" >> $env:GITHUB_OUTPUT | |
| echo "verdict=$verdict" >> $env:GITHUB_OUTPUT | |
| echo "stats=Harmless: $harmless, Malicious: $malicious, Suspicious: $suspicious, Undetected: $undetected" >> $env:GITHUB_OUTPUT | |
| } | |
| } | |
| if (-not $analysisComplete) { | |
| Write-Host "Analysis did not complete within timeout period" | |
| echo "scan_url=" >> $env:GITHUB_OUTPUT | |
| echo "scan_id=$analysisId" >> $env:GITHUB_OUTPUT | |
| } | |
| - name: Update release notes with VirusTotal scan | |
| if: steps.get_last_tag.outputs.has_changes == 'true' && steps.virustotal_scan.outcome == 'success' && steps.virustotal_scan.outputs.scan_url | |
| run: | | |
| $vtLink = "${{ steps.virustotal_scan.outputs.scan_url }}" | |
| $vtFileHash = "${{ steps.virustotal_scan.outputs.file_hash }}" | |
| $vtVerdict = "${{ steps.virustotal_scan.outputs.verdict }}" | |
| $vtStats = "${{ steps.virustotal_scan.outputs.stats }}" | |
| if ([string]::IsNullOrWhiteSpace($vtLink) -or $vtLink -eq "https://www.virustotal.com/gui/file/") { | |
| Write-Host "No valid VirusTotal scan URL available, skipping update" | |
| exit 0 | |
| } | |
| $changelog = Get-Content CHANGELOG.md -Raw | |
| $fileHashInfo = "" | |
| if (-not [string]::IsNullOrWhiteSpace($vtFileHash) -and $vtFileHash -ne "unknown") { | |
| $fileHashInfo = "`n- **File Hash (SHA256)**: $vtFileHash" | |
| } | |
| $vtSection = "`n`n## 🔒 Security Scan`n`nThis release has been scanned with VirusTotal for your safety.`n`n- **Scan Result**: $vtVerdict`n- **Statistics**: $vtStats$fileHashInfo`n`n[View full scan report]($vtLink)" | |
| $updatedChangelog = $changelog + $vtSection | |
| Set-Content CHANGELOG.md -Value $updatedChangelog -NoNewline | |
| - name: Create GitHub Release | |
| if: steps.get_last_tag.outputs.has_changes == 'true' | |
| uses: softprops/action-gh-release@v1 | |
| with: | |
| tag_name: v${{ steps.increment_version.outputs.version }} | |
| name: Release v${{ steps.increment_version.outputs.version }} | |
| body_path: CHANGELOG.md | |
| files: YAEP-v${{ steps.increment_version.outputs.version }}.zip | |
| draft: false | |
| prerelease: false | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |