Skip to content
This repository was archived by the owner on Apr 6, 2026. It is now read-only.

Build and Release

Build and Release #54

Workflow file for this run

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 }}