_tmp/build.ps1
| # Consolidated build and publish script # Combines all functionality from Publish-Module.psm1 with a -Publish flag param( [string]$Version = "", [string[]]$Arch = @("win-x64", "win-x86"), [switch]$Release, [switch]$Debug, [switch]$Git, [switch]$Docker, [switch]$Publish, [switch]$Github, [switch]$Ghcr, [switch]$Nuget, [string]$Repo ) # Gitignore template $gitignore_template = @" bin/ obj/ *.user *.suo *.userosscache *.sln.docstates .vs/ *.nupkg *.snupkg *.log *.DS_Store *.swp *.scc *.pdb *.db *.db-shm *.db-wal *.sqlite *.sqlite3 *.bak *.tmp *.cache *.exe *.dll *.pdb *.xml *.json *.config *.log *.env "@ function Get-Username { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [ValidateSet("github", "docker")] [string]$Service ) $service = $Service.ToLower() $username = $null switch ($service) { "github" { # 1. Prefer explicit environment variable if ($env:GITHUB_USERNAME) { Write-Host "Using GITHUB_USERNAME environment variable: $env:GITHUB_USERNAME" $username = $env:GITHUB_USERNAME break } # 2. Try to extract from git remote if (Test-Path ".git") { try { $remotes = git remote -v 2>$null foreach ($remote in $remotes) { if ($remote -match "github\.com[:/]([^/]+)/") { $username = $matches[1] Write-Host "Extracted GitHub username '$username' from git remote" break } } } catch { Write-Warning "Failed to read git remotes: $_" } if ($username) { break } } # 3. Fallback to system username if ($env:USERNAME) { Write-Host "Falling back to system USERNAME: $env:USERNAME" $username = $env:USERNAME break } Write-Error "Could not determine GitHub username. Set GITHUB_USERNAME environment variable or ensure a valid git remote exists." return $null } "docker" { # 1. Prefer explicit environment variable if ($env:DOCKER_USERNAME) { Write-Host "Using DOCKER_USERNAME environment variable: $env:DOCKER_USERNAME" $username = $env:DOCKER_USERNAME break } # 2. Fallback to system username if ($env:USERNAME) { Write-Host "Falling back to system USERNAME: $env:USERNAME" $username = $env:USERNAME break } Write-Error "Could not determine Docker username. Set DOCKER_USERNAME environment variable or ensure USERNAME is set." return $null } } if ($username) { return $username.ToLower() } else { Write-Error "Could not determine username for service '$Service'." return $null } } function Bump-Version { param([string]$oldVersion) $parts = $oldVersion -split '\.' # Ensure at least 4 parts if (-not $oldVersion -or ($oldVersion -notmatch '^\d+(\.\d+){0,3}$')) { $oldVersion = "1.0.0.0" } while ($parts.Count -lt 4) { $parts += '0' } $major = [int]$parts[0] $minor = [int]$parts[1] $patch = [int]$parts[2] $build = [int]$parts[3] $build++ if ($build -gt 9) { $build = 0 $patch++ } # If patch ever needs to roll over, add logic here $newVersion = "$major.$minor.$patch.$build" Write-Host "Bumped Version: $oldVersion -> $newVersion" return $newVersion } function Set-Version { param( [xml]$projXml, $versionNode, [string]$newVersion, [string]$csproj ) $versionNode.Version = $newVersion $projXml.Save($csproj) Write-Host "Updated version to $newVersion in $csproj" } function Kill-ProcessesByName { param ( [string[]]$Names ) foreach ($procName in $Names) { $procs = Get-Process -Name $procName -ErrorAction SilentlyContinue if ($procs) { Write-Host "Killing running process(es) named $procName..." foreach ($proc in $procs) { try { Stop-Process -Id $proc.Id -Force -ErrorAction Stop Write-Host "Killed process $($proc.Id) ($($proc.ProcessName))" } catch { Write-Host "Failed to kill process $($proc.Id): $_" -ForegroundColor Yellow } } } } } function Clear-BuildArtifacts { param( [string]$ProjectDir, [string]$OutputBinDir ) Write-Host "Performing comprehensive build cleanup..." -ForegroundColor Yellow # Kill any running dotnet processes Kill-ProcessesByName -Names @("dotnet") # Run dotnet clean Write-Host "Running dotnet clean..." dotnet clean if ($LASTEXITCODE -eq 0) { Write-Host "dotnet clean completed successfully" -ForegroundColor Green } else { Write-Host "dotnet clean completed with warnings" -ForegroundColor Yellow } # Remove bin and obj directories completely $directoriesToRemove = @( $OutputBinDir, "$ProjectDir/obj", "$ProjectDir/bin" ) foreach ($dir in $directoriesToRemove) { if (Test-Path $dir) { Write-Host "Removing directory: $dir" Remove-Item -Path $dir -Recurse -Force -ErrorAction SilentlyContinue if (-not (Test-Path $dir)) { Write-Host "Successfully removed: $dir" -ForegroundColor Green } else { Write-Host "Failed to remove: $dir" -ForegroundColor Red } } } # Recreate the output bin directory if (-not (Test-Path $OutputBinDir)) { New-Item -ItemType Directory -Path $OutputBinDir -Force | Out-Null Write-Host "Recreated output directory: $OutputBinDir" -ForegroundColor Green } Write-Host "Build cleanup completed" -ForegroundColor Green } function Dotnet-Publish { param( [string]$Config, [string]$Arch, [string]$ProjectFramework, [string]$AssemblyName, [ValidateSet("DLL", "FrameworkExe", "StandaloneExe")] [string]$BuildType, [switch]$Clean ) Write-Host "Building $BuildType for $Config on $arch..." if ($Clean) { Write-Host "Running dotnet clean..." dotnet clean if ($LASTEXITCODE -eq 0) { Write-Host "dotnet clean completed successfully" -ForegroundColor Green } else { Write-Host "dotnet clean completed with warnings" -ForegroundColor Yellow } } # Build the dotnet publish command $publishArgs = @( "publish", "-c", $Config, "-r", $Arch ) # Add build type specific arguments switch ($BuildType) { "DLL" { # Basic DLL build - no additional args needed } "FrameworkExe" { $publishArgs += @("--self-contained", "false") } "StandaloneExe" { $publishArgs += @( "--self-contained", "true", "/p:PublishSingleFile=true", "/p:IncludeAllContentForSelfExtract=true" ) } } # Add release optimizations if ($Config -eq "Release") { $publishArgs += @( "/p:OptimizeImplicitlyTriggeredBuild=true", "/p:EnableCompressionInSingleFile=true", "/p:DebugType=None", "/p:DebugSymbols=false" ) # Additional optimizations for standalone builds if ($BuildType -eq "StandaloneExe") { $publishArgs += @( "/p:PublishTrimmed=true", "/p:TrimMode=link", "/p:EnableUnsafeBinaryFormatterSerialization=false", "/p:EnableUnsafeUTF7Encoding=false", "/p:EventSourceSupport=false", "/p:HttpActivityPropagationSupport=false", "/p:InvariantGlobalization=true", "/p:MetadataUpdaterSupport=false" # "/p:UseSystemTextJson=false" ) } } # Execute dotnet publish and capture output $publishOutput = dotnet $publishArgs 2>&1 $publishExitCode = $LASTEXITCODE if ($publishExitCode -ne 0) { Write-Host "Error during $BuildType dotnet publish (Exit code: $publishExitCode)" -ForegroundColor Red return $null } # Try to extract output path from dotnet publish output $outputPath = $null # Look for the publish path (line with -> that ends with \publish\) $lines = $publishOutput -split "`n" foreach ($line in $lines) { if ($line -match ".* -> (.+\\publish\\)$") { $outputPath = $matches[1].Trim() Write-Host "Extracted output path from $BuildType publish: $outputPath" -ForegroundColor Green return $outputPath } } # Fallback: look for any line with -> that contains publish foreach ($line in $lines) { if ($line -match ".* -> (.+publish.+)$") { $outputPath = $matches[1].Trim() Write-Host "Extracted output path from $BuildType publish (fallback): $outputPath" -ForegroundColor Green return $outputPath } } Write-Host "Could not extract output path from $BuildType publish output" -ForegroundColor Yellow Write-Host "Publish output: $publishOutput" return $null } function Find-BuiltFile { param( [string]$Config, [string]$Arch, [string]$ProjectFramework, [string]$AssemblyName, [string]$FileExtension, [string]$FileType = "file", [bool]$IsPublish = $false ) $searchPaths = @() # Adjust search paths based on $IsPublish if ($IsPublish) { $searchPaths = @( "bin/$Config/$ProjectFramework/$Arch/publish/", "bin/$Config/$Arch/publish/", "bin/$Config/publish/", "bin/" ) } else { $searchPaths = @( "bin/$Config/$ProjectFramework/$Arch/", "bin/$Config/$Arch/", "bin/$Config/", "bin/" ) } $filePattern = "$AssemblyName$FileExtension" Write-Host "Looking for $FileType $filePattern (IsPublish: $IsPublish)" foreach ($path in $searchPaths) { $fullPath = Join-Path (Join-Path $ProjectDir $path) "$AssemblyName$FileExtension" Write-Host "Searching path: $fullPath" if (Test-Path $fullPath) { $foundFile = Get-Item $fullPath if ($foundFile) { Write-Host "Found $FileType at: $($foundFile.FullName)" -ForegroundColor Green return $foundFile } } } # If file not found, provide debugging information # Write-Host "$FileType not found" -ForegroundColor Red # Write-Host "Debug: Listing all files in bin/$Config/ directory:" # if (Test-Path "bin/$Config/") { # Get-ChildItem -Path "bin/$Config/" -Recurse | ForEach-Object { Write-Host " - $($_.FullName)" } # } # # Also check specifically in the publish directory # $publishPath = "bin/$Config/$ProjectFramework/$Arch/publish/" # if (Test-Path $publishPath) { # Write-Host "Debug: Listing all files in publish directory ($publishPath):" # Get-ChildItem -Path $publishPath | ForEach-Object { Write-Host " - $($_.Name)" } # } Write-Host "$FileType $AssemblyName$FileExtension not found" -ForegroundColor Red return $null } function Commit-Git { Write-Host "Committing changes to git..." if (-not (Test-Path ".git")) { Write-Host "Initializing new git repository..." git init if ($LASTEXITCODE -eq 0) { Write-Host "Git repository initialized successfully" -ForegroundColor Green } else { Write-Host "Failed to initialize git repository" -ForegroundColor Red return $false } git branch -M main if ($LASTEXITCODE -eq 0) { Write-Host "Git branch set to main" -ForegroundColor Green } else { Write-Host "Failed to set git branch to main" -ForegroundColor Red } } if (-not (Test-Path ".gitignore")) { Write-Host "Creating .gitignore file..." $gitignore_template | Out-File -Encoding utf8 ".gitignore" if ($LASTEXITCODE -eq 0) { Write-Host ".gitignore file created successfully" -ForegroundColor Green } else { Write-Host "Failed to create .gitignore file" -ForegroundColor Red } } Write-Host "Adding files to git..." git add . if ($LASTEXITCODE -eq 0) { Write-Host "Files added to git successfully" -ForegroundColor Green } else { Write-Host "Failed to add files to git" -ForegroundColor Red return $false } $datetime = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' Write-Host "Committing changes..." git commit -m "Build at $datetime" if ($LASTEXITCODE -eq 0) { Write-Host "Changes committed successfully" -ForegroundColor Green return $true } else { Write-Host "Failed to commit changes" -ForegroundColor Red return $false } } function Push-Git { Write-Host "Pushing changes to git..." git push if ($LASTEXITCODE -eq 0) { Write-Host "Changes pushed to git successfully" -ForegroundColor Green return $true } else { Write-Host "Failed to push changes to git" -ForegroundColor Red return $false } } function Start-DockerIfNeeded { Write-Host "Checking Docker status..." # Check if Docker daemon is accessible try { docker info --format "{{.ServerVersion}}" 2>$null | Out-Null if ($LASTEXITCODE -eq 0) { Write-Host "Docker daemon is running" -ForegroundColor Green return $true } } catch { Write-Host "Docker daemon not accessible" -ForegroundColor Yellow } Write-Host "Docker daemon not running. Checking Docker service..." -ForegroundColor Yellow # Check and start Docker Windows service if needed try { $dockerService = Get-Service -Name "com.docker.service" -ErrorAction SilentlyContinue if ($dockerService) { if ($dockerService.Status -ne "Running") { Write-Host "Starting Docker service (com.docker.service)..." -ForegroundColor Yellow Start-Service -Name "com.docker.service" -ErrorAction Stop Write-Host "Docker service started successfully" -ForegroundColor Green # Wait a moment for the service to fully start Start-Sleep -Seconds 3 # Check if Docker daemon is now accessible try { docker info --format "{{.ServerVersion}}" 2>$null | Out-Null if ($LASTEXITCODE -eq 0) { Write-Host "Docker daemon is now running after service start" -ForegroundColor Green return $true } } catch { Write-Host "Docker daemon still not accessible after service start" -ForegroundColor Yellow } } else { Write-Host "Docker service is already running" -ForegroundColor Green } } else { Write-Host "Docker service (com.docker.service) not found" -ForegroundColor Yellow } } catch { Write-Host "Failed to manage Docker service: $_" -ForegroundColor Yellow } Write-Host "Attempting to start Docker Desktop..." -ForegroundColor Yellow # Try to start Docker Desktop application $dockerDesktopPaths = @( "${env:ProgramFiles}\Docker\Docker\Docker Desktop.exe", "${env:ProgramFiles(x86)}\Docker\Docker\Docker Desktop.exe", "${env:LOCALAPPDATA}\Programs\Docker\Docker\Docker Desktop.exe" ) $dockerStarted = $false foreach ($path in $dockerDesktopPaths) { if (Test-Path $path) { Write-Host "Starting Docker Desktop from: $path" try { Start-Process -FilePath $path -ErrorAction Stop $dockerStarted = $true break } catch { Write-Host "Failed to start Docker Desktop from $path : $_" -ForegroundColor Yellow } } } if (-not $dockerStarted) { Write-Host "Could not find Docker Desktop executable. Trying to start via Start-Process..." -ForegroundColor Yellow try { Start-Process "Docker Desktop" -ErrorAction Stop $dockerStarted = $true } catch { Write-Host "Failed to start Docker Desktop: $_" -ForegroundColor Yellow } } if ($dockerStarted) { Write-Host "Docker Desktop starting... waiting for daemon to be ready..." -ForegroundColor Yellow # Wait for Docker daemon to be ready (up to 60 seconds) $maxWaitTime = 60 $waitTime = 0 $interval = 2 while ($waitTime -lt $maxWaitTime) { Start-Sleep -Seconds $interval $waitTime += $interval try { docker info --format "{{.ServerVersion}}" 2>$null | Out-Null if ($LASTEXITCODE -eq 0) { Write-Host "Docker daemon is now ready!" -ForegroundColor Green return $true } } catch { # Continue waiting } Write-Host "Still waiting for Docker daemon... ($waitTime/$maxWaitTime seconds)" -ForegroundColor Yellow } Write-Host "Docker daemon did not start within $maxWaitTime seconds" -ForegroundColor Red return $false } Write-Host "Failed to start Docker Desktop" -ForegroundColor Red return $false } function Build-DockerImages { param( [string]$projectDir, [string]$projectName, [string]$newVersion, [string[]]$buildConfigs, [string]$repo ) Write-Host "Building Docker images..." # Ensure Docker is running before proceeding if (-not (Start-DockerIfNeeded)) { Write-Host "Docker is not available. Skipping Docker image builds." -ForegroundColor Red return } # Get GitHub username for the source label $githubUsername = Get-Username -Service "github" if (-not $githubUsername) { Write-Warning "Could not determine GitHub username for Docker source label" $githubUsername = "unknown" } # Find Dockerfile(s) $dockerfiles = Get-ChildItem -Path $projectDir -Filter "Dockerfile*" -Recurse -ErrorAction SilentlyContinue if ($dockerfiles.Count -eq 0) { Write-Host "No Dockerfile found in $projectDir. Creating a default Dockerfile..." $dockerfileContent = @" FROM mcr.microsoft.com/dotnet/runtime:8.0 AS base WORKDIR /app FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build WORKDIR /src COPY ["$projectName.csproj", "./"] RUN dotnet restore "$projectName.csproj" COPY . . WORKDIR "/src" RUN dotnet build "$projectName.csproj" -c Release -o /app/build FROM build AS publish RUN dotnet publish "$projectName.csproj" -c Release -o /app/publish /p:UseAppHost=false FROM base AS final WORKDIR /app COPY --from=publish /app/publish . LABEL org.opencontainers.image.source=https://github.com/$githubUsername/$repo ENTRYPOINT ["dotnet", "$projectName.dll"] "@ $dockerfilePath = Join-Path $projectDir "Dockerfile" $dockerfileContent | Out-File -FilePath $dockerfilePath -Encoding UTF8 $dockerfiles = @(Get-Item $dockerfilePath) Write-Host "Created default Dockerfile at $dockerfilePath" } # Process each Dockerfile to ensure it has the source label foreach ($dockerfile in $dockerfiles) { $dockerfileContent = Get-Content $dockerfile.FullName -Raw $sourceLabel = "LABEL org.opencontainers.image.source=https://github.com/$githubUsername/$repo" # Check if the source label already exists if ($dockerfileContent -notmatch [regex]::Escape("org.opencontainers.image.source")) { Write-Host "Adding source label to Dockerfile: $($dockerfile.Name)" # Find the best place to add the label - after FROM statements but before ENTRYPOINT/CMD $lines = Get-Content $dockerfile.FullName $newLines = @() $labelAdded = $false $lastFromIndex = -1 # First pass: find the last FROM statement for ($i = 0; $i -lt $lines.Count; $i++) { if ($lines[$i] -match "^\s*FROM\s+") { $lastFromIndex = $i } } # Second pass: add the label after the last FROM statement for ($i = 0; $i -lt $lines.Count; $i++) { $newLines += $lines[$i] # Add the label after the last FROM statement if (-not $labelAdded -and $i -eq $lastFromIndex) { $newLines += $sourceLabel $labelAdded = $true } } # If no FROM statement found, add at the beginning (though this shouldn't happen) if (-not $labelAdded) { $newLines = @($sourceLabel) + $newLines } # Write the updated content back to the file $newLines | Out-File -FilePath $dockerfile.FullName -Encoding UTF8 Write-Host "Added source label to $($dockerfile.Name)" -ForegroundColor Green } else { Write-Host "Source label already exists in $($dockerfile.Name)" -ForegroundColor Yellow } } foreach ($dockerfile in $dockerfiles) { Write-Host "Processing Dockerfile: $($dockerfile.FullName)" # Determine configuration based on Dockerfile name $dockerfileName = $dockerfile.Name $configToBuild = $null if ($dockerfileName -eq "Dockerfile") { # Default Dockerfile builds release $configToBuild = "Release" } elseif ($dockerfileName -eq "Dockerfile.debug") { # Dockerfile.debug builds debug $configToBuild = "Debug" } elseif ($dockerfileName -match "^Dockerfile\.(.+)$") { # Dockerfile.(something) builds the (something) configuration $configToBuild = $matches[1] } # Only build if this configuration is requested if ($configToBuild -and $buildConfigs -contains $configToBuild) { $configTag = if ($configToBuild -eq "Release") { "release" } else { "debug" } # Build Docker image using repository name as base $dockerImageName = if ($repo) { $repo.ToLower() } else { $projectName.ToLower() } $dockerTag = "${dockerImageName}:$configTag-$newVersion" $dockerLatestTag = "${dockerImageName}:$configTag-latest" Write-Host "Building Docker image: $dockerTag" docker build -f $dockerfile.FullName -t $dockerTag --build-arg CONFIGURATION=$configToBuild . if ($LASTEXITCODE -eq 0) { Write-Host "Docker image built successfully: $dockerTag" -ForegroundColor Green # Tag as latest docker tag $dockerTag $dockerLatestTag Write-Host "Tagged as latest: $dockerLatestTag" -ForegroundColor Green # For release builds, also tag as plain "latest" if ($configToBuild -eq "Release") { $plainLatestTag = "${dockerImageName}:latest" docker tag $dockerTag $plainLatestTag Write-Host "Tagged as plain latest: $plainLatestTag" -ForegroundColor Green } } else { Write-Host "Failed to build Docker image: $dockerTag" -ForegroundColor Red } } else { Write-Host "Skipping Dockerfile $dockerfileName - configuration '$configToBuild' not in requested build configs: $($buildConfigs -join ', ')" -ForegroundColor Yellow } } } function Publish-GitHubRelease { param( [string]$projectName, [string]$newVersion, [string]$outputBinDir, [string]$repo ) Write-Host "Publishing to GitHub Releases..." # Check if gh CLI is available if (-not (Get-Command gh -ErrorAction SilentlyContinue)) { Write-Error "GitHub CLI (gh) is not installed or not in PATH." return $false } # Get GitHub username $githubUsername = Get-Username -Service "gitHub" if (-not $githubUsername) { return $false } # Construct full repo name if only repo name is provided $fullRepoName = if ($repo -like "*/*") { $repo } else { "$githubUsername/$repo" } # Create release $releaseTitle = "Release $newVersion" $releaseBody = "Automated release for version $newVersion" Write-Host "Creating GitHub release: $releaseTitle" gh release create $newVersion --title $releaseTitle --notes $releaseBody --repo $fullRepoName if ($LASTEXITCODE -ne 0) { Write-Host "Failed to create GitHub release" -ForegroundColor Red return $false } else { Write-Host "GitHub release created successfully" -ForegroundColor Green } # Upload assets in parallel - only get files from the main bin directory, not subdirectories $assets = Get-ChildItem -Path $outputBinDir -File | Where-Object { $_.Extension -match '\.(exe|dll|nupkg)$' } Write-Host "Found $($assets.Count) assets to upload:" foreach ($asset in $assets) { $sizeMB = [Math]::Round($asset.Length / 1MB, 2) # Try multiple methods to calculate MD5 hash $hash = "N/A" try { # Method 1: Use Get-FileHash with MD5 (PowerShell 5.1+) $hashResult = Get-FileHash -Path $asset.FullName -Algorithm MD5 -ErrorAction Stop if ($hashResult -and $hashResult.Hash) { $hash = $hashResult.Hash Write-Host " MD5 calculated using Get-FileHash" -ForegroundColor DarkGray } } catch { Write-Host " Get-FileHash failed, trying alternative method..." -ForegroundColor DarkGray try { # Method 2: Use .NET MD5 class directly $md5 = [System.Security.Cryptography.MD5]::Create() $fileStream = [System.IO.File]::OpenRead($asset.FullName) $hashBytes = $md5.ComputeHash($fileStream) $fileStream.Close() $md5.Dispose() $hash = [System.BitConverter]::ToString($hashBytes).Replace("-", "").ToLower() Write-Host " MD5 calculated using .NET MD5 class" -ForegroundColor DarkGray } catch { Write-Host " .NET MD5 calculation failed, trying PowerShell MD5..." -ForegroundColor DarkGray try { # Method 3: Use PowerShell's built-in hash calculation $fileContent = Get-Content -Path $asset.FullName -Raw -Encoding Byte $md5 = New-Object -TypeName System.Security.Cryptography.MD5CryptoServiceProvider $hashBytes = $md5.ComputeHash($fileContent) $hash = [System.BitConverter]::ToString($hashBytes).Replace("-", "").ToLower() $md5.Dispose() Write-Host " MD5 calculated using PowerShell MD5" -ForegroundColor DarkGray } catch { Write-Host " All MD5 calculation methods failed for $($asset.Name)" -ForegroundColor Yellow $hash = "N/A" } } } # Generate a color from the MD5 hash (use first 6 hex digits, map to ConsoleColor) $colorMap = @( 'Black', 'DarkBlue', 'DarkGreen', 'DarkCyan', 'DarkRed', 'DarkMagenta', 'DarkYellow', 'Gray', 'DarkGray', 'Blue', 'Green', 'Cyan', 'Red', 'Magenta', 'Yellow', 'White' ) if ($hash -ne "N/A") { $colorIndex = [Convert]::ToInt32($hash.Substring(0, 2), 16) % $colorMap.Count $color = $colorMap[$colorIndex] } else { $color = "Gray" } Write-Host (" - {0} [{1} MB] (MD5: {2})" -f $asset.Name, $sizeMB, $hash) -ForegroundColor $color } if ($assets.Count -eq 0) { Write-Warning "No assets found in $outputBinDir. Checking if directory exists and has content..." if (Test-Path $outputBinDir) { Write-Host "Directory exists. Contents:" Get-ChildItem -Path $outputBinDir -Recurse | ForEach-Object { Write-Host " - $($_.Name) ($($_.FullName))" } } else { Write-Host "Directory does not exist: $outputBinDir" } return $false } $uploadJobs = @() foreach ($asset in $assets) { Write-Host "Starting upload for asset: $($asset.Name)" $job = Start-Job -ScriptBlock { param($assetPath, $version, $repo) try { $output = gh release upload $version $assetPath --repo $repo 2>&1 $exitCode = $LASTEXITCODE return @{ Name = [System.IO.Path]::GetFileName($assetPath) ExitCode = $exitCode Output = $output } } catch { return @{ Name = [System.IO.Path]::GetFileName($assetPath) ExitCode = 1 Output = $_.Exception.Message } } } -ArgumentList $asset.FullName, $newVersion, $fullRepoName $uploadJobs += $job } # Wait for all uploads to complete and collect results Write-Host "Waiting for all asset uploads to complete..." $results = $uploadJobs | Wait-Job | Receive-Job # Report results foreach ($result in $results) { if ($result.ExitCode -eq 0) { Write-Host "Successfully uploaded: $($result.Name)" -ForegroundColor Green } else { Write-Host "Failed to upload: $($result.Name)" -ForegroundColor Red if ($result.Output) { Write-Host " Error: $($result.Output)" -ForegroundColor Red } } } # Clean up jobs $uploadJobs | Remove-Job return $true } function Publish-NuGet { param( [string]$projectName, [string]$newVersion, [string]$outputBinDir ) Write-Host "Publishing to NuGet..." $NugetApiKey = $env:NUGET_API_KEY if (-not $NugetApiKey) { Write-Error "NUGET_API_KEY environment variable is required for NuGet publishing." return $false } # Find .nupkg files $nupkgFiles = Get-ChildItem -Path $outputBinDir -Filter "*.nupkg" -Recurse if ($nupkgFiles.Count -eq 0) { Write-Host "No .nupkg files found to publish" return $false } $success = $true foreach ($nupkg in $nupkgFiles) { Write-Host "Publishing NuGet package: $($nupkg.Name)" dotnet nuget push $nupkg.FullName --api-key $NugetApiKey --source https://api.nuget.org/v3/index.json if ($LASTEXITCODE -eq 0) { Write-Host "Successfully published: $($nupkg.Name)" -ForegroundColor Green } else { Write-Host "Failed to publish: $($nupkg.Name)" -ForegroundColor Red $success = $false } } return $success } function Publish-DockerHub { param( [string]$projectName, [string]$newVersion, [string[]]$buildConfigs, [string]$repo ) Write-Host "Publishing to Docker Hub..." # Ensure Docker is running before proceeding if (-not (Start-DockerIfNeeded)) { Write-Host "Docker is not available. Skipping Docker Hub publishing." -ForegroundColor Red return $false } # Get Docker username $dockerUsername = Get-Username -Service "docker" if (-not $dockerUsername) { return $false } $success = $true foreach ($config in $buildConfigs) { $configTag = if ($config -eq "Release") { "release" } else { "debug" } # Use repo name if provided, otherwise fall back to project name $imageBaseName = if ($repo) { $repo } else { $projectName.ToLower() } $localImageName = $imageBaseName.ToLower() $dockerImageName = "$dockerUsername/$imageBaseName" $dockerTag = "${dockerImageName}:$configTag-$newVersion" $dockerLatestTag = "${dockerImageName}:$configTag-latest" Write-Host "Tagging local image for Docker Hub: $dockerTag" docker tag "${localImageName}:$configTag-$newVersion" $dockerTag docker tag "${localImageName}:$configTag-latest" $dockerLatestTag # For release builds, also tag the plain "latest" tag if ($config -eq "Release") { $plainLatestTag = "${dockerImageName}:latest" docker tag "${localImageName}:$configTag-$newVersion" $plainLatestTag } Write-Host "Pushing to Docker Hub: $dockerTag" docker push $dockerTag Write-Host "Pushing to Docker Hub: $dockerLatestTag" docker push $dockerLatestTag # For release builds, also push plain "latest" tag if ($config -eq "Release") { $plainLatestTag = "${dockerImageName}:latest" Write-Host "Pushing to Docker Hub: $plainLatestTag" docker push $plainLatestTag } if ($LASTEXITCODE -eq 0) { Write-Host "Successfully published to Docker Hub: $dockerImageName" -ForegroundColor Green } else { Write-Host "Failed to push to Docker Hub: $dockerImageName" -ForegroundColor Red $success = $false } } return $success } function Publish-GHCR { param( [string]$projectName, [string]$newVersion, [string[]]$buildConfigs, [string]$repo ) Write-Host "Publishing to GitHub Container Registry (GHCR)..." # Ensure Docker is running before proceeding if (-not (Start-DockerIfNeeded)) { Write-Host "Docker is not available. Skipping GHCR publishing." -ForegroundColor Red return $false } # Get GitHub username $githubUsername = Get-Username -Service "GitHub" if (-not $githubUsername) { return $false } # Ensure lowercase for Docker compliance $githubUsername = $githubUsername.ToLower() $success = $true foreach ($config in $buildConfigs) { $configTag = if ($config -eq "Release") { "release" } else { "debug" } # Use repo name if provided, otherwise fall back to project name $imageBaseName = if ($repo) { $repo } else { $projectName.ToLower() } $localImageName = $imageBaseName.ToLower() $ghcrImageName = "ghcr.io/$githubUsername/$imageBaseName" $ghcrTag = "${ghcrImageName}:$configTag-$newVersion" $ghcrLatestTag = "${ghcrImageName}:$configTag-latest" Write-Host "Tagging local image for GHCR: $ghcrTag" docker tag "${localImageName}:$configTag-$newVersion" $ghcrTag docker tag "${localImageName}:$configTag-latest" $ghcrLatestTag # For release builds, also tag the plain "latest" tag if ($config -eq "Release") { $plainLatestTag = "${ghcrImageName}:latest" docker tag "${localImageName}:$configTag-$newVersion" $plainLatestTag } Write-Host "Pushing to GHCR: $ghcrTag" docker push $ghcrTag Write-Host "Pushing to GHCR: $ghcrLatestTag" docker push $ghcrLatestTag # For release builds, also push plain "latest" tag if ($config -eq "Release") { $plainLatestTag = "${ghcrImageName}:latest" Write-Host "Pushing to GHCR: $plainLatestTag" docker push $plainLatestTag } if ($LASTEXITCODE -eq 0) { Write-Host "Successfully published to GHCR: $ghcrImageName" -ForegroundColor Green } else { Write-Host "Failed to push to GHCR: $ghcrImageName" -ForegroundColor Red $success = $false } } return $success } function Build-Project { param( [string]$Version = "", [string[]]$Arch = @("win-x64", "win-x86"), [switch]$Release, [switch]$Debug, [switch]$Git, [switch]$Docker ) $ErrorActionPreference = 'Stop' # Find all .csproj files or .sln files Write-Host "Searching for project files..." $slnFiles = Get-ChildItem -Path (Get-Location) -Filter *.sln -Recurse -ErrorAction SilentlyContinue $csprojFiles = Get-ChildItem -Path (Get-Location) -Filter *.csproj -Recurse -ErrorAction SilentlyContinue if ($slnFiles.Count -gt 0) { Write-Host "Found .sln file(s): $($slnFiles.Name -join ', ')" Write-Host "Using solution file: $($slnFiles[0].FullName)" # For now, we'll use the first .sln file and build all projects in it # In the future, we could parse the .sln file to get specific projects $csprojFiles = Get-ChildItem -Path (Get-Location) -Filter *.csproj -Recurse -ErrorAction SilentlyContinue } elseif ($csprojFiles.Count -gt 0) { Write-Host "Found .csproj file(s): $($csprojFiles.Name -join ', ')" } else { Write-Error "No .csproj or .sln files found." return $false } foreach ($csproj in $csprojFiles) { Write-Host "Processing $($csproj.FullName)..." # Project and output variables (define as early as possible) $projectName = [System.IO.Path]::GetFileNameWithoutExtension($csproj.Name) Write-Host "Project name: $projectName" $projectDir = $csproj.DirectoryName Write-Host "Project directory: $projectDir" Push-Location $projectDir [xml]$projectXml = Get-Content $csproj $projectAssemblyNameNode = $projectXml.Project.PropertyGroup | Where-Object { $_.AssemblyName } | Select-Object -First 1 $projectVersionNode = $projectXml.Project.PropertyGroup | Where-Object { $_.Version } | Select-Object -First 1 $projectRIDNode = $projectXml.Project.PropertyGroup | Where-Object { $_.RuntimeIdentifier } | Select-Object -First 1 $projectRIDsNode = $projectXml.Project.PropertyGroup | Where-Object { $_.RuntimeIdentifiers } | Select-Object -First 1 $projectFrameworkNode = $projectXml.Project.PropertyGroup | Where-Object { $_.Framework } | Select-Object -First 1 $projectTargetFrameworkNode = $projectXml.Project.PropertyGroup | Where-Object { $_.TargetFramework } | Select-Object -First 1 $projectFramework = $null if ($projectTargetFrameworkNode -and $projectTargetFrameworkNode.TargetFramework) { $projectFramework = $projectTargetFrameworkNode.TargetFramework } elseif ($projectFrameworkNode -and $projectFrameworkNode.Framework) { $projectFramework = $projectFrameworkNode.Framework } Write-Host "Project framework: $projectFramework" # Determine architectures to build for $architectures = @() if ($Arch -and $Arch.Count -gt 0) { $architectures = $Arch } elseif ($projectRIDNode -and $projectRIDNode.RuntimeIdentifier) { $architectures = @($projectRIDNode.RuntimeIdentifier) } elseif ($projectRIDsNode -and $projectRIDsNode.RuntimeIdentifiers) { $architectures = $projectRIDsNode.RuntimeIdentifiers -split ';' } else { $architectures = @('win-x64', 'win-x86') } Write-Host "Building for architectures: $($architectures -join ', ')" # Determine build configurations $buildConfigs = @() if ($Release) { $buildConfigs += "Release" } if ($Debug) { $buildConfigs += "Debug" } # Default to both Release and Debug if no configuration specified if ($buildConfigs.Count -eq 0) { $buildConfigs = @("Release", "Debug") } Write-Host "Build configurations: $($buildConfigs -join ', ')" $outputFrameworkSuffix = ".$projectFramework.$arch.exe" $outputSelfcontainedSuffix = ".standalone.$arch.exe" $outputBinarySuffix = ".$arch" $outputType = $projectXml.Project.PropertyGroup | Where-Object { $_.OutputType } | Select-Object -First 1 Write-Host "Output type: $outputType" $outputIsExe = $false if ($outputType.OutputType) { $outputTypeValue = $outputType.OutputType.ToString() $outputIsExe = ($outputTypeValue -ieq 'Exe' -or $outputTypeValue -ieq 'WinExe') } $outputBinDir = Join-Path $projectDir 'bin' Write-Host "Output binary directory: $outputBinDir" $outputAssemblyName = $null if ($projectAssemblyNameNode -and $projectAssemblyNameNode.AssemblyName -and $projectAssemblyNameNode.AssemblyName -ne "") { $outputAssemblyName = $projectAssemblyNameNode.AssemblyName } else { $outputAssemblyName = $projectName } Write-Host "Output assembly name: $outputAssemblyName" if (-not $projectVersionNode) { Write-Host "No <Version> property found in any <PropertyGroup> in $csproj. Creating one with default version 1.0.0.0." $firstPropertyGroup = $projectXml.Project.PropertyGroup | Select-Object -First 1 if (-not $firstPropertyGroup) { Write-Error "No <PropertyGroup> found in $csproj to add <Version> property." return $false } $newVersion = '1.0.0.0' $versionElement = $projectXml.CreateElement('Version') $versionElement.InnerText = $newVersion $firstPropertyGroup.AppendChild($versionElement) | Out-Null $projectXml.Save($csproj) Write-Host "Created <Version> property with value $newVersion in $csproj." } else { $oldVersion = $projectVersionNode.Version Write-Host "Old version: $oldVersion" if ($Version) { Set-Version -projXml $projectXml -versionNode $projectVersionNode -newVersion $Version -csproj $csproj $newVersion = $Version } else { $newVersion = Bump-Version -oldVersion $oldVersion Set-Version -projXml $projectXml -versionNode $projectVersionNode -newVersion $newVersion -csproj $csproj } Write-Host "New version: $newVersion" } # Perform comprehensive cleanup before building Clear-BuildArtifacts -ProjectDir $projectDir -OutputBinDir $outputBinDir $outputFrameworkExe = $null; $outputStandaloneExe = $null; $outputBinPath = $null # Build for each configuration and architecture combination foreach ($config in $buildConfigs) { foreach ($arch in $architectures) { Write-Host "Building for configuration: $config, architecture: $arch" # Determine suffix based on configuration and architecture $configSuffix = if ($config -eq "Release") { ".release" } else { ".debug" } $outputFrameworkSuffixWithConfig = ".$projectFramework.$arch$configSuffix.exe" $outputSelfcontainedSuffixWithConfig = ".standalone.$arch$configSuffix.exe" $outputBinarySuffixWithConfig = ".$arch$configSuffix" # Build DLL $outputPath = Dotnet-Publish -Config $config -Arch $arch -ProjectFramework $projectFramework -AssemblyName $outputAssemblyName -BuildType "DLL" # Look for DLL in the extracted path first, then fall back to Find-BuiltFile $dllPath = $null if ($outputPath -and (Test-Path $outputPath)) { $dllFile = Join-Path $outputPath "$outputAssemblyName.dll" if (Test-Path $dllFile) { $dllPath = Get-Item $dllFile Write-Host "Found DLL in extracted path: $($dllPath.FullName)" -ForegroundColor Green } } # Fall back to Find-BuiltFile if we couldn't find it in the extracted path if (-not $dllPath) { Write-Host "Falling back to Find-BuiltFile for DLL..." -ForegroundColor Yellow $dllPath = Find-BuiltFile -IsPublish:$true -Config $config -Arch $arch -ProjectFramework $projectFramework -AssemblyName $outputAssemblyName -FileExtension ".dll" -FileType "DLL" } if ($dllPath) { $dllDest = Join-Path $outputBinDir "$outputAssemblyName$outputBinarySuffixWithConfig.dll" Copy-Item $dllPath.FullName $dllDest -Force Write-Host "DLL built successfully: $dllDest" -ForegroundColor Green } if ($LASTEXITCODE -ne 0) { Write-Host "Error during dotnet publish $($LASTEXITCODE)" -ForegroundColor Red } # Build Framework-dependent EXE $outputPath = Dotnet-Publish -Config $config -Arch $arch -ProjectFramework $projectFramework -AssemblyName $outputAssemblyName -BuildType "FrameworkExe" # Look for EXE in the extracted path first, then fall back to Find-BuiltFile $outputFrameworkExe = $null if ($outputPath -and (Test-Path $outputPath)) { $exeFile = Join-Path $outputPath "$outputAssemblyName.exe" if (Test-Path $exeFile) { $outputFrameworkExe = Get-Item $exeFile Write-Host "Found framework EXE in extracted path: $($outputFrameworkExe.FullName)" -ForegroundColor Green } } # Fall back to Find-BuiltFile if we couldn't find it in the extracted path if (-not $outputFrameworkExe) { Write-Host "Falling back to Find-BuiltFile for framework EXE..." -ForegroundColor Yellow $outputFrameworkExe = Find-BuiltFile -IsPublish:$true -Config $config -Arch $arch -ProjectFramework $projectFramework -AssemblyName $outputAssemblyName -FileExtension ".exe" -FileType "framework EXE" } $fwExeName = "$outputAssemblyName$outputFrameworkSuffixWithConfig" if ($outputFrameworkExe) { Copy-Item $outputFrameworkExe.FullName (Join-Path $outputBinDir $fwExeName) -Force Write-Host "Framework-dependent EXE built successfully: $fwExeName" -ForegroundColor Green } # Build Self-contained EXE $outputPath = Dotnet-Publish -Config $config -Arch $arch -ProjectFramework $projectFramework -AssemblyName $outputAssemblyName -BuildType "StandaloneExe" # Look for EXE in the extracted path first, then fall back to Find-BuiltFile $outputStandaloneExe = $null if ($outputPath -and (Test-Path $outputPath)) { $exeFile = Join-Path $outputPath "$outputAssemblyName.exe" if (Test-Path $exeFile) { $outputStandaloneExe = Get-Item $exeFile Write-Host "Found standalone EXE in extracted path: $($outputStandaloneExe.FullName)" -ForegroundColor Green } } # Fall back to Find-BuiltFile if we couldn't find it in the extracted path if (-not $outputStandaloneExe) { Write-Host "Falling back to Find-BuiltFile for standalone EXE..." -ForegroundColor Yellow $outputStandaloneExe = Find-BuiltFile -IsPublish:$true -Config $config -Arch $arch -ProjectFramework $projectFramework -AssemblyName $outputAssemblyName -FileExtension ".exe" -FileType "standalone EXE" } $scExeName = "$outputAssemblyName$outputSelfcontainedSuffixWithConfig" if ($outputStandaloneExe) { Copy-Item $outputStandaloneExe.FullName (Join-Path $outputBinDir $scExeName) -Force Write-Host "Self-contained EXE built successfully: $scExeName" -ForegroundColor Green } # For upload, always use the arch-suffixed names if (Test-Path (Join-Path $outputBinDir $fwExeName)) { $outputBinPath = Join-Path $outputBinDir $fwExeName } elseif (Test-Path (Join-Path $outputBinDir $scExeName)) { $outputBinPath = Join-Path $outputBinDir $scExeName } } } if ($Git) { $commitSuccess = Commit-Git if (-not $commitSuccess) { Write-Host "Git commit failed" -ForegroundColor Red } } # Build Docker images if requested if ($Docker) { Build-DockerImages -projectDir $projectDir -projectName $projectName -newVersion $newVersion -buildConfigs $buildConfigs -repo $Repo } Pop-Location } return $true } function Publish-Project { param( [string]$Version = "", [string[]]$Arch = @("win-x64", "win-x86"), [switch]$Nuget, [switch]$Github, [switch]$Git, [string]$Repo, [switch]$Release, [switch]$Debug, [switch]$Docker, [switch]$Ghcr ) Write-Host "Publish-Project function called with:" -ForegroundColor Cyan Write-Host " Github: $Github" -ForegroundColor Cyan Write-Host " Repo: '$Repo'" -ForegroundColor Cyan Write-Host " Release: $Release" -ForegroundColor Cyan Write-Host " Debug: $Debug" -ForegroundColor Cyan $ErrorActionPreference = 'Stop' # Get project info for publishing $csprojFiles = Get-ChildItem -Path (Get-Location) -Filter *.csproj -Recurse -ErrorAction SilentlyContinue if ($csprojFiles.Count -eq 0) { Write-Error "No .csproj files found for publishing." return $false } $csproj = $csprojFiles[0] # Use first project for publishing $projectName = [System.IO.Path]::GetFileNameWithoutExtension($csproj.Name) $projectDir = $csproj.DirectoryName $outputBinDir = Join-Path $projectDir 'bin' # Get current version [xml]$projectXml = Get-Content $csproj $projectVersionNode = $projectXml.Project.PropertyGroup | Where-Object { $_.Version } | Select-Object -First 1 $newVersion = if ($projectVersionNode) { $projectVersionNode.Version } else { "1.0.0.0" } # Determine build configurations $buildConfigs = @() if ($Release) { $buildConfigs += "Release" } if ($Debug) { $buildConfigs += "Debug" } if ($buildConfigs.Count -eq 0) { $buildConfigs = @("Release", "Debug") } # Push to git first (before publishing) if ($Git) { $pushSuccess = Push-Git if (-not $pushSuccess) { Write-Host "Git push failed" -ForegroundColor Red } } $publishSuccess = $true # Publish to GitHub Releases if ($Github) { Write-Host "Publishing to GitHub Releases..." -ForegroundColor Cyan $githubSuccess = Publish-GitHubRelease -projectName $projectName -newVersion $newVersion -outputBinDir $outputBinDir -repo $Repo if (-not $githubSuccess) { Write-Host "GitHub publishing failed!" -ForegroundColor Red $publishSuccess = $false } else { Write-Host "GitHub publishing completed successfully!" -ForegroundColor Green } } # Publish to NuGet if ($Nuget) { $nugetSuccess = Publish-NuGet -projectName $projectName -newVersion $newVersion -outputBinDir $outputBinDir if (-not $nugetSuccess) { $publishSuccess = $false } } # Publish to Docker Hub if ($Docker) { $dockerSuccess = Publish-DockerHub -projectName $projectName -newVersion $newVersion -buildConfigs $buildConfigs -repo $Repo if (-not $dockerSuccess) { $publishSuccess = $false } } # Publish to GHCR if ($Ghcr) { $ghcrSuccess = Publish-GHCR -projectName $projectName -newVersion $newVersion -buildConfigs $buildConfigs -repo $Repo if (-not $ghcrSuccess) { $publishSuccess = $false } } if ($publishSuccess) { Write-Host "All publishing operations completed successfully!" -ForegroundColor Green } else { Write-Host "Some publishing operations failed." -ForegroundColor Yellow } return $publishSuccess } # Main execution logic Write-Host "Debug - Parameters received:" Write-Host " Version: '$Version'" Write-Host " Arch: $($Arch -join ', ')" Write-Host " Release: $Release" Write-Host " Debug: $Debug" Write-Host " Git: $Git" Write-Host " Docker: $Docker" Write-Host " Publish: $Publish" Write-Host " Github: $Github" Write-Host " Ghcr: $Ghcr" Write-Host " Nuget: $Nuget" Write-Host " Repo: '$Repo'" # Always build first Write-Host "Building project..." $buildSuccess = Build-Project -Version $Version -Arch $Arch -Release:$Release -Debug:$Debug -Git:$Git -Docker:$Docker if (-not $buildSuccess) { Write-Error "Build failed" exit 1 } # Then optionally publish if ($Publish) { Write-Host "Starting publishing phase..." -ForegroundColor Cyan $publishResult = Publish-Project -Version $Version -Arch $Arch -Nuget:$Nuget -Github:$Github -Git:$Git -Repo $Repo -Release:$Release -Debug:$Debug -Docker:$Docker -Ghcr:$Ghcr if ($publishResult) { Write-Host "Publishing completed successfully!" -ForegroundColor Green } else { Write-Host "Publishing failed!" -ForegroundColor Red exit 1 } } |