Skip to content

Commit c9253f8

Browse files
committed
Enhance CI/release workflow and app robustness
Add comprehensive CI and release improvements (coverage collection, Codecov upload, SBOM generation), split release workflow into build/smoke-test/release stages with deterministic version resolution, artifact uploads, winget manifest updates and changelog generation. Add a pre-commit hook to block large or unwanted artifacts. Introduce runtime improvements: smoke-test startup mode, improved unhandled/dispatcher/task-exception reporting, ThreadPilotException error codes, single-instance behavior adjustments, and logging enhancements. UI and UX changes include a new Logs tab and LogViewer wiring, DWM title-bar theming helper, navigation serialization/unsaved-changes guard, system-tray update backoff logic, and related viewmodel/service integrations. Also add installer template, helper classes, tests, docs, packaging and build scripts, and minor .gitignore updates.
1 parent 275ac72 commit c9253f8

91 files changed

Lines changed: 4992 additions & 471 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.githooks/pre-commit.ps1

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
$ErrorActionPreference = "Stop"
2+
3+
$forbiddenPatterns = @(
4+
"*.log",
5+
"*.pdb",
6+
"*.tmp",
7+
"*.temp"
8+
)
9+
10+
$forbiddenDirectories = @(
11+
".kilo/",
12+
".roo/",
13+
".cursor/",
14+
".aider",
15+
"__pycache__/",
16+
"checkpoint"
17+
)
18+
19+
$maxFileSizeBytes = 100MB
20+
$stagedFiles = git diff --cached --name-only --diff-filter=ACM
21+
22+
if (-not $stagedFiles) {
23+
exit 0
24+
}
25+
26+
$errors = New-Object System.Collections.Generic.List[string]
27+
28+
foreach ($relativePath in $stagedFiles) {
29+
$normalizedPath = $relativePath.Replace('\\', '/').ToLowerInvariant()
30+
31+
foreach ($forbiddenDirectory in $forbiddenDirectories) {
32+
if ($normalizedPath.Contains($forbiddenDirectory.TrimEnd('/').ToLowerInvariant())) {
33+
$errors.Add("Forbidden artifact path staged: $relativePath")
34+
break
35+
}
36+
}
37+
38+
foreach ($pattern in $forbiddenPatterns) {
39+
if ($relativePath -like $pattern) {
40+
$errors.Add("Forbidden file pattern staged: $relativePath")
41+
break
42+
}
43+
}
44+
45+
if (-not (Test-Path -LiteralPath $relativePath)) {
46+
continue
47+
}
48+
49+
$fileInfo = Get-Item -LiteralPath $relativePath -ErrorAction SilentlyContinue
50+
if ($null -ne $fileInfo -and -not $fileInfo.PSIsContainer -and $fileInfo.Length -gt $maxFileSizeBytes) {
51+
$sizeMb = [Math]::Round($fileInfo.Length / 1MB, 2)
52+
$errors.Add("File exceeds 100MB limit: $relativePath ($sizeMb MB)")
53+
}
54+
}
55+
56+
if ($errors.Count -gt 0) {
57+
Write-Host "Pre-commit checks failed:" -ForegroundColor Red
58+
foreach ($error in $errors) {
59+
Write-Host " - $error" -ForegroundColor Red
60+
}
61+
62+
Write-Host "Commit aborted. Clean staged artifacts and retry." -ForegroundColor Yellow
63+
exit 1
64+
}
65+
66+
exit 0

.github/workflows/ci-devsecops.yml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,13 @@ jobs:
3737
run: dotnet build "ThreadPilot_1.sln" --configuration Release --no-restore
3838

3939
- name: Run tests
40-
run: dotnet test "ThreadPilot_1.sln" --configuration Release --no-build --verbosity normal
40+
run: dotnet test "ThreadPilot_1.sln" --configuration Release --no-build --verbosity normal --collect:"XPlat Code Coverage" --results-directory TestResults
41+
42+
- name: Upload coverage
43+
uses: codecov/codecov-action@v4
44+
with:
45+
files: '**/coverage.cobertura.xml'
46+
fail_ci_if_error: false
4147

4248
- name: Dependency vulnerability audit
4349
shell: pwsh

.github/workflows/release.yml

Lines changed: 208 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,26 +10,53 @@ permissions:
1010
contents: write
1111

1212
jobs:
13-
build-and-release:
13+
build:
1414
runs-on: windows-latest
15+
outputs:
16+
version: ${{ steps.version.outputs.version }}
17+
tag: ${{ steps.version.outputs.tag }}
1518

1619
steps:
1720
- name: Checkout
1821
uses: actions/checkout@v4
22+
with:
23+
fetch-depth: 0
1924

2025
- name: Setup .NET
2126
uses: actions/setup-dotnet@v4
2227
with:
2328
dotnet-version: "8.0.x"
2429

30+
- name: Resolve version
31+
id: version
32+
shell: pwsh
33+
run: |
34+
$refName = "${{ github.ref_name }}"
35+
if ($refName -match '^v\d+\.\d+\.\d+$') {
36+
$version = $refName.TrimStart('v')
37+
$tag = $refName
38+
}
39+
else {
40+
[xml]$project = Get-Content "ThreadPilot.csproj"
41+
$version = $project.Project.PropertyGroup.Version
42+
if ([string]::IsNullOrWhiteSpace($version)) {
43+
throw "Unable to resolve version from tag or ThreadPilot.csproj"
44+
}
45+
46+
$tag = "v$version"
47+
}
48+
49+
"version=$version" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
50+
"tag=$tag" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
51+
2552
- name: Restore
2653
run: dotnet restore "ThreadPilot_1.sln"
2754

2855
- name: Build
2956
run: dotnet build "ThreadPilot_1.sln" --configuration Release --no-restore
3057

3158
- name: Test
32-
run: dotnet test "ThreadPilot_1.sln" --configuration Release --no-build
59+
run: dotnet test "ThreadPilot_1.sln" --configuration Release --no-build --collect:"XPlat Code Coverage" --results-directory TestResults
3360

3461
- name: Publish self-contained single-file
3562
run: dotnet publish "ThreadPilot.csproj" --configuration Release -p:PublishProfile=WinX64-SingleFile
@@ -52,7 +79,7 @@ jobs:
5279
shell: pwsh
5380
run: |
5481
$ErrorActionPreference = "Stop"
55-
$version = "${{ github.ref_name }}".TrimStart('v')
82+
$version = "${{ steps.version.outputs.version }}"
5683
$sourceDir = (Resolve-Path "artifacts/release/singlefile").Path
5784
5885
& iscc.exe "/DMyAppVersion=$version" "/DMyAppSourceDir=$sourceDir" "Installer/setup.iss"
@@ -118,7 +145,7 @@ jobs:
118145
shell: pwsh
119146
run: |
120147
$ErrorActionPreference = "Stop"
121-
$version = "${{ github.ref_name }}"
148+
$version = "${{ steps.version.outputs.tag }}"
122149
New-Item -ItemType Directory -Force -Path "artifacts/release/packages" | Out-Null
123150
New-Item -ItemType Directory -Force -Path "artifacts/release/package-staging" | Out-Null
124151
@@ -206,15 +233,185 @@ jobs:
206233
"$($hash.Hash) $($_.Name)" | Out-File -FilePath $hashFile -Append -Encoding utf8
207234
}
208235
236+
- name: Update winget installer manifest
237+
shell: pwsh
238+
run: |
239+
$ErrorActionPreference = "Stop"
240+
$version = "${{ steps.version.outputs.version }}"
241+
$tag = "${{ steps.version.outputs.tag }}"
242+
$installer = Get-ChildItem "artifacts/release/installer/*.exe" -File | Select-Object -First 1
243+
if (-not $installer)
244+
{
245+
throw "Installer executable not found."
246+
}
247+
248+
$sha = (Get-FileHash $installer.FullName -Algorithm SHA256).Hash.ToUpperInvariant()
249+
$url = "https://github.com/${{ github.repository }}/releases/download/$tag/$($installer.Name)"
250+
$manifestPath = "winget/manifests/p/PrimeBuild/ThreadPilot/$version/PrimeBuild.ThreadPilot.installer.yaml"
251+
252+
if (-not (Test-Path $manifestPath))
253+
{
254+
throw "Winget installer manifest not found at $manifestPath"
255+
}
256+
257+
$content = Get-Content $manifestPath -Raw
258+
$content = $content.Replace("{{INSTALLER_URL}}", $url).Replace("{{INSTALLER_SHA256}}", $sha)
259+
Set-Content -Path $manifestPath -Value $content -Encoding utf8
260+
261+
- name: Generate SBOM
262+
shell: pwsh
263+
run: |
264+
$ErrorActionPreference = "Stop"
265+
dotnet tool install --global Microsoft.Sbom.DotNetTool
266+
$env:PATH += ";$env:USERPROFILE\.dotnet\tools"
267+
sbom-tool generate -b ./artifacts -bc . -pn ThreadPilot -pv "${{ steps.version.outputs.version }}" -ps PrimeBuild -nsb https://github.com/PrimeBuild-pc/ThreadPilot
268+
269+
- name: Upload portable artifact
270+
uses: actions/upload-artifact@v4
271+
with:
272+
name: release-portable
273+
path: artifacts/release/singlefile
274+
275+
- name: Upload installer artifact
276+
uses: actions/upload-artifact@v4
277+
with:
278+
name: release-installer
279+
path: artifacts/release/installer
280+
281+
- name: Upload packages artifact
282+
uses: actions/upload-artifact@v4
283+
with:
284+
name: release-packages
285+
path: artifacts/release/packages
286+
287+
- name: Upload msix artifact
288+
uses: actions/upload-artifact@v4
289+
with:
290+
name: release-msix
291+
path: artifacts/release/msix
292+
293+
- name: Upload checksums artifact
294+
uses: actions/upload-artifact@v4
295+
with:
296+
name: release-checksums
297+
path: artifacts/release/SHA256SUMS.txt
298+
299+
- name: Upload winget manifests
300+
uses: actions/upload-artifact@v4
301+
with:
302+
name: winget-manifests
303+
path: winget/manifests/p/PrimeBuild/ThreadPilot/${{ steps.version.outputs.version }}
304+
305+
- name: Upload SBOM
306+
uses: actions/upload-artifact@v4
307+
with:
308+
name: sbom
309+
path: _manifest/spdx_2.2/manifest.spdx.json
310+
311+
smoke-test:
312+
runs-on: windows-latest
313+
needs: build
314+
315+
steps:
316+
- uses: actions/download-artifact@v4
317+
with:
318+
name: release-portable
319+
path: smoke
320+
321+
- name: Smoke test
322+
shell: pwsh
323+
run: |
324+
$ErrorActionPreference = "Stop"
325+
$exe = Get-ChildItem "smoke" -Recurse -Filter "ThreadPilot.exe" -File | Select-Object -First 1
326+
if (-not $exe)
327+
{
328+
throw "ThreadPilot.exe not found in smoke-test artifact."
329+
}
330+
331+
$process = Start-Process -FilePath $exe.FullName -ArgumentList "--smoke-test" -Wait -PassThru
332+
if ($process.ExitCode -ne 0)
333+
{
334+
throw "Smoke test failed with exit code $($process.ExitCode)."
335+
}
336+
337+
release:
338+
runs-on: windows-latest
339+
needs:
340+
- build
341+
- smoke-test
342+
343+
steps:
344+
- name: Checkout
345+
uses: actions/checkout@v4
346+
with:
347+
fetch-depth: 0
348+
349+
- name: Download installer artifact
350+
uses: actions/download-artifact@v4
351+
with:
352+
name: release-installer
353+
path: release-assets/installer
354+
355+
- name: Download packages artifact
356+
uses: actions/download-artifact@v4
357+
with:
358+
name: release-packages
359+
path: release-assets/packages
360+
361+
- name: Download msix artifact
362+
uses: actions/download-artifact@v4
363+
with:
364+
name: release-msix
365+
path: release-assets/msix
366+
367+
- name: Download checksums artifact
368+
uses: actions/download-artifact@v4
369+
with:
370+
name: release-checksums
371+
path: release-assets
372+
373+
- name: Download winget manifests
374+
uses: actions/download-artifact@v4
375+
with:
376+
name: winget-manifests
377+
path: winget-manifests
378+
379+
- name: Download SBOM artifact
380+
uses: actions/download-artifact@v4
381+
with:
382+
name: sbom
383+
path: sbom
384+
385+
- name: Generate changelog
386+
id: git-cliff
387+
uses: orhun/git-cliff-action@v3
388+
with:
389+
config: cliff.toml
390+
args: --latest
391+
392+
- name: Contributors file check (manual maintenance mode)
393+
shell: pwsh
394+
run: |
395+
if (-not (Test-Path "CONTRIBUTORS.md")) {
396+
throw "CONTRIBUTORS.md not found."
397+
}
398+
399+
Write-Host "CONTRIBUTORS.md is maintained manually in this workflow."
400+
209401
- name: Publish GitHub release
210402
uses: softprops/action-gh-release@v2
211403
with:
404+
tag_name: ${{ needs.build.outputs.tag }}
405+
name: ThreadPilot ${{ needs.build.outputs.tag }}
406+
body: ${{ steps.git-cliff.outputs.content }}
212407
files: |
213-
artifacts/release/installer/*.exe
214-
artifacts/release/packages/*.zip
215-
artifacts/release/msix/**/*.msix
216-
artifacts/release/msix/**/*.appx
217-
artifacts/release/msix/**/*.msixbundle
218-
artifacts/release/msix/**/*.appxbundle
219-
artifacts/release/SHA256SUMS.txt
408+
release-assets/installer/*.exe
409+
release-assets/packages/*.zip
410+
release-assets/msix/**/*.msix
411+
release-assets/msix/**/*.appx
412+
release-assets/msix/**/*.msixbundle
413+
release-assets/msix/**/*.appxbundle
414+
release-assets/SHA256SUMS.txt
415+
winget-manifests/**/*.yaml
416+
sbom/manifest.spdx.json
220417
generate_release_notes: true

.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ x86/
1919
bld/
2020
artifacts/
2121

22+
# Keep release documentation tracked
23+
!docs/release/
24+
!docs/release/*.md
25+
2226
# Test and coverage
2327
[Tt]est[Rr]esult*/
2428
[Bb]uild[Ll]og.*
@@ -124,6 +128,12 @@ AppData/
124128
# Project-specific extras
125129
.claude/
126130
.continue/
131+
.kilo/
132+
.roo/
133+
.cursor/
134+
.aider*/
135+
__pycache__/
136+
checkpoint*/
127137
CPUSetSetter-main/
128138
*.bak
129139
*.backup

0 commit comments

Comments
 (0)