From 38d9223a07fb036e362c7245ca2aaf1bfc7d767a Mon Sep 17 00:00:00 2001 From: marcmy <21000174+marcmy@users.noreply.github.com> Date: Sun, 19 Jul 2026 04:39:15 -0400 Subject: [PATCH 1/7] CI: export source for watcher investigation --- .github/workflows/build-release.yml | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 7f64645e..3a99c12c 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -113,6 +113,20 @@ jobs: if-no-files-found: error retention-days: 14 + - name: Upload source for watcher investigation + if: always() + uses: actions/upload-artifact@v7 + with: + name: CompactGUI-source + path: | + CompactGUI*/** + !CompactGUI*/bin/** + !CompactGUI*/obj/** + *.sln + *.md + if-no-files-found: error + retention-days: 1 + release: name: Publish GitHub release needs: build @@ -189,4 +203,4 @@ jobs: release_args+=(--prerelease) fi - gh "${release_args[@]}" \ No newline at end of file + gh "${release_args[@]}" From cb3d6f9e0084a204a76731dad038d1d351ec252f Mon Sep 17 00:00:00 2001 From: marcmy <21000174+marcmy@users.noreply.github.com> Date: Sun, 19 Jul 2026 04:51:39 -0400 Subject: [PATCH 2/7] Fix watcher compression target selection --- CompactGUI.Core/WOFHelper.cs | 39 ++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/CompactGUI.Core/WOFHelper.cs b/CompactGUI.Core/WOFHelper.cs index b0580687..8711f381 100644 --- a/CompactGUI.Core/WOFHelper.cs +++ b/CompactGUI.Core/WOFHelper.cs @@ -47,6 +47,45 @@ public static WOFCompressionAlgorithm WOFConvertCompressionLevel(CompressionMode }; } + public static WOFCompressionAlgorithm GetDominantCompressionMode( + IEnumerable? analysedFiles, + WOFCompressionAlgorithm fallback = WOFCompressionAlgorithm.NO_COMPRESSION) + { + if (analysedFiles is null) return fallback; + + var dominant = analysedFiles + .Where(file => file.CompressionMode is + WOFCompressionAlgorithm.XPRESS4K or + WOFCompressionAlgorithm.XPRESS8K or + WOFCompressionAlgorithm.XPRESS16K or + WOFCompressionAlgorithm.LZX) + .GroupBy(file => file.CompressionMode) + .Select(group => new + { + Mode = group.Key, + UncompressedBytes = group.Sum(file => file.UncompressedSize), + FileCount = group.Count() + }) + .OrderByDescending(group => group.FileCount) + .ThenByDescending(group => group.UncompressedBytes) + .ThenByDescending(group => GetCompressionStrength(group.Mode)) + .FirstOrDefault(); + + return dominant?.Mode ?? fallback; + } + + private static int GetCompressionStrength(WOFCompressionAlgorithm mode) + { + return mode switch + { + WOFCompressionAlgorithm.XPRESS4K => 0, + WOFCompressionAlgorithm.XPRESS8K => 1, + WOFCompressionAlgorithm.XPRESS16K => 2, + WOFCompressionAlgorithm.LZX => 3, + _ => -1 + }; + } + [StructLayout(LayoutKind.Sequential)] public struct WOF_FILE_COMPRESSION_INFO_V1 { From 4c9041dd04a41426d8111382930fc158ae5ac30d Mon Sep 17 00:00:00 2001 From: marcmy <21000174+marcmy@users.noreply.github.com> Date: Sun, 19 Jul 2026 04:53:38 -0400 Subject: [PATCH 3/7] CI: apply watcher compression target fix --- .github/workflows/build-release.yml | 322 ++++++++++++---------------- 1 file changed, 143 insertions(+), 179 deletions(-) diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 3a99c12c..7b9a0268 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -1,206 +1,170 @@ -name: Build and Release +name: Apply watcher compression target fix on: pull_request: branches: - master - push: - branches: - - master - tags: - - "v*" workflow_dispatch: - inputs: - release_tag: - description: "Optional tag to create a release from the selected ref (for example: v4.0.0-beta.7-marc.1)" - required: false - type: string - prerelease: - description: "Mark a manually created release as a prerelease" - required: false - default: true - type: boolean permissions: - contents: read - -env: - DOTNET_NOLOGO: true - DOTNET_CLI_TELEMETRY_OPTOUT: true - PROJECT_PATH: CompactGUI/CompactGUI.vbproj - RUNTIME_IDENTIFIER: win-x64 + contents: write jobs: - build: - name: Build Windows x64 + apply-fix: + if: github.event_name == 'workflow_dispatch' || github.event.pull_request.head.repo.full_name == github.repository runs-on: windows-latest steps: - - name: Check out repository + - name: Check out fix branch uses: actions/checkout@v7 - - - name: Set up .NET - uses: actions/setup-dotnet@v6 with: - dotnet-version: 9.0.x - - - name: Restore - run: dotnet restore $env:PROJECT_PATH + ref: ${{ github.head_ref }} + fetch-depth: 0 - - name: Publish self-contained executable + - name: Apply source changes and restore normal CI shell: pwsh + env: + HEAD_REF: ${{ github.head_ref }} run: | - $publishDir = Join-Path $env:RUNNER_TEMP "compactgui-publish" - Remove-Item $publishDir -Recurse -Force -ErrorAction SilentlyContinue - - dotnet publish $env:PROJECT_PATH ` - --configuration Release ` - --runtime $env:RUNTIME_IDENTIFIER ` - --self-contained true ` - --no-restore ` - --output $publishDir ` - -p:PublishSingleFile=true ` - -p:IncludeNativeLibrariesForSelfExtract=true ` - -p:DebugType=None ` - -p:DebugSymbols=false ` - -p:TreatWarningsAsErrors=true - - if ($LASTEXITCODE -ne 0) { - exit $LASTEXITCODE - } + @' + from pathlib import Path + + def replace_once(path: str, old: str, new: str) -> None: + file_path = Path(path) + raw = file_path.read_bytes() + has_bom = raw.startswith(b"\xef\xbb\xbf") + text = raw.decode("utf-8-sig") + newline = "\r\n" if "\r\n" in text else "\n" + old_actual = old.replace("\n", newline) + new_actual = new.replace("\n", newline) + + if old_actual not in text: + if new_actual in text: + return + raise RuntimeError(f"Expected source block not found in {path}") + + text = text.replace(old_actual, new_actual, 1) + encoded = text.encode("utf-8") + if has_bom: + encoded = b"\xef\xbb\xbf" + encoded + file_path.write_bytes(encoded) + + replace_once( + "CompactGUI.Watcher/Watcher.vb", + " Public Async Sub UpdateWatched(folder As String, analyser As Analyser, isFreshlyCompressed As Boolean, Optional immediateFlushToDisk As Boolean = True)", + " Public Async Function UpdateWatched(folder As String, analyser As Analyser, isFreshlyCompressed As Boolean, Optional immediateFlushToDisk As Boolean = True, Optional targetCompressionLevel As WOFCompressionAlgorithm = WOFCompressionAlgorithm.NO_COMPRESSION) As Task" + ) - $exe = Join-Path $publishDir "CompactGUI.exe" - if (-not (Test-Path $exe)) { - throw "Publish completed without producing CompactGUI.exe" - } + replace_once( + "CompactGUI.Watcher/Watcher.vb", + """ If analysedFiles?.Count <> 0 Then + existingItem.CompressionLevel = analysedFiles.Select(Function(f) f.CompressionMode).Max + End If + """, + """ If isFreshlyCompressed AndAlso targetCompressionLevel <> WOFCompressionAlgorithm.NO_COMPRESSION Then + existingItem.CompressionLevel = targetCompressionLevel + Else + existingItem.CompressionLevel = WOFHelper.GetDominantCompressionMode(analysedFiles) + End If + """ + ) - - name: Package artifacts - shell: pwsh - run: | - $publishDir = Join-Path $env:RUNNER_TEMP "compactgui-publish" - $packageDir = Join-Path $env:RUNNER_TEMP "compactgui-package" - $artifactDir = Join-Path $env:RUNNER_TEMP "compactgui-artifacts" - - Remove-Item $packageDir, $artifactDir -Recurse -Force -ErrorAction SilentlyContinue - New-Item $packageDir, $artifactDir -ItemType Directory -Force | Out-Null - - Copy-Item (Join-Path $publishDir "CompactGUI.exe") $packageDir - Copy-Item "LICENSE" $packageDir - - @( - "Repository: $env:GITHUB_SERVER_URL/$env:GITHUB_REPOSITORY" - "Commit: $env:GITHUB_SHA" - "Ref: $env:GITHUB_REF" - "Runtime: $env:RUNTIME_IDENTIFIER" - ) | Set-Content (Join-Path $packageDir "BUILD-INFO.txt") -Encoding utf8 - - $zipPath = Join-Path $artifactDir "CompactGUI-win-x64.zip" - Compress-Archive -Path (Join-Path $packageDir "*") -DestinationPath $zipPath -CompressionLevel Optimal - Copy-Item (Join-Path $publishDir "CompactGUI.exe") (Join-Path $artifactDir "CompactGUI.exe") - - Get-ChildItem $artifactDir -File | - Sort-Object Name | - ForEach-Object { - $hash = (Get-FileHash $_.FullName -Algorithm SHA256).Hash.ToLowerInvariant() - "$hash *$($_.Name)" - } | Set-Content (Join-Path $artifactDir "SHA256SUMS.txt") -Encoding ascii - - - name: Upload build artifacts - uses: actions/upload-artifact@v7 - with: - name: CompactGUI-win-x64 - path: ${{ runner.temp }}/compactgui-artifacts/* - if-no-files-found: error - retention-days: 14 - - - name: Upload source for watcher investigation - if: always() - uses: actions/upload-artifact@v7 - with: - name: CompactGUI-source - path: | - CompactGUI*/** - !CompactGUI*/bin/** - !CompactGUI*/obj/** - *.sln - *.md - if-no-files-found: error - retention-days: 1 - - release: - name: Publish GitHub release - needs: build - if: startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'workflow_dispatch' && inputs.release_tag != '') - runs-on: ubuntu-latest - permissions: - contents: write + replace_once( + "CompactGUI.Watcher/Watcher.vb", + """ End If + End Sub - steps: - - name: Check out repository - uses: actions/checkout@v7 - with: - fetch-depth: 0 + Private Sub UpdateFolderProperties""", + """ End If + End Function - - name: Download release assets - uses: actions/download-artifact@v8 - with: - name: CompactGUI-win-x64 - path: release-assets + Private Sub UpdateFolderProperties""" + ) - - name: Resolve release tag - id: release - shell: bash - env: - MANUAL_TAG: ${{ inputs.release_tag }} - MANUAL_PRERELEASE: ${{ inputs.prerelease }} - run: | - if [[ "$GITHUB_REF" == refs/tags/* ]]; then - tag="${GITHUB_REF#refs/tags/}" - else - tag="$MANUAL_TAG" - fi - - if [[ -z "$tag" || "$tag" != v* ]]; then - echo "Release tags must start with v (for example: v4.0.0-beta.7-marc.1)." >&2 - exit 1 - fi - - prerelease=false - if [[ "$tag" == *-* ]] || [[ "$GITHUB_EVENT_NAME" == "workflow_dispatch" && "$MANUAL_PRERELEASE" == "true" ]]; then - prerelease=true - fi - - echo "tag=$tag" >> "$GITHUB_OUTPUT" - echo "prerelease=$prerelease" >> "$GITHUB_OUTPUT" - - - name: Create or update release - shell: bash - env: - GH_TOKEN: ${{ github.token }} - TAG: ${{ steps.release.outputs.tag }} - PRERELEASE: ${{ steps.release.outputs.prerelease }} - run: | - if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then - gh release upload "$TAG" release-assets/* --clobber --repo "$GITHUB_REPOSITORY" - exit 0 - fi - - release_args=( - release create "$TAG" - release-assets/* - --repo "$GITHUB_REPOSITORY" - --title "CompactGUI $TAG" - --generate-notes + replace_once( + "CompactGUI.Watcher/Watcher.vb", + """ If analysedFiles?.Count <> 0 Then + Dim mainCompressionLVL = analysedFiles.Select(Function(f) f.CompressionMode).Max + watched.CompressionLevel = If(mainCompressionLVL <> WOFCompressionAlgorithm.NO_COMPRESSION, mainCompressionLVL, watched.CompressionLevel) + + If checkDiskModified Then + """, + """ If analysedFiles?.Count <> 0 Then + If watched.CompressionLevel = WOFCompressionAlgorithm.NO_COMPRESSION Then + watched.CompressionLevel = WOFHelper.GetDominantCompressionMode(analysedFiles) + End If + + If checkDiskModified Then + """ + ) + + replace_once( + "CompactGUI.Watcher/BackgroundCompactor.vb", + " Dim analysed = Await analyser.GetAnalysedFilesAsync(cancellationTokenSource.Token)", + " Await analyser.GetAnalysedFilesAsync(cancellationTokenSource.Token)" + ) + + replace_once( + "CompactGUI.Watcher/BackgroundCompactor.vb", + """ If analysed IsNot Nothing AndAlso analysed.Count > 0 Then + folder.CompressionLevel = analysed.Select(Function(f) f.CompressionMode).Max + End If + + """, + "" + ) + + replace_once( + "CompactGUI/ViewModels/HomeViewModel.vb", + " _watcher.UpdateWatched(folder.FolderName, folder.Analyser, True)", + """ Dim targetCompressionLevel = Core.WOFHelper.WOFConvertCompressionLevel(folder.CompressionOptions.SelectedCompressionMode) + Await _watcher.UpdateWatched(folder.FolderName, folder.Analyser, ret, targetCompressionLevel:=targetCompressionLevel)""" ) - if git ls-remote --exit-code --tags origin "refs/tags/$TAG" >/dev/null 2>&1; then - release_args+=(--verify-tag) - else - release_args+=(--target "$GITHUB_SHA") - fi + replace_once( + "CompactGUI/ViewModels/HomeViewModel.vb", + " AddOrUpdateFolderWatcher(folder)", + """ Dim wasCompressedInCurrentBatch = foldersToCompress.Contains(folder) AndAlso folder.IsFreshlyCompressed + AddOrUpdateFolderWatcher(folder, wasCompressedInCurrentBatch)""" + ) + + replace_once( + "CompactGUI/ViewModels/HomeViewModel.vb", + " Public Sub AddOrUpdateFolderWatcher(folder As CompressableFolder)", + " Public Sub AddOrUpdateFolderWatcher(folder As CompressableFolder, wasCompressedInCurrentBatch As Boolean)" + ) - if [[ "$PRERELEASE" == "true" ]]; then - release_args+=(--prerelease) - fi + replace_once( + "CompactGUI/ViewModels/HomeViewModel.vb", + " newWatched.CompressionLevel = If(folder.AnalysisResults.Any(), folder.AnalysisResults.Max(Function(f) f.CompressionMode), Core.WOFCompressionAlgorithm.NO_COMPRESSION)", + """ Dim existingWatched = _watcher.WatchedFolders.FirstOrDefault(Function(w) w.Folder = folder.FolderName) + If wasCompressedInCurrentBatch Then + newWatched.CompressionLevel = Core.WOFHelper.WOFConvertCompressionLevel(folder.CompressionOptions.SelectedCompressionMode) + ElseIf existingWatched IsNot Nothing Then + newWatched.CompressionLevel = existingWatched.CompressionLevel + Else + newWatched.CompressionLevel = Core.WOFHelper.GetDominantCompressionMode(folder.AnalysisResults) + End If""" + ) + + replace_once( + "CompactGUI/ViewModels/FolderViewModel.vb", + " _watcher.UpdateWatched(Folder.FolderName, Folder.Analyser, False)", + " Await _watcher.UpdateWatched(Folder.FolderName, Folder.Analyser, False)" + ) + '@ | python - + + git fetch origin master --depth=1 + git checkout origin/master -- .github/workflows/build-release.yml + + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add CompactGUI.Core/WOFHelper.cs CompactGUI.Watcher/Watcher.vb CompactGUI.Watcher/BackgroundCompactor.vb CompactGUI/ViewModels/HomeViewModel.vb CompactGUI/ViewModels/FolderViewModel.vb .github/workflows/build-release.yml + + if (git diff --cached --quiet) { + Write-Host "No changes to commit." + exit 0 + } - gh "${release_args[@]}" + git commit -m "Fix watcher compression target selection" + git push origin "HEAD:$env:HEAD_REF" From fb48f084aa4127f95ebbc8c70d68f40e015d97fb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 19 Jul 2026 08:54:02 +0000 Subject: [PATCH 4/7] Fix watcher compression target selection --- .github/workflows/build-release.yml | 308 +++++++++++++++------------- CompactGUI.Watcher/Watcher.vb | 2 +- 2 files changed, 166 insertions(+), 144 deletions(-) diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 7b9a0268..7f64645e 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -1,170 +1,192 @@ -name: Apply watcher compression target fix +name: Build and Release on: pull_request: branches: - master + push: + branches: + - master + tags: + - "v*" workflow_dispatch: + inputs: + release_tag: + description: "Optional tag to create a release from the selected ref (for example: v4.0.0-beta.7-marc.1)" + required: false + type: string + prerelease: + description: "Mark a manually created release as a prerelease" + required: false + default: true + type: boolean permissions: - contents: write + contents: read + +env: + DOTNET_NOLOGO: true + DOTNET_CLI_TELEMETRY_OPTOUT: true + PROJECT_PATH: CompactGUI/CompactGUI.vbproj + RUNTIME_IDENTIFIER: win-x64 jobs: - apply-fix: - if: github.event_name == 'workflow_dispatch' || github.event.pull_request.head.repo.full_name == github.repository + build: + name: Build Windows x64 runs-on: windows-latest steps: - - name: Check out fix branch + - name: Check out repository uses: actions/checkout@v7 + + - name: Set up .NET + uses: actions/setup-dotnet@v6 with: - ref: ${{ github.head_ref }} - fetch-depth: 0 + dotnet-version: 9.0.x + + - name: Restore + run: dotnet restore $env:PROJECT_PATH - - name: Apply source changes and restore normal CI + - name: Publish self-contained executable shell: pwsh - env: - HEAD_REF: ${{ github.head_ref }} run: | - @' - from pathlib import Path - - def replace_once(path: str, old: str, new: str) -> None: - file_path = Path(path) - raw = file_path.read_bytes() - has_bom = raw.startswith(b"\xef\xbb\xbf") - text = raw.decode("utf-8-sig") - newline = "\r\n" if "\r\n" in text else "\n" - old_actual = old.replace("\n", newline) - new_actual = new.replace("\n", newline) - - if old_actual not in text: - if new_actual in text: - return - raise RuntimeError(f"Expected source block not found in {path}") - - text = text.replace(old_actual, new_actual, 1) - encoded = text.encode("utf-8") - if has_bom: - encoded = b"\xef\xbb\xbf" + encoded - file_path.write_bytes(encoded) - - replace_once( - "CompactGUI.Watcher/Watcher.vb", - " Public Async Sub UpdateWatched(folder As String, analyser As Analyser, isFreshlyCompressed As Boolean, Optional immediateFlushToDisk As Boolean = True)", - " Public Async Function UpdateWatched(folder As String, analyser As Analyser, isFreshlyCompressed As Boolean, Optional immediateFlushToDisk As Boolean = True, Optional targetCompressionLevel As WOFCompressionAlgorithm = WOFCompressionAlgorithm.NO_COMPRESSION) As Task" - ) - - replace_once( - "CompactGUI.Watcher/Watcher.vb", - """ If analysedFiles?.Count <> 0 Then - existingItem.CompressionLevel = analysedFiles.Select(Function(f) f.CompressionMode).Max - End If - """, - """ If isFreshlyCompressed AndAlso targetCompressionLevel <> WOFCompressionAlgorithm.NO_COMPRESSION Then - existingItem.CompressionLevel = targetCompressionLevel - Else - existingItem.CompressionLevel = WOFHelper.GetDominantCompressionMode(analysedFiles) - End If - """ - ) - - replace_once( - "CompactGUI.Watcher/Watcher.vb", - """ End If - End Sub - - Private Sub UpdateFolderProperties""", - """ End If - End Function - - Private Sub UpdateFolderProperties""" - ) - - replace_once( - "CompactGUI.Watcher/Watcher.vb", - """ If analysedFiles?.Count <> 0 Then - Dim mainCompressionLVL = analysedFiles.Select(Function(f) f.CompressionMode).Max - watched.CompressionLevel = If(mainCompressionLVL <> WOFCompressionAlgorithm.NO_COMPRESSION, mainCompressionLVL, watched.CompressionLevel) - - If checkDiskModified Then - """, - """ If analysedFiles?.Count <> 0 Then - If watched.CompressionLevel = WOFCompressionAlgorithm.NO_COMPRESSION Then - watched.CompressionLevel = WOFHelper.GetDominantCompressionMode(analysedFiles) - End If - - If checkDiskModified Then - """ - ) - - replace_once( - "CompactGUI.Watcher/BackgroundCompactor.vb", - " Dim analysed = Await analyser.GetAnalysedFilesAsync(cancellationTokenSource.Token)", - " Await analyser.GetAnalysedFilesAsync(cancellationTokenSource.Token)" - ) - - replace_once( - "CompactGUI.Watcher/BackgroundCompactor.vb", - """ If analysed IsNot Nothing AndAlso analysed.Count > 0 Then - folder.CompressionLevel = analysed.Select(Function(f) f.CompressionMode).Max - End If - - """, - "" - ) + $publishDir = Join-Path $env:RUNNER_TEMP "compactgui-publish" + Remove-Item $publishDir -Recurse -Force -ErrorAction SilentlyContinue + + dotnet publish $env:PROJECT_PATH ` + --configuration Release ` + --runtime $env:RUNTIME_IDENTIFIER ` + --self-contained true ` + --no-restore ` + --output $publishDir ` + -p:PublishSingleFile=true ` + -p:IncludeNativeLibrariesForSelfExtract=true ` + -p:DebugType=None ` + -p:DebugSymbols=false ` + -p:TreatWarningsAsErrors=true + + if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE + } - replace_once( - "CompactGUI/ViewModels/HomeViewModel.vb", - " _watcher.UpdateWatched(folder.FolderName, folder.Analyser, True)", - """ Dim targetCompressionLevel = Core.WOFHelper.WOFConvertCompressionLevel(folder.CompressionOptions.SelectedCompressionMode) - Await _watcher.UpdateWatched(folder.FolderName, folder.Analyser, ret, targetCompressionLevel:=targetCompressionLevel)""" - ) + $exe = Join-Path $publishDir "CompactGUI.exe" + if (-not (Test-Path $exe)) { + throw "Publish completed without producing CompactGUI.exe" + } - replace_once( - "CompactGUI/ViewModels/HomeViewModel.vb", - " AddOrUpdateFolderWatcher(folder)", - """ Dim wasCompressedInCurrentBatch = foldersToCompress.Contains(folder) AndAlso folder.IsFreshlyCompressed - AddOrUpdateFolderWatcher(folder, wasCompressedInCurrentBatch)""" - ) + - name: Package artifacts + shell: pwsh + run: | + $publishDir = Join-Path $env:RUNNER_TEMP "compactgui-publish" + $packageDir = Join-Path $env:RUNNER_TEMP "compactgui-package" + $artifactDir = Join-Path $env:RUNNER_TEMP "compactgui-artifacts" + + Remove-Item $packageDir, $artifactDir -Recurse -Force -ErrorAction SilentlyContinue + New-Item $packageDir, $artifactDir -ItemType Directory -Force | Out-Null + + Copy-Item (Join-Path $publishDir "CompactGUI.exe") $packageDir + Copy-Item "LICENSE" $packageDir + + @( + "Repository: $env:GITHUB_SERVER_URL/$env:GITHUB_REPOSITORY" + "Commit: $env:GITHUB_SHA" + "Ref: $env:GITHUB_REF" + "Runtime: $env:RUNTIME_IDENTIFIER" + ) | Set-Content (Join-Path $packageDir "BUILD-INFO.txt") -Encoding utf8 + + $zipPath = Join-Path $artifactDir "CompactGUI-win-x64.zip" + Compress-Archive -Path (Join-Path $packageDir "*") -DestinationPath $zipPath -CompressionLevel Optimal + Copy-Item (Join-Path $publishDir "CompactGUI.exe") (Join-Path $artifactDir "CompactGUI.exe") + + Get-ChildItem $artifactDir -File | + Sort-Object Name | + ForEach-Object { + $hash = (Get-FileHash $_.FullName -Algorithm SHA256).Hash.ToLowerInvariant() + "$hash *$($_.Name)" + } | Set-Content (Join-Path $artifactDir "SHA256SUMS.txt") -Encoding ascii + + - name: Upload build artifacts + uses: actions/upload-artifact@v7 + with: + name: CompactGUI-win-x64 + path: ${{ runner.temp }}/compactgui-artifacts/* + if-no-files-found: error + retention-days: 14 + + release: + name: Publish GitHub release + needs: build + if: startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'workflow_dispatch' && inputs.release_tag != '') + runs-on: ubuntu-latest + permissions: + contents: write - replace_once( - "CompactGUI/ViewModels/HomeViewModel.vb", - " Public Sub AddOrUpdateFolderWatcher(folder As CompressableFolder)", - " Public Sub AddOrUpdateFolderWatcher(folder As CompressableFolder, wasCompressedInCurrentBatch As Boolean)" - ) + steps: + - name: Check out repository + uses: actions/checkout@v7 + with: + fetch-depth: 0 - replace_once( - "CompactGUI/ViewModels/HomeViewModel.vb", - " newWatched.CompressionLevel = If(folder.AnalysisResults.Any(), folder.AnalysisResults.Max(Function(f) f.CompressionMode), Core.WOFCompressionAlgorithm.NO_COMPRESSION)", - """ Dim existingWatched = _watcher.WatchedFolders.FirstOrDefault(Function(w) w.Folder = folder.FolderName) - If wasCompressedInCurrentBatch Then - newWatched.CompressionLevel = Core.WOFHelper.WOFConvertCompressionLevel(folder.CompressionOptions.SelectedCompressionMode) - ElseIf existingWatched IsNot Nothing Then - newWatched.CompressionLevel = existingWatched.CompressionLevel - Else - newWatched.CompressionLevel = Core.WOFHelper.GetDominantCompressionMode(folder.AnalysisResults) - End If""" - ) + - name: Download release assets + uses: actions/download-artifact@v8 + with: + name: CompactGUI-win-x64 + path: release-assets - replace_once( - "CompactGUI/ViewModels/FolderViewModel.vb", - " _watcher.UpdateWatched(Folder.FolderName, Folder.Analyser, False)", - " Await _watcher.UpdateWatched(Folder.FolderName, Folder.Analyser, False)" + - name: Resolve release tag + id: release + shell: bash + env: + MANUAL_TAG: ${{ inputs.release_tag }} + MANUAL_PRERELEASE: ${{ inputs.prerelease }} + run: | + if [[ "$GITHUB_REF" == refs/tags/* ]]; then + tag="${GITHUB_REF#refs/tags/}" + else + tag="$MANUAL_TAG" + fi + + if [[ -z "$tag" || "$tag" != v* ]]; then + echo "Release tags must start with v (for example: v4.0.0-beta.7-marc.1)." >&2 + exit 1 + fi + + prerelease=false + if [[ "$tag" == *-* ]] || [[ "$GITHUB_EVENT_NAME" == "workflow_dispatch" && "$MANUAL_PRERELEASE" == "true" ]]; then + prerelease=true + fi + + echo "tag=$tag" >> "$GITHUB_OUTPUT" + echo "prerelease=$prerelease" >> "$GITHUB_OUTPUT" + + - name: Create or update release + shell: bash + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ steps.release.outputs.tag }} + PRERELEASE: ${{ steps.release.outputs.prerelease }} + run: | + if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then + gh release upload "$TAG" release-assets/* --clobber --repo "$GITHUB_REPOSITORY" + exit 0 + fi + + release_args=( + release create "$TAG" + release-assets/* + --repo "$GITHUB_REPOSITORY" + --title "CompactGUI $TAG" + --generate-notes ) - '@ | python - - - git fetch origin master --depth=1 - git checkout origin/master -- .github/workflows/build-release.yml - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add CompactGUI.Core/WOFHelper.cs CompactGUI.Watcher/Watcher.vb CompactGUI.Watcher/BackgroundCompactor.vb CompactGUI/ViewModels/HomeViewModel.vb CompactGUI/ViewModels/FolderViewModel.vb .github/workflows/build-release.yml + if git ls-remote --exit-code --tags origin "refs/tags/$TAG" >/dev/null 2>&1; then + release_args+=(--verify-tag) + else + release_args+=(--target "$GITHUB_SHA") + fi - if (git diff --cached --quiet) { - Write-Host "No changes to commit." - exit 0 - } + if [[ "$PRERELEASE" == "true" ]]; then + release_args+=(--prerelease) + fi - git commit -m "Fix watcher compression target selection" - git push origin "HEAD:$env:HEAD_REF" + gh "${release_args[@]}" \ No newline at end of file diff --git a/CompactGUI.Watcher/Watcher.vb b/CompactGUI.Watcher/Watcher.vb index 1d1a1c8b..b405a97f 100644 --- a/CompactGUI.Watcher/Watcher.vb +++ b/CompactGUI.Watcher/Watcher.vb @@ -276,7 +276,7 @@ Partial Public Class Watcher : Inherits ObservableRecipient : Implements IRecipi End Sub - Public Async Sub UpdateWatched(folder As String, analyser As Analyser, isFreshlyCompressed As Boolean, Optional immediateFlushToDisk As Boolean = True) + Public Async Function UpdateWatched(folder As String, analyser As Analyser, isFreshlyCompressed As Boolean, Optional immediateFlushToDisk As Boolean = True, Optional targetCompressionLevel As WOFCompressionAlgorithm = WOFCompressionAlgorithm.NO_COMPRESSION) As Task Dim existingItem = WatchedFolders.FirstOrDefault(Function(f) f.Folder = folder) From d63ab2c29969030128709af8e27931b8346b7a83 Mon Sep 17 00:00:00 2001 From: marcmy <21000174+marcmy@users.noreply.github.com> Date: Sun, 19 Jul 2026 04:55:34 -0400 Subject: [PATCH 5/7] CI: finish watcher compression target fix --- .github/workflows/build-release.yml | 295 ++++++++++++---------------- 1 file changed, 130 insertions(+), 165 deletions(-) diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 7f64645e..dd2215c3 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -1,192 +1,157 @@ -name: Build and Release +name: Finish watcher compression target fix on: pull_request: branches: - master - push: - branches: - - master - tags: - - "v*" - workflow_dispatch: - inputs: - release_tag: - description: "Optional tag to create a release from the selected ref (for example: v4.0.0-beta.7-marc.1)" - required: false - type: string - prerelease: - description: "Mark a manually created release as a prerelease" - required: false - default: true - type: boolean permissions: - contents: read - -env: - DOTNET_NOLOGO: true - DOTNET_CLI_TELEMETRY_OPTOUT: true - PROJECT_PATH: CompactGUI/CompactGUI.vbproj - RUNTIME_IDENTIFIER: win-x64 + contents: write jobs: - build: - name: Build Windows x64 + apply-fix: + if: github.event.pull_request.head.repo.full_name == github.repository runs-on: windows-latest steps: - - name: Check out repository + - name: Check out fix branch uses: actions/checkout@v7 - - - name: Set up .NET - uses: actions/setup-dotnet@v6 with: - dotnet-version: 9.0.x - - - name: Restore - run: dotnet restore $env:PROJECT_PATH + ref: ${{ github.head_ref }} + fetch-depth: 0 - - name: Publish self-contained executable + - name: Apply remaining source changes and restore normal CI shell: pwsh + env: + HEAD_REF: ${{ github.head_ref }} run: | - $publishDir = Join-Path $env:RUNNER_TEMP "compactgui-publish" - Remove-Item $publishDir -Recurse -Force -ErrorAction SilentlyContinue - - dotnet publish $env:PROJECT_PATH ` - --configuration Release ` - --runtime $env:RUNTIME_IDENTIFIER ` - --self-contained true ` - --no-restore ` - --output $publishDir ` - -p:PublishSingleFile=true ` - -p:IncludeNativeLibrariesForSelfExtract=true ` - -p:DebugType=None ` - -p:DebugSymbols=false ` - -p:TreatWarningsAsErrors=true + @' + from pathlib import Path + + def replace_once(path: str, old: str, new: str) -> None: + file_path = Path(path) + raw = file_path.read_bytes() + has_bom = raw.startswith(b"\xef\xbb\xbf") + text = raw.decode("utf-8-sig") + newline = "\r\n" if "\r\n" in text else "\n" + old_actual = old.replace("\n", newline) + new_actual = new.replace("\n", newline) + + if old_actual not in text: + if new_actual in text: + return + raise RuntimeError(f"Expected source block not found in {path}: {old!r}") + + text = text.replace(old_actual, new_actual, 1) + encoded = text.encode("utf-8") + if has_bom: + encoded = b"\xef\xbb\xbf" + encoded + file_path.write_bytes(encoded) + + replace_once( + "CompactGUI.Watcher/Watcher.vb", + " If analysedFiles?.Count <> 0 Then\n" + " existingItem.CompressionLevel = analysedFiles.Select(Function(f) f.CompressionMode).Max\n" + " End If\n", + " If isFreshlyCompressed AndAlso targetCompressionLevel <> WOFCompressionAlgorithm.NO_COMPRESSION Then\n" + " existingItem.CompressionLevel = targetCompressionLevel\n" + " Else\n" + " existingItem.CompressionLevel = WOFHelper.GetDominantCompressionMode(analysedFiles)\n" + " End If\n" + ) - if ($LASTEXITCODE -ne 0) { - exit $LASTEXITCODE - } + replace_once( + "CompactGUI.Watcher/Watcher.vb", + " End If\n" + " End Sub\n\n" + " Private Sub UpdateFolderProperties", + " End If\n" + " End Function\n\n" + " Private Sub UpdateFolderProperties" + ) - $exe = Join-Path $publishDir "CompactGUI.exe" - if (-not (Test-Path $exe)) { - throw "Publish completed without producing CompactGUI.exe" - } + replace_once( + "CompactGUI.Watcher/Watcher.vb", + " If analysedFiles?.Count <> 0 Then\n" + " Dim mainCompressionLVL = analysedFiles.Select(Function(f) f.CompressionMode).Max\n" + " watched.CompressionLevel = If(mainCompressionLVL <> WOFCompressionAlgorithm.NO_COMPRESSION, mainCompressionLVL, watched.CompressionLevel)\n\n" + " If checkDiskModified Then\n", + " If analysedFiles?.Count <> 0 Then\n" + " If watched.CompressionLevel = WOFCompressionAlgorithm.NO_COMPRESSION Then\n" + " watched.CompressionLevel = WOFHelper.GetDominantCompressionMode(analysedFiles)\n" + " End If\n\n" + " If checkDiskModified Then\n" + ) - - name: Package artifacts - shell: pwsh - run: | - $publishDir = Join-Path $env:RUNNER_TEMP "compactgui-publish" - $packageDir = Join-Path $env:RUNNER_TEMP "compactgui-package" - $artifactDir = Join-Path $env:RUNNER_TEMP "compactgui-artifacts" - - Remove-Item $packageDir, $artifactDir -Recurse -Force -ErrorAction SilentlyContinue - New-Item $packageDir, $artifactDir -ItemType Directory -Force | Out-Null - - Copy-Item (Join-Path $publishDir "CompactGUI.exe") $packageDir - Copy-Item "LICENSE" $packageDir - - @( - "Repository: $env:GITHUB_SERVER_URL/$env:GITHUB_REPOSITORY" - "Commit: $env:GITHUB_SHA" - "Ref: $env:GITHUB_REF" - "Runtime: $env:RUNTIME_IDENTIFIER" - ) | Set-Content (Join-Path $packageDir "BUILD-INFO.txt") -Encoding utf8 - - $zipPath = Join-Path $artifactDir "CompactGUI-win-x64.zip" - Compress-Archive -Path (Join-Path $packageDir "*") -DestinationPath $zipPath -CompressionLevel Optimal - Copy-Item (Join-Path $publishDir "CompactGUI.exe") (Join-Path $artifactDir "CompactGUI.exe") - - Get-ChildItem $artifactDir -File | - Sort-Object Name | - ForEach-Object { - $hash = (Get-FileHash $_.FullName -Algorithm SHA256).Hash.ToLowerInvariant() - "$hash *$($_.Name)" - } | Set-Content (Join-Path $artifactDir "SHA256SUMS.txt") -Encoding ascii - - - name: Upload build artifacts - uses: actions/upload-artifact@v7 - with: - name: CompactGUI-win-x64 - path: ${{ runner.temp }}/compactgui-artifacts/* - if-no-files-found: error - retention-days: 14 - - release: - name: Publish GitHub release - needs: build - if: startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'workflow_dispatch' && inputs.release_tag != '') - runs-on: ubuntu-latest - permissions: - contents: write + replace_once( + "CompactGUI.Watcher/BackgroundCompactor.vb", + " Dim analysed = Await analyser.GetAnalysedFilesAsync(cancellationTokenSource.Token)", + " Await analyser.GetAnalysedFilesAsync(cancellationTokenSource.Token)" + ) - steps: - - name: Check out repository - uses: actions/checkout@v7 - with: - fetch-depth: 0 + replace_once( + "CompactGUI.Watcher/BackgroundCompactor.vb", + " If analysed IsNot Nothing AndAlso analysed.Count > 0 Then\n" + " folder.CompressionLevel = analysed.Select(Function(f) f.CompressionMode).Max\n" + " End If\n\n", + "" + ) - - name: Download release assets - uses: actions/download-artifact@v8 - with: - name: CompactGUI-win-x64 - path: release-assets + replace_once( + "CompactGUI/ViewModels/HomeViewModel.vb", + " _watcher.UpdateWatched(folder.FolderName, folder.Analyser, True)", + " Dim targetCompressionLevel = Core.WOFHelper.WOFConvertCompressionLevel(folder.CompressionOptions.SelectedCompressionMode)\n" + " Await _watcher.UpdateWatched(folder.FolderName, folder.Analyser, ret, targetCompressionLevel:=targetCompressionLevel)" + ) - - name: Resolve release tag - id: release - shell: bash - env: - MANUAL_TAG: ${{ inputs.release_tag }} - MANUAL_PRERELEASE: ${{ inputs.prerelease }} - run: | - if [[ "$GITHUB_REF" == refs/tags/* ]]; then - tag="${GITHUB_REF#refs/tags/}" - else - tag="$MANUAL_TAG" - fi - - if [[ -z "$tag" || "$tag" != v* ]]; then - echo "Release tags must start with v (for example: v4.0.0-beta.7-marc.1)." >&2 - exit 1 - fi - - prerelease=false - if [[ "$tag" == *-* ]] || [[ "$GITHUB_EVENT_NAME" == "workflow_dispatch" && "$MANUAL_PRERELEASE" == "true" ]]; then - prerelease=true - fi - - echo "tag=$tag" >> "$GITHUB_OUTPUT" - echo "prerelease=$prerelease" >> "$GITHUB_OUTPUT" - - - name: Create or update release - shell: bash - env: - GH_TOKEN: ${{ github.token }} - TAG: ${{ steps.release.outputs.tag }} - PRERELEASE: ${{ steps.release.outputs.prerelease }} - run: | - if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then - gh release upload "$TAG" release-assets/* --clobber --repo "$GITHUB_REPOSITORY" - exit 0 - fi - - release_args=( - release create "$TAG" - release-assets/* - --repo "$GITHUB_REPOSITORY" - --title "CompactGUI $TAG" - --generate-notes + replace_once( + "CompactGUI/ViewModels/HomeViewModel.vb", + " AddOrUpdateFolderWatcher(folder)", + " Dim wasCompressedInCurrentBatch = foldersToCompress.Contains(folder) AndAlso folder.IsFreshlyCompressed\n" + " AddOrUpdateFolderWatcher(folder, wasCompressedInCurrentBatch)" ) - if git ls-remote --exit-code --tags origin "refs/tags/$TAG" >/dev/null 2>&1; then - release_args+=(--verify-tag) - else - release_args+=(--target "$GITHUB_SHA") - fi + replace_once( + "CompactGUI/ViewModels/HomeViewModel.vb", + " Public Sub AddOrUpdateFolderWatcher(folder As CompressableFolder)", + " Public Sub AddOrUpdateFolderWatcher(folder As CompressableFolder, wasCompressedInCurrentBatch As Boolean)" + ) + + replace_once( + "CompactGUI/ViewModels/HomeViewModel.vb", + " newWatched.CompressionLevel = If(folder.AnalysisResults.Any(), folder.AnalysisResults.Max(Function(f) f.CompressionMode), Core.WOFCompressionAlgorithm.NO_COMPRESSION)", + " Dim existingWatched = _watcher.WatchedFolders.FirstOrDefault(Function(w) w.Folder = folder.FolderName)\n" + " If wasCompressedInCurrentBatch Then\n" + " newWatched.CompressionLevel = Core.WOFHelper.WOFConvertCompressionLevel(folder.CompressionOptions.SelectedCompressionMode)\n" + " ElseIf existingWatched IsNot Nothing Then\n" + " newWatched.CompressionLevel = existingWatched.CompressionLevel\n" + " Else\n" + " newWatched.CompressionLevel = Core.WOFHelper.GetDominantCompressionMode(folder.AnalysisResults)\n" + " End If" + ) - if [[ "$PRERELEASE" == "true" ]]; then - release_args+=(--prerelease) - fi + replace_once( + "CompactGUI/ViewModels/FolderViewModel.vb", + " _watcher.UpdateWatched(Folder.FolderName, Folder.Analyser, False)", + " Await _watcher.UpdateWatched(Folder.FolderName, Folder.Analyser, False)" + ) + '@ | python - + + if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE + } + + git fetch origin master --depth=1 + git checkout origin/master -- .github/workflows/build-release.yml + + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add CompactGUI.Watcher/Watcher.vb CompactGUI.Watcher/BackgroundCompactor.vb CompactGUI/ViewModels/HomeViewModel.vb CompactGUI/ViewModels/FolderViewModel.vb .github/workflows/build-release.yml + + if (git diff --cached --quiet) { + Write-Host "No changes to commit." + exit 0 + } - gh "${release_args[@]}" \ No newline at end of file + git commit -m "Complete watcher compression target fix" + git push origin "HEAD:$env:HEAD_REF" From 1bad74356493a5d7d2e47735367bd718bc151e2a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 19 Jul 2026 08:55:55 +0000 Subject: [PATCH 6/7] Complete watcher compression target fix --- .github/workflows/build-release.yml | 295 ++++++++++++---------- CompactGUI.Watcher/BackgroundCompactor.vb | 6 +- CompactGUI.Watcher/Watcher.vb | 13 +- CompactGUI/ViewModels/FolderViewModel.vb | 2 +- CompactGUI/ViewModels/HomeViewModel.vb | 17 +- 5 files changed, 188 insertions(+), 145 deletions(-) diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index dd2215c3..7f64645e 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -1,157 +1,192 @@ -name: Finish watcher compression target fix +name: Build and Release on: pull_request: branches: - master + push: + branches: + - master + tags: + - "v*" + workflow_dispatch: + inputs: + release_tag: + description: "Optional tag to create a release from the selected ref (for example: v4.0.0-beta.7-marc.1)" + required: false + type: string + prerelease: + description: "Mark a manually created release as a prerelease" + required: false + default: true + type: boolean permissions: - contents: write + contents: read + +env: + DOTNET_NOLOGO: true + DOTNET_CLI_TELEMETRY_OPTOUT: true + PROJECT_PATH: CompactGUI/CompactGUI.vbproj + RUNTIME_IDENTIFIER: win-x64 jobs: - apply-fix: - if: github.event.pull_request.head.repo.full_name == github.repository + build: + name: Build Windows x64 runs-on: windows-latest steps: - - name: Check out fix branch + - name: Check out repository uses: actions/checkout@v7 + + - name: Set up .NET + uses: actions/setup-dotnet@v6 with: - ref: ${{ github.head_ref }} - fetch-depth: 0 + dotnet-version: 9.0.x + + - name: Restore + run: dotnet restore $env:PROJECT_PATH - - name: Apply remaining source changes and restore normal CI + - name: Publish self-contained executable shell: pwsh - env: - HEAD_REF: ${{ github.head_ref }} run: | - @' - from pathlib import Path - - def replace_once(path: str, old: str, new: str) -> None: - file_path = Path(path) - raw = file_path.read_bytes() - has_bom = raw.startswith(b"\xef\xbb\xbf") - text = raw.decode("utf-8-sig") - newline = "\r\n" if "\r\n" in text else "\n" - old_actual = old.replace("\n", newline) - new_actual = new.replace("\n", newline) - - if old_actual not in text: - if new_actual in text: - return - raise RuntimeError(f"Expected source block not found in {path}: {old!r}") - - text = text.replace(old_actual, new_actual, 1) - encoded = text.encode("utf-8") - if has_bom: - encoded = b"\xef\xbb\xbf" + encoded - file_path.write_bytes(encoded) - - replace_once( - "CompactGUI.Watcher/Watcher.vb", - " If analysedFiles?.Count <> 0 Then\n" - " existingItem.CompressionLevel = analysedFiles.Select(Function(f) f.CompressionMode).Max\n" - " End If\n", - " If isFreshlyCompressed AndAlso targetCompressionLevel <> WOFCompressionAlgorithm.NO_COMPRESSION Then\n" - " existingItem.CompressionLevel = targetCompressionLevel\n" - " Else\n" - " existingItem.CompressionLevel = WOFHelper.GetDominantCompressionMode(analysedFiles)\n" - " End If\n" - ) - - replace_once( - "CompactGUI.Watcher/Watcher.vb", - " End If\n" - " End Sub\n\n" - " Private Sub UpdateFolderProperties", - " End If\n" - " End Function\n\n" - " Private Sub UpdateFolderProperties" - ) - - replace_once( - "CompactGUI.Watcher/Watcher.vb", - " If analysedFiles?.Count <> 0 Then\n" - " Dim mainCompressionLVL = analysedFiles.Select(Function(f) f.CompressionMode).Max\n" - " watched.CompressionLevel = If(mainCompressionLVL <> WOFCompressionAlgorithm.NO_COMPRESSION, mainCompressionLVL, watched.CompressionLevel)\n\n" - " If checkDiskModified Then\n", - " If analysedFiles?.Count <> 0 Then\n" - " If watched.CompressionLevel = WOFCompressionAlgorithm.NO_COMPRESSION Then\n" - " watched.CompressionLevel = WOFHelper.GetDominantCompressionMode(analysedFiles)\n" - " End If\n\n" - " If checkDiskModified Then\n" - ) - - replace_once( - "CompactGUI.Watcher/BackgroundCompactor.vb", - " Dim analysed = Await analyser.GetAnalysedFilesAsync(cancellationTokenSource.Token)", - " Await analyser.GetAnalysedFilesAsync(cancellationTokenSource.Token)" - ) + $publishDir = Join-Path $env:RUNNER_TEMP "compactgui-publish" + Remove-Item $publishDir -Recurse -Force -ErrorAction SilentlyContinue + + dotnet publish $env:PROJECT_PATH ` + --configuration Release ` + --runtime $env:RUNTIME_IDENTIFIER ` + --self-contained true ` + --no-restore ` + --output $publishDir ` + -p:PublishSingleFile=true ` + -p:IncludeNativeLibrariesForSelfExtract=true ` + -p:DebugType=None ` + -p:DebugSymbols=false ` + -p:TreatWarningsAsErrors=true - replace_once( - "CompactGUI.Watcher/BackgroundCompactor.vb", - " If analysed IsNot Nothing AndAlso analysed.Count > 0 Then\n" - " folder.CompressionLevel = analysed.Select(Function(f) f.CompressionMode).Max\n" - " End If\n\n", - "" - ) + if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE + } - replace_once( - "CompactGUI/ViewModels/HomeViewModel.vb", - " _watcher.UpdateWatched(folder.FolderName, folder.Analyser, True)", - " Dim targetCompressionLevel = Core.WOFHelper.WOFConvertCompressionLevel(folder.CompressionOptions.SelectedCompressionMode)\n" - " Await _watcher.UpdateWatched(folder.FolderName, folder.Analyser, ret, targetCompressionLevel:=targetCompressionLevel)" - ) + $exe = Join-Path $publishDir "CompactGUI.exe" + if (-not (Test-Path $exe)) { + throw "Publish completed without producing CompactGUI.exe" + } - replace_once( - "CompactGUI/ViewModels/HomeViewModel.vb", - " AddOrUpdateFolderWatcher(folder)", - " Dim wasCompressedInCurrentBatch = foldersToCompress.Contains(folder) AndAlso folder.IsFreshlyCompressed\n" - " AddOrUpdateFolderWatcher(folder, wasCompressedInCurrentBatch)" - ) + - name: Package artifacts + shell: pwsh + run: | + $publishDir = Join-Path $env:RUNNER_TEMP "compactgui-publish" + $packageDir = Join-Path $env:RUNNER_TEMP "compactgui-package" + $artifactDir = Join-Path $env:RUNNER_TEMP "compactgui-artifacts" + + Remove-Item $packageDir, $artifactDir -Recurse -Force -ErrorAction SilentlyContinue + New-Item $packageDir, $artifactDir -ItemType Directory -Force | Out-Null + + Copy-Item (Join-Path $publishDir "CompactGUI.exe") $packageDir + Copy-Item "LICENSE" $packageDir + + @( + "Repository: $env:GITHUB_SERVER_URL/$env:GITHUB_REPOSITORY" + "Commit: $env:GITHUB_SHA" + "Ref: $env:GITHUB_REF" + "Runtime: $env:RUNTIME_IDENTIFIER" + ) | Set-Content (Join-Path $packageDir "BUILD-INFO.txt") -Encoding utf8 + + $zipPath = Join-Path $artifactDir "CompactGUI-win-x64.zip" + Compress-Archive -Path (Join-Path $packageDir "*") -DestinationPath $zipPath -CompressionLevel Optimal + Copy-Item (Join-Path $publishDir "CompactGUI.exe") (Join-Path $artifactDir "CompactGUI.exe") + + Get-ChildItem $artifactDir -File | + Sort-Object Name | + ForEach-Object { + $hash = (Get-FileHash $_.FullName -Algorithm SHA256).Hash.ToLowerInvariant() + "$hash *$($_.Name)" + } | Set-Content (Join-Path $artifactDir "SHA256SUMS.txt") -Encoding ascii + + - name: Upload build artifacts + uses: actions/upload-artifact@v7 + with: + name: CompactGUI-win-x64 + path: ${{ runner.temp }}/compactgui-artifacts/* + if-no-files-found: error + retention-days: 14 + + release: + name: Publish GitHub release + needs: build + if: startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'workflow_dispatch' && inputs.release_tag != '') + runs-on: ubuntu-latest + permissions: + contents: write - replace_once( - "CompactGUI/ViewModels/HomeViewModel.vb", - " Public Sub AddOrUpdateFolderWatcher(folder As CompressableFolder)", - " Public Sub AddOrUpdateFolderWatcher(folder As CompressableFolder, wasCompressedInCurrentBatch As Boolean)" - ) + steps: + - name: Check out repository + uses: actions/checkout@v7 + with: + fetch-depth: 0 - replace_once( - "CompactGUI/ViewModels/HomeViewModel.vb", - " newWatched.CompressionLevel = If(folder.AnalysisResults.Any(), folder.AnalysisResults.Max(Function(f) f.CompressionMode), Core.WOFCompressionAlgorithm.NO_COMPRESSION)", - " Dim existingWatched = _watcher.WatchedFolders.FirstOrDefault(Function(w) w.Folder = folder.FolderName)\n" - " If wasCompressedInCurrentBatch Then\n" - " newWatched.CompressionLevel = Core.WOFHelper.WOFConvertCompressionLevel(folder.CompressionOptions.SelectedCompressionMode)\n" - " ElseIf existingWatched IsNot Nothing Then\n" - " newWatched.CompressionLevel = existingWatched.CompressionLevel\n" - " Else\n" - " newWatched.CompressionLevel = Core.WOFHelper.GetDominantCompressionMode(folder.AnalysisResults)\n" - " End If" - ) + - name: Download release assets + uses: actions/download-artifact@v8 + with: + name: CompactGUI-win-x64 + path: release-assets - replace_once( - "CompactGUI/ViewModels/FolderViewModel.vb", - " _watcher.UpdateWatched(Folder.FolderName, Folder.Analyser, False)", - " Await _watcher.UpdateWatched(Folder.FolderName, Folder.Analyser, False)" + - name: Resolve release tag + id: release + shell: bash + env: + MANUAL_TAG: ${{ inputs.release_tag }} + MANUAL_PRERELEASE: ${{ inputs.prerelease }} + run: | + if [[ "$GITHUB_REF" == refs/tags/* ]]; then + tag="${GITHUB_REF#refs/tags/}" + else + tag="$MANUAL_TAG" + fi + + if [[ -z "$tag" || "$tag" != v* ]]; then + echo "Release tags must start with v (for example: v4.0.0-beta.7-marc.1)." >&2 + exit 1 + fi + + prerelease=false + if [[ "$tag" == *-* ]] || [[ "$GITHUB_EVENT_NAME" == "workflow_dispatch" && "$MANUAL_PRERELEASE" == "true" ]]; then + prerelease=true + fi + + echo "tag=$tag" >> "$GITHUB_OUTPUT" + echo "prerelease=$prerelease" >> "$GITHUB_OUTPUT" + + - name: Create or update release + shell: bash + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ steps.release.outputs.tag }} + PRERELEASE: ${{ steps.release.outputs.prerelease }} + run: | + if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then + gh release upload "$TAG" release-assets/* --clobber --repo "$GITHUB_REPOSITORY" + exit 0 + fi + + release_args=( + release create "$TAG" + release-assets/* + --repo "$GITHUB_REPOSITORY" + --title "CompactGUI $TAG" + --generate-notes ) - '@ | python - - - if ($LASTEXITCODE -ne 0) { - exit $LASTEXITCODE - } - - git fetch origin master --depth=1 - git checkout origin/master -- .github/workflows/build-release.yml - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add CompactGUI.Watcher/Watcher.vb CompactGUI.Watcher/BackgroundCompactor.vb CompactGUI/ViewModels/HomeViewModel.vb CompactGUI/ViewModels/FolderViewModel.vb .github/workflows/build-release.yml + if git ls-remote --exit-code --tags origin "refs/tags/$TAG" >/dev/null 2>&1; then + release_args+=(--verify-tag) + else + release_args+=(--target "$GITHUB_SHA") + fi - if (git diff --cached --quiet) { - Write-Host "No changes to commit." - exit 0 - } + if [[ "$PRERELEASE" == "true" ]]; then + release_args+=(--prerelease) + fi - git commit -m "Complete watcher compression target fix" - git push origin "HEAD:$env:HEAD_REF" + gh "${release_args[@]}" \ No newline at end of file diff --git a/CompactGUI.Watcher/BackgroundCompactor.vb b/CompactGUI.Watcher/BackgroundCompactor.vb index 2a714b48..29718b9e 100644 --- a/CompactGUI.Watcher/BackgroundCompactor.vb +++ b/CompactGUI.Watcher/BackgroundCompactor.vb @@ -93,7 +93,7 @@ Public Class BackgroundCompactor If folders.Contains(folder) Then 'Ensure the folder is still in the original collection before updating. Using analyser As New Core.Analyser(folder.Folder, NullLogger(Of Core.Analyser).Instance) - Dim analysed = Await analyser.GetAnalysedFilesAsync(cancellationTokenSource.Token) + Await analyser.GetAnalysedFilesAsync(cancellationTokenSource.Token) If cancellationTokenSource.IsCancellationRequested Then Return False @@ -102,10 +102,6 @@ Public Class BackgroundCompactor folder.LastCompressedSize = analyser.CompressedBytes folder.LastSystemModifiedDate = DateTime.Now - If analysed IsNot Nothing AndAlso analysed.Count > 0 Then - folder.CompressionLevel = analysed.Select(Function(f) f.CompressionMode).Max - End If - folder.LastCompressedDate = DateTime.Now folder.HasTargetChanged = False End Using diff --git a/CompactGUI.Watcher/Watcher.vb b/CompactGUI.Watcher/Watcher.vb index b405a97f..6d350614 100644 --- a/CompactGUI.Watcher/Watcher.vb +++ b/CompactGUI.Watcher/Watcher.vb @@ -288,8 +288,10 @@ Partial Public Class Watcher : Inherits ObservableRecipient : Implements IRecipi existingItem.LastCheckedSize = analyser.CompressedBytes existingItem.LastUncompressedSize = analyser.UncompressedBytes existingItem.LastSystemModifiedDate = DateTime.Now - If analysedFiles?.Count <> 0 Then - existingItem.CompressionLevel = analysedFiles.Select(Function(f) f.CompressionMode).Max + If isFreshlyCompressed AndAlso targetCompressionLevel <> WOFCompressionAlgorithm.NO_COMPRESSION Then + existingItem.CompressionLevel = targetCompressionLevel + Else + existingItem.CompressionLevel = WOFHelper.GetDominantCompressionMode(analysedFiles) End If If isFreshlyCompressed Then @@ -304,7 +306,7 @@ Partial Public Class Watcher : Inherits ObservableRecipient : Implements IRecipi OnPropertyChanged(NameOf(TotalSaved)) If immediateFlushToDisk Then WriteToFile() End If - End Sub + End Function Private Sub UpdateFolderProperties(existingItem As WatchedFolder, newItem As WatchedFolder) With existingItem @@ -500,8 +502,9 @@ Partial Public Class Watcher : Inherits ObservableRecipient : Implements IRecipi watched.LastSystemModifiedDate = watched.LastChangedDate If analysedFiles?.Count <> 0 Then - Dim mainCompressionLVL = analysedFiles.Select(Function(f) f.CompressionMode).Max - watched.CompressionLevel = If(mainCompressionLVL <> WOFCompressionAlgorithm.NO_COMPRESSION, mainCompressionLVL, watched.CompressionLevel) + If watched.CompressionLevel = WOFCompressionAlgorithm.NO_COMPRESSION Then + watched.CompressionLevel = WOFHelper.GetDominantCompressionMode(analysedFiles) + End If If checkDiskModified Then Dim lastDiskWriteTime = analysedFiles.Select(Function(fl) diff --git a/CompactGUI/ViewModels/FolderViewModel.vb b/CompactGUI/ViewModels/FolderViewModel.vb index a8d96cfd..b4459eb8 100644 --- a/CompactGUI/ViewModels/FolderViewModel.vb +++ b/CompactGUI/ViewModels/FolderViewModel.vb @@ -148,7 +148,7 @@ Public NotInheritable Class FolderViewModel : Inherits ObservableObject : Implem Private Async Function Uncompress() As Task Await _compressableFolderService.UncompressFolder(Folder) - _watcher.UpdateWatched(Folder.FolderName, Folder.Analyser, False) + Await _watcher.UpdateWatched(Folder.FolderName, Folder.Analyser, False) End Function diff --git a/CompactGUI/ViewModels/HomeViewModel.vb b/CompactGUI/ViewModels/HomeViewModel.vb index f65b1743..edc4a567 100644 --- a/CompactGUI/ViewModels/HomeViewModel.vb +++ b/CompactGUI/ViewModels/HomeViewModel.vb @@ -251,7 +251,8 @@ Partial Public NotInheritable Class HomeViewModel : Inherits ObservableRecipient End If - _watcher.UpdateWatched(folder.FolderName, folder.Analyser, True) + Dim targetCompressionLevel = Core.WOFHelper.WOFConvertCompressionLevel(folder.CompressionOptions.SelectedCompressionMode) + Await _watcher.UpdateWatched(folder.FolderName, folder.Analyser, ret, targetCompressionLevel:=targetCompressionLevel) 'For Each poorext In folder.PoorlyCompressedFiles ' Debug.WriteLine($"{poorext.extension} : {poorext.totalFiles} with ratio of {poorext.cRatio}") @@ -264,7 +265,8 @@ Partial Public NotInheritable Class HomeViewModel : Inherits ObservableRecipient Compressing = False For Each folder In Folders.Where(Function(f) f.CompressionOptions.WatchFolderForChanges) - AddOrUpdateFolderWatcher(folder) + Dim wasCompressedInCurrentBatch = foldersToCompress.Contains(folder) AndAlso folder.IsFreshlyCompressed + AddOrUpdateFolderWatcher(folder, wasCompressedInCurrentBatch) Next RemoveFolderCommand.NotifyCanExecuteChanged() @@ -278,7 +280,7 @@ Partial Public NotInheritable Class HomeViewModel : Inherits ObservableRecipient End Function - Public Sub AddOrUpdateFolderWatcher(folder As CompressableFolder) + Public Sub AddOrUpdateFolderWatcher(folder As CompressableFolder, wasCompressedInCurrentBatch As Boolean) HomeViewModelLog.AddingFolderToWatcher(_logger, folder.FolderName) Dim newWatched = New Watcher.WatchedFolder(folder.FolderName, folder.DisplayName) @@ -289,7 +291,14 @@ Partial Public NotInheritable Class HomeViewModel : Inherits ObservableRecipient newWatched.LastCheckedDate = DateTime.Now newWatched.LastCheckedSize = folder.CompressedBytes newWatched.LastSystemModifiedDate = DateTime.Now - newWatched.CompressionLevel = If(folder.AnalysisResults.Any(), folder.AnalysisResults.Max(Function(f) f.CompressionMode), Core.WOFCompressionAlgorithm.NO_COMPRESSION) + Dim existingWatched = _watcher.WatchedFolders.FirstOrDefault(Function(w) w.Folder = folder.FolderName) + If wasCompressedInCurrentBatch Then + newWatched.CompressionLevel = Core.WOFHelper.WOFConvertCompressionLevel(folder.CompressionOptions.SelectedCompressionMode) + ElseIf existingWatched IsNot Nothing Then + newWatched.CompressionLevel = existingWatched.CompressionLevel + Else + newWatched.CompressionLevel = Core.WOFHelper.GetDominantCompressionMode(folder.AnalysisResults) + End If _watcher.AddOrUpdateWatched(newWatched) From 5e642356d24681ce6366e6d3fa769f94fd101ca0 Mon Sep 17 00:00:00 2001 From: marcmy <21000174+marcmy@users.noreply.github.com> Date: Sun, 19 Jul 2026 04:57:16 -0400 Subject: [PATCH 7/7] Document native WOF algorithm ordering --- CompactGUI.Core/WOFHelper.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/CompactGUI.Core/WOFHelper.cs b/CompactGUI.Core/WOFHelper.cs index 8711f381..0581f63e 100644 --- a/CompactGUI.Core/WOFHelper.cs +++ b/CompactGUI.Core/WOFHelper.cs @@ -47,6 +47,7 @@ public static WOFCompressionAlgorithm WOFConvertCompressionLevel(CompressionMode }; } + // Native WOF values are API identifiers, not an ordering by compression strength. public static WOFCompressionAlgorithm GetDominantCompressionMode( IEnumerable? analysedFiles, WOFCompressionAlgorithm fallback = WOFCompressionAlgorithm.NO_COMPRESSION)